Dex打包关于65536的问题
由于DEX文件格式限制,一个DEX文件中的method个数采用使用原生类型short来索引文件的方法,也就是4个字节共计最多表达65536个method,field/class个数也均有此限制,对于DEX文件,则是将工程所需要全部class文件合并压缩到一个DEX文件期间,也就是Android打包的DEX过程中,单个DEX文件可被引用的方法总数(自己开发的代码以及所引用的Android框架、类库的代码)被限制为66536。
Android是怎么通过R文件找到真正的资源文件
aapt工具对每个资源文件都生成了唯一的ID,这些ID保存在R.java文件中。资源ID是一个4字节的的无符号证书,在R.java文件中用16进程表示。其中,最高的1字节表示Package ID,次高1个字节表示Type ID,最低2字节表示Entry ID。还生成一个文件resources.arsc,相当于一个资源索引表,也可以理解成一个map,map的key是资源ID,value是资源在apk文件中的路径。
混淆 ProGuard
因为Java代码是非常容易反编码的,况且Android开发的应用程序是用Java代码写的,为了很好的保护Java源代码,我们需要对编译好的后的class文件进行混淆。
ProGuard是一个混淆代码的开源项目,它的主要作用是混淆代码,但是其实它主要有4个功能如下:
- 压缩(Shrink):检测并移除代码中无用的类、字段、方法和特性(Attribute)
- 优化(Optimize):字节码进行优化,移除无用的指令。
- 混淆(Obfuscate):使用a、b、c、d这样简短而无意义的名称,对垒、字段和方法进行重命名。
- 预检测(Preveirfy):在Java平台对处理后的代码进行预检测,确保加载class文件是可执行的。
ProGurad的使用
- 开启混淆
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
- 混淆过程
ProGuard由shrink、optimize、obfuscate和preverify四个步骤组成,每个步骤都是可选的,需要哪些步骤都可以在脚本 proguard-rules.pro 中配置。
- 压缩(Shrink):默认开启,侦测并移除代码中无用的类、字段、方法和特性,减少应用体积,并且会在优化动作执行之后再次执行(因为优化后可能会再次暴露一些未使用的类和成员)。
-dontshrink 关闭混淆 - 优化(Optimize):默认开启,分析和优化字节码,让应用运行的更快。
-dontoptimize 关闭优化,默认混淆配置文件开始
-optimizationpasses n 表示proguard对代码进行迭代优化的次数,Android一般为5 - 混淆(Obfuscate):默认开启,使用a、b、c、d这样简短而无意义的名称,对类、字段和方法进行重命名,增大反编译难度。
-dontobfuscate 关闭混淆 - 预检(Preverify):在java平台上对处理后的代码进行预检。
上面三个步骤使代码大小更小、更高效,也更难被逆向工程。
这里引入到一个Entry Point(入口点) 概念,Entry Point是在ProGurad过程中不会被处理的类或方法。在压缩的过程中,ProGuard会从上述的Entry Point开始递归遍历,搜索哪些类和类的成员在使用,对于没有使用的类和类的成员,就会在压缩端被丢弃,在接下来的优化过程中,那些非Entry Point类、方法都会被设置为private、static或final,不实用的参数会被移除,此外,有些方法会被标记为内联的,在混淆的不会走中,ProGuard会对非Entry Point的类和方法进行重命名。
-
ProGuard的工具目录
工具目录
- 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。
- 基本命令
- keep类关键字
- keep:保留类和类中的成员,防止他们被混淆
- keepnames:保留类和类中的成员防止被混淆,但成员如果没有被引用将被删除
- keepclassmember:只保留类中的成员,防止被混淆和移除
- keepclassmembernames:值保留类中的成员,但是如果成员没有被引用将被删除
- keepclasseswithmember:如果当前类中包含指定的方法,则保留类和类成员,否则将被混淆。
- keepclasseswithmembernames:如果当前类中包含指定的方法,则保留类和类成员,如果类成员没有被引用则会被移除。
- keepattributes Signature:避免混淆泛型
- keepattributes SourceFile,LineNumberTable:抛出异常时保留行号。
- dontwarn:忽视警告
- optimizationpasses 5:代码混淆的压缩比,0~7之间,默认是5,一般不做修改
- 编写ProGuard文件
- 基本混淆
# 代码混淆压缩比,在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选项
- 不需要混淆设置
# 枚举类不能被混淆
-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);
}
- APP量身定制
- 保留实体类和成员被混淆
# 保留实体类和成员不被混淆
-keep public class com.xxxx.entity.** {
public void set*(***);
public *** get*();
public *** is*();
}
- 保留内部类
# 保留内嵌类不被混淆
-keep class com.example.xxx.MainActivity$* { *; }
- 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);
}
- JavaScript的处理
# 保留JS方法不被混淆
-keepclassmembers class com.example.xxx.MainActivity$JSInterface1 {
<methods>;
}
- 处理的反射
在程序中使用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")
- 自定义View的处理
但凡在Layout目录下XML布局文件配置的自定义View,都不能进行混淆。为此要遍历Layout下所有XML布局文件,找到那些自定义的View,然后确认其是否在ProGuard文件中保留。有一种思路是,在我们使用自定义View时,前面都必须加上我们的包名,比如com.a.b.customview,我们可以遍历所有Layout下的XML布局文件,查找所有匹配的com.a.b标签即可 - 第三方jar包的处理
一般而言,这些SDK是经过ProGuard混淆的,而我们所需要做的就是避免这些SDK的类和方法在我们APP被混淆。
# 针对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
# 对alipay的混淆处理
-libraryjars libs/alipaysdk.jar
-dontwarn com.alipay.android.app.**
-keep public class com.alipay.** { *; }
注意事项
- 如何确保混淆不会对项目产生影响
测试工作要基于混淆进行,才能尽早发现问题,开发团队的冒烟测试,也是要基于混淆包,发版前,重点的功能和模块要额外的测试,包括推送,分享等 - 打包时忽略警告
当打包的时候,会发现很多could not reference class之类的warning信息,如果确认App在运行中和那些以后能用没有什么关系,可以添加-dontwarn 标签,就不会提示这些警告信息了。 - 对于自定义类库的混淆处理
比如我们引用了一个叫做AndroidLib的类库,我们需要对Lib也进行混淆,然后在主项目的混淆文件中保留AndroidLib中的类和类成员 - 使用annotation避免混淆
另一种类或者属性被混淆的方式时,使用annotation,如下:
@keep
@keepPublicGetterSetters
public class Bean{
public boolean booleanProperty;
public int intProperty;
public String stringProperty;
}