Android插件化基础3----Android的编译打包流程详解

时间:2022-06-09
本文章向大家介绍Android插件化基础3----Android的编译打包流程详解,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

本片文章的主要内容如下:

  • 1、关于APK
  • 2、官网流程简述
  • 3、相关工具介绍
  • 4、打包流程详解
  • 5、关于Android自动打包工具aapt的概述
  • 6、面试中关于APK打包的问题
  • 7、混淆

一、关于APK

.apk文件其实就是一个压缩包,把文件的后缀改成.zip,用压缩软件解压搜就可的下图(我是mac)

解压缩.png

我们就把上面的内容简单介绍下:

  • AndroidManifest.xmlAndroidManifest.xml: 是每个android程序必须的文件,它位于整个项目的根目录,描述了package中暴露的组件(activity,service等),他们的实现类、处理数据、启动模式等。除了可以声明程序中的Activity、Service、ContentProvider、Receive,还能指定permissions(权限控制)和instrumentation(测试)。这个文件很重要,里面有我们的四大组件和申请的权限
  • classes.dex: 它是Android平台上的可执行文件,Android虚拟机Dalvik支持的字节码文件格式Google在Android平台上使用自己的Dalvik虚拟机来定义,这种虚拟机执行的并非Java字节码,而是另一种字节码:dex格式的字节码。在编译Java代码之后,通过Android平台上的工具可以将Java字节码转换成Dex字节码。虽然Google称Dalvik是为了移动设备定做的,但是业界很多人认为这是为了规避向sun(现在是oracle)申请Javalicense。这个DalvikVM针对手机程序/CPU做过最佳化,可以同样执行许多VM而不会占用太多的资源。class.dex也是由Java的class文件重新编排而来,我们也可以通过反编译工具把dex文件转换成class文件。如果做了拆包那么会有classes1.dex,classes2.dex ...多个classes.dex文件。
  • META-INF: 签名文件夹:里面存放三个文件,有两个是对的资源文件做的SHA1 hash处理,一个是签名和公钥证书。
  • res: 资源文件夹,和咱们开发中使用的res是同一个东西
  • resources.arsc: 这个文件记录了所有应用程序资源目录的信息,包括每一个资源名称、类型、值、ID以及所配置的维度信息。我们可以将这个resources.arsc可以理解为资源索引表,这个资源索引表在给定资源ID和设备配置信息的情况下,能够在应用程序目录中快速找到最匹配的资源。
  • lib: lib文件夹里面存放的是so动态链接库,so动态链接库是不需要做处理apk打包一些压缩处理的。

二、官方流程简述

现在官网支持中文哦,强烈大家先去把官网读一遍 Android Developer官网,点击查看 如下图

官网.png

下图的是官网对于Android编译打包流程的介绍:

官方流程.png

虚线方框是打包APK的操作,现在开发Android都是使用的Android Studio基于gradle来构建项目,所有打包操作都是执行gradle脚本来完成,gradle编译脚本具有强大的功能,我们可以在里面完成多渠道,多版本,不同版本使用不同代码,不同的资源,编译后的文件重命名,混淆签名验证等等配置,虽然都是基于AndroidSdk的platform-tools的文件夹下面的工工具来完成的,但是有了gradle这个配置文件,这样就便捷了。

三、相关工具介绍

相关介绍.png.png

PS:这里补充下apkbuilder在SDK3.0之前使用apkbuilder去打包,在SDK3.0之后就弃用了,而使用sdklib.jar打包apk。

如果你有下载Android系统源码,会发现源码目录下搜索apkbuilder,在sdk中有apkbuilder文件夹,里面有个readme文档,说明如下:

The apkbuilder command linetool is deprecated, and is not maintained anymore.

It is lacking recent buildimprovements such as support for Library Projects.

Its source code has been movedinto sdklib.

It is recommended to directlyuse the com.android.sdklib.build.ApkBuilder class instead.

