补齐Android技能树 - 从害怕到玩转Android代码混淆

504aa322082c9f96bf88910d96388e63.jpeg

/   今日科技快讯   /

近日,华为常务董事、ICT基础设施业务管理委员会主任汪涛在华为全联接大会上提出了“5.5G”时代是迈向智能世界必由之路的观点,发布了发布《迈向智能世界》系列白皮书,并倡议产业采取8方面举措,围绕智能世界需求共同努力进一步定义和完善5.5G时代产业愿景和标准,加速5.5G时代到来。

/   作者简介   /

本篇文章来自coder_pig的投稿,文章主要分享了如何玩转Android代码混淆,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

coder_pig的博客地址:

https://juejin.cn/post/6966526844552085512

/   引言   /

Android代码混淆,老生常谈了,不过大部分Android仔对它的认识可能还处于这样的阶段(比如:写这篇文章前的我):

1、日常开发Debug包时不用混淆,正式发布Release包前开启代码混淆;

2、混淆好处① → 类、方法、变量名变成短且无意义的名字,提高反编译后代码的阅读成本;

3、混淆好处② → 删除无用的类、方法与属性,缩减了APK包的大小;

4、混淆好处③ → 对字节码进行优化,移除无用指令,应用运行更快;

5、怎么混淆 → 主项目的 build.gradle 设置 minifyEnabled true,proguard-rules.pro 加入混淆规则;

6、混淆规则哪里来 → 网上搜索通用混淆模板复制粘贴,项目依赖到的第三方库官方文档复制粘贴;

大都止步于此,好一点的还知道下ProGuard听过R8,了解混淆配置语法,会自定义混淆规则。

会上面这些,日常开发已经很够用了,但是现在IT行业这么“卷”,面试时,面试官问下:

混淆具体做了啥?有看过混淆源码吗?说下底层原理...

也说得过去吧(手动狗头保命~)

539bc097a80f76cdf285ccd8612c3c6f.png

所以本节稍微深入点探索下Android中的代码混淆~

da492c5f2f35feb88758589cb0a0697a.png

/   日常使用   /

Tips:照惯例,写下简单例子演示日常使用,走走过场,只对混淆原理感兴趣的可以跳过这Part~

混淆前后的APK对比

新建项目,引下Kotlin相关依赖,协程等 (app层级的build.gradle):

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.0'
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7'
}

新建MainActivity.kt,请求URL,加载内容:

class MainActivity : AppCompatActivity(), CoroutineScope by MainScope()  {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        et_url.setText("https://www.baidu.com")
        bt_load.setOnClickListener {
            launch {
                tv_content.text = "开始加载请求..."
                tv_content.text = "加载完毕,网页内容如下:\n\n\n ${loadUrl(et_url.text.toString())}"
            }
        }
    }

    private suspend fun loadUrl(url: String) = withContext(Dispatchers.IO) {
        var content = ""
        (URL(url).openConnection() as HttpURLConnection).apply {
            requestMethod = "GET"
            content = dealResponse(inputStream)
            disconnect()
        }
        return@withContext content
    }

    private fun dealResponse(inputStream: InputStream): String {
        val reader = BufferedReader(InputStreamReader(inputStream))
        return StringBuffer().apply {
            var str = reader.readLine()
            while (null != str) {
                append(str)
                str = reader.readLine()
            }
        }.toString()
    }

    override fun onDestroy() {
        super.onDestroy()
        cancel()
    }
}

运行下康康:

b7426cccf72d4d1be5e8328ec46e5860.png

app层级的build.gradle加下release的签名和编译配置:

signingConfigs {
    release {
        storeFile file('test.jks')
        storePassword '123456'
        keyAlias 'test'
        keyPassword '123456'
    }
}

buildTypes {
    release {
        // 启用代码压缩、优化及混淆
        minifyEnabled true
        // 启用资源压缩,需配合 minifyEnabled=true 使用
        shrinkResources true
        // 指定混淆保留规则
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        // 包签名
        signingConfig signingConfigs.release
    }
}

执行 gradle assemble 打下包,静待打包完毕,先康康Debug包:

f451e076fa1e8081a8441ef2d1b0e4a2.png

23333,裸奔,把APK直接拖到反编译工具 jadx-gui 里,看代码无压力:

f5dbcbf996c5e6e0e04002c934f0e97d.png

再康康Release包:

0e92f46b17366f02d957853e2792813a.png

