Android 代码优化与混淆
1.android代码优化
andriod gradle 插件3.4.0版本以上,不在使用ProGurad执行编译代码优化工作,转而使用R8编译器一起处理代码
1) 压缩代码,检测依赖库,安全的移除未使用的类,字段,方法和属性,使用 minifyEnabled true 启用代码压缩
android {
...
buildTypes {
release {
minifyEnabled true //启用R8代码压缩
proguardFiles getDefaultProguardFile(
'proguard-android-optimize.txt'),
// List additional ProGuard rules for the given build type here. By default,
// Android Studio creates and includes an empty rules file for you (located
// at the root directory of each module).
'proguard-rules.pro'
}
}
}
2) 压缩资源,移除未使用的资源,包括应用的库依赖项中未使用的资源,要启用资源压缩,请在 build.gradle 文件中将 shrinkResources 属性设为 true(在用于代码压缩的 minifyEnabled 旁边)
android {
...
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}
3) 混淆,减少类,成员名称长度,从而减小dex文件体积
R8 使用 ProGuard 规则文件来修改其默认行为并更好地了解应用的结构,如充当应用代码入口点的类。
<module-dir>/proguard-rules.pro
默认情况下,此文件不应用任何规则,这里可以应用你自己的规则,progrard规则请看后面的proguard混淆规则
Android Gradle 插件在编译时候会生成 proguard-android-optimize.txt(其中包含对大多数 Android 项目都有用的规则),并启用 @Keep* 注解
AAR 库:<library-dir>/proguard.txt
如果某个 AAR 库是使用它自己的 ProGuard 规则文件发布的,并且您将该 AAR 库添加为编译时依赖项,则 R8 会在编译项目时自动应用其规则。
JAR 库:<library-dir>/META-INF/proguard/
因为 ProGuard 规则是累加的,所以 AAR 库依赖项包含的某些规则无法移除,并且可能会影响应用其他部分的编译。
例,如果某个库包含停用代码优化的规则,该规则将针对整个项目停用优化。
4) 代码优化,检查并重写代码,以进一步减小应用 DEX 文件的大小。
如果您的代码从不采用给定 if/else 语句的 else {} 分支,R8 可能会移除 else {} 分支的代码。
如果您的代码只在一个位置调用某个方法,R8 可能会移除该方法而将其内嵌在这一个调用点。
如果 R8 确定某个类只有一个唯一的子类且该类本身未实例化(例如,一个抽象基类仅由一个具体实现类使用),那么 R8 可以将这两个类组合在一起并从应用中移除一个类。
R8代码优化举例,如下代码
class MyActivity extends Activity {
@Override void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String name = this.getClass().getSimpleName();//这是一个很普通的getClass方法调用,但是这个会产生反射调用
Log.e(name, "Hello!");
}
}
//-----------------------------------------ByteCode--------------------------------------------------
[0003d0] MyActivity.onCreate:(Landroid/os/Bundle;)V
0000: invoke-super {v1, v2}, Landroid/app/Activity;.onCreate:(Landroid/os/Bundle;)V
0003: invoke-virtual {v1}, Ljava/lang/Object;.getClass:()Ljava/lang/Class;
0006: move-result-object v2
0007: invoke-virtual {v2}, Ljava/lang/Class;.getSimpleName:()Ljava/lang/String; //invoke-virtual调用
000a: move-result-object v2
R8会扫描程序,并分析知道MyActivity根本没有被继承,即使你没有给它加final,这个时候R8会使用MyActivity.class来取代this.getClass()的调用,最终的代码可能如下所示
//-----------------------------------------ByteCode--------------------------------------------------
0000: invoke-super {v1, v2}, Landroid/app/Activity;.onCreate:(Landroid/os/Bundle;)V
0003: const-string v2, "MyActivity"
开启更积极的R8优化
R8 包含一组额外的优化功能,默认情况下未启用这些功能。您可以通过在项目的 gradle.properties 文件中添加以下代码来启用这些额外的优化功能:
android.enableR8.fullMode=true
由于额外的优化功能使得 R8 的行为与 ProGuard 不同,因此它们可能要求您添加额外的 ProGuard 规则以避免运行时问题。例如,假设您的代码通过 Java Reflection API 引用一个类。默认情况下,R8 假设您打算在运行时检查和操纵该类的对象(即使您的代码实际上并不这样做),因此它会自动保留该类及其静态初始化程序。不过,在使用“完整模式”时,R8 不会做出这种假设,如果 R8 断言您的代码从不在运行时使用该类,它会从应用的最终 DEX 中移除该类。也就是说,如果要保留该类及其静态初始化程序,需要在规则文件中添加相应的保留规则。
让R8生成移除代码报告
为了防止优化后出现问题,并帮助开发者发现优化后的问,我们可以查看R8的优化报告,找到具体优化了那些东西,对于要生成报告的模块,请将-printusage
当启用R8并编译应用的时候,R8会输出指定路径的报告
##### 移除代码报告例子如下
androidx.drawerlayout.R$attr
androidx.vectordrawable.R
androidx.appcompat.app.AppCompatDelegateImpl
public void setSupportActionBar(androidx.appcompat.widget.Toolbar)
public boolean hasWindowFeature(int)
public void setHandleNativeActionModesEnabled(boolean)
android.view.ViewGroup getSubDecor()
public void setLocalNightMode(int)
final androidx.appcompat.app.AppCompatDelegateImpl$AutoNightModeManager getAutoNightModeManager()
public final androidx.appcompat.app.ActionBarDrawerToggle$Delegate getDrawerToggleDelegate()
private static final boolean DEBUG
private static final java.lang.String KEY_LOCAL_NIGHT_MODE
static final java.lang.String EXCEPTION_HANDLER_MESSAGE_SUFFIX
...
生成保留规则确定的入口点报告
如果要查看 R8 根据项目的保留规则确定的入口点的报告,请在自定义规则文件中添加 -printseeds
com.example.myapplication.MainActivity
androidx.appcompat.R$layout: int abc_action_menu_item_layout
androidx.appcompat.R$attr: int activityChooserViewStyle
androidx.appcompat.R$styleable: int MenuItem_android_id
androidx.appcompat.R$styleable: int[] CoordinatorLayout_Layout
androidx.lifecycle.FullLifecycleObserverAdapter
...
2.proguard 混淆原理
ProGuard能够对Java类中的代码进行压缩(Shrink),优化(Optimize),混淆(Obfuscate),预检(Preveirfy)。
压缩(Shrink):在压缩处理这一步中,用于检测和删除没有使用的类,字段,方法和属性。
优化(Optimize):在优化处理这一步中,对字节码进行优化,并且移除无用指令。
混淆(Obfuscate):在混淆处理这一步中,使用a,b,c等无意义的名称,对类,字段和方法进行重命名。
预检(Preveirfy):在预检这一步中,主要是在Java平台上对处理后的代码进行预检。
2.proguard-rules.pro 混淆配置
keep 选项
-keep [,modifier,...] class_specification
指定需要保留的类和类成员(作为公共类库,应该保留所有可公开访问的public方法)
-keepclassmembers [,modifier,...] class_specification
指定需要保留的类成员:变量或者方法
-keepclasseswithmembers [,modifier,...] class_specification
指定保留的类和类成员,条件是所指定的类成员都存在(既在压缩阶段没有被删除的成员,效果和keep差不多)
-keepnames class_specification
[-keep allowshrinking class_specification 的简写]
指定要保留名称的类和类成员,前提是在压缩阶段未被删除。仅用于模糊处理
-keepclassmembernames class_specification
[-keepclassmembers allowshrinking class_specification 的简写]
指定要保留名称的类成员,前提是在压缩阶段未被删除。仅用于模糊处理
-keepclasseswithmembernames class_specification
[-keepclasseswithmembers allowshrinking class_specification 的简写]
指定要保留名称的类成员,前提是在压缩阶段后所指定的类成员都存在。仅用于模糊处理
Keep选项概述对比
作用范围 | 保持所指定类、成员 | 所指定类、成员在压缩阶段没有被删除,才能被保持 |
---|---|---|
类和类成员 | -keep | -keepnames |
仅类成员 | -keepclassmembers | -keepclassmembernames |
类和类成员(前提是成员都存在) | -keepclasseswithmembers | -keepclasseswithmembernames |
Proguard通配符
通配符 | 描述 |
---|---|
匹配类中的所有字段 | |
匹配类中所有的方法 | |
匹配类中所有的构造函数 | |
* | 匹配任意长度字符,不包含包名分隔符(.) |
** | 匹配任意长度字符,包含包名分隔符(.) |
*** | 匹配任意参数类型 |
keep使用例子
# 保留所有的本地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
# 保留在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 {
*** get*();
void set*(***);
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
#这里需要注意,其实目前最新的View有第四个构造参数,所以混淆的时候一定记得加上,这样防止出现崩溃
}
# 保留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;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 对于R(资源)下的所有类及其方法,都不能被混淆
-keep class **.R$* {
*;
}
# 对于带有回调函数onXXEvent的,不能被混淆
-keepclassmembers class * {
void *(**On*Event);
}
# 保留实体类和成员不被混淆
# 对于实体,保留它们的set和get方法,对于boolean型get方法,有人喜欢命名isXXX的方式,所以不要遗漏。
#一种好的做法是把所有实体都放在一个包下进行管理,这样只写一次混淆就够了,避免以后在别的包中新增的实体而忘记保留,代码在混淆后因为找不到相应的实体类而崩溃。
-keep public class com.xxxx.entity.** {
public void set*(***);
public *** get*();
public *** is*();
}
# 保留内嵌类不被混淆
# 内部类经常会被混淆,结果在调用的时候为空就崩溃了,最好的解决方法就是把这个内部类拿出来,单独成为一个类。如果一定要内置,那么这个类就必须在混淆的时候保留,建议少用内部类
-keep class com.example.xxx.MainActivity$* { *; }
#针对WebView的处理
#如果使用的是腾讯的x5浏览器,请到腾讯x5浏览器官网获得keep的具体内容
-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.we使用annotationbViewClient {
public void *(android.webkit.webView, java.lang.String)
}
#对应的Java层的js方法也要进行keep
-keepclassmembers class com.example.xxx.JSInterface{
<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”)
在混淆的时候,要在项目中搜索一下上述方法,将相应的类或者方法的名称进行保留而不被混淆。
第三方开发库的混淆
一般情况第三方的开发库都会提供混淆的配置,如果没有,可以使用下面类似的模板
# 针对android-support-v4.jar的解决方案,可以按照此模板进行修改
#不是每个第三方SDK都需要-dontwarn 指令,这取决于混淆时第三方SDK是否出现警告,需要的时候再加上。
-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
对于自定义类库的混淆处理,比如我们引用了一个叫做AndroidLib的类库,我们需要对Lib也进行混淆,然后在主项目的混淆文件中保留AndroidLib中的类和类的成员。
Anroid 官方建议 不混淆的,如
android.app.backup.BackupAgentHelper
android.preference.Preference
com.android.vending.licensing.ILicensingService
使用annotation避免混淆
@keep
@keepPublicGetterSetters
public class DemoBean{
public boolean booleanProperty;
public int intProperty;
public String stringProperty;
}
对于Kotlin的支持
目前Progurad对kotlin的支持版本还是beta版本
#针对kotlin代码混淆的keep内容
-keep class kotlin.** { *; }
-keep class kotlin.Metadata { *; }
-dontwarn kotlin.**
#maping 枚举方法需要keep
-keepclassmembers class **$WhenMappings {
<fields>;
}
#针对metadata元数据的public方法
-keepclassmembers class kotlin.Metadata {
public <methods>;
}
#针对checkParameterIsNotNull这个方法
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
}
最后要说的一点
因为现在的R8,我找到了一些新功能,比如支持java内联替换功能,但是我测试没有成功,如果哪位有测试成功了,请与我分享一下.非常感谢.个人认为R8的内联功能还是非常实用的,能够实现很多有趣的玩法.最后感谢大家.