就是说明此命令行工具新版本SDK目录去掉了,不再支持使用,它的代码放进了sdklib.jar可以直接使用。 其实,以前的apkbuilder.bat内部也是执行

com.android.sdklib.build.ApkBuilderMain

这个类其实就是在sdklib里面。

四、打包流程详解

提供一张APK打包流程图如下:

APK打包流程图.png

整体概述如下:

  • 1 打包资源文件,生成R.java文件
  • 2 处理aidl文件,生成相应的.java文件
  • 3 编译工程源码,生成相应的class文件
  • 4 转换所有的class文件,生成classes.dex文件
  • 5 打包生成apk
  • 6 对apk文件进行签名
  • 7 对签名后的apk进行对齐处理

下面我们就详细看下

(一)、打包资源文件,生成R.java文件

1、输入
  • 项目工程中res中的文件夹,我们称之为Resource文件
  • 项目工程中assert的文件夹,我们称之为Assert文件
  • AndroidManifest.xml文件
  • Android基础库(Android.jar文件)
2、工具:

aapt

3、过程:

生成过程主要是调用了aapt源码目录下的Resouce.cpp文件的buildResources()函数,该函数首先检查AndroidManifest.xml的合法性,然后对res目录下的资源目录进行处理,处理函数为makeFileResource(),处理的内容包括资源文件名的合法性检查,向资源表table添加条目等,处理完后调用compileResourceFile()函数编译res与asserts目录下的资源并生成resource.arsc文件,compileResourceFile()函数位于appt源码目录的ResourceTable.cpp文件中,该函数最后会调用parseAndAddEntry()函数生成R.java文件,完成资源编译后,接下来调用compileXmlfile()函数对res目录的子目录下的xml文件进行编译,这样处理过的xml文件就简单的被"加密"了,最后将所有资源与编译生成的resource.arsc文件以及"加密"过的AndroidManifest.xml打包压缩成resources.ap_文件。

上面涉及的源码代码位置在:

4、输出:

打包好的资源包括:

  • resources.ap_文件
  • R.java文件
5、补充:

打包资源的工具aapt,大部分文本格式的XML资源文件会被编译成二进制格式的XML资源文件,除了assets和res/raw资源被原封不动地打包进APK之外,其他资源都会被编译或者处理。

PS:

  • 除了assets和res/raw资源被原封不动地打包进APK之外,其它的资源都会被编译或者处理,除了assets资源之外,其他的资源都会被赋予一个资源ID。
  • resources.arsc是清单文件,但是resources.arsc跟R.java区别还是非常大的,R.java里面的只是id列表,并且里面的id值不重复。但是我们我们知道drawable-xdpi或者drawable-xxdpi这些不同分辨率的文件夹存放的图片和名称和id是一样的,在运行的时候是怎么根据设备的分别率来选择对应的分辨率的图片?这时候就需要resources.arsc这个文件了,resources.arsc里面会对所有的资源id进行组装,在apk运行是会根据设备的情况来采用不同的资源。resource.arsc文件的作用就是通过一样的ID,根据不同的配置索引到最佳的资源现在UI中。
  • R.java 是我们在写代码时候引用的res资源的id表,resources.arsc是程序在运行时候用到的资源表。R.java是给程序员读的,resources.arsc是给机器读的。

大体情况如下:

image.png

(二)、处理aidl文件,生成相应的.java文件

1、输入:

源码文件、aidl文件、framework.aidl文件

2、工具:

AIDL工具

3、过程:
4、输出:

对应的.java文件

5、补充:

对于没有使用到的aidl的android工程,这一步可以跳过,aidl工具解析接口定义文件并生成相应的.java文件,供程序调用

(三)、编译工程源码,生成相应的class文件

1、输入:

源码文件包括

  • R.java
  • AIDL生成的.java文件
  • 库jar文件
2、工具:

javac 工具

3、过程:

