注册

如何避免别人的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。


还有就是毫无意义的封装该删掉了,比如如今时代竟然还有关于ActivityViewModelBaseXXX。原本现在的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文件,替换进压缩包。


image.png


如果是JAR,这样:


image.png


找不到AAR文件,比如是用implementation依赖的库?随便进一个类,这样找:


image.png


image.png


这样就能找到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相关任务类:


image.png


然后配置一个"Configuration"


image.png


image.png


image.png


端口号改一下,避免冲突就行,建议弄大一些,避免电脑对这方面有权限之类的限制。直接点OK就好了。


R8Task类只有几百行,很容易看到混淆相关的入口方法runR8,打上断点,然后这样让R8运行起来:./gradlew assembleRelease "-Dorg.gradle.debug=true" "-Dorg.gradle.debug.port=15000" --no-daemon。然后gradle就会等待我们附加上去才会继续运行,这时候就可以点Debug按钮了。


image.png


这样,我们需要用gradle任务控制哪个参数,一目了然,自己的各个模块、第三方SDK文件的混淆配置都在这了:


image.png


接下来寻找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并正常启动,还是弹出强制更新窗口。


(艺高人胆大,不要学...)




如果上文有错误或建议,请指出。


作者:k3x1n
来源:juejin.cn/post/7453809061906645011

0 个评论

要回复文章请先登录注册