Android 代码混淆

为什么要做代码混淆?

当我们的应用完成开发,要上线前,为了给用户提供更好的体验,都会对应用再做一些优化处理,例如包体压缩、代码优化、安全处理,而作为最简单的优化方案,就是开启代码压缩优化。
只需要在项目配置文件中简单的加几个配置,就可以在打正式包编译过程中自动实现,代码压缩,字节码优化及代码混淆,为用户提供一个提及更小、更加安全的应用。这里测试创建了一个android工程,
在不开混淆及压缩情况下,打出正式包大小2.7M,而打开混淆及压缩后,打出的包体大小只有1.4M,缩减接近50%,所以为了给用户提供一个更小更安全的应用,一定要了解代码混淆。

代码混淆的发展历史

2016年以前,android都是使用的sun/org编译器进行代码编译,编译过程如下:

jack&jill以前.webp
2016年 android N google推出了自己的编译器jack&jill,jack&jill编译流程

Jack and Jill Application Build.webp
不过jack&jill过于简化的编译流程,不便于添加优化,在2017年google废弃了Jack&Jill,重新回到了之前的编译流程,只是优化了重写了dex编译器,称为D8,与Proguard一同对代码进行优化。

Java.png
在2019年1月 google在AGP 3.3推出了R8,将脱糖、压缩、混淆、优化和 dex 处理整合到了一个步骤中,但在当时R8还有很多问题,会造成应用不可知的问题,直到2019年4月 google推出AGP 3.4,将R8作为了代码优化的默认工具,其编译流程如下:

java.png
R8相对于D8时期版本有了很大的优化,下面是一组对比图:

Shrinking + Dexing time.png

Dex file size.png

Apk size.png

代码混淆原理

My App.png
android提供了R8工具,该工具只针对android项目进行代码优化,R8会以androidManifest.xml中注册的activity service receiver contentprovider作为入口点,根据上图中分析原理,梳理出哪些是无用代码,然后删除那些无用代码。所以这里对于不用的四大组件,要及时的从AndroidManifest.xml中删除, 否则该入口点及其关联的代码及资源就不会被优化掉。

如何使用代码混淆?

1. 配置使用

要想使用google提供的包体优化工具,只需要在项目的build.gradle文件中添加以下配置

signingConfigs {
    debug {
        keyAlias ‘alias’
        keyPassword ‘keypassword’
        storeFile file("${rootDir}/debug.keystore")
        storePassword 'storepassword'
    }
    release {
        keyAlias ‘alias’
        keyPassword ‘keypassword’
        storeFile file("${rootDir}/release.keystore")
        storePassword ‘storepassword’
    }
}

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

复制代码

添加完以上配置,我们就可以打一个正式包,看看包体有什么变化了,新建的一个demo 从2.7M -》 1.4M,缩减了接近一半,当然一个维护了很久的项目不会有这么大比例的缩减,
我们公司的项目从 120M -》 100M 减少了 20M。
接下来运行一下混淆之后的apk,对于一个大的项目,在启动之后,可能没有点几下就崩溃了,报找不到类或方法的异常;这是因为优化工具Proguard(或R8)在做优化时,对
一些类及方法进行了去除或重命名,而我们某些代码是通过字符串匹配的方式来找到要执行的类和方法的,而对于这种方式调用的,优化工具分析有点问题。在android中有以下几种
情况需要大家注意一下:

* Android 四大组件
* native方法
* Java 反射用到的类
* 自定义控件
* 枚举类
* JavaBean
* Parcelable、Serializable 序列化类
* WebView 与 JS 交互所用到的类和方法
复制代码

以上这些都会通过字符串匹配的方式调用类或方法,对于这些不能被优化和混淆的类和方法,需要在混淆配置文件(proguardFiles getDefaultProguardFile(‘proguard-android-optimize.txt’), ‘proguard-rules.pro’)中添加
优化配置。接下来我们看下,应该如何编写这个配置文件,保证我们的应用可以正常执行,同时包体最小化。

2. 混淆规则;

混淆文件的加载是以叠加的方式来处理的,我们可以在proguardFiles后面填写多个文件(当前填写了俩proguard-android-optimize.txt, proguard-rules.pro),Proguard(或R8)会合并这些混淆规则进行打包构建,所以组件里面
加的混淆规则也会影响主应用的混淆配置。

