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

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开发——如何解决三方库中的类名冲突问题的主要内容,如果未能解决你的问题,请参考以下文章

Android开发——如何解决三方库中的类名冲突问题

iOS项目中引用第三方库引发冲突的解决方法

如何解决两个不使用命名空间的第三方库之间的类名冲突?

Android 解决 jar/aar 包类名冲突

如何解决 Cocoa touch 框架中的符号名称冲突

应用程序和第三方库android之间的库版本冲突