Android混淆相关

背景

写这篇文章的背景是开发过程中对线上apk包进行反编译后发现所有的类名居然都没有被混淆,猜想可能是混淆配置出了问题,但是由于项目的现状是集成了将近600个的aar形成的apk, 混淆规则高达2w行,所以尽管能定位大概方向,但这仍然是一项耗时耗力的工作。于是就此展开了问题排查

思路及过程

集成工程打包后会在build/outputs目录下会生成总的混淆文件proguard-all-configuration.txt, 这个文件包含了打包过程中各个aar通过consumerProguardFiles配置的混淆文件。(该文件生成及consumerProguardFiles后面详细介绍)

所以查找方向是看下从哪个版本开始出现问题,然后比较前后2个版本混淆规则有什么变化,比较之后查出以下混淆规则影响了代码的混淆

-keep,allowshrinking class * {
    @com.alibaba.android.arouter.facade.annotation.Autowired
    <fields>;
}

于是我们就开始了验证,由于编译apk太慢,本地得20多分钟,再加上这混淆规则是aar内置的,没法进行注释,所以本地编译apk验证行不通,于是我们想到可以使用以下命令来操作

java -jar proguard-base-6.0.3.jar 
@/Users/star/Documents/projects/Integration/product/build/outputs/proguard-all-configuration.txt

将上面有问题的混淆规则注释之后,跑上面的命令,跑完之后对比mapping文件,发现混淆正常了,于是进行了分析为什么上面的混淆规则会导致类名混淆失效:

-keep,allowshrinking就是keepnames的缩写,使用keep或者keepnames的缺点在于不管大括号里面的条件是否满足,满足大括号之前的类名都会被保持,由于上面条件匹配的是*,所以导致App中所有类名都被保持了,这也是找到了问题所在,
但是keepnameskeep好一点就是允许不用的类或属性被移除 。

由于混淆规则三方aar可以自定义,所以后续也有出类似的问题,但是经过本次教训之后下次查找就方便多了,比如下面的写法同样会导致所有类名不会被混淆

-keep class * {
    @com.didi.trace.annotations.TraceEvent
    <methods>;
}

混淆相关知识点

1. 混淆作用

混淆即为Proguard, Proguard是一个集文件压缩优化混淆校验等功能的工具

压缩(Shrinking):默认开启,用以减小应用体积,移除未被使用的类和成员,并且会在优化动作执行之后再次执行(因为优化后可能会再次暴露一些未被使用的类和成员)。

-dontshrink 关闭压缩

优化(Optimization):默认开启,在字节码级别执行优化,让应用运行的更快。

-dontoptimize 关闭优化
-optimizationpasses n 表示proguard对代码进行迭代优化的次数,Android一般为5

混淆(Obfuscation):默认开启,增大反编译难度,类和类成员会被随机命名,除非用keep保护。

-dontobfuscate 关闭混淆

最后一步还校验处理后的代码

混淆后默认会在工程目录app/build/outputs/mapping/release下生成一个mapping.txt文件,这就是混淆规则,我们可以根据这个文件把混淆后的代码反推回源本的代码,所以这个文件很重要,注意保护好。原则上,代码混淆后越乱越无规律越好,但有些地方我们是要避免混淆的,否则程序运行就会出错,所以下面讲如何让自己的部分代码避免混淆从而防止出错。

2. 混淆开启

在Android Studio中配合Gradle构建工具进行代码混淆很简单,需要在如下配置,然后我们就可以到proguard-rules.pro文件中加入我们的混淆规则了。

  buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

3. 混淆配置

  • proguard关键字
关键字描述
keep保留类和类中的成员不被混淆和移除(成员需要条件)
keepnames保留类和类中的成员不被混淆(成员需要条件),但是成员没有引用会被移除
keepclassmembers保留类中的成员不被混淆和移除(成员需要条件)
keepclassmembernames保留类中的成员不被混淆(成员需要条件),但是成员没有引用会被移除
keepclasseswithmembers保留类和类中的成员不被混淆和移除(成员需要条件)
keepclasseswithmembersnames保留类和类中的成员不被混淆(成员需要条件),但是成员没有引用会被移除
  • proguard通配符
通配符描述
<fields>匹配类中的所有字段
<methods>匹配类中的所有方法
<init>匹配类中的所有的构造函数
*匹配任意长度字符,不包含包名分隔符(.)
**匹配任意长度字符,包含包名分隔符(.)

一些容易搞乱的点:

  1. 不管是使用哪个关键字,如果想对成员保持不混淆,那么都需要使用条件进行限制
  2. keepkeepclasseswithmembers都可以保证类名及类成员不会被混淆,但keepclassmembers只能限制成员不被混淆
  3. keepkeepclasseswithmembers的区别在于,不管大括号后面的条件是否满足keep关键字都能保证类名不被混淆,但keepclasseswithmembers只有大括号后面的条件满足才能保证类名不被混淆
  4. names结尾的成员没有引用会被移除,非names结尾成员没有引用不会被移除

4. 实例及坑

//这行代码会保留utils下面的所有类名不被混淆,但是成员仍然会被混淆,因为没有成员限制条件
-keep class com.didi.onecar.utils.*