这里调用了javac编译工程的src目录下所有的java源文件,生成的class文件位于工程的binclassess目录下,上面假定编译源代码时程序是基于android SDK 开发的,实际开发过程中,也有可能会使用android NDK来编译native代码,因此,如果可能的话,这一步还需要使用android NDK编译C/C++代码,当然,编译C/C++代码的步骤也可以提前到第一步或第二步。

4、输出:

.class文件

(四)、转换所有的class文件,生成classes.dex文件

1、输入:

.class文件,主要包括AIDL生成的.class文件,R生成的.class文件,源文件生成的.class文件、.jar库文件

2、工具:

dx

3、过程:

前面提到,Android系统的dalvik虚拟机的可执行文件为dex格式,程序运行所需的classes.dex文件就是在这一步生成的,使用的工具为dx,dx工具主要的工作是将java字节码转换为dalvik字节码、压缩常量池、消除冗余信息等。

4、输出:

.dex文件

5、补充:
5.1 class与dex
  • class基于栈
  • dex基于寄存器

class与dex.png

5.2 优化
  • 1、优化常量池
  • 2、基于寄存器跟容易操作硬件内容,适合移动端

(五)、打包生成apk

1、输入:
  • 打包后的资源文件
  • 打包后的类文件,主要是指.dex文件
  • libs文件,包括so文件
2、工具:

3.0之前用apkbuilder工具,但是apkbuilder内部也是引用sdklib的ApkBuilderMain,所以3.0之后直接使用了sdklib的ApkBuilderMain

3、过程:

打包工具为apkbuilder,apkbuilder为一个脚本文件,实际调用的是android-sdk/tools/lib/sdklib.jar文件中的com.android.sdklib.build.ApkBuilderMain类。它的代码实现位于android系统源码的sdk/sdkmanager/libs/sdklib/src/com/android/sdklib/build/ApkBuilderMain.java文件,代码构建了一个ApkBuilder类,然后以包含resources.arsc文件为基础生成一个apk文件,这个文件一般为ap_结尾,接着调用addSourceFolder()函数添加工程资源,addSourceFolder()会调用processFileForResource()函数往apk文件中添加资源,处理内容包括res目录和asserts目录中的文件,添加完资源后调用addResourceFromJar()函数往apk文件中写入依赖库,接着调用 addNativeLibraries()函数添加工程libs目录下的Nativie库,最后调用sealApk(),关闭apk文件。

4、输出:

未签名的.apk文件

(六)、对apk文件进行签名

1、输入:

未签名的.apk文件

2、工具:

jarsigner

3、过程:

android的应用程序需要签名才能在android设备上安装,签名apk文件有两种情况:

  • 在调用应用程序时,也就是我们通常称为的debug模式的签名,平时开发的时候,在编译调试程序时会自己使用一个debug.keystore对apk进行签名
  • 正式发布时对应用程序打包进行签名,这种情况下需要提供一个符合android开发文档中要求的签名文件。 签名也是分两种:
  • 1 是使用JDK中提供的jarsigner工具签名
  • 2 是使用android源码中提供的signapk工具,它的代码位于android系统源码build/tools/signapk目录下
4、输出:

签名的apk文件

(七)、对签名后的apk进行对齐处理

对齐的作用就是减少运行内存的使用。

1、输入:

签名后的.apk文件

2、工具:

zipalign工具

3、过程:

这一步需要使用的工具为zipalign,它位于android-sdk/tools目录,源码位于android系统资源的build/tools/zipalign目录,它的主要工作是将apk包进行对齐处理,使apk包中的所有资源文件举例文件起始偏移为4字节的整数倍,这样通过内存映射访问apk时的速度会更快,验证apk文件是否对齐过的工作由ZipAlign.cpp文件的verify()函数完成,处理对齐的工作则由process()函数完成。

4、输出:

对齐后的apk文件

整体的细节流程如下图:

流程细节.png

五、关于Android自动打包工具aapt概述

