• 【Android性能优化】:ProGuard,混淆,R8优化


    作者:小余同学

    前言

    使用java编写的源代码编译后生成了对于的class文件,但是class文件是一个非常标准的文件,市面上很多软件都可以对class文件进行反编译,为了我们app的安全性,就需要使用到Android代码混淆这一功能。针对 Java 的混淆,ProGuard 就是常用的混淆工具,且他不仅仅是混淆工具,它同时可以对代码进行 压缩、优化 、混淆。

    下面我们来简单介绍下ProGuard工作流程

    1ProGuard 工作流程ProGuard工作过程包括四个步骤:shrink,optimize,obfuscate,preverigy。这四个步骤都是可选,但是顺序都是不变的

    • shrink:检测并删除项目中未使用到的类,字段,方法以及属性。
    • optimize:优化字节码,移除无用指令,或者进行指令优化。
    • obfuscate:代码混淆,将代码中的类,字段,方法和属性等名称使用无意义的名称进行表示,减少代码反编译后的可读性。
    • preverify:针对 Java 1.6 及以上版本进行预校验, 校验 StackMap /StackMapTable 属性.在编译时可以关闭,加快编译速度

    2 Android中的代码优化以及混淆

    Android构建中,在AGP3.4.0之前也是使用的ProGuard 进行代码优化混淆,但是在3.4.0之后,谷歌将这一工作赋予给了性能更佳的R8编译器。虽然摒弃了ProGuard,但是R8编译器还是兼容ProGuard的配置规则。使用R8编译器可以做以下优化:

    • 1.代码缩减
    • 2.资源缩减
    • 3.代码混淆
    • 4.代码优化
    1.代码缩减:

    代码缩减指的是:R8编译期智能检测代码中未使用到的类、字段、方法和属性等,并移除。

    比如你项目中依赖了很多库,但是只使用了库里面少部分代码,为了移除这部分代码,R8会根据配置文件确定应用代码的所有入口点:包括应用启动的第一个Activity或者服务等,R8会根据入口,检测应用代码,并构建出一张图表,列出应用运行过程中可能访问的方法,成员变量和类等,并对图中没有关联到的代码,视为可移除代码。

    如下图:

    图中入口位置:MainActivity,整个调用链路中,使用到了foo,bar函数以及AwesomeApi类中的faz函数,所以这部分代码会被构建到依赖图中,而OkayApi类以及其baz函数都未访问到,则这部分代码就可以被优化。使用方式:

    android {
        buildTypes {
            release {
                ...
                minifyEnabled true
            }
        }
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    minifyEnabled 设置为true,会默认启用R8代码缩减功能。代码优化需要注意的两种情况:1.反射调用的情况2.JNI调用的情况R8并未对反射以及JNI等情况进行检测,如果配置文件中未处理,则这部分代码就会被丢弃,会出现NoClassFindException的异常,如何解决呢?

    • 1.1:在配置文件中使用-keep对这部分类进行说明
    -keep public class MyClass
    
    • 1
    • 1.2:给需要保留的代码添加@keep注解

    前提条件:1.声明了使用AndroidX 2.使用AGP默认的ProGuard文件。

    R8配置文件

    R8使用ProGuard 规则文件来决定哪部分代码需要保留,配置文件来源分为下面几个:

    • 1.AndroidStudio:

    位置:/proguard-rules.pro说明:

    创建新的模块时,会在当前模块目录下创建一个默认的:proguard-rules.pro 文件

    • 2.AGP插件

    位置:由AGP在编译时生成的proguard-android-optimize.txt说明:

    Android Gradle 插件会生成 proguard-android-optimize.txt(其中包含了对大多数 Android 项目都有用的规则),并启用 @Keep* 注解。

    编译后在\build\intermediates\proguard-files\目录下会生成3个文件:

    proguard-android-optimize.txt-4.1.1:需要进行optimize代码优化的ProGuard配置文件。proguard-android.txt-4.1.1:表示不需要进行optimize代码优化的ProGuard文件。4.1.1:表示当前模块的AGP插件版本

    • 3.库依赖项

    位置:AAR 库:/proguard.txt
    JAR 库:/META-INF/proguard/说明:

    引入的aar或者jar包的库中,默认包含proguard优化规则,则在编译过程中也会被纳入R8配置项中,所以特别注意aar中引入的proguad和原项目规则冲突的情况。

    • 4.AAPT2(Android资源打包工具)

    位置:使用 minifyEnabled true 构建项目后:/build/intermediates/proguard-rules/debug/aapt_rules.txt说明:

    AAPT2 会根据对应用清单中的类、布局及其他应用资源的引用,生成保留规则。例如,AAPT2 会为您在应用清单中注册为入口点的每个 activity 添加一个保留规则。

    • 5.自定义配置文件

    位置:默认情况下,当您使用 Android Studio 创建新模块时,IDE 会创建 /proguard-rules.pro,以便您添加自己的规则。说明:

    您可以添加其他配置,R8 会在编译时应用这些配置。如果您将 minifyEnabled 属性设为 true,R8 会将来自上述所有可用来源的规则组合在一起,但是需要注意依赖库引入导致的规则冲突问题。

    如果需要输出 R8 在构建项目时应用的所有规则的完整报告:可以添加下面语句到proguard-rules.pro中。

    // You can specify any path and filename.
    -printconfiguration ~/tmp/full-r8-config.txt
    
    • 1
    • 2

    如果需要添加额外的proguad文件:可以通过将相应文件添加到模块的 build.gradle 文件的 proguardFiles 属性中:如分别给每个productflavor添加规则可以使用下面这种方式:

    android {
        ...
        buildTypes {
            release {
                minifyEnabled true
                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'
            }
        }
        flavorDimensions "version"
        productFlavors {
            flavor1 {
                ...
            }
            flavor2 {
                proguardFile 'flavor2-rules.pro'
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    2.资源缩减

    资源缩减是在****代码缩减之后进行的,只有去除了不需要的代码后, 才可以知道哪些代码里面的资源也是不被引入,可以移除的。资源缩减只要在模块gradle下面添加shrinkResources属性即可:

    android {
        ...
        buildTypes {
            release {
                shrinkResources true
                minifyEnabled true
                ...
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    注意:资源缩减需要提前开启代码缩减minifyEnabled。当然你也可以对资源文件添加白名单

    
    
    
    • 1
    • 2
    • 3
    • 4

    使用tools:keep指定需要保留的资源文件,使用tools:discard指定可以舍弃的资源文件

    3.代码混淆

    混淆指的是将类名,方法名,属性名使用无意义的字符来表示:看下图:

    如何进行混淆?

    混淆的一些基本规则:

    • 一颗星:表示保留当前包下的类名,如果有子包,子包中的类会被混淆
    -keep class cn.hadcn.test.*
    
    • 1
    • 两颗星:表示保留当前包下的类名,如果有子包,子包中的类名也会被保留。
    -keep class cn.hadcn.test.**
    
    • 1
    • 上面的方式虽然保留了类名,但是内容还是会被混淆,使用下面方式保留内容:
    -keep class cn.hadcn.test.* {*;}
    
    • 1
    • 在此基础上,我们也可以使用Java的基本规则来保护特定类不被混淆,比如我们可以用extends,implements等这些Java规则。如下例子就避免所有继承Activity的类被混淆:
    -keep public class * extends android.app.Activity
    
    • 1
    • 如果我们要保留一个类中的内部类不被混淆则需要用$符号,如下例子表示保持ScriptFragment内部类JavaScriptInterface中的所有public内容不被混淆。
    -keepclassmembers class cc.ninty.chat.ui.fragment.ScriptFragment$JavaScriptInterface {
       public *;
    }
    
    • 1
    • 2
    • 3
    • 再者,如果一个类中你不希望保持全部内容不被混淆,而只是希望保护类下的特定内容,就可以使用:
    ;     //匹配所有构造器
    ;   //匹配所有域
    ;  //匹配所有方法方法
    
    • 1
    • 2
    • 3
    • 你还可以在或前面加上privatepublicnative等来进一步指定不被混淆的内容,如
    -keep class cn.hadcn.test.One {
        public ;
    }
    
    • 1
    • 2
    • 3
    • 当然你还可以加入参数,比如以下表示用JSONObject作为入参的构造函数不会被混淆:
    -keep class cn.hadcn.test.One {
       public (org.json.JSONObject);
    }
    
    • 1
    • 2
    • 3
    • 有时候你是不是还想着:我不需要保持类名,我只需要把该类下的特定方法保持不被混淆就好,那你就不能用keep方法了,keep方法会保持类名,而需要用keepclassmembers ,如此类名就不会被保持,为了便于对这些规则进行理解,官网给出了以下表格:

    android打包混淆及语法规则详解_android

    # -keep关键字
    # keep:包留类和类中的成员,防止他们被混淆
    # keepnames:保留类和类中的成员防止被混淆,但成员如果没有被引用将被删除
    # keepclassmembers :只保留类中的成员,防止被混淆和移除。
    # keepclassmembernames:只保留类中的成员,但如果成员没有被引用将被删除。
    # keepclasseswithmembers:如果当前类中包含指定的方法,则保留类和类成员,否则将被混淆。
    # keepclasseswithmembernames:如果当前类中包含指定的方法,则保留类和类成员,如果类成员没有被引用,则会被移除。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    注意事项
    • 1.使用AS4.1.1版本进行混淆编译后,在 /build/outputs/mapping/release/下面会生成以下四个文件:

    • usage:未使用的文件,也就是移除后的文件。

    • seeds:未进行混淆的类和成员。

    • mapping:混淆前后的映射文件,这个文件在使用反混淆的时候有用。

    • configuration:所有ProGuard文件整合后的规则文件:

    • 2.有使用jni的情况下,jni方法不可混淆

    -keepclasseswithmembernames class * {    
        native ;
    }
    
    • 1
    • 2
    • 3
    • 3.反射用到的类不混淆。

    • 4.AndroidMainfest中的类不混淆,所有四大组件和Application的子类和Framework层下所有的类默认不会进行混淆,自定义的View默认也不会被混淆。所以像网上贴的很多排除自定义View,或四大组件被混淆的规则在Android Studio中是无需加入的。

    • 5.与服务端交互时,使用GSON、fastjson等框架解析服务端数据时,所写的JSON对象类不混淆,否则无法将JSON解析成对应的对象;

    • 6.使用第三方开源库或者引用其他第三方的SDK包时,如果有特别要求,也需要在混淆文件中加入对应的混淆规则;

    • 7.有用到WebView的JS调用也需要保证写的接口方法不混淆,原因和第一条一样;

    • 8.Parcelable的子类和Creator静态成员变量不混淆,否则会产生Android.os.BadParcelableException异常;

    # 保持Parcelable不被混淆    
    -keep class * implements Android.os.Parcelable {         
        public static final Android.os.Parcelable$Creator *;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 9.使用enum类型时需要注意避免以下两个方法混淆,因为enum类的特殊性,以下两个方法会被反射调用,见第二条规则。
    -keepclassmembers enum * {  
        public static **[] values();  
        public static ** valueOf(java.lang.String);  
    }
    
    • 1
    • 2
    • 3
    • 4
    混淆模板

    下面是一个混淆模板:可根据自身需要进行添加和删除

    #--------------------------1.实体类---------------------------------
    # 如果使用了Gson之类的工具要使被它解析的JavaBean类即实体类不被混淆。(这里填写自己项目中存放bean对象的具体路径)
    -keep class com.php.soldout.bean.**{*;}
    
    #--------------------------2.第三方包-------------------------------
    
    #Gson
    -keepattributes Signature
    -keepattributes *Annotation*
    -keep class sun.misc.Unsafe { *; }
    -keep class com.google.gson.stream.** { *; }
    -keep class com.google.gson.examples.android.model.** { *; }
    -keep class com.google.gson.* { *;}
    -dontwarn com.google.gson.**
    
    #butterknife
    -keep class butterknife.** { *; }
    -dontwarn butterknife.internal.**
    -keep class **$$ViewBinder { *; }
    
    #-------------------------3.与js互相调用的类------------------------
    
    
    #-------------------------4.反射相关的类和方法----------------------
    
    
    #-------------------------5.基本不用动区域--------------------------
    #指定代码的压缩级别
    -optimizationpasses 5
    
    #包明不混合大小写
    -dontusemixedcaseclassnames
    
    #不去忽略非公共的库类
    -dontskipnonpubliclibraryclasses
    -dontskipnonpubliclibraryclassmembers
    
    #混淆时是否记录日志
    -verbose
    
    #优化  不优化输入的类文件
    -dontoptimize
    
    #预校验
    -dontpreverify
    
    # 保留sdk系统自带的一些内容 【例如:-keepattributes *Annotation* 会保留Activity的被@override注释的onCreate、onDestroy方法等】
    -keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod
    
    # 记录生成的日志数据,gradle build时在本项根目录输出
    # apk 包内所有 class 的内部结构
    -dump proguard/class_files.txt
    # 未混淆的类和成员
    -printseeds proguard/seeds.txt
    # 列出从 apk 中删除的代码
    -printusage proguard/unused.txt
    # 混淆前后的映射
    -printmapping proguard/mapping.txt
    
    
    # 避免混淆泛型
    -keepattributes Signature
    # 抛出异常时保留代码行号,保持源文件以及行号
    -keepattributes SourceFile,LineNumberTable
    
    #-----------------------------6.默认保留区-----------------------
    # 保持 native 方法不被混淆
    -keepclasseswithmembernames class * {
        native ;
    }
    
    -keepclassmembers public class * extends android.view.View {
     public (android.content.Context);
     public (android.content.Context, android.util.AttributeSet);
     public (android.content.Context, android.util.AttributeSet, int);
     public void set*(***);
    }
    
    #保持 Serializable 不被混淆
    -keepclassmembers class * implements java.io.Serializable {
        static final long serialVersionUID;
        private static final java.io.ObjectStreamField[] serialPersistentFields;
        !static !transient ;
        !private ;
        !private ;
        private void writeObject(java.io.ObjectOutputStream);
        private void readObject(java.io.ObjectInputStream);
        java.lang.Object writeReplace();
        java.lang.Object readResolve();
    }
    
    # 保持自定义控件类不被混淆
    -keepclasseswithmembers class * {
        public (android.content.Context,android.util.AttributeSet);
    }
    # 保持自定义控件类不被混淆
    -keepclasseswithmembers class * {
        public (android.content.Context,android.util.AttributeSet,int);
    }
    # 保持自定义控件类不被混淆
    -keepclassmembers class * extends android.app.Activity {
        public void *(android.view.View);
    }
    
    # 保持枚举 enum 类不被混淆
    -keepclassmembers enum * {
        public static **[] values();
        public static ** valueOf(java.lang.String);
    }
    
    # 保持 Parcelable 不被混淆
    -keep class * implements android.os.Parcelable {
      public static final android.os.Parcelable$Creator *;
    }
    
    # 不混淆R文件中的所有静态字段,我们都知道R文件是通过字段来记录每个资源的id的,字段名要是被混淆了,id也就找不着了。
    -keepclassmembers class **.R$* {
        public static ;
    }
    
    #如果引用了v4或者v7包
    -dontwarn android.support.**
    
    # 保持哪些类不被混淆
    -keep public class * extends android.app.Appliction
    -keep public class * extends android.app.Activity
    -keep public class * extends android.app.Fragment
    -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.preference.Preference
    
    -keep class com.zhy.http.okhttp.**{*;}
    -keep class com.wiwide.util.** {*;}
    
    # ============忽略警告,否则打包可能会不成功=============
    -ignorewarnings
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    4.代码优化

    为了进一步缩减应用,R8 会在更深的层次上检查代码,以移除更多不使用的代码,或者在可能的情况下重写代码,以使其更简洁。下面是此类优化的几个示例:

    • 如果您的代码从未采用过给定 if/else 语句的 else {} 分支,R8 可能会移除 else {} 分支的代码。
    • 如果您的代码只在一个位置调用某个方法,R8 可能会移除该方法并将其内嵌在这一个调用点。
    • 如果 R8 确定某个类只有一个唯一子类且该类本身未实例化(例如,一个仅由一个具体实现类使用的抽象基类),它就可以将这两个类组合在一起并从应用中移除一个类。

    更多优化点可以查看:Jake Wharton 写的关于 R8 优化的博文。

    3总结本篇

    文章主要讲解了关于R8编译器在整个编译过程中对apk代码以及资源的一些优化操作,主要集中在代码缩减,资源缩减,代码混淆,代码优化这几部分,其中对代码混淆做了一个比较全面的分析。

    为了帮助到大家更好的全面清晰的掌握好性能优化,准备了相关的核心笔记(还含底层逻辑):https://qr18.cn/FVlo89

    性能优化核心笔记:https://qr18.cn/FVlo89

    启动优化

    内存优化

    UI优化

    网络优化

    Bitmap优化与图片压缩优化https://qr18.cn/FVlo89

    多线程并发优化与数据传输效率优化

    体积包优化

    《Android 性能监控框架》:https://qr18.cn/FVlo89

    《Android Framework学习手册》:https://qr18.cn/AQpN4J

    1. 开机Init 进程
    2. 开机启动 Zygote 进程
    3. 开机启动 SystemServer 进程
    4. Binder 驱动
    5. AMS 的启动过程
    6. PMS 的启动过程
    7. Launcher 的启动过程
    8. Android 四大组件
    9. Android 系统服务 - Input 事件的分发过程
    10. Android 底层渲染 - 屏幕刷新机制源码分析
    11. Android 源码分析实战

  • 相关阅读:
    C语言程序设计算法题 -- lab03(G-K)
    山西电力市场日前价格预测【2023-11-19】
    使用 ggseg 对大脑分区统计结果进行可视化,如对皮层厚度差异T值进行可视化
    数仓4.0(可视化报表)
    Go语言基础学习教程(一)导学部分
    VR全景看房:超越传统的看房方式
    如何对用OpenCV开发的API进行测试 (Google Test 版本)
    第九章:用Python处理省份城市编码数据
    第三章 多维随机变量及其分布
    C语言学习示例代码汇总
  • 原文地址:https://blog.csdn.net/weixin_61845324/article/details/133274686