Android开发——如何解决三方库中的类名冲突问题
Posted SEU_Calvin
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android开发——如何解决三方库中的类名冲突问题相关的知识,希望对你有一定的参考价值。
文章目录
背景
周末在某论坛上, 看到一个有意思的问题, 一个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开发经验, 可以比较轻松的想到几种可行方案:
- 如果某个库可以获取到源码, 那么尽量使用源码引入;
- 是否可以经过二次混淆重新命名;
- Android Transform直接修改字节码;
- 有没有其他方式可以直接修改本地aar文件.
三、继续思考
3.1 源码引入
这没什么好说的, 可以源码当然选择源码接入, 但是一般情况是拿不到源码的, 此方案排除.
3.2 是否可以经过二次混淆改名
我们知道, 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)
果然报错了, 那就说明了一个问题:
- 冲突检测是在混淆之前进行的, 又因为R8是在dex生成的过程中进行混淆的, 因此打包流程中, 在生成dex阶段, 先进行重复类检测, 再进行release混淆.
明白了这个流程, 就知道想通过打release包进行二次混淆绕过错误的方法, 是不可行的.
3.3 Android Transform
从 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的方式, 在流程上是一定可行的, 但是在实际工程中的实现难度大且性能低下, 原因有以下几点:
- 修改包名要连带所有import到的地方全部修改, 因此非常容易出错, 且只有在运行时才会崩溃, 测试难度极大;
- 每次编译打包都需要进行Transform, 影响编译速度;
(Tips:在AGP7.0中 Transform 已经被标记为废弃了,并且将在AGP8.0中移除。改用了一种编译更快且代码更简洁的API AsmClassVisitorFactory, 感兴趣可以查看这篇文章)
3.4 直接修改本地aar文件
综上所述, 如果可以直接修改本地的aar文件, 并且从远程依赖修改为本地依赖, 就可以避免Transform带来的编译性能问题, 接下来就是如何稳定修改包名以及修改所有import的问题了, 刚好有一个工具可以帮我们解决该问题, 那就是jarjar.jar, 因此整个的修改流程如下:
3.4.1 找到要修改的本地aar
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
3.4.2 下载jarjar.jar
- code.google.com/p/jarjar/ 直接官网下载1.4的最新版本
- 不能架梯子的小伙伴,可以直接下载百度网盘里的备份,密码: 3t53
3.4.3 jarjar的快速使用
- 新建一个文件夹,将jarjar-1.4.jar和需要修改包名的jar包放在该文件夹下
- 在该文件夹下新建一个txt文本,取名rule.txt
- 打开rule.txt,输入如下内容并保存
## 内容格式: rule <要改变的包名称> <改变的名称>
## a.a.a.a.a下所有的类改名为a.a.a.a.change
rule a.a.a.a.a.** a.a.a.a.change.@1
- 通过cmd执行如下命令, 便生成了经过修改之后的temp.jar:
java -jar jarjar-1.4.jar process rule.txt quicklogin-3.2.1.jar temp.jar
3.4.4 aar重新打包
对于aar的重新打包, 这里多说一嘴, 网上最常见的【错误】流程是:
- 将aar尾缀改成zip
- 解压,修改文件
- 再打包成zip,再改aar后缀
这种方式得出的aar在AS里依旧会被识别成zip,导致无法导入依赖. 而后续正确流程应该如下所示:
- 对于3.4.1中解压出来的tempFolder中的旧jar, 即classes.jar删除;
- 将3.4.3中获取到的temp.jar, 直接拷贝到tempFolder中, 并改名为classes.jar
- 重新打包aar, 命令如下(注意空格后有一个点)
jar cvf newAAR.aar -C tempFolder/ .
这样就获得了一个最终的新的aar文件, 将该aar拖拽到Android Studio中, 其目录结构如下图所示, 可以看到包名已经修改完成, 具体的自定义修改规则, 可以通过自定义3.4.3中的rule文件来实现.
最终直接将该aar通过本地依赖的方式集成到工程中, 既可以避免Transform方案引入的编译性能问题, 又可以解决三方库类名冲突的问题.
3.4.5 jarjar的其他用途
jarjar替换包名的思路很简单,就是遍历jar包然后基于ASM修改class文件。rule文件中的规则为一行一条,除了rule,还有其他2种形式的指令:
//用来替换类名,所有用到被替换类的类都会跟着被改变
rule <pattern> <result>
//用来移除指定的类,在rule之前执行
zap <pattern>
//只会保留指定的Package的名称,在rule之后执行
keep <pattern>
其中,pattern是需要匹配的名称。2个星号是替换所有匹配的,1个星号是只替换当前包下的类。result是取代后的名称,可以使用@1、@2这类的符号表示要使用第几个pattern的*或**所代表的字串。
3.4 小彩蛋
在思考本文提到的二次混淆的解法中, 有个同事提到, 如果一个类名足够短, 那么就不会再被混淆了, 为了验证这个观点是否是正确的, 专门写了个demo, 先写一个简单的类, 再在代码中进行调用以保证不会在打包后被优化删除:
然后直接打release包进行混淆, 查看mapping文件, 发现还是会被混淆:
xx.xx.xx.xx.a.a():14:15 -> c
总结
- 对于遇到三方库中的类名冲突的开发者, Transform方案会稍显笨重, 简单且有效的方案就是使用jarjar工具对aar中的jar进行修改, 并对aar进行重新打包, 并在工程中将远程依赖转为本地aar依赖;
- 对于三方库作者, 最好定制混淆规则, 避免和其他库冲突, 给使用者带来不必要的困扰;
- 在Android的打包流程中, 第一步是aidl处理, 源码编译, 三方库class收集; 第二步是Transform链式串联修改字节码; 第三步是dex阶段, 先进行重复类名检测, 再对字节码混淆; 第四步是对包进行签名;
- 即便是类名、方法名再简单, 该被混淆还是会被混淆.
拓展阅读
以上是关于Android开发——如何解决三方库中的类名冲突问题的主要内容,如果未能解决你的问题,请参考以下文章