体积着实少了一些,而且变量名都变成了abcd,顺带提下这个Load Proguard mappings,点击加载混淆文件(mapping.txt)后可以去掉代码混淆:

e64ea5bdc477cd208fc13d06913cf6aa.png

同样拖到jadx-gui里康康:

692d76bdeea1da01ce30dfa43c9ecaef.png

可读性明显降低~

混淆后App Crash日志定位问题

不知道你有没有想过:混淆后的APK如果报错,日志信息会是怎样的呢?改下代码验证下,直接在点击处抛出一个空指针异常试试康:

79439250fb1c8710d3ccfcd71e0c19c9.png

单凭这里的b.b.a.a.onClick(Unknow Srouce:2),似乎很难直接定位到错误代码的真实位置。

一种低效的解决方法:自行对照混淆后生成的 mapping.txt 文件,比如直接搜上面的b.b.a.a:

a99a048a8931c04a77cfd4ed93afa618.png

顺着往下看不难发现问题所在,但日常开发不建议用此法,这里好找只是因为示例代码简单,推荐另一种方法:

去混淆,如果你的应用有发布到Google Play的话,可以照着官方文档走:

47afc5e7b8e70cfda6a859b5cdf8115d.png

没有上传到Google Play也没关系,直接用 android-sdk/tools/proguard/bin/proguardgui.bat:

0898278568f98338f5e3ee44ee45a5ca.png

ProGuard和R8的混淆都可以去,老版本的脚本可能不支持R8,更新下sdk即可,不想更新的也可以直接用我提供的脚本包去R8混淆,下载地址可以去原文获取,使用流程如下图所示:

31a74c7171f803f3a203cafc8670f6fc.png

去混淆前后对比:

662f5f6c361c531cbba1729de3136c26.png

可以的,日常使用就讲到这里,接着过一下混淆的详细规则。

/   混淆规则详解   /

Tips:没必要死记,点赞mark下,要用的时候再回来查就好,遗漏的欢迎在评论区提出~

混淆设置参数

-optimizationpasses 5                       # 代码混淆的压缩比例,值介于0-7,默认5
-verbose                                    # 混淆时记录日志
-dontoptimize                               # 不优化输入的类文件
-dontshrink                                 # 关闭压缩
-dontpreverify                              # 关闭预校验(作用于Java平台,Android不需要,去掉可加快混淆)
-dontoptimize                               # 关闭代码优化
-dontobfuscate                              # 关闭混淆
-ignorewarnings                             # 忽略警告
-dontwarn com.squareup.okhttp.**            # 指定类不输出警告信息
-dontusemixedcaseclassnames                 # 混淆后类型都为小写
-dontskipnonpubliclibraryclasses            # 不跳过非公共的库的类
-printmapping mapping.txt                   # 生成原类名与混淆后类名的映射文件mapping.txt
-useuniqueclassmembernames                  # 把混淆类中的方法名也混淆
-allowaccessmodification                    # 优化时允许访问并修改有修饰符的类及类的成员
-renamesourcefileattribute SourceFile       # 将源码中有意义的类名转换成SourceFile,用于混淆具体崩溃代码
-keepattributes SourceFile,LineNumberTable  # 保留行号
-keepattributes *Annotation*,InnerClasses,Signature,EnclosingMethod # 避免混淆注解、内部类、泛型、匿名类
-optimizations !code/simplification/cast,!field/ ,!class/merging/   # 指定混淆时采用的算法

保持不被混淆的设置

语法组成:

[保持命令] [类] {
    [成员] 
}

保持命令:

-keep                           # 防止类和类成员被移除或被混淆;
-keepnames                      # 防止类和类成员被混淆;
-keepclassmembers                # 防止类成员被移除或被混淆;
-keepclassmembernames           # 防止类成员被混淆;
-keepclasseswithmembers         # 防止拥有该成员的类和类成员被移除或被混淆;
-keepclasseswithmembernames     # 防止拥有该成员的类和类成员被混淆;

类:

  • 具体的类

  • 访问修饰符 → public、private、protected

  • 通配符(*) → 匹配任意长度字符,但不包含包名分隔符(.)

  • 通配符(**) → 匹配任意长度字符,且包含包名分隔符(.)

  • extends → 匹配实现了某个父类的子类

  • implements → 匹配实现了某接口的类

  • $ → 内部类