(一) 概述

在Android.mk中有LOCAL_AAPT_FLAGS配置项,在gradle中也有aaptOptions,那么aapt到底是干什么?

aapt即Android Asset Packaging Tool (Android 打包工具),在SDK的build-tools目录下。大家可以自行查看。它可以将资源文件编译成二级制文件,尽管你可能没有直接使用过aapt工具,但是build scripts 和IDE插件会使用这个工具打包APK文件构成Android应用程序

(二)aapt打包流程

aapt传统的打包主要指的是res和Java代码的打包,aapt打包走的是单线程,流水式的任务从上到下进行打包构建。传统的aapt打包,aapt会执行2次,第一次是生成R.java,参与javac编译,第二次是对res里面的资源文件进行编译,最后将Dex文件与编译好的资源文件打包成apk,进行签名。整个流程下来没有任务缓存,没有并发,也没有增量,每次构建都是一个全新的流程。所以每次构建时间也比较恒定,代码量,资源量越多,构建的时间越慢。

如下图

aapt.png

六、面试中关于APK打包的问题

1、为什么第一步需要用aapt把xml文件编译成二进制文件?

主要是因为两个原因:

  • 首先二进制格式的XML文件占用空间更小。因为所有的XML元素的标签、属性名称、属性值和内容所涉及到的字符串都会被统一收集到一个字符串资源池中去,并且会去重。有了这个字符串资源池,原来使用字符串的地方就会被替换成一个索引字符串资源池的整数值,从而可以减少文件的大小
  • 其次是二进制的XML文件解析速度更快,这是由于二进制的XML元素里面不再包含有字符串值,因此可以避免了进行字符串解析,从而提高速度。

2、Dex打包关于65536的问题

这个问题是由于DEX文件格式限制,一个DEX文件中的method个数采用使用原生类型short来索引文件的方法,也就是4个字节共计最多表达65536个method,field/class个数也均有此限制,对于DEX文件,则是将工程所需要全部class文件合并压缩到一个DEX文件期间,也就是Android打包的DEX过程中,单个DEX文件可被引用的方法总数(自己开发的代码以及所引用的Android框架、类库的代码)被限制为66536。

3、打包流程中最后一步,为什么要对齐?

对齐是为了加快资源的访问速度。如果每个资源的开始位置上都是一个资源之后的4n字节,那么访问下一个资源就不用遍历,直接跳到4字节,那么访问下一个资源就不用遍历,直接跳到4*n字节处判断是不是一个新的资源即可。有点类似于资源数组化,数组的访问速度当然比链表块

4、Android是怎么通过R文件找到真正的资源文件?

aapt工具对每个资源文件都生成了唯一的ID,这些ID保存在R.java文件中。资源ID是一个4字节的的无符号证书,在R.java文件中用16进程表示。其中,最高的1字节表示Package ID,次高1个字节表示Type ID,最低2字节表示Entry ID。只有一个ID 如何能引用到实际资源?实际上aapt工具还生成一个文件resources.arsc,相当于一个资源索引表,或者你理解成一个map也行,map的key是资源ID,value是资源在apk文件中的路径。resource.arsc里面还有其他信息,这里就不多说了。通过resource.arsc配合,就能引用到实际的资源文件。

七、混淆

说到打包就不能不提一下混淆,说到混淆就不能不提ProGuard。

(一)、ProGurad简介

  • 因为Java代码是非常容易反编码的,况且Android开发的应用程序是用Java代码写的,为了很好的保护Java源代码,我们需要对编译好的后的class文件进行混淆。

ProGuard是一个混淆代码的开源项目,它的主要作用是混淆代码,但是其实它主要有4个功能如下:

  • 1 压缩(Shrink):检测并移除代码中无用的类、字段、方法和特性(Attribute)
  • 2 优化(Optimize):字节码进行优化,移除无用的指令。
  • 3 混淆(Obfuscate):使用a、b、c、d这样简短而无意义的名称,对垒、字段和方法进行重命名。
  • 4 预检测(Preveirfy):在Java平台对处理后的代码进行预检测,确保加载class文件是可执行的。

