从事Android 已有2 、3年光景,但一直没有深入了解过混淆,和深入学习探究过混淆,只是知道有这么一个topic,直到前些天,遇到了一个问题 下定决心系统学习相关知识点。问题来源:自己再debug状态下没有任何问题,但是打了release包就有问题,要不是闪退,要不就是网络请求没有效果,数据请求错误。然后自己把release的混淆关闭,一切也没有问题,这个时候可以锁定问题出在混淆上了。
于是乎,开始学习混淆方面的知识点,那我们知道混淆就是加固app,还有减少不必要的代码,优化代码,混淆代码,如下图:
这里的问题肯定是和代码混淆有关,开始排查吧。
当时,debug包一切都好,release包则是闪退,闪退的原因是 用反射获取的类没有找到,然后我弃用了反射方法,改用其他的方法,然后接着是数据请求数据获取失败,然后chalse 抓包,服务端数据已经返回到app端,但数据就是获取不到,很可能就是json转化错误,由于之前对混淆不熟,但也有一些了解,当这三点现象一起出现,我确定是混淆引起的,于是重新排查混淆规则,发现我把baseResp类写到了 基础公共模块里,忘记添加keep 此类,然后添加规则,数据就有了,在把反射方法改回来,也是ok的。困扰了2天的问题就解决了,于是系统的记录有关混淆知识点。
参考文档
google 文档:https://developer.android.com/studio/build/shrink-code#keep-code
proguard网址: https://www.guardsquare.com/en/products/proguard/manual/usage
proguard 和google 的关系是什么呢? proguard 是一家专门从事app 加固的公司,有相关的技术;而google 再android相关的加固技术采用了这家公司的技术,于是我们再app的gradle 文件中可以看到相关的声明:
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.Release_Key
}
这样两家公司其实一种合作关系,类似的这种合作还有很多,并不是android里面的所有知识都是google 开发的,比如构建工具gradle 也不是google公司的,androidStudio 开发工具也是购买Intellij 公司的产品,二次开发专门针对android开发者的。
先看看关于proguard 网站的介绍,我们需要学习混淆语法,混淆哪些类,混淆注意事项,其他相关的混淆知识点。
混淆语法:
混淆采取的非黑即白的做法,当我们开启混淆后,默认一切代码都混淆,但混淆会引起一些问题,比如反射、数据转换(反射是靠字符串寻找类,若把类名混淆则反射时就无法找到)。这时候我们需要指明哪些不需要混淆。
保留 | 防止被移除或者被重命名 | 防止被重命名 |
---|---|---|
类和类成员 | -keep | -keepnames |
仅类成员 | -keepclassmembers | -keepclassmembernames |
如果拥有某成员,保留类和类成员 | -keepclasseswithmembers | -keepclasseswithmembernames |
上面表格,发现其功能很相似,可以见名知意,我们关注第二列,因为第二列的功能包含了第三列,再用到自己的代码中,若不追求极致混淆,我们可以一律用第二列的替代第三列。
混淆公式:
[保持命令] [类] {
[成员]
}
“类”代表类相关的限定条件,它将最终定位到某些符合该限定条件的类。它的内容可以使用:
- 具体的类
- 访问修饰符(
public
、protected
、private
) - 通配符
*
,匹配任意长度字符,但不含包名分隔符(.) - 通配符
**
,匹配任意长度字符,并且包含包名分隔符(.) extends
,即可以指定类的基类implement
,匹配实现了某接口的类- $,内部类
“成员”代表类成员相关的限定条件,它将最终定位到某些符合该限定条件的类成员。它的内容可以使用:
- <init> 匹配所有构造器
- <fields> 匹配所有域
- <methods> 匹配所有方法
- 通配符
*
,匹配任意长度字符,但不含包名分隔符(.) - 通配符
**
,匹配任意长度字符,并且包含包名分隔符(.) - 通配符
***
,匹配任意参数类型 …
,匹配任意长度的任意类型参数。比如void test(…)就能匹配任意void test(String a)
或者是void test(int a, String b)
这些方法。- 访问修饰符(
public
、protected
、private
)
接下来,一些场景的应用场景,掌握这几种就可以了;
混淆一个包下的所有类(不包括子包)
-keep class com.sjh.proguraddemo.* { *;} //com.sjh.proguraddemo 为文件路径(也是包名)
混淆一个包下的所有类(包括子包)
-keep class com.sjh.proguraddemo.** { *;} //com.sjh.proguraddemo 为文件路径(也是包名)
对比这两个,后面比前面的多了一个 * ,因为一个* 不能匹配. 所以无法跨越子包,通常为了方便省事情,后者用的更多 ; 另外, {*;} 表示类里面的所有属性、方法不混淆。
另外,类前面可以加修饰符,pulic private 等等,当然了一般外部类都是public,
不混淆所有的构造方法
-keep public class com.sjh.demo.baseClass {
// 该类下的所有构造函数不混淆,同理还有 字段、方法
[访问修饰符] <init>
}
针对第三方jar,aar等第三方库引用文件,一般库提供者会提供混淆代码,直接复制过来ok,但是有些没有提供的,一般采取均不混淆,直接找到第三方库的跟目录,然后:
-keep class com.xx.xx.** {*;} //包的目录
针对某个特定的类(Constants)不混淆:(直接写出类的全路径名即可)
-keep class com.sjh.proguarddemo.Constants { *; }
不混淆某个类的子类或者实现某个接口的类
-keep class * extends com.sjh.proguarddemo.BaseClass{ *; } //类
-keep class * implement com.sjh.proguarddemo.BaseInterface{ *; } // 接口
不混淆某个类的特定方法
-keepclassmembers class com.sjh.proguarddemo.BaseClass {
public void method(java.lang.String); // 括号里要指明参数类型,或者用 ... 表示无论什么类型
// 参数
}
不混淆内部类
-keep class com.sjh.proguarddemo.BaseClass$* {
*;
}
知道以上用法就够了,当然,网上有很多的那种混淆公共模板,我们可以复制到工程中。
哪些类不要混淆
- Android系统组件
- Parcelable ,需要使用序列化的
- java 序列化
- 枚举
- native 方法
- 注释 annotations
- 用到反射的地方
- 解析服务器数据的model
- ...
要知道为什么这些类不能混淆,我们知道混淆的根本就是把之前的类名、方法名、属性名用无意义的字符串替换,那凡是依赖这些特定的字符串的地方就都不能混淆,比如说数据类model,解释可以 https://medium.com/androiddevelopers/practical-proguard-rules-examples-5640a3907dc9 ;
混淆的多工程配置问题
在模块化开发中,我们有多个module, 我们需要对每个module 进行单独配置混淆文件,不要统一在 主工程中统一配置,这样其实有了关联,最好不要有关联。至于这个问题,有很多人已经谈到过,
先看一张图
其gradle 语法变了,官网介绍 。可以参考 点击1 、点击2 。
混淆文件Mapping.txt
如果开启混淆后,在我们构建app时输出相应的混淆映射文件
这个mapping 有什么用呢?比如说,我们app用了第三方Crash日志收集平台,当我们开启了混淆时,我们在其平台上看到的日志信息可能是大量的无意义的符号,没有说出真正的崩溃类,这时候我们就需要把这个mapping文件上传上去,平台会根据这个文件把日志还原,开发人员就可以看到真正有意义的日志了。
混淆工具
每个平台都有最常规的混淆工具 , java代码常用的是ProGuard ,Python有python的,其实还有很多的混淆工具,这个后面详细解析。