成员:

  • 匹配所有构造器 → <init>

  • 匹配所有域 → <field>

  • 匹配所有方法 → <methods>

  • 访问修饰符 → public、private、protected

  • 除了 * 和 ** 通配符外,还支持 *** 通配符,匹配任意参数类型

  • ... → 匹配任意长度的任意类型参数,如void test(...)可以匹配不同参数个数的test方法 

常用自定义混淆规则范例:

# 不混淆某个类的类名,及类中的内容
-keep class cn.coderpig.myapp.example.Test { *; }

# 不混淆指定包名下的类名,不包括子包下的类名
-keep class cn.coderpig.myapp*

# 不混淆指定包名下的类名,及类里的内容
-keep class cn.coderpig.myapp* {*;}

# 不混淆指定包名下的类名,包括子包下的类名
-keep class cn.coderpig.myapp**

# 不混淆某个类的子类
-keep public class * extends cn.coderpig.myapp.base.BaseFragment

# 不混淆实现了某个接口的类
-keep class * implements cn.coderpig.myapp.dao.DaoImp

# 不混淆类名中包含了"entity"的类,及类中内容
-keep class **.*entity*.** {*;}

# 不混淆内部类中的所有public内容
-keep class cn.coderpig.myapp.widget.CustomView$OnClickInterface {
    public *;
}

# 不混淆指定类的所有方法
-keep cn.coderpig.myapp.example.Test {
    public <methods>;
}

# 不混淆指定类的所有字段
-keep cn.coderpig.myapp.example.Test {
    public <fields>;
}

# 不混淆指定类的所有构造方法
-keep cn.coderpig.myapp.example.Test {
    public <init>;
}

# 不混淆指定参数作为形参的方法
-keep cn.coderpig.myapp.example.Test {
    public <methods>(java.lang.String);
}

# 不混淆类的特定方法
-keep cn.coderpig.myapp.example.Test {
    public test(java.lang.String);
}

# 不混淆native方法
-keepclasseswithmembernames class * {
    native <methods>;
}

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

#不混淆资源类
-keepclassmembers class **.R$* {
    public static <fields>;
}

# 不混淆自定义控件
-keep public class * entends android.view.View {
    *** get*();
    void set*(***);
    public <init>;
}

# 不混淆实现了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();
}

# 不混淆实现了parcelable接口的类成员
-keep class * implements android.os.Parcelable {
    public static final android.os.Parcelable$Creator *;
}

# 注意事项:
# 
# ① jni方法不可混淆,方法名需与native方法保持一致;
# ② 反射用到的类不混淆,否则反射可能出问题;
# ③ 四大组件、Application子类、Framework层下的类、自定义的View默认不会被混淆,无需另外配置;
# ④ WebView的JS调用接口方法不可混淆;
# ⑤ 注解相关的类不混淆;
# ⑥ GSON、Fastjson等解析的Bean数据类不可混淆;
# ⑦ 枚举enum类中的values和valuesof这两个方法不可混淆(反射调用);
# ⑧ 继承Parceable和Serializable等可序列化的类不可混淆;
# ⑨ 第三方库或SDK,请参考第三方提供的混淆规则,没提供的话,建议第三方包全部不混淆;

混淆规则的叠加

不知道你有没有想过,上面日常使用的创建的代码示例,proguard-rules.pro没有配置混淆规则,却混淆了?

其实是因为混淆规则是叠加的,而混淆规则的来源不止主模块里的proguard-rules.pro,还有这些:

① <module-dir>/proguard-rules.pro

不止主模块有proguard-rules.pro,子模块也可以有,因为规则是叠加的,故某个模块的配置都可能影响其它模块。

② proguard-android-optimize.txt

AGP编译时生成,其中包含了对大多数Android项目都有用的规则,并且启用 @Keep* 注解。

AGP提供的规则文件还有proguard-defaults.txt或proguard-android.txt,可通过 getDefaultProguardFile 进行设置,不过建议还是使用这个文件(多了些优化配置)。

③ <module-dir>/build/intermediates/proguard-rules/debug/aapt_rules.txt

自动生成,AAPT2会根据对应用清单中的类、布局及其他应用资源的引用,生成保留规则,如不混淆每个Activity。

④ AAR库 → <library-dir>/proguard.txt

⑤ Jar库 → <library-dir>/META-INF/proguard/

如果想查看所有规则叠加后的混淆规则,可在主目录的 proguard-rules.pro 添加下述配置: 