ProGuard的官网,根据官网的翻译:

Progurad是一个Java类文件的压缩器、优化器、混淆器、预检测器。压缩环节会检测以及移除没有用到的类、字段、方法以及属性。优化环节会分析以及优化方法的字节码。混淆环节会用无意义的端变量去重命名类、变量、方法。这些步骤让代码更加精简、更搞笑,也更难被逆向破解。

PS:

  • 1、 如果仅仅是为了代码混淆,ProGuard有一个兄弟产品DexGuard,有兴趣的可以去试试,地址在http://www.saikoa.com/dexguard
  • 2 、ProGurad是一个开源项目在SourceForge上进行维护,地址在http://ProGuard.sourceforge.net。从上述地址下载ProGuard之后,能同时看到官方文档和示例,不过是英文的,目前市面上没有相应的中文翻译版,也没有一片详尽的介绍文章。

(二)、ProGurad的使用

现在大多数开发者都是用了Android Studio,只有很少的一部分才使用Eclipse,所以我两部分都说下

1、Android Studio中如何开启混淆

在build.gradle中修改minifyEnable修改为true即可

代码如下

 buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
2、Eclipse中如何开启混淆

在Eclipse中,文件根目录有如下两个文件 projiect.propertiesproguard-project.txt。开启混淆打包只需要在 projiect.properties 中,被注释的有如下一句话

proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt

把他注释去掉即可

在开启混淆中,Android Studio和eclipse中都有一个文件proguard-android.txt,这是混淆的一个默认文件,该默认文件为android提供的一个默认规则,如果我们的工程没有引入第三方库等,那么很简单的就能混淆了。

(三)、ProGuard工作原理

ProGuard 由shrink、optimize、obfuscate和preverify四个步骤组成,每个步骤都是可选的,需要那些步骤都可以在脚本中配置。

ProGuard流程如下:

ProGuard流程图.png

Input jars、Library jars——>shrink ——>Shrunk code——>optimize ——>Optim.code——>obfuscate——>Obfusc.code ——>preverify——>Output jars、Library jars

ProGuard使用Library jars来辅助对input jars类之间的依赖关系进行解析,Library jars本身不会被处理,也不会被包含到output jars中。

混淆中会移除没有用到的代码,所以这里就产生一个疑问,ProGuard怎么知道这个代码没有被用到?

这里引入到一个Entry Point(入口点) 概念,Entry Point是在ProGurad过程中不会被处理的类或方法。在压缩的过程中,ProGuard会从上述的Entry Point开始递归遍历,搜索哪些类和类的成员在使用,对于没有使用的类和类的成员,就会在压缩端被丢弃,在接下来的优化过程中,那些非Entry Point类、方法都会被设置为private、static或final,不实用的参数会被移除,此外,有些方法会被标记为内联的,在混淆的不会走中,ProGuard会对非Entry Point的类和方法进行重命名。

(四)、ProGuard的工具目录

ProGuard的工具目录.png

  • bin目录: bin目录中包含了几个bat和shell脚本,通过这些脚本可以直接执行proguard.jar,proguardgui.jar和retrace.jar。如果将bin目录添加到环境变量中,就可以直接在命令行中执行progurad,proguardgui和retrace命令了,避免每次都要输入 java -jar +
  • lib目录: lib目录包含了Proguard工具对应的jar文件,其中又包含三个文件:proguard.jar,proguardgui.jar和retrace.jar。
    • proguard.jar:Progurad的四项核心功能shrink.optimize,obfuscate和preverify的执行都是由progurad.jar来完成,不过proguard.jar只能通过领命行方式来使用。
  • proguardgui.jar:是Proguard提供的一个图形界面工具,通过proguardgui.jar可以方便的查看和编辑Proguard配置,以及调用proguard.jar来执行一次优化过程。
  • retrace.jar主要是在debug时使用。混淆之后的jar文件执行过程如果出现异常,生成的异常信息将很难被解读,方法调用的堆栈都是一些混淆之后的名字,通过retrace.jar可以将异常的堆栈信息中的方法名还原成混淆前的名字,方便程序解决bug。

