如何避免别人的SDK悄悄破坏你App的混淆规则,记一次APK体积优化
所谓的包体积优化,其实根本取决于,更早接触项目的前辈留下了多少机会。最近在重新配置项目的R8规则,有将近10%的优化空间。这里分享一下。
很多关于配置混淆规则的博客,教人随意添加-keep class * entends androidx.**
或者-dontoptimize
、-dontshrink
,甚至给了所谓的“常用万能混淆规则”,估计一些SDK开发者也干脆复制了他们的代码,然后影响到了依赖这些SDK的项目。
好在很容易编写gradle任务改掉SDK向APK贡献的混淆规则。本文AGP版本7.3.1
。
降低包体积 · 先优化我方代码
删掉dontoptimize
先改自己模块的缺点。
主要是自己模块的-dontoptimize
直接删掉。包括proguard-android.txt
改为proguard-android-optimize.txt
,这两个文件的区别之一就是是否包含了-dontoptimize
:
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
通常去掉-dontoptimize
之后,包体积就有明显降低了,如果你删了之后,包体积无任何变化,就说明没删干净,或者是第三方SDK依然在用它,下文继续处理。
多观察printconfiguration
可以在混淆规则里添加一个-printconfiguration 'configuration.txt'
,
然后打个minifyEnabled true
的包,再用AS直接找到configuration.txt
文件,这里就是项目和第三方SDK配置的所有混淆规则。
我们项目到这里就开始崩溃了,主要是GSON相关问题,查前辈的博客要警惕,因为有两种方案:
-dontoptimize
-dontshrink
# 然后就是各种keep...
以及:
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
前者更容易被搜到,但这种做法等于第一步白做了。
如果项目用的Gson库比较旧,按照后者去配。
如果用的是2.11及更高版本,其实也不会遇到这个崩溃问题了,因为它开始内置自己需要的混淆规则,无需我们配置。从configuration.txt
就能看到:
# The proguard configuration file for the following section is /home/这里无所谓忽略/transformed/rules/lib/META-INF/proguard/gson.pro
# 太长了,这里忽略
# Keep class TypeToken (respectively its generic signature) if present
-if class com.google.gson.reflect.TypeToken
-keep,allowobfuscation class com.google.gson.reflect.TypeToken
# Keep any (anonymous) classes extending TypeToken
-keep,allowobfuscation class * extends com.google.gson.reflect.TypeToken
# Keep classes with @JsonAdapter annotation
-keep,allowobfuscation,allowoptimization @com.google.gson.annotations.JsonAdapter class *
# 太长了,这里忽略
# End of content from /home/这里无所谓忽略/transformed/rules/lib/META-INF/proguard/gson.pro
改掉不良习惯
比如我们这里竟然有-keep class * implements java.io.Serializable
、-keep class * extends java.io.Serializable
,然后Serializable
接口一直起到的就是@Keep
的作用。后来又当需要往intent传入数据时,刚好发现某个类,正好啊,实现了Serializable接口,直接putSerializable
···这也算是代码越来越劣化的因素之一了。
这个规则对包体积影响还是比较大的,因为Kotlin里面的一些lambda(比如by lazy { xxx }
)编译后生成的类也会间接实现Serializable。
还有就是毫无意义的封装该删掉了,比如如今时代竟然还有关于Activity
和ViewModel
的BaseXXX
。原本现在的androidX各种库,只需要一行代码就可创建viewBinding、ViewModel实例,就别再冒着ViewBinding、ViewModel不能被混淆的缺陷,用反射泛型等“高级”技巧“封装”个几十行的BaseXXX...
剩下的一些优化就是根据业务逻辑去降低keep
的范围就好了,主要是小心反射(包括JNI、属性动画)、序列化等少数特殊场景。
降低包体积 · 改变别人
上文提到,如果做了一些优化之后打包效果毫无变化,那就是第三方SDK有问题了。
可以添加这样的混淆规则:-whyareyoukeeping class 这里就写你发现没被混淆的类名
,然后打包时就会输出哪个文件的哪一行规则keep了这个类。
某广告SDK配置了-keep class * entends androidx.**
和-keep class * implements androidx.**
,我不敢推测它们到底在反射调用androidX的哪一部分,反正造成了我们各种ViewModel,ViewBinding等androidX子类没有被混淆、大约3~5%包体积的无用代码没有被R8移除。
SDK本来也不需要反射调用我们自己的业务代码。我需要把它改为-keep class !我们app的包名.**, * entends androidx.**
。
不要想着通过/home/这里无所谓忽略/transformed/...
这个文件去修改第三方库的混淆配置,因为每次打包时,这个目录内容会重新生成。(已踩坑)
方法一:直接解压替换文件
找到不优雅的混淆规则后,如何修改?
如果是AAR文件,可以直接解压软件打开这个AAR,找到proguard.txt文件,替换进压缩包。
如果是JAR,这样:
找不到AAR文件,比如是用implementation
依赖的库?随便进一个类,这样找:
这样就能找到implementation
背后的jar或aar文件了,然后改为用文件依赖的方式。
如果有多个SDK配置了不优雅的规则怎么办?一个个找、一个个改显然比较麻烦,未来更新这些SDK的版本时还要再次修改,所以要探索一下能否通过gradle任务完成这件事。
方法二:编写gradle任务
通过这次,这是我第一次尝试给gradle插件下断点,真是降低了太多观察源码的成本,特此记录...
首先要能方便的在AndroidStudio中查看AGP源码,技巧:直接在app模块build.gradle依赖AGP。为了不影响编译,这里用compileOnly而不是implementation。
compileOnly 'com.android.tools.build:gradle:7.3.1'
然后就可以轻松找到R8相关任务类:
然后配置一个"Configuration"
端口号改一下,避免冲突就行,建议弄大一些,避免电脑对这方面有权限之类的限制。直接点OK就好了。
R8Task类只有几百行,很容易看到混淆相关的入口方法runR8
,打上断点,然后这样让R8运行起来:./gradlew assembleRelease "-Dorg.gradle.debug=true" "-Dorg.gradle.debug.port=15000" --no-daemon
。然后gradle就会等待我们附加上去才会继续运行,这时候就可以点Debug按钮了。
这样,我们需要用gradle任务控制哪个参数,一目了然,自己的各个模块、第三方SDK文件的混淆配置都在这了:
接下来寻找proguardConfigurationFiles
的来源,这里分析过程略过,最终可以确定它来自于ProguardConfigurableTask
这个task的成员configurationFiles
。于是可以编写如下任务:
import com.android.build.gradle.internal.tasks.ProguardConfigurableTask
ConfigurableFileCollection cf;
def fixFoolishRules = tasks.register('fixFoolishRules') {
var iterator = cf.iterator()
while (iterator.hasNext()) {
var item = iterator.next()
if(item.absolutePath.contains("这里过滤一下需要修改的sdk文件名")){
var content = "# file: ${item.absolutePath}\n"
var foolish = "-keep public class * extends androidx.**\n"
var fixed = "-keep public class !自己业务逻辑包名.**, * extends androidx.**\n"
var newContent = item.getText().replace(foolish, fixed)
item.write(content.concat(newContent))
}
}
}
tasks.withType(ProguardConfigurableTask).configureEach { task ->
cf = ((ProguardConfigurableTask)task).configurationFiles
task.finalizedBy(fixFoolishRules)
}
这里有个无所谓的小问题:为什么不能在tasks.register('fixFoolishRules') {
里面直接ProguardConfigurableTask.configurationFiles
,而是要在tasks.withType(ProguardConfigurableTask)...{
里面用这个额外的cf
变量获取,否则会有如下报错,暂时没研究了。
Could not determine the dependencies of task ':app:minifyReleaseWithR8'.
> Could not create task ':app:fixFoolishRules'.
> No such property: configurationFiles for class: com.android.build.gradle.internal.tasks.ProguardConfigurableTask
Possible solutions: configurationFiles
再记录一个踩过的坑:ConfigurableFileCollection
这个类本身继承了FileCollection
接口,而这个接口继承了Iterable<File>
。所以直接用它去遍历就好了。如果尝试去找它的files
成员,进行删除和增加,反而没什么意义,因为每次调用它getFiles
都是在生成一个新的Set对象。
不用担心直接修改这些文件,而不是替换configurationFiles
集合。因为我上文也提到了,/home/这里无所谓忽略/transformed/rules/lib/META-INF/proguard/gson.pro
这种文件每次编译时都会重新生成。
既要激进,又要保守
有一家SDK比较坑,它们的文档,以及AAR内置的混淆规则漏掉了某个包里面的类,但是,我估计他们开发环境一直配着-dontoptimize
,导致他们不会触发这个问题。
还好,一初始化他们的SDK就崩溃了,很容易发现,也就没有带到线上。
但如果有什么SDK犯了类似错误,而且是那种开发阶段不会触发,后续通过热更新或者在线配置之类的触发,那就完蛋了。所以为了避免他们犯错,我主动解包在我们App启动期间就会初始化的SDK,把他们SDK内部代码特有的包名或者类统统全部添加-keep
!
另外就是准备做一个类似于微信频繁崩溃时会触发的“安全模式”(也好像是“修复模式”?忘了名字,以后有时间研究一下他们)。
如果App启动后,发现上次启动成功到进程结束未超过5秒,则先等待版本更新接口返回数据,再决定:是初始化第三方SDK并正常启动,还是弹出强制更新窗口。
(艺高人胆大,不要学...)
如果上文有错误或建议,请指出。
来源:juejin.cn/post/7453809061906645011