# 输出所有规则叠加后的混淆规则
-printconfiguration ./build/outputs/mapping/full-config.txt

资源压缩

资源压缩其实分为两步,资源合并与资源移除,前者无论是否配置 shrinkResources true,AGP构建APK时都会执行,当存在两个或更多名称相同的资源才会进行资源合并,AGP会从重复项中选择优先级更高的文件,并只将此资源传递给AAPT2,以供在APK中分发。

级联优先顺序:依赖项 → 主资源 → 渠道 → 构建类型

比如:重复资源存在于主资源及渠道中,Gradle会选择渠道中的资源;但如果重复资源在同一层次出现,如src/main/res/和src/main/res2中有重复资源,Gradle就会报资源合并错误。

对应打包Task中的 processDebugResources,将aapt编译后的flat产物和合并后的清单文件进行链接处理生成 ._ap 文件(包含资源文件、清单文件、资源关系映射表文件resources.arsc)及 R.java 文件(保存了资源类型、资源名称及地址的映射关系)。

说完资源合并,接着说下资源移除,开启资源压缩后,所有未被使用的资源默认会被移除,如果你想定义那些资源需要保留,可以在 res/raw/ 路径下创建一个xml文件,如 keep.xml,配置示例如下(此文件不会打包到APK中,支持通配符*,此类文件可有多份):

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    <!-- 定义哪些资源要被保留 -->
    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
    <!-- 定义哪些资源需要被移除 -->
    tools:discard="@layout/unused2"
    <!-- 开启严苟模式,可选值strict,safe,前者严格按照keep和discard指定的资源保留 -->
    <!-- 后者保守删除未引用资源,如代码中使用Resources.getIdentifier()引用的资源会保留 -->
    tools:shrinkMode="strict"/>

另外,还可以在build.gradle中添加 resConfigs 来移除不需要的备用资源文件,如只保留中文:

android {
    ...
    defaultConfig {
        resConfigs "zh-rCN" // 不用支持国际化只需打包中文资源
    }
}

/   从脱糖引出ProGuard、DX、D8、R8   /

Tips:此结点内容部分抽取自 《Android CPU, Compilers, D8 & R8》,阅读原文可能更加容易理解~

我们都知道Java是跨平台的,一次编译,到处运行,同一套Java代码可以在Windows、Linux、Mac上运行,背后依赖于不同平台/版本的 JVM(Java虚拟机),Java代码编译后生成 .class 字节码文件,再由JVM翻译成特定平台的 机器码,然后运行。

JVM的内部组成图如下:

8b38cf49335fa753ef6c752dfa5b35de.png

  • 类加载器:加载编译后的.class,链接、检测损坏的字节码,定位并初始化静态变量及静态代码; 

  • 运行时数据:栈、堆、方法变量等; 

  • 执行引擎:执行已经加载的代码、清理生成的所有垃圾(gc); 

运行程序时,Interpreter(解释器) 会将字节码解释为机器码然后运行,当发现有重复执行的代码时,会切换为 JIT编译器。JIT编译器会将重复的代码编译为本地机器码,当同样的方法被调用时,直接运行本地机器码,从而提高系统性能。

48a0f8710326fca15a7ad9b8fe3c0ba5.png

JVM的设计是面向无限电量/存储的设备,Android设备与之相比,太弱鸡了(电量、内存大小、存储等小的可怜)。

不能直接使用,于是Google自己设计了一套用于Android平台的Java虚拟机——Dalvik,支持已转换为 .dex (Dalvik Executable) 压缩格式的Java应用程序的运行。

与JVM字节码基于栈不同,Dalvik基于寄存器(变量都存储在寄存器中),后者更加高效且需要更少的空间。

.java和.kt代码文件被Java、Kotlin编译器协作编译为.class,而后编译为.dex文件,最后打包到.apk文件中。

把APK安装到设备上,当点击应用图标时,系统会启动一个新的Dalvik进程,并将应用包含的dex代码加载进来,在运行时交由Interpreter或JIT编译,然后就可以看到应用的界面了: 

53c4b0bd22b84e0f43c53d3e3aecabbd.png

在Dalvik中,应用的每次运行都需要执行编译操作,而这段时间是计入程序的执行时间,所以程序的启动速度会有点慢,当然也有好处, 应用安装速度快。

在Android 4.4.4后,Google开始引入 Dalvik 的替代品——ART,从JIT(Just In Time,即时编译) 到 AOT (Ahead-Of-Time,预编译),应用在首次安装时用dex2oat将 dex 编译为 .oat 二进制文件。

