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

简介: 本节稍微深入点探索下Android中的代码混淆~

0x2、混淆规则详解


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


1. 混淆设置参数


-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/   # 指定混淆时采用的算法


2. 保持不被混淆的设置


语法组成:


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


保持命令:


-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,请参考第三方提供的混淆规则,没提供的话,建议第三方包全部不混淆;


3. 混淆规则的叠加


不知道你有没有想过:上面日常使用的创建的代码示例,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


4. 资源压缩


资源压缩其实分为两步:资源合并资源移除,前者无论是否配置 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" // 不用支持国际化只需打包中文资源
    }
}    


0x3、从脱糖引出ProGuard、DX、D8、R8


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


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


JVM的内部组成图如下:


网络异常,图片无法展示
|


  • 类加载器:加载编译后的.class,链接、检测损坏的字节码,定位并初始化静态变量及静态代码;
  • 运行时数据:栈、堆、方法变量等;
  • 执行引擎:执行已经加载的代码、清理生成的所有垃圾(gc);


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


网络异常,图片无法展示
|


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


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

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


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


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


网络异常,图片无法展示
|


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


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


网络异常,图片无法展示
|


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


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


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


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


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


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


网络异常,图片无法展示
|


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


0x4、用 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时可能会报错:


网络异常,图片无法展示
|


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


0x5、D8干了啥?


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


  • transformClassesWithDexBuilderForDebug → 将class打包成dex
  • transformDexArchiveWithExternalLibsDexMergerForDebug → 打包第三库的dex
  • transformDexArchiveWithDexMergerForDebug → 打包最终的dex


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


网络异常,图片无法展示
|


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


网络异常,图片无法展示
|


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


网络异常,图片无法展示
|


launchProcessing()dexArchiveBuilder.convert()


网络异常,图片无法展示
|


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


网络异常,图片无法展示
|


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


网络异常,图片无法展示
|


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


相关文章
|
2月前
|
安全 Java 网络安全
Android远程连接和登录FTPS服务代码(commons.net库)
Android远程连接和登录FTPS服务代码(commons.net库)
26 1
|
2月前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异:从代码到用户体验
【10月更文挑战第5天】在移动应用开发的广阔天地中,安卓和iOS两大平台各占半壁江山。它们在技术架构、开发环境及用户体验上有着根本的不同。本文通过比较这两种平台的开发过程,揭示背后的设计理念和技术选择如何影响最终产品。我们将深入探讨各自平台的代码示例,理解开发者面临的挑战,以及这些差异如何塑造用户的日常体验。
|
3月前
|
存储 Java Android开发
🔥Android开发大神揭秘:从菜鸟到高手,你的代码为何总是慢人一步?💻
在Android开发中,每位开发者都渴望应用响应迅速、体验流畅。然而,代码执行缓慢却是常见问题。本文将跟随一位大神的脚步,剖析三大典型案例:主线程阻塞导致卡顿、内存泄漏引发性能下降及不合理布局引起的渲染问题,并提供优化方案。通过学习这些技巧,你将能够显著提升应用性能,从新手蜕变为高手。
32 2
|
4月前
|
JSON JavaScript 前端开发
Android调用Vue中的JavaScript代码
Android调用Vue中的JavaScript代码
42 3
|
4月前
|
安全 Java 网络安全
Android远程连接和登录FTPS服务代码(commons.net库)
很多文章都介绍了FTPClient如何连接ftp服务器,但却很少有人说如何连接一台开了SSL认证的ftp服务器,现在代码来了。
104 2
|
5月前
|
存储 Java Android开发
🔥Android开发大神揭秘:从菜鸟到高手,你的代码为何总是慢人一步?💻
【7月更文挑战第28天】在Android开发中,每位开发者都追求极致的用户体验。然而,“代码执行慢”的问题时常困扰着开发者。通过案例分析,我们可探索从新手到高手的成长路径。
41 3
|
4月前
|
Java Android开发
Android项目架构设计问题之要提升代码的可读性和管理性如何解决
Android项目架构设计问题之要提升代码的可读性和管理性如何解决
42 0
|
5月前
|
API Android开发
Android 监听Notification 被清除实例代码
Android 监听Notification 被清除实例代码
|
6月前
|
JavaScript 前端开发 Android开发
kotlin安卓在Jetpack Compose 框架下使用webview , 网页中的JavaScript代码如何与native交互
在Jetpack Compose中使用Kotlin创建Webview组件,设置JavaScript交互:`@Composable`函数`ComposableWebView`加载网页并启用JavaScript。通过`addJavascriptInterface`添加`WebAppInterface`类,允许JavaScript调用Android方法如播放音频。当页面加载完成时,执行`onWebViewReady`回调。
|
5月前
|
Web App开发 JavaScript 前端开发
Android端使用WebView注入一段js代码实现js调用android
Android端使用WebView注入一段js代码实现js调用android
126 0