接下来我们看下,Proguard都提供了哪些混淆配置,具体配置参数及意义如下:

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

复制代码

上面的配置,让我们可以灵活的选择使用proguard工具中的哪些优化,接下来要介绍的是,如何配置可以保证我们的类不被修改,应用正常执行。
Proguard提供了一些keep选项,让我们可以灵活的配置类 接口 方法及变量保持其原始性,具体如下:
proguard提供了以下keep关键字

保留 防止被移除或重命名 防止被重命名(未使用的会被移除)
类和类成员 -keep -keepnames
仅类成员 -keepclassmembers -keepclassmembernames
如类含有某成员,保留类及其成员 -keepclasseswithmembers -keepclasseswithmembernames

为了方便批量的添加,proguard提供了以下通配符,

类名 通配符如下:

通配符 含义
? 匹配单个字符,包名分隔符(.)除外
* 匹配除(.)外的任意字符
** 匹配任意字符(包含.),如com.rush.**匹配com.rush包下的所有类及其所有子包的类。

字段和方法 通配符如下:

通配符 含义
<init> 匹配所有构造方法
<fields> 匹配所有字段
<methods> 匹配所有方法
? 匹配单个字符,包名分隔符(.)除外
* 匹配除(.)外的任意字符

类型 通配符如下:

标题
% 匹配原始类型,如int, boolean等
? 匹配任意单个字符
* 匹配除包名分隔符(.)外的任意字符
** 匹配任意字符,包括包名分隔符(.)
*** 匹配任意类型(原始类型、非原始类型、数组或非数组类型)
匹配任意参数个数,任意参数类型

除了以上通配符,proguard还支持implements和extends关键字,对子类进行保留。
proguard(或R8)所支持的类的定义如下:

[@annotationtype] [[!]public|final|abstract|@ ...] [!]interface|class|enum classname
    [extends|implements [@annotationtype] classname]
[{
    [@annotationtype] [[!]public|private|protected|static|volatile|transient ...] <fields> |
                                                                      (fieldtype fieldname);
    [@annotationtype] [[!]public|private|protected|static|synchronized|native|abstract|strictfp ...] <methods> |
                                                                                           <init>(argumenttype,...) |
                                                                                           classname(argumenttype,...) |
                                                                                           (returntype methodname(argumenttype,...));
    [@annotationtype] [[!]public|private|protected|static ... ] *;
    ...
}]
复制代码

以上就是keep规则的一些说明,接下来我们看一个实例来做说明理解

示例:

-keep class com.rush.Test  # 保留com.rush.Test (可以是接口也可以是类)
-keep interface com.rush.InterfaceTest #保留接口com.rush.InterfaceTest(这里只能是接口)
-keep class com.rush.** {
    <init>; # 保留所有构造方法
    public <fields>; # 保留所有的public变量
    public <methods>; # 保留所有的public方法
    public *** get*(); #保留所有以get开头的无参方法
    void set*(***); #保留全部以set开头的方法
}
-keep public class * extends android.app.Activity # 保留activity的子类不被混淆

复制代码

通过上面的知识,我们就可以拿一个项目来进行配置实践了。

在android中gradle tools默认给提供了一些混淆配置,在打完正式包以后,我们可以在 build/intermediates/proguard-files/目录下,看到所使用的混淆配置

默认混淆配置.png
同时会在build/outputs/mapping/release/目录下,生成混淆日志及映射文件

混淆日志.png

mapping.txt → 原始与混淆过的类、方法、字段名称间的转换,在遇到崩溃时,可以通过该文件进行还原;
seeds.txt → 未进行混淆的类与成员;
usage.txt → APK中移除的代码;
resources.txt → 资源优化记录文件,哪些资源引用了其他资源,哪些资源在使用,哪些资源被移除;
复制代码

对于上面这四个文件,不一定都会生成,可以通过以下命令设置输出:

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

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

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

复制代码

总结

通过android包体瘦身,开始深入了解代码混淆,随着研究的深入,发现自己对代码混淆的了解是那么的浅显,连使用也只是用到了冰山一角,而对于里面具体的实现原理及相关联的知识点,知道的就更少了,且掌握的也都是一些碎片化的知识点,通过这次研究,有了一个大概的面貌,而对于面貌下的细节了解的依然很少。

参考

本文作者:自如大前端研发中心-薛健强

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享