897f0ceb793a1d6d61223774870d1f09.png

点击应用图标启动时,ART直接加载.oat文件并运行,启动速度明显提升,避免了重复编译,减少了CPU的使用频率,也降低了功耗,当然缺点也是有的:更长的应用安装时间 和 更大的存储空间占用。

除了应用安装会触发dex2oat编译外,OTA升级、系统启动(首次/非首次)、系统空闲时也可能会触发,具体要看对应系统的配置。

上面说过Android虚拟机采用 基于寄存器的指令集(opcodes),这样会存在一个问题,更高版本Java新引入的语法特性不能在上面直接使用。

为了让我们能使用上Java 8的特性,Google使用 Transformation 来增加了一步编译过程 → 脱糖(desugaring)。

当使用当前Android版本不支持的高版本jdk语法时,在编译期转换为其支持的低版本jdk语法。

脱糖实现的大概发展历程如下:

c49a260acde9d36390c440bf8280ca0b.png

至此,相信你对ProGuard、DX、D8和R8在混淆过程中起的作用有了一个基础的认知~ 

用 ProGuard 还是 R8?

答:如果没有历史包袱,直接R8,毕竟兼容绝大部分的ProGuard规则,更快的编译速度,对Kotlin更友好。

还是简单描述下两者吧:

  • ProGuard → 压缩、优化和混淆Java字节码文件的免费工具,proguard

  • R8 → ProGuard的替代工具,支持现有ProGuard规则,更快更强,AGP 3.4.0或更高版本,默认使用R8混淆编译器。

如果不想用R8,想用回ProGuard的话(可以但没必要),可以在 gradle.properties 文件中添加下述配置禁用R8:

android.enableR8=false
android.enableR8.libraries=false

编译APK时可能会报错:

ced5298251d2d3df17f038f2729429be.png

在 proguard-rules.pro 文件中加上 -ignorewarnings 即可解决。

另外,使用ProGuard或R8构建项目会在 build\outputs\mapping\release 输出下述文件:

  • mapping.txt → 原始与混淆过的类、方法、字段名称间的转换;

  • seeds.txt → 未进行混淆的类与成员;

  • usage.txt → APK中移除的代码;

  • resources.txt → 资源优化记录文件,哪些资源引用了其他资源,哪些资源在使用,哪些资源被移除;

Tips:上述文件不一定都有,R8可以在 proguard-rules.pro 文件添加下述配置输出对应文件:

# 输出mapping.txt文件
-printmapping ./build/outputs/mapping/mapping.txt

# 输出seeds.txt文件
-printseeds ./build/outputs/mapping/seeds.txt

# 输出usage.txt文件
-printusage ./build/outputs/mapping/usage.txt

D8干了啥?

接着跟下源码,康康D8具体都做了啥,在上一节《补齐Android技能树——从AGP构建过程到APK打包过程》 中我们摸清了APK的打包Task链,打包成Dex经历的Task有三个:

  • transformClassesWithDexBuilderForDebug → 将class打包成dex

  • transformDexArchiveWithExternalLibsDexMergerForDebug → 打包第三库的dex 

  • transformDexArchiveWithDexMergerForDebug → 打包最终的dex

三个Task最终都是通过DX或D8来打dex,跟下第一个Task:DexArchiveBuilderTransform.transform()

e289afe26046a4bc4005130d4c115cd3.png

这里拿到需要 待脱糖的文件列表,接着往下就来到熟悉的:处理目录下的class 和 .jar里的class 了。

1530f238fdc8d6f5c9d9adb1e5c4e285.png

这里把待脱糖的文件列表传到 convertToDexArchive() 里了,跟下:

50d4dacb6074600957b3537915311aa1.png

跟 launchProcessing() → dexArchiveBuilder.convert()

d2fa1c7e5606a1c44bc022c384e00582.png

抽象类,跟下具体实现类 D8DexArchiveBuilder.convert(),

13950b361dac933974527820c0421511.png

D8Command类是D8命令行配置类,就是将上面进行的这些配置转换成d8打包命令而已:

dbc3c5be1cf1f434e5efc8248b202d5a.png

命令行配置参数详解可以参见官方文档:d8,这里就不去刨d8的源码了,知道D8起做的作用是:脱糖 + 将.class字节码转换成dex 就好。

R8又干了啥?