//这行代码会保留项目中所有类名不被混淆,isConfirmVipPopuShow不被混淆, 其他仍被混淆
//如果不想混淆所有类名,keep应该替换为keepclasseswithmembers
-keep class * {
	public static boolean isConfirmVipPopuShow(int);
}
//或者写死class限制条件
-keep class com.star.utils.ApolloBusinessUtil{
	public static boolean isConfirmVipPopuShow(int);
}
  1. 正常写混淆的时候尽量不要直接使用keep关键字,如果使用,请把条件限制明确,不要直接使用-keep class *
  2. 尽量使用keepclassmemberskeepclasseswithmembers关键字

5. 需要不被混淆的情况

  • jni方法不可混淆,因为这个方法需要和native方法保持一致;
-keepclasseswithmembernames class * { # 保持native方法不被混淆    
    native <methods>;
}
  • 反射用到的类不混淆(否则反射可能出现问题)
  • AndroidMainfest中的类不混淆,四大组件和Application的子类和Framework层下所有的类默认不会进行混淆。自定义的View默认也不会被混淆
  • 与服务端交互时,使用GSON、fastjson等框架解析服务端数据时,所写的JSON对象类不混淆,否则无法将JSON解析成对应的对象
  • 使用第三方开源库或者引用其他第三方的SDK包时,如果有特别要求,也需要在混淆文件中加入对应的混淆规则
  • 有用到WebView的JS调用也需要保证写的接口方法不混淆
  • Parcelable的子类和Creator静态成员变量不混淆,否则会产生Android.os.BadParcelableException异常
-keep class * implements Android.os.Parcelable { # 保持Parcelable不被混淆           
    public static final Android.os.Parcelable$Creator *;
}
  • 使用enum类型时需要注意避免以下两个方法混淆,因为enum类的特殊性,以下两个方法会被反射调用
-keepclassmembers enum * {  
    public static **[] values();  
    public static ** valueOf(java.lang.String);  
}

6.组件化混淆实现方案

组件化是指将业务模块抽离成单独的项目,可单独编译和调试,最终集成到一个主工程中。

每个业务模块都可能会有自己的混淆规则,我们希望主工程中集成aar的时候不用像使用jar包一样还要再进行相应的混淆配置。同时每个aar库自己维护自己的混淆配置,不再主工程中统一配置,也方便代码的维护和修改。

默认情况下,我们使用Android studio 的gradle 打包方式生成的aar文件中是不包含混淆配置的,如果想添加,则使用consumerProguardFiles关键字来配置, 这个一般在defaultConfig中进行配置:

 defaultConfig {
	    ...
        consumerProguardFiles 'consumer-rules.pro'
    }

consumerProguardFiles方法接收的参数是一个文件(混淆文件)的数组,因此我们在使用的时候可以配置多个文件,比如

consumerProguardFiles 'proguard-a.pro','proguard-b.pro'

使用consumerProguardFiles方法后我们解压我们的aar会发现其中多了一个proguard.txt文件,打开这个proguard.txt你会发现里面的内容和我们写的混淆配置是一样的。

如果consumerProguardFiles 后面传入了多个混淆配置文件,最终生成的aar中也仅仅只有一个proguard.txt文件,多个proguard*文件是内容merge到这个proguard.txt文件中的。

我们在主工程的混淆规则里增加了如下配置,用来输出总的混淆规则:

-printconfiguration build/outputs/proguard-all-configuration.txt

上面我们说通过命令验证混淆就是使用的该文件

执行混淆命令为:

java -jar  proguard-base-6.0.3.jar  @混淆文件

说下这个proguard-base-6.0.3.jar这个文件,以前打包混淆是用Android sdk里面内置的jar去处理的,目录在Android sdk下面文件 /tools/proguard/lib/proguard.jar,但是我们执行的时候发现命令老是报错,发现这个文件不支持java 8新语法,后面经过查找,发现google不用这个文件了,而是在gradle中内置了新的处理混淆的jar, 目录在 /.gradle/caches/modules-2/files-2.1/net.sf.proguard/proguard-base/6.0.3/c1e9f79efdc3963b29920f8ec8f08e4c8652fa7f/proguard-base-6.0.3.jar

gradle构建过程请结合实践参考该文章gradle构建过程

所以,总结一下,如果以后项目中再出现混淆的问题,首先去看build/outouts/proguard-all-configuration.txt这个总的混淆规则,如果规则太长或者看不出来什么问题,那么就可以使用命令进行折半查找,经过试验,效率还是很高的。

思考与改进

经过上面问题排查,发现这不是偶然的问题,而是开发过程中一定会遇到的问题,那针对这种问题,我们不能每次都去查找然后督促业务aar进行修改,这是一个耗时耗力的事情,所以我们需要一个方案从根本上解决这个问题。

另外aar中的混淆配置在打包到apk中是对全部apk中的Java代码的混淆配置生效的,如果某个aar的混淆配置中包含dontobfuscate配置指令,那么我们的apk将不会混淆。

所以在集成工程打包的时候,我们必须要做一些混淆的拦截,不符合规范的混淆将不能打包成功
比如上面keep class *dontobfuscate等一些必须拦截的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值