周末在某论坛上, 看到一个有意思的问题, 一个Android同行发帖提问: 如果两个三方库都经过了混淆, 导致凑巧包名+类名冲突了, 该如何解决。两个冲突的三方库目录如下图所示:
写了个Demo工程, 引入帖子里提到的两个库, 一个是抖音平台的, 一个是易盾的.
//gradle文件中添加:
repositories {
//引入相关maven地址
mavenCentral()
maven { url 'https://artifact.bytedance.com/repository/AwemeOpenSDK' }
}
//引入帖子里提到的两个指定版本库
implementation 'com.bytedance.ies.ugc.aweme:opensdk-common:0.1.6.2'
implementation 'io.github.yidun:quicklogin:3.2.1'
尝试运行, 错误信息如下, 可以看到是在dexBuilderDebug过程中报错的, 即dex阶段:
两个三方库, 如果没有混淆, 基本上是不可能出现类名冲突的问题. 但是基于商业保密等原因, 把自己的三方库源码进行部分混淆的作法还是相对常见的, 通用的做法如下图所示, 由此导致的类名冲突问题似乎是无法避免的.
//模块中的build.gradle修改
//开启模块混淆
minifyEnabled true
//模块中的proguard-rules.pro
对某些入口函数进行keep, 否则让用户调用类似于a.a();的代码就太low了
针对类名冲突的问题, 基于自己的Android开发经验, 可以比较轻松的想到几种可行方案:
这没什么好说的, 可以源码当然选择源码接入, 但是一般情况是拿不到源码的, 此方案排除.
我们知道, debug包一般都不会开混淆, 那么如果我们直接打release包, 是否可以通过二次混淆来“将错就错”的把这个冲突问题解决呢?
这就涉及到了打包流程问题, 到底是混淆在前面, 还是类名冲突检测在前面; 因此在官网上搜一下Android的打包流程, 基本上网上搜到的都是下面这个图, 没有涉及到混淆、和类名冲突检测的内容.
没有现成的资料只能自己测试, 直接打release包:
* What went wrong:
Execution failed for task ':app:checkReleaseDuplicateClasses'.
> A failure occurred while executing com.android.build.gradle.internal.tasks.CheckDuplicatesRunnable
> Duplicate class a.a.a.a.a.a found in modules jetified-opensdk-common-0.1.6.2-runtime (com.bytedance.ies.ugc.aweme:opensdk-common:0.1.6.2) and jetified-quicklogin-3.2.1-runtime (io.github.yidun:quicklogin:3.2.1)
果然报错了, 那就说明了一个问题:
明白了这个流程, 就知道想通过打release包进行二次混淆绕过错误的方法, 是不可行的.
从 1.5.0-beta1 开始,Gradle插件包含了一个Transform API,允许第三方插件在将已编译的 class 类文件转换为 dex 文件之前对其进行操作。Transform 是一个链式结构,每个Transform都是一个Gradle的Task,Android 编译器通过 TaskManager 将每个Transform串联起来。
既然Transform是在生成dex之前进行的, 那么3.2中的打包流程会更精细化为:
aidl处理, 源码编译, 三方库class收集;
Transform链式串联修改字节码;
dex阶段, 进行重复类名检测, 字节码混淆;
对包进行签名.
因此, 使用Transform的方式, 在流程上是一定可行的, 但是在实际工程中的实现难度大且性能低下, 原因有以下几点:
(Tips:在AGP7.0中 Transform 已经被标记为废弃了,并且将在AGP8.0中移除。改用了一种编译更快且代码更简洁的API AsmClassVisitorFactory, 感兴趣可以查看这篇文章)
综上所述, 如果可以直接修改本地的aar文件, 并且从远程依赖修改为本地依赖, 就可以避免Transform带来的编译性能问题, 接下来就是如何稳定修改包名以及修改所有import的问题了, 刚好有一个工具可以帮我们解决该问题, 那就是jarjar.jar, 因此整个的修改流程如下:
Android Studio项目中通过远程implement添加的依赖,会自动到maven库中下载相应版本的aar。那么这些文件都下载到哪里了呢?其实Android Studio中所有项目都共用同一个本地缓存库,路径是:
\Users\.gradle\caches\modules-2\files-2.1
然后通过:包名\模块名\版本号\哈希值\jar或aar文件, 即可找到本案例中我们要修改quicklogin-3.2.1.aar, 将该aar拖拽到Android Studio中, 可以看到其目录结构:
因为我们要用到的jarjar工具是对.jar文件进行修改, 因此需要先对aar进行如下解压:
//解压aar
unzip quicklogin-3.2.1.aar -d tempFolder
## 内容格式: rule <要改变的包名称> <改变的名称>
## a.a.a.a.a下所有的类改名为a.a.a.a.change
rule a.a.a.a.a.** a.a.a.a.change.@1
java -jar jarjar-1.4.jar process rule.txt quicklogin-3.2.1.jar temp.jar
对于aar的重新打包, 这里多说一嘴, 网上最常见的【错误】流程是:
这种方式得出的aar在AS里依旧会被识别成zip,导致无法导入依赖. 而后续正确流程应该如下所示:
jar cvf newAAR.aar -C tempFolder/ .
这样就获得了一个最终的新的aar文件, 将该aar拖拽到Android Studio中, 其目录结构如下图所示, 可以看到包名已经修改完成, 具体的自定义修改规则, 可以通过自定义3.4.3中的rule文件来实现.
最终直接将该aar通过本地依赖的方式集成到工程中, 既可以避免Transform方案引入的编译性能问题, 又可以解决三方库类名冲突的问题.
jarjar替换包名的思路很简单,就是遍历jar包然后基于ASM修改class文件。rule文件中的规则为一行一条,除了rule,还有其他2种形式的指令:
//用来替换类名,所有用到被替换类的类都会跟着被改变
rule <pattern> <result>
//用来移除指定的类,在rule之前执行
zap <pattern>
//只会保留指定的Package的名称,在rule之后执行
keep <pattern>
其中,pattern是需要匹配的名称。2个星号是替换所有匹配的,1个星号是只替换当前包下的类。result是取代后的名称,可以使用@1、@2这类的符号表示要使用第几个pattern的*或**所代表的字串。
在思考本文提到的二次混淆的解法中, 有个同事提到, 如果一个类名足够短, 那么就不会再被混淆了, 为了验证这个观点是否是正确的, 专门写了个demo, 先写一个简单的类, 再在代码中进行调用以保证不会在打包后被优化删除:
然后直接打release包进行混淆, 查看mapping文件, 发现还是会被混淆:
xx.xx.xx.xx.a.a():14:15 -> c