不知道,细心你的有没有发现,D8竟在r8的包里:

8dd5dd5d0ea3326b26c8979042f091eb.png

接着在TaskManager.java搜下D8,经过各种跳转,来到源头:createPostCompilationTasks(),可以看到在创建D8相关的Transform前还做了一些其他的操作~

42d6770ecfca9f5888d96a96fed08e70.png

R8 - 执行前

从注释不难看出,这些Tasks就是用来把.class转dex文件的,还加了一些可选步骤如混淆、jacoco(代码覆盖率工具),还创建了一个 TransformManager 实例,用来管理各种TransformManager。继续:

430e7bdf9ea56b2230522a6be7625211.png

这里引起了我的好奇,可能创建脱糖Task?脱糖不是在D8里进行的吗?跟一下代码:

5b8f1dba641b531665a026639d5861a9.png

8af64e1d306152dcf2e8e1f6f1acc63d.png

这里的DESUGAR,猜测是旧版本的D8兼容,AS 3.0引入的,而现在默认是D8,所以这里其实不会创建脱糖Task。继续:

ffd7203440a0c8dc5c34c692f963ffac.png

获取外部扩展,合并Java资源,对合并算法感兴趣的可以点进去MergeJavaResourcesTransform.transform()看下,这里不展开讲~

eca2c20912850fb4be9c781056ea1072.png

继续往下走:

edae0e0380afced76423b3f158a73677.png

R8 - Java代码压缩

再往下走,就碰到关键词R8了:

06a18dc447cd5857cb316063aba113a3.png

这里有下述三个maybe:

  • maybeCreateJavaCodeShrinkerTransform → Java代码压缩

  • maybeCreateResourcesShrinkerTransform → 资源压缩

  • maybeCreateDexSplitterTransform → dex分割

先看第一个:

c5ebbb3ba26467c38300981c0d78ce25.png

3be45be95b82e36908c265681de05470.png

这里区分PROGUARD和R8,创建不同的混淆TransformTask,关注createR8Transform()核心代码如下:

// 前面获取dex的文件列表、混淆列表等,初始化R8Transform实例时传入
R8Transform transform =
        new R8Transform(
                variantScope,
                userMainDexListFiles,
                userMainDexListProguardRules,
                inputProguardMapping,
                variantScope.getOutputProguardMappingFile());

// 处理混淆规则,callback用于在混淆后执行后续操作
return applyProguardRules(
        variantScope,
        inputProguardMapping,
        variantScope.getOutputProguardMappingFile(),
        testedVariantData,
        transform,
        callback);

跟下 applyProguardRules(),关注下述核心代码(前面的是和测试相关的):

// This is a "normal" variant in an app/library.
applyProguardConfigForNonTest(transform, variantScope);

跟下此方法:

ae589f0d3869fff1c34c81abe225f192.png

补充其他混淆规则的来源,如AAPT生成的混淆文件,判断如果是AAR的话,直接keep。

04560ed6b0b96318a58f2cb68d5ea9fa.png

跟下R8Transform.transform(),又是参数,最后调用下述方法:

01e87e10e3d17665ca26305c35805c90.png

最后在r8Tool.kt中定位到了此方法,核心代码如下:

// 初始化了一个r8CommandBuilder实例
val r8CommandBuilder = CompatProguardCommandBuilder(!useFullR8, D8DiagnosticsHandler(messageReceiver))

// 然后调用一系列方法,如混淆相关
addMainDexRules()
setMainDexListConsumer()
addProguardConfigurationFiles()
addProguardConfiguration()
setProguardMapOutputPath()

// 配置相关:是否禁用缩小、摇树、脱糖、编译模式
setDisableMinification(toolConfig.disableMinification)
setDisableTreeShaking(toolConfig.disableTreeShaking)
setDisableDesugaring(toolConfig.disableDesugaring)
setMode(compilationMode)
setProgramConsumer(programConsumer)
...

// 初始化r8ProgramResourceProvider实例,用来给R8提供所有资源
val r8ProgramResourceProvider = R8ProgramResourceProvider()

// 各种传参设置

// 最后调用R8.run()
ClassFileProviderFactory(libraries).use { libClasspath ->
    r8CommandBuilder.addLibraryResourceProvider(libClasspath.orderedProvider)
    R8.run(r8CommandBuilder.build())
}