(五)、ProGuard的基本命令

1、关键字:
  • keep关键字
    • keep:保留类和类中的成员,防止他们被混淆
    • keepnames:保留类和类中的成员防止被混淆,但成员如果没有被引用将被删除
    • keepclassmember:只保留类中的成员,防止被混淆和移除
    • keepclassmembernames:值保留类中的成员,但是如果成员没有被引用将被删除
    • keepclasseswithmember:如果当前类中包含指定的方法,则保留类和类成员,否则将被混淆。
    • keepclasseswithmembernames:如果当前类中包含指定的方法,则保留类和类成员,如果类成员没有被引用则会被移除。
    • keepattributes Signature:避免混淆泛型
    • keepattributes SourceFile,LineNumberTable:抛出异常时保留行号。
  • dontwarn:忽视警告
  • optimizationpasses 5:代码混淆的压缩比,0~7之间,默认是5,一般不做修改
2、编写ProGuard文件

编写ProGuard文件有3个步骤:

  • 第一步,基本混淆
  • 第二步,针对APP量身定制
  • 第三步,针对第三方jar包的解决方法

下面我们就来详细看下

2.1 基本混淆

基本混淆又可以分为

  • 基本指令
  • 需要保留的东西
2.1.1 基本混淆

混淆文件的基本配置信息,任何APP都要使用,可以作为模板使用,具体如下:

# 代码混淆压缩比,在0和7之间,默认为5,一般不需要改
-optimizationpasses 5
 
# 混淆时不使用大小写混合,混淆后的类名为小写
-dontusemixedcaseclassnames
 
# 指定不去忽略非公共的库的类
-dontskipnonpubliclibraryclasses
 
# 指定不去忽略非公共的库的类的成员
-dontskipnonpubliclibraryclassmembers
 
# 不做预校验,preverify是proguard的4个步骤之一
# Android不需要preverify,去掉这一步可加快混淆速度
-dontpreverify
 
# 有了verbose这句话,混淆后就会生成映射文件
# 包含有类名->混淆后类名的映射关系
# 然后使用printmapping指定映射文件的名称
-verbose
-printmapping proguardMapping.txt
 
# 指定混淆时采用的算法,后面的参数是一个过滤器
# 这个过滤器是谷歌推荐的算法,一般不改变
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
 
# 保护代码中的Annotation不被混淆,这在JSON实体映射时非常重要,比如fastJson
-keepattributes *Annotation*
 
# 避免混淆泛型,这在JSON实体映射时非常重要,比如fastJson
-keepattributes Signature
 
//抛出异常时保留代码行号,在异常分析中可以方便定位
-keepattributes SourceFile,LineNumberTable

-dontskipnonpubliclibraryclasses用于告诉ProGuard,不要跳过对非公开类的处理。默认情况下是跳过的,因为程序中不会引用它们,有些情况下人们编写的代码与类库中的类在同一个包下,并且对包中内容加以引用,此时需要加入此条声明。

-dontusemixedcaseclassnames,这个是给Microsoft Windows用户的,因为ProGuard假定使用的操作系统是能区分两个只是大小写不同的文件名,但是Microsoft Windows不是这样的操作系统,所以必须为ProGuard指定-dontusemixedcaseclassnames选项
2.1.2 需要保留的东西
# 保留所有的本地native方法不被混淆
-keepclasseswithmembernames class * {
    native <methods>;
}

# 保留了继承自Activity、Application这些类的子类
# 因为这些子类有可能被外部调用
# 比如第一行就保证了所有Activity的子类不要被混淆
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService

