Android 代码优化与混淆

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 /usage.txt 添加到自定义规则文件内。

当启用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 /seeds.txt。当启用 R8 并编译应用时,R8 会输出一个包含您指定的路径和文件名的报告。保留的入口点的报告与以下输出类似:

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的内联功能还是非常实用的,能够实现很多有趣的玩法.最后感谢大家.


文章作者: 孙老师
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 孙老师 !
 上一篇
DEX文件结构与解析 DEX文件结构与解析
DEX文件结构与解析Dex是Dalvik虚拟机的执行文件,对于每一个开发者来说,他的结构对于开发这至关重要,也是我们优化的一个方面,虽然很多工作是Android Stuido和其对应的工具进行的,但是我们需要知道他的基础结构和工作原理.由于
2019-12-28 孙老师
下一篇 
如何调试已经加壳的APK文件 如何调试已经加壳的APK文件
今天有个任务,让我去调试一个APK,需要查看它的性能参数,内存/CPU等的占用,第一时间拿出工具准备反编译,但是发现此包使用了360加固,不确定是否有签名校验,如果有,我就没办法修改AndroidManifest.xml文件来打开调试了.
2019-12-11 孙老师
  目录