具体的逻辑,可以追溯到 R8.class → run(),做了这些事:

  • 代码删除:通过语法树静态分析技术、发现并删除未使用的代码,如未实例化的Class等;

  • 代码优化:对运行时代码进行优化,删除死代码、未使用的参数,选择性内联、类合并等;

  • 代码混淆:优化标识符名字,减少代码量,会判断混淆规则中是否允许修改标识符名字;

  • 行号重新映射等。

短短333行的处理代码,逻辑复杂,恐怖如斯,感兴趣的自己看吧,我是啃不动了...

R8 - 资源压缩

接着跟下第二个 maybeCreateResourcesShrinkerTransform(): 

907d6a0604fcd0b4ccc39de25b1ff628.png

跟下ShrinkResourcesTransform.transform(),

de290d2824ff94a0e82fe137bb2ffe34.png

跟下SplitterRunnable.run(),看下具体是怎么压缩资源的,核心代码如下:

// ① 创建资源优化记录文件
File reportFile = null;
if (params.mappingFile != null) {
    File logDir = params.mappingFile.getParentFile();
    if (logDir != null) {
        reportFile = new File(logDir, "resources.txt");
    }
}

// ② 分析资源及使用情况
analyzer.analyze();

// ③ 重写.ap_文件(上面AAPT2生成的),去掉没有用到的资源,实际上是没有删除本地资源的!
analyzer.rewriteResourceZip(params.uncompressedResourceFile, params.compressedResourceFile);

// ③ 导出统计数据

解压AAPT生成的.ap_文件,然后判断是是否为未使用资源,是的话移除,感兴趣可以跟下 rewriteResourceZip()。

R8 - 调D8拆Dex

只有使用了 Multidex 才会走这里,跟下代码:

22366c3e4d8005067434365cfcbd24b8.png

跟下DexSplitterTransform.transform()。

a742fc3b3fb9d60854931e3b7fe73f7f.png

跟下DexSplitterTool.Builder。

1f4f55500e6e8844d43dfc930b61fde7.png

跟下DexSplitter.run()。

269b8156ae3562bebd9ae7a39ec94b5a.png

跟下DexSplitterHelper.run()。

07ac2e682f890391112aef33b3614314.png

所以最后还是用的D8打Dex,总算把大概的过程过完了~

d07391e5f24e2a09e61508e42865114e.png

/   自定义混淆字典   /

之前在反编译人家的APP时看到标识符竟然不是abcd,而是中文和特殊字符,怎么做到的呢?其实不难,自定义一个混淆字典就好,在app的proguard-rules的同级目录创建一个文件,比如dictionary,内容示例如下:

﹢
﹣
×
÷
...太长省略

接着在proguard-rules添加下述配置:

-obfuscationdictionary ./dictionary
-classobfuscationdictionary ./dictionary
-packageobfuscationdictionary ./dictionary

再接着gradle assemble打下包,用jadx打开生成的Release APK康康:

87edb407e9c5e0ff14d1fcdb4f766cfd.png

23333,效果极佳,喜欢反编译别人APK的同学看到了,不会打我吧!

09a208dfb34528acd49937a926bd1e06.png

/   模块化混淆   /

混淆的开启由app模块控制,与子模块无关。建议在app模块设置公共混淆规则,子模块设置专属混淆规则,子模块区分project和aar:

# Project类型,配置方法同app模块
buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

# AAR类型
android {
    defaultConfig {
        consumerProguardFiles 'lib-proguard-rules.txt'
    }
    ...
}

当然,你想让混淆规则都由app模块控制也是可以的,移除模块时记得删掉对应的混淆就好~

/   小结   /

淦(gàn),看源码到吐血,Gradle相关的东西真的是无底洞啊,学完一个又一个,还有著名的ASM字节码插桩,基于APK构建过程Task的HOOK的各种开源性能优化/检测工具等。

不学了,学不动了,休息下,我在想写一个 依赖库自动检测+混淆自动生成的插件 会不会很好玩呢?当然,只是想想。

不过源码过完流程了解了心里有底,再看别人这些开源项目时就不会跟看天书一样了~

ef3034e8c659986946258dabe729a336.gif

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

Android 13 Developer Preview一览

一个解决滑动冲突的新思路,无缝嵌套滑动

欢迎关注我的公众号

学习技术或投稿

1b513a500867a292ebc282a51a85ba8c.png

6f3a5fd9223291cd30a282a76698825a.jpeg

长按上图,识别图中二维码即可关注

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值