最近在公司做的工作都是插件化相關(guān)不脯,所以看了很多插件化的框架李丰。整個(gè)插件化的方案現(xiàn)在是比較成熟的,怎樣處理ClassLoader廉嚼,怎么替換Activity生命周期,怎么去處理Receiver和Service倒戏,幾個(gè)主流的框架基本上都是大同小異怠噪。我們團(tuán)隊(duì)選用了AndroidPluginFramework這個(gè)框架,具體的BenchMark其實(shí)在很多框架下面都可以看到杜跷。如何選取還是取決于自身需求傍念,在插件化這塊其實(shí)主流的需求一般是兩種:
- 完全獨(dú)立的插件矫夷。就是給一個(gè)APK和宿主沒有關(guān)系,宿主可以不安裝的情況下調(diào)起這個(gè)APK憋槐,讓用戶無感知双藕。
-
非獨(dú)立插件。這個(gè)其實(shí)是大部分公司的需求阳仔,就是隨著公司業(yè)務(wù)的發(fā)展忧陪,客戶端承載的業(yè)務(wù)越來越多,這個(gè)時(shí)候無論是從團(tuán)隊(duì)合作的角度還是動(dòng)態(tài)化的角度近范,都希望各個(gè)業(yè)務(wù)之間解耦嘶摊,發(fā)布能更加獨(dú)立和動(dòng)態(tài)。這種模式下评矩,一般會(huì)抽出一個(gè)公共庫叶堆,給各個(gè)組件提供基本功能,比如手淘還未開源的Atlas的結(jié)構(gòu)斥杜。
借用一張架構(gòu)圖虱颗,公共庫抽象出中間件那個(gè)模塊,提供給各個(gè)組件基本的能力
AndroidPluginFramework這個(gè)框架是支持這兩種場景的果录,但我們實(shí)際業(yè)務(wù)場景是第二種,非獨(dú)立插件咐熙。公共庫中包含里基本的網(wǎng)絡(luò)弱恒,緩存,以及UI框架棋恼。當(dāng)然獨(dú)立插件最后出來也是獨(dú)立的APK返弹。基本背景介紹完了爪飘,接下來開始講講本文的主題吧义起,關(guān)于插件化時(shí)代碼混淆的問題。這個(gè)問題應(yīng)該很容易想到师崎,我們公共庫中提供出去給插件使用的類應(yīng)該只在宿主中有一份默终,宿主打包的時(shí)候把公共庫打包到宿主的APK中,插件只應(yīng)該在編譯過程中用到犁罩,gradle中以provide的方式依賴這些代碼齐蔽,比如我們工程中mobilebase是公共依賴庫
provide files(project(':mobilebase').getBuildDir().absolutePath + '/intermediates/bundles/release/classes.jar')
provide files(project(':mobilebase').getBuildDir().absolutePath + '/outputs/rClasses.jar')
當(dāng)然就會(huì)遇到一個(gè)問題是宿主在打release包的時(shí)候,會(huì)混淆mobilebase類床估,此時(shí)插件是不知道混淆的規(guī)則的含滴,所以當(dāng)插件想去調(diào)用公共庫時(shí)就會(huì)ClassNotFound或者method不對(duì)。如何解決這個(gè)問題丐巫,有兩種思路
- 完全不混淆mobilebase谈况,keep住mobilebase中的所有東西勺美。這個(gè)方案適用于你的公共庫夠薄的情況,比如你各個(gè)組件之間公用的東西很少碑韵,那適用這個(gè)方案赡茸。
- 使用相同的混淆規(guī)則。這個(gè)其實(shí)聽上去相對(duì)合理一點(diǎn)的方案泼诱,宿主和插件使用相同的混淆的規(guī)則坛掠,理所當(dāng)然能解決上面的問題。
我們其實(shí)公共庫里的東西還是有點(diǎn)多的治筒,所以準(zhǔn)備用第二種方案屉栓。
Proguard在開啟混淆時(shí),會(huì)在app的 ****/build/outpust/mapping**** 目錄下生成四個(gè)文件
dump.txt
說明 APK 中所有類文件的內(nèi)部結(jié)構(gòu)耸袜。
mapping.txt
提供原始與混淆過的類友多、方法和字段名稱之間的轉(zhuǎn)換。
seeds.txt
列出未進(jìn)行混淆的類和成員堤框。
usage.txt
列出從 APK 移除的代碼域滥。
其中mapping文件是混淆的規(guī)則,故我們只需要把這個(gè)文件用到插件的混淆配置中即可蜈抓。所以拷貝這個(gè)文件到插件的目錄启绰,在插件的proguard-rules中添加
-applymapping mapping.txt
表示復(fù)用mappting,但由于混淆規(guī)則中的很多類插件是沒有的沟使,所以會(huì)有很多的Warning,所以我們配置一下ignore掉這些w委可,最終插件的混淆配置如下
-optimizationpasses 5
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-dontpreverify
-verbose
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
-ignorewarnings
-printseeds
-applymapping mapping.txt
OK,到這兒本以為能混淆的配置可以了腊嗡,但運(yùn)行時(shí)發(fā)現(xiàn)着倾,插件中調(diào)用到公共庫的地方并沒有被正常混淆燕少,還是找不到混淆后的方法卡者。調(diào)研了發(fā)現(xiàn)provide的包gradle不會(huì)去混淆。客们。崇决。
接下來就得去折騰multidex了
首先現(xiàn)在的問題是,我們不能把公共庫打包到插件的apk中底挫,但是以provide方式依賴又會(huì)出現(xiàn)無法混淆的問題嗽桩。看了下AndroidPluginFramework官方提供的混淆建議(作者表示也沒有試過)
具體方法:
1凄敢、開啟混淆編譯宿主碌冶,保留mapping文件
2、將插件的build.gradle文件中的provided配置換成compile涝缝, 因?yàn)閜rovided方式提供的包不會(huì)被混淆
3扑庞、在插件的混淆配置中apply編譯宿主時(shí)產(chǎn)生的mapping文件譬重。
4、接著在插件編譯腳本中開啟multdex編譯罐氨。并配置multdex的mainlist臀规,使得原先所有provided的包的class被打入到副dex中。
這樣插件編譯完成后栅隐,會(huì)有2個(gè)dex塔嬉,1個(gè)是插件自己需要的代碼,1個(gè)是原先provided后來改成了compile的那些包租悄。
5谨究、再將這個(gè)原provided的包形成的dex,也就是副dex從apk中刪除泣棋,再對(duì)插件apk重新簽名胶哲。
簡單來說就是先全部混淆,再利用multidex把之前provide的類全部打到第二個(gè)dex中潭辈,再刪除第二個(gè)dex鸯屿,再重新簽名得到混淆后的插件APK。
那就去試用一下multidex這個(gè)官方的拆包的庫把敢,至于這個(gè)multidex的原理以及大量的坑網(wǎng)上都能搜到很多的分析文章寄摆,美團(tuán)也有很多技術(shù)分析文章
我說說我在實(shí)施這個(gè)過程中的坑吧,就是如何實(shí)現(xiàn)把指定的class打包到Class2.dex中修赞。因?yàn)槲覀冃枰巡寮念惔虻街鱠ex婶恼,其余provide的類打包到第二個(gè)dex中。
這個(gè)問題網(wǎng)上最多的答案是在你插件的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'
}
}
這段腳本的意思是當(dāng)你插件的gradle的task graph掃描完成的時(shí)候榔组,在dexXXX的任務(wù)中插入幾個(gè)參數(shù)熙尉,
- --main-dex-list= 這個(gè)是一個(gè)txt文件指明你想哪些類打包到主dex
- --minimal-main-dex 最小化主dex联逻,保證主dex中只有上面參數(shù)指定的類
- --set-max-idx-number 每個(gè)dex中最多的方法數(shù)(不太確定搓扯,大概是這個(gè)意思,默認(rèn)值65535)
網(wǎng)上的答案大部分是這段腳本包归,但你發(fā)現(xiàn)********并不會(huì)生效********锨推。因?yàn)閐exXXXDebug這個(gè)任務(wù)只在gradle1.5以下才有,之后就被隱藏了公壤。
我現(xiàn)在版本是2.2應(yīng)該怎么配置换可?
android{
dexOptions {
additionalParameters += '--main-dex-list=maindexlist.txt'
additionalParameters += '--minimal-main-dex'
additionalParameters += '--set-max-idx-number=20000'
}
}
在Android配置中添加這段即可,當(dāng)然gradle1.5之后開始提供更多的第三方接口厦幅,所以也可以嘗試使用
https://github.com/ceabie/DexKnifePlugin 這個(gè)分包插件來完成沾鳄。
配置上以上混淆配置和multidex后,再打包插件确憨,發(fā)現(xiàn)主dex中并沒有我們預(yù)料的那些類译荞,反而少了很多瓤的,反編譯來看,貌似我們配置到maindexlist.txt中的類被混淆了吞歼。
網(wǎng)上查到原來maindexlist.txt中需要配置混淆之后的類名圈膏,這個(gè)就坑了,實(shí)用性大減篙骡,分包實(shí)在混淆之后稽坤,所以流程上來說確實(shí)要配置混淆之后的類名。為了簡化這個(gè)過程糯俗,我最終選擇keep住插件中的所有類尿褪,只會(huì)混淆公共依賴類。
那插件中的類怎么才能全部寫到maindexlist中叶骨,當(dāng)然寫個(gè)腳本掃描一下代碼目錄即可
#!/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
上面得腳本放到插件根目錄下茫多,打包前跑一遍便會(huì)自動(dòng)生成maindexlist.txt.
完成上述之后即可正確混淆,分包也正確,混淆規(guī)則和宿主一致忽刽。
截下來就是對(duì)于打出來的插件包天揖,刪除class2.dex并且重新打包簽名
這也應(yīng)該是由腳本來完成的工作,由于對(duì)于jar命令暫時(shí)發(fā)現(xiàn)只有update這個(gè)操作跪帝,所以比較low的方式創(chuàng)建了一個(gè)叫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!
注意一點(diǎn)是必須要從之前的apk中拷貝出META_INF下面的幾個(gè)文件,才能完成正常的重新簽名伞剑,否則插件lib在校驗(yàn)簽名時(shí)會(huì)報(bào)失敯呋!:
以上就是整個(gè)插件打包和混淆的過程,由于剛接觸插件化不久黎泣,如果有更合理的混淆方案恕刘,請(qǐng)告知一下,搞這塊還是挺蛋疼的抒倚,記錄一下褐着!