最近在公司做的工作都是插件化相关,所以看了很多插件化的框架。整个插件化的方案现在是比较成熟的,怎样处理ClassLoader,怎么替换Activity生命周期,怎么去处理Receiver和Service,几个主流的框架基本上都是大同小异。我们团队选用了AndroidPluginFramework这个框架,具体的BenchMark其实在很多框架下面都可以看到。如何选取还是取决于自身需求,在插件化这块其实主流的需求一般是两种:
- 完全独立的插件。就是给一个APK和宿主没有关系,宿主可以不安装的情况下调起这个APK,让用户无感知。
-
非独立插件。这个其实是大部分公司的需求,就是随着公司业务的发展,客户端承载的业务越来越多,这个时候无论是从团队合作的角度还是动态化的角度,都希望各个业务之间解耦,发布能更加独立和动态。这种模式下,一般会抽出一个公共库,给各个组件提供基本功能,比如手淘还未开源的Atlas的结构。
借用一张架构图,公共库抽象出中间件那个模块,提供给各个组件基本的能力
atlas.png
AndroidPluginFramework这个框架是支持这两种场景的,但我们实际业务场景是第二种,非独立插件。公共库中包含里基本的网络,缓存,以及UI框架。当然独立插件最后出来也是独立的APK。基本背景介绍完了,接下来开始讲讲本文的主题吧,关于插件化时代码混淆的问题。这个问题应该很容易想到,我们公共库中提供出去给插件使用的类应该只在宿主中有一份,宿主打包的时候把公共库打包到宿主的APK中,插件只应该在编译过程中用到,gradle中以provide的方式依赖这些代码,比如我们工程中mobilebase是公共依赖库
provide files(project(':mobilebase').getBuildDir().absolutePath + '/intermediates/bundles/release/classes.jar')
provide files(project(':mobilebase').getBuildDir().absolutePath + '/outputs/rClasses.jar')
当然就会遇到一个问题是宿主在打release包的时候,会混淆mobilebase类,此时插件是不知道混淆的规则的,所以当插件想去调用公共库时就会ClassNotFound或者method不对。如何解决这个问题,有两种思路
- 完全不混淆mobilebase,keep住mobilebase中的所有东西。这个方案适用于你的公共库够薄的情况,比如你各个组件之间公用的东西很少,那适用这个方案。
- 使用相同的混淆规则。这个其实听上去相对合理一点的方案,宿主和插件使用相同的混淆的规则,理所当然能解决上面的问题。
我们其实公共库里的东西还是有点多的,所以准备用第二种方案。
Proguard在开启混淆时,会在app的 ****/build/outpust/mapping**** 目录下生成四个文件
dump.txt
说明 APK 中所有类文件的内部结构。
mapping.txt
提供原始与混淆过的类、方法和字段名称之间的转换。
seeds.txt
列出未进行混淆的类和成员。
usage.txt
列出从 APK 移除的代码。
其中mapping文件是混淆的规则,故我们只需要把这个文件用到插件的混淆配置中即可。所以拷贝这个文件到插件的目录,在插件的proguard-rules中添加
-applymapping mapping.txt
表示复用mappting,但由于混淆规则中的很多类插件是没有的,所以会有很多的Warning,所以我们配置一下ignore掉这些w,最终插件的混淆配置如下
-optimizationpasses 5
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-dontpreverify
-verbose
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
-ignorewarnings
-printseeds
-applymapping mapping.txt
OK,到这儿本以为能混淆的配置可以了,但运行时发现,插件中调用到公共库的地方并没有被正常混淆,还是找不到混淆后的方法。调研了发现provide的包gradle不会去混淆。。。
接下来就得去折腾multidex了
首先现在的问题是,我们不能把公共库打包到插件的apk中,但是以provide方式依赖又会出现无法混淆的问题。看了下AndroidPluginFramework官方提供的混淆建议(作者表示也没有试过)
具体方法:
1、开启混淆编译宿主,保留mapping文件
2、将插件的build.gradle文件中的provided配置换成compile, 因为provided方式提供的包不会被混淆
3、在插件的混淆配置中apply编译宿主时产生的mapping文件。
4、接着在插件编译脚本中开启multdex编译。并配置multdex的mainlist,使得原先所有provided的包的class被打入到副dex中。
这样插件编译完成后,会有2个dex,1个是插件自己需要的代码,1个是原先provided后来改成了compile的那些包。
5、再将这个原provided的包形成的dex,也就是副dex从apk中删除,再对插件apk重新签名。
简单来说就是先全部混淆,再利用multidex把之前provide的类全部打到第二个dex中,再删除第二个dex,再重新签名得到混淆后的插件APK。
那就去试用一下multidex这个官方的拆包的库,至于这个multidex的原理以及大量的坑网上都能搜到很多的分析文章,美团也有很多技术分析文章
我说说我在实施这个过程中的坑吧,就是如何实现把指定的class打包到Class2.dex中。因为我们需要把插件的类打到主dex,其余provide的类打包到第二个dex中。
这个问题网上最多的答案是在你插件的build.gradle中插入如下脚本
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
def listFile = project.rootDir.absolutePath + '/plugintest/maindexlist.txt'
println "root dir:" + project.rootDir.absolutePath
println "dex task found:" + dx.name
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += '--multi-dex'
dx.additionalParameters += '--main-dex-list=' + listFile
dx.additionalParameters += '--minimal-main-dex'
dx.additionalParameters += '--set-max-idx-number=20000'
}
}
这段脚本的意思是当你插件的gradle的task graph扫描完成的时候,在dexXXX的任务中插入几个参数,
- --main-dex-list= 这个是一个txt文件指明你想哪些类打包到主dex
- --minimal-main-dex 最小化主dex,保证主dex中只有上面参数指定的类
- --set-max-idx-number 每个dex中最多的方法数(不太确定,大概是这个意思,默认值65535)
网上的答案大部分是这段脚本,但你发现********并不会生效********。因为dexXXXDebug这个任务只在gradle1.5以下才有,之后就被隐藏了。
我现在版本是2.2应该怎么配置?
android{
dexOptions {
additionalParameters += '--main-dex-list=maindexlist.txt'
additionalParameters += '--minimal-main-dex'
additionalParameters += '--set-max-idx-number=20000'
}
}
在Android配置中添加这段即可,当然gradle1.5之后开始提供更多的第三方接口,所以也可以尝试使用
https://github.com/ceabie/DexKnifePlugin 这个分包插件来完成。
配置上以上混淆配置和multidex后,再打包插件,发现主dex中并没有我们预料的那些类,反而少了很多,反编译来看,貌似我们配置到maindexlist.txt中的类被混淆了。
网上查到原来maindexlist.txt中需要配置混淆之后的类名,这个就坑了,实用性大减,分包实在混淆之后,所以流程上来说确实要配置混淆之后的类名。为了简化这个过程,我最终选择keep住插件中的所有类,只会混淆公共依赖类。
那插件中的类怎么才能全部写到maindexlist中,当然写个脚本扫描一下代码目录即可
#!/bin/bash
SPATH=`pwd`'/src/main/java'
function walk()
{
for file in `ls $1`
do
local path=$1"/"$file
if [ -d $path ]
then
echo "DIR $path"
walk $path
else
a=${path#*/plugintest/src/main/java/}
echo ${a/.java/.class}>>maindexlist.txt
fi
done
}
echo $SPATH
walk $SPATH
上面得脚本放到插件根目录下,打包前跑一遍便会自动生成maindexlist.txt.
完成上述之后即可正确混淆,分包也正确,混淆规则和宿主一致。
截下来就是对于打出来的插件包,删除class2.dex并且重新打包签名
这也应该是由脚本来完成的工作,由于对于jar命令暂时发现只有update这个操作,所以比较low的方式创建了一个叫class2.dex的空文件用于覆盖打包后apk中的class2.dex。
脚本如下
#!/bin/bash
KEYSTORE_NAME=your key file
KEYSTORE_ALIAS=your key alias
KEYSTORE_STOREPASS=your key store password
KEYSTORE_KEYPASS=your key password
INPUT_APK=./build/outputs/apk/plugintest-release.apk
CLASS2=classes2.dex
META_INF=./META-INF
UNSIGNED=./build/outputs/apk/plugintest-release.apk
SIGNED=./build/outputs/apk/plugintest-release_resign.apk
OPT=./build/outputs/apk/plugintest-release_resign_align.apk
jar -uf $UNSIGNED $CLASS2
jar -uf $UNSIGNED $META_INF
echo Replace OK!
jarsigner -sigalg MD5withRSA -digestalg SHA1 -keystore $KEYSTORE_NAME -storepass $KEYSTORE_STOREPASS -keypass $KEYSTORE_KEYPASS -signedjar $SIGNED $UNSIGNED $KEYSTORE_ALIAS
echo Signe OK!
rm -r $OPT
zipalign 4 $SIGNED $OPT
echo Zipalign ok!
#rm -r $UNSIGNED
#rm -r $SIGNED
echo Operate OK!
注意一点是必须要从之前的apk中拷贝出META_INF下面的几个文件,才能完成正常的重新签名,否则插件lib在校验签名时会报失败:
WechatIMG88.jpeg
以上就是整个插件打包和混淆的过程,由于刚接触插件化不久,如果有更合理的混淆方案,请告知一下,搞这块还是挺蛋疼的,记录一下!
网友评论
欢迎订阅《最近是狗爷的独家号》https://toutiao.io/subjects/56636