# 如果有引用android-support-v4.jar包,可以添加下面这行
-keep public class com.null.test.ui.fragment.** {*;}

# 保留Activity中的方法参数是view的方法,
# 从而我们在layout里面编写onClick就不会影响
-keepclassmembers class * extends android.app.Activity {
    public void * (android.view.View);
}

# 枚举类不能被混淆
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# 保留自定义控件(继承自View)不能被混淆
-keep public class * extends android.view.View {
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
    public void set*(***);
    *** get* ();
}

# 保留Parcelable序列化的类不能被混淆
-keep class * implements android.os.Parcelable{
    public static final android.os.Parcelable$Creator *;
}

# 保留Serializable 序列化的类不被混淆
-keepclassmembers class * implements java.io.Serializable {
   static final long serialVersionUID;
   private static final java.io.ObjectStreamField[] serialPersistentFields;
   !static !transient <fields>;
   private void writeObject(java.io.ObjectOutputStream);
   private void readObject(java.io.ObjectInputStream);
   java.lang.Object writeReplace();
   java.lang.Object readResolve();
}

# 对R文件下的所有类及其方法,都不能被混淆
-keepclassmembers class **.R$* {
    *;
}

# 对于带有回调函数onXXEvent的,不能混淆
-keepclassmembers class * {
    void *(**On*Event);
}
2.2 针对APP量身定制

针对APP量身定制里面又包含

  • 1、保留实体类和成员被混淆
  • 2、内部类
  • 3、对WebView的处理
  • 4、对JavaScript的处理
  • 5、处理反射
  • 6、对于自定义View的解决方案

下面我们就来一一介绍

2.2.1 保留实体和成员比混淆

对于实体,保留他们的set和get方法,对于boolean型get方法,有人喜欢命名isXXX,所以不要遗漏。如下:

# 保留实体类和成员不被混淆
-keep public class com.xxxx.entity.** {
    public void set*(***);
    public *** get*();
    public *** is*();
}

一种好的做法就是把所有实体都放到一个包下进行管理,遮掩只写一次混淆就够了,避免以后在别的包中新增的实体而忘记保留,代码在混淆后因为找不到相应的实体类而崩溃。

2.2.2 内部类

内部类经常会被混淆,结果在调用的时候为空就崩溃了,最好的解决办法就是把这个内部类拿出来,单独成为一个类。如果一定要内置,那么这个类就必须在混淆的时候保留,比如:

# 保留内嵌类不被混淆
-keep class com.example.xxx.MainActivity$* { *; }

这个$符号就是用来分割内部类与其母体的标志。

2.2.3 对WebView的处理
# 对WebView的处理
-keepclassmembers class * extends android.webkit.WebViewClient {
    public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
    public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.WebViewClient {
    public void *(android.webkit.WebView, java.lang.String);
}
2.2.4 对JavaScript的处理
# 保留JS方法不被混淆
-keepclassmembers class com.example.xxx.MainActivity$JSInterface1 {
    <methods>;
}

其中JSInterface是MainActivity的子类

2.2.5 处理反射

在程序中使用SomeClass.class.method这样静态方法,在ProGuard中是在压缩过程中被保留的,那么对于Class.forName("SomeClass")呢,SomeClass不会被压缩过程中移除,它会检查程序中使用的Class.forName方法,对参数SomeClass法外开恩,不会被移除。但是在混淆过程中,无论是Class.forName("SomeClass"),还是SomeClass.class,都不能蒙混过关,SomeClass这个类名称会被混淆,因此,我们要在ProGuard.cfg文件中保留这个名称。

Class.forName("SomeClass")
SomeClass.class
SomeClass.class.getField("someField")
SomeClass.class.getDeclaredField("someField")
SomeClass.class.getMethod("someMethod", new Class[] {})
SomeClass.class.getMethod("someMethod", new Class[] { A.class })
SomeClass.class.getMethod("someMethod", new Class[] { A.class, B.class })
SomeClass.class.getDeclaredMethod("someMethod", new Class[] {})
SomeClass.class.getDeclaredMethod("someMethod", new Class[] { A.class })
SomeClass.class.getDeclaredMethod("someMethod", new Class[] { A.class, B.class })
AtomicIntegerFieldUpdater.newUpdater(SomeClass.class, "someField")
AtomicLongFieldUpdater.newUpdater(SomeClass.class, "someField")
AtomicReferenceFieldUpdater.newUpdater(SomeClass.class, SomeType.class, "someField")

在混淆的时候,要在项目中搜索一下上述方法,将相应的类或者方法名称进行保留而不被混淆。

2.2.6 对于自定义View的解决方案

但凡在Layout目录下XML布局文件配置的自定义View,都不能进行混淆。为此要遍历Layout下所有XML布局文件,找到那些自定义的View,然后确认其是否在ProGuard文件中保留。有一种思路是,在我们使用自定义View时,前面都必须加上我们的包名,比如com.a.b.customview,我们可以遍历所有Layout下的XML布局文件,查找所有匹配的com.a.b标签即可

但凡在Layout目录下的XML布局文件配置的自定义View,都不能进行混淆。为此要遍历Layout下的所有的XML布局文件,找到那些自定义View,然后确认其是否在ProGuard文件中保留。有一种思路是,在我们使用自定义View时,前面都必须加上我们的包名,比如com.a.b.customeview,我们可以遍历所有Layout下的XML布局文件,查找所有匹配com.a.b的标签即可

2.3 针对第三方jar包的解决方案

我们在Android项目中不可避免要使用很多第三方提供的SDK,一般而言,这些SDK是经过ProGuard混淆的,而我们所需要做的就是避免这些SDK的类和方法在我们APP被混淆。

2.3.1 针对android-support-v4.jar的解决方案
# 针对android-support-v4.jar的解决方案
-libraryjars libs/android-support-v4.jar
-dontwarn android.support.v4.**
-keep class android.support.v4.**  { *; }
-keep interface android.support.v4.app.** { *; }
-keep public class * extends android.support.v4.**
-keep public class * extends android.app.Fragment
2.3.2 其他的第三方jar包的解决方案

这个就取决于第三方包的混淆策略,一般都有在各自的SDK中有关于混淆的说明文字,比如支付宝如下:

# 对alipay的混淆处理
-libraryjars libs/alipaysdk.jar
-dontwarn com.alipay.android.app.**
-keep public class com.alipay.**  { *; }

PS:

值得注意的是,不是每个第三方SDK都需要-dontwarn指令、这取决于混淆时第三方SDK是否出现警告,需要的时候再机上。

(六)、ProGuard的混淆的注意事项

在使用ProGuard过程中,还有一些注意事项如下:

  • 1、如何确保混淆不会对项目产生影响 测试工作要基于混淆进行,才能尽早发现问题,开发团队的冒烟测试,也是要基于混淆包,发版前,重点的功能和模块要额外的测试,包括推送,分享等
  • 2、打包时忽略警告 当打包的时候,会发现很多could not reference class之类的warning信息,如果确认App在运行中和那些以后能用没有什么关系,可以添加-dontwarn 标签,就不会提示这些警告信息了。
  • 3、对于自定义类库的混淆处理 比如我们引用了一个叫做AndroidLib的类库,我们需要对Lib也进行混淆,然后在主项目的混淆文件中保留AndroidLib中的类和类成员
  • 4、使用annotation避免混淆 另一种类或者属性被混淆的方式时,使用annotation,如下:

@keep @keepPublicGetterSetters public class Bean{ public boolean booleanProperty; public int intProperty; public String stringProperty; }

答:xml里面都是各种字符,不利于快速遍历。编译成二进制文件,用数字替换各种符号,一方面能快速访问,另一方面也能减少大小。