From:Android插件化開發(fā)指南
目錄
- 預(yù)備知識(shí)
1.1 簡(jiǎn)介
?插件化的用途
?插件化的發(fā)展史
1.2 Binder原理
1.3 Activity工作原理
?App啟動(dòng)流程 / App內(nèi)部頁(yè)面跳轉(zhuǎn)
1.4 PMS
1.5 ClassLoader
1.6 反射
1.7 代理模式 - 插件化知識(shí)
2.1 加載外部類
2.2 插件的Application
2.3 訪問插件中的類
?方案1:把插件dex合并到宿主dex
?方案2:為每個(gè)插件創(chuàng)建ClassLoader
?方案3:Hook App原生的ClassLoader
2.4 訪問插件中的資源
?2.4.1 資源簡(jiǎn)介
??AssetManager
??Resources
?2.4.2 資源訪問
??方案1:在宿主Activity中創(chuàng)建插件的AssetManager
??方案2:宿主插件共用AssetManager
?2.4.3 資源id沖突
??方案1:修改AAPT構(gòu)建工具
??方案2:修改R.java & resources.arsc文件
2.5 最簡(jiǎn)單的實(shí)現(xiàn)一個(gè)插件化 - 插件化四大組件
3.1 Activity
?3.1.1 動(dòng)態(tài)框架
??上半場(chǎng):用Stub欺騙AMS
??下半場(chǎng):?jiǎn)?dòng)真實(shí)Activity
??解決LaunchMode問題
?3.1.2 靜態(tài)代理that框架
??解決LaunchMode問題
3.2 Service
?3.2.1 動(dòng)態(tài)框架
??startService的解決方案
??bindService的解決方案
?3.2.2 靜態(tài)方案
3.3 BroadcastReceiver
?3.3.1 動(dòng)態(tài)方案
??動(dòng)態(tài)廣播的解決方案
??靜態(tài)廣播的解決方案
???方案1:把靜態(tài)廣播轉(zhuǎn)換為動(dòng)態(tài)廣播
???方案2:占位StubReceiver
?3.3.2 靜態(tài)框架 - 插件化相關(guān)知識(shí)
4.1 基于Fragment的插件化
4.2 插件的混淆
?方案1:不混淆公共庫(kù)midlib
?方案2:混淆公共庫(kù)midlib
4.3 增量更新
4.4 so的插件化
?4.4.1 so知識(shí)的簡(jiǎn)介
?4.4.2 so的加載流程
?4.4.3 so的加載方法
?4.4.4 基于System.loadLibrary的so插件化
?4.4.5 基于System.load的so插件化
4.5 自定義Gradle
?Extension動(dòng)態(tài)設(shè)置
?afterEvaluate應(yīng)用
1. 預(yù)備知識(shí)
1.1 簡(jiǎn)介
插件化的用途
游戲平臺(tái),按需下載第队⊥馑。【體積 & 更新】PC是译红,而Android采用服務(wù)器動(dòng)態(tài)下發(fā)腳本以现。
動(dòng)態(tài)更新:增加新功能或完整的模塊匀归,80%用于修復(fù)線上bug。
換膚:用于游戲領(lǐng)域运敢,王者榮耀的換膚校仑,上線新英雄,調(diào)整數(shù)據(jù)者冤。
ABTest肤视,數(shù)據(jù)驅(qū)動(dòng)產(chǎn)品。
獨(dú)立性 & 開發(fā)效率【組件化涉枫?邢滑??】
插件化的未來:虛擬機(jī)技術(shù) — — 應(yīng)用雙開愿汰。
國(guó)內(nèi)對(duì)RN等相關(guān)技術(shù)的需求遠(yuǎn)大于插件化困后;GooglePlay不允許插件化App的存在。
插件化的發(fā)展史
2012.07 —— 大眾點(diǎn)評(píng)衬廷,基于Fragment摇予。
2013.03 —— 淘寶Atlas,未開源吗跋。
2014.03 —— 任玉剛侧戴,that框架靜態(tài)代理宁昭,非Hook。
2014.11 —— 提出StubActivity欺騙AMS酗宋。
2014.12 —— Android Studio1.0积仗,可以借助Gradle。
2015.08 —— 360手機(jī)助手張勇蜕猫,DroidPlugin寂曹。
2015.12 —— 林光亮Small框架。
此時(shí)回右,插件化中遇到的技術(shù)難題都已解決隆圆。【開始關(guān)注:熱修復(fù)技術(shù)和RN】
2017.06 —— 360手機(jī)助手RePlugin翔烁。
可見渺氧,一項(xiàng)技術(shù)5年時(shí)間內(nèi)由雛形到成熟。
1.2 Binder原理
- Client蹬屹、Service阶女、ServiceManager三者關(guān)系。
- AIDL
- Binder哩治、IBinder、IInterface衬鱼、Stub.asInterface()业筏、asBinder()、onTransact()
- 問題:
1)類結(jié)構(gòu)層級(jí)設(shè)計(jì)原理鸟赫?
2)跨進(jìn)程與同進(jìn)程是如何區(qū)分蒜胖?
3)onTransact在同一進(jìn)程如何被調(diào)用?
4)一次通信的過程抛蚤,數(shù)據(jù)如何傳遞和解析台谢?
1.3 Activity工作原理
App啟動(dòng)流程 / App內(nèi)部頁(yè)面跳轉(zhuǎn)
- Launcher在一個(gè)不同的進(jìn)程。
- App安裝時(shí)岁经,Android系統(tǒng)中的PackageManagerService從apk包中的AndroidManifest文件中讀取信息朋沮。
- Launcher、App缀壤、AMS關(guān)系樊拓。
- 啟動(dòng)流程√聊剑【ActivityThread中有main入口筋夏,即主線程】
mMainThread的類型為ActivityThread
ActivityThread中持有Instrumentation【儀表盤】的引用。在performLaunchActivity()中activity.attach(...)將其傳遞給Activity類图呢。
ApplicationThread extends IApplicationThread.Stub
ActivityManagerService extends IActivityManager.Stub
App端通過獲取IActivityManager調(diào)用AMS中的方法条篷。
AMS端通過獲取IApplicationThread調(diào)用App端的方法骗随,如:bindApplication。ApplicationThread調(diào)用ActivityThread的sendMessage赴叹,通過H類調(diào)用分發(fā)鸿染。
最終:Instrumentation newActivity > callActivityOnCreate > onCreate。
Activity中哪個(gè)類扮演Stub稚瘾,哪個(gè)類扮演Proxy牡昆?
ServiceManager.getService("xxx") // 連接池?
一個(gè)應(yīng)用中Context的個(gè)數(shù) = Service個(gè)數(shù)+Activity個(gè)數(shù)+1(Application的)
getApplicationContext在ContextImpl中實(shí)現(xiàn)摊欠,返回的就是在ActivityThread main()方法中初始化的Application對(duì)象丢烘。
1.4 PMS
PMS加載包的信息,將其封裝在LoadedApk這個(gè)類對(duì)象中些椒,然后可以從中取出AndroidManifest中的信息播瞳。
結(jié)束安裝的時(shí)候,都會(huì)把安裝信息保存在xml文件中免糕,當(dāng)Android系統(tǒng)再次啟動(dòng)時(shí)赢乓,會(huì)重新安裝所有的apk,就可以直接讀取之前保存的xml文件石窑。
Android的5個(gè)安裝目錄:data/app-private牌芋、data/app、system/app松逊、vender/app躺屁、system/framework。
Android系統(tǒng)重啟后经宏,會(huì)重新安裝所有的App犀暑,這是由PMS類完成,并且App首次安裝到手機(jī)上也是由PMS完成烁兰。
PMS中的一個(gè)類PackageParse耐亏,用來解析AndroidManifest文件,通過反射調(diào)用generatePackageInfo()來獲取插件中的四大組件沪斟。
涉及到AIDL:IPackageManager
1.5 ClassLoader
DexClassLoader可以根據(jù)optimizedDirectory加載需要的【dex广辰、apk、jar】文件主之,并創(chuàng)建一個(gè)DexFile對(duì)象轨域,也可以從外部SD卡加載。
對(duì)于App而言杀餐,Apk文件中有一個(gè)classes.dex干发,他是Apk的主dex,通過PathClassLoader加載史翘,它的父類是BaseDexClassLoader枉长。MultiDex把一個(gè)dex文件拆分成多個(gè)dex文件冀续,每個(gè)dex的方法數(shù)量不超過65536個(gè),classes.des主dex由PathClassLoader加載必峰,其它c(diǎn)lasses2.dex等會(huì)在App啟動(dòng)后使用DexClassLoader加載洪唐。
可以讓classes.dex中只保留App啟動(dòng)時(shí)所需要的類以及首頁(yè)的代碼,從而確保App進(jìn)入首頁(yè)時(shí)間最少吼蚁。
如何手動(dòng)指定classes2.dex中包含哪些類的代碼凭需?
gradle配置:
dexOptions{
additionalParameters += '--main-dex-list=maindexlist.txt'
}
增加maindexlist.txt文件,里面包括要將哪些文件保留在主dex中肝匆,注意是class文件粒蜈。如:
ljg/aaa/a.class
ljg/aaa/b.class
后面有一個(gè)詳細(xì)的例子。
1.6 反射
- 如何反射一個(gè)泛型類
- 網(wǎng)絡(luò)數(shù)據(jù)解析 & Json2Bean是如何利用反射實(shí)現(xiàn)的旗国?
- setAccessible(true)的本質(zhì)枯怖?【跳過校驗(yàn)】
- jOOR庫(kù),在Android中不支持反射final類型的字段能曾,因?yàn)椋篈ndroid的Field類中沒有定義final字段度硝。
1.7 代理模式
- 動(dòng)態(tài)代理的原理?
- 生成的代理類是什么樣子的寿冕?
- PMS是系統(tǒng)服務(wù)蕊程,為什么沒有辦法Hook?
只能Hook App自己進(jìn)程的東西驼唱,Hook永遠(yuǎn)只在Client端存捺,若在Service端那就是病毒了。所以曙蒸,App只能對(duì)App所在的進(jìn)程進(jìn)程Hook,所影響的范圍也僅限于App本身岗钩。
Java動(dòng)態(tài)代理只能代理接口纽窟,不能代理類, 為什么兼吓?如何破臂港?
Java動(dòng)態(tài)代理是由Java內(nèi)部的反射機(jī)制來實(shí)現(xiàn)的,而cglib動(dòng)態(tài)代理底層則是借助asm來實(shí)現(xiàn)的视搏。https://blog.csdn.net/u010111422/article/details/69062338
在Hook過程中审孽,什么時(shí)候用靜態(tài)代理【暴露類】,什么時(shí)候用動(dòng)態(tài)代理【暴露接口】
Hook AMS => ActivityManagerNative :: gDefault :: Singleton :: mInstance
Hook ActivityThread => sCurrentActivityThread :: mH :: mCallBack
2. 插件化知識(shí)
2.1 加載外部類
ClassLoader classLoader = new DexClassLoader("assets/aaa.apk", getAbsolutePath(), null, getClassLoader());
Class mLoadClassBean = classLoader.loadClass("plugin.test.AaaBean");
Object beanObject = mLoadClassBean.newInstance();
Method method = mLoadClassBean.getMethod("getName");
method.setAccessible(true);
String name = (String)method.invoke(beanObject);
利用反射可以不用引起其對(duì)象浑娜,調(diào)用其類中的方法時(shí)也要通過反射佑力。如果使用接口編程,在反射出對(duì)象后筋遭,可以直接類型轉(zhuǎn)換為該接口對(duì)象打颤,從而可以直接調(diào)用類中的方法暴拄,不再通過反射。
2.2 插件的Application
插件Application的onCreate是沒有機(jī)會(huì)調(diào)用的,除非我們?cè)谒拗髯远x的Application的onCreate方法中利用反射來執(zhí)行插件們的onCreate方法。因此枚驻,插件Application沒有生命周期编曼,它就是一個(gè)普通的類。
2.3 訪問插件中的類
方案1:把插件dex合并到宿主dex
BaseDexClassLoader :: pathList :: dexElements[ ]
方案2:為每個(gè)插件創(chuàng)建ClassLoader
為每個(gè)插件創(chuàng)建一個(gè)ClassLoader屯援,把LoadedApk類中mClassLoader替換為插件的ClassLoader。
ActivityThread :: currentActivityThread :: mPackages
mPackages中緩存dex文件。
為插件創(chuàng)建loadedApk鲸沮,然后mPackages.put(packageName, loadedApk)
loadedApk :: mClassLoader 賦值為插件的ClassLoader。
缺陷:Hook的點(diǎn)太多
方案3:Hook App原生的ClassLoader
修改App原生的ClassLoader【mPackageInfo :: mClassLoader】养距。構(gòu)建一個(gè)SuperClassLoader類诉探,它內(nèi)部有一個(gè)mClassLoaderList變量,即持有所有插件ClassLoader的集合棍厌。于是SuperClassLoader的loadClass()方法肾胯,會(huì)先嘗試使用宿主的ClassLoader【即系統(tǒng)的】加載類,如果不能加載耘纱,就遍歷插件的ClassLoader敬肚。
注意:使用該方案加載插件中的類時(shí),不能再使用Class.forName()方法來反射插件中的類了束析,因?yàn)镃lass.forName會(huì)使用BootClassLoader來加載類艳馒,這個(gè)類并沒有被Hook。應(yīng)該使用:getClassLoader().loadClass()來反射類员寇。
2.4 訪問插件中的資源
2.4.1 資源簡(jiǎn)介
將插件放在宿主的assets目錄中弄慰,App啟動(dòng)時(shí)會(huì)把a(bǔ)ssets目錄中的東西加載到內(nèi)存中〉妫【assets目錄不編譯】
AssetManager
AssetManager的addAssetPath方法可以解決資源的插件化陆爽。由于apk下載后不會(huì)解壓到本地,所以無法直接獲取到assets的絕對(duì)路徑扳缕。只能通過AssetManager類的open方法來獲取assets目錄下的文件資源慌闭。AssetManager中的addAssetPath方法,App啟動(dòng)時(shí)會(huì)把當(dāng)前apk的路徑傳遞進(jìn)去躯舔,從而能夠訪問當(dāng)前apk的所有資源驴剔。傳插件的路徑時(shí),就能訪問插件中的資源了粥庄。
Resources
Resources是外暴露的類 => 調(diào)用AssetManager中的方法 => 訪問resources.arsc文件丧失。resources.arsc在打包時(shí)生成。
2.4.2 資源訪問
方案1:在宿主Activity中創(chuàng)建插件的AssetManager
宿主中讀取插件里的資源:
1)反射創(chuàng)建AssetManager對(duì)象惜互,調(diào)用addAssetPath方法利花,把插件的路徑添加到這個(gè)AssetManager對(duì)象中科侈,這個(gè)對(duì)象只為該插件服務(wù)。并根據(jù)該AssetManager對(duì)象創(chuàng)建相應(yīng)的Resources和Theme對(duì)象炒事。
2)重寫Activity的getAsset()臀栈、getResources()和getTheme()方法,返回新創(chuàng)建的插件對(duì)象挠乳。【如果沒有則默認(rèn)讀取宿主中的資源】
3)宿主中加載外部插件权薯,生成該插件的ClassLoader。通過反射獲取插件中的類睡扬,從而讀取插件中的資源盟蚣。
// 插件中被調(diào)用的方法
public String getStringF(Context context){
return context.getResources().getString(R.string.hello);
}
注意:反射調(diào)用插件中的getStringF方法時(shí),傳入的context是宿主中的MainActivity.this卖怜,因?yàn)樗拗鰽ctivity的getResources已經(jīng)被覆寫屎开,此時(shí)返回的是該插件的AssetManager所創(chuàng)建的Resources對(duì)象。
當(dāng)宿主需要某個(gè)插件中的資源時(shí)马靠,才會(huì)loadResource奄抽,即利用反射為某插件生成AssetManager對(duì)象和與其相關(guān)的Resources、Theme甩鳄,再反射調(diào)用addAssetPath方法逞度。宿主默認(rèn)是加載自己的資源。
將插件中的getStringF()移到宿主中去定義了妙啃,插件不做任何事
R.java中的內(nèi)部類:
R.java中的string類:
該R.java會(huì)存在apk包的classes.dex文件中档泽,宿主可以直接訪問插件中R.java的內(nèi)部類如:string、id揖赴、color等馆匿。
Class stringClass = pluginClassLoader.loadClass("com.ljg.plugin.R$string");
int resId = stringClass.getDeclaredField("a_plus");
tv.setText(getResources().getString(resId));
其中,getResources()方法返回插件的Resources燥滑。
插件如何訪問插件中的資源呢渐北?插件不能自動(dòng)加載自身的資源,因?yàn)樵摬寮械馁Y源并沒有addAssetPath到資源池中突倍。所以,跟宿主訪問一樣盆昙,一樣需要反射AssetManager并調(diào)用addAssetPath羽历,同時(shí)還要覆寫getAsset()、getResources()和getTheme()方法淡喜。
總結(jié):該方案不會(huì)合并宿主和插件的資源秕磷,進(jìn)入到哪個(gè)插件,就為這個(gè)插件創(chuàng)建AssetManager和Resource對(duì)象炼团,AssetManager通過反射調(diào)用addAssetPath方法澎嚣,把插件自己的資源添加進(jìn)去疏尿,當(dāng)宿主進(jìn)入到一個(gè)插件的時(shí)候,就把AssetManager切換為該插件的AssetManager易桃,所以插件就只能加載到插件中的資源了褥琐。
方案2:宿主插件共用AssetManager
構(gòu)建一個(gè)超級(jí)AssetManager對(duì)象,在addAssetPath時(shí)晤郑,添加宿主和所有插件的資源敌呈。該Resources為全局變量≡烨蓿【宿主和插件如何共享數(shù)據(jù)磕洪??诫龙?】
注意:插件Activity中必須覆寫getResources()方法析显,返回超級(jí)Resources全局變量。
public Resources getResources() {
return PluginManager.mSuperResources;
}
方案2會(huì)存在資源id沖突問題签赃,如何解決呢谷异?在下一節(jié)介紹。
2.4.3 資源id沖突
背景:把宿主和插件的資源合并到一起姊舵,通過AssetManager的addAssetPath來實(shí)現(xiàn)晰绎,此方案會(huì)產(chǎn)生資源id沖突。
原因:宿主App和各插件App都是各自打包括丁。
思路:Hook App打包過程中的aapt階段荞下。
Android打包流程:
1、aapt史飞。為res目錄的資源生成R.java文件尖昏,同時(shí)為AndroidManifest.xml生成Manifest.java文件。
2构资、aidl抽诉。把項(xiàng)目中定義的aidl文件生成相應(yīng)的Java代碼。
3吐绵、javac迹淌。自己編寫的代碼+aapt生成的Java文件+aidl生成的Java文件,編譯成class文件己单。
4唉窃、proguard∥屏混淆的同時(shí)生成proguardMapping.txt文件纹份。
5、dex。自己項(xiàng)目中生成的class文件+第三方庫(kù)的class文件蔓涧,轉(zhuǎn)換為dex文件件已。
6、aapt元暴。打包篷扩,把res目錄下的資源、assets目錄下的文件昨寞,打包成一個(gè).ap_文件瞻惋。
7、apkbuilder援岩。將所有的dex文件+.ap_文件+AndroidManifest.xml打包為.apk文件歼狼。
8、jarsigner享怀。對(duì)apk進(jìn)行簽名羽峰。
9、zipalign添瓷。對(duì)要發(fā)布的apk文件進(jìn)行對(duì)齊操作梅屉,以便運(yùn)行時(shí)節(jié)省內(nèi)存。
方案1:修改AAPT構(gòu)建工具
資源id的定義格式:public static final int fade_in=0x7f050023鳞贷;該十六進(jìn)制由三部分組成:PackageId【7f】+ TypeId 【05】+ EntryId【0023】
PackageId:apk包的id坯汤,默認(rèn)為0x7f。
TypeId:資源類型值搀愧,如:layout惰聂、id、string咱筛、drawable搓幌。
具體過程:
1)修改AAPT這個(gè)Android SDK工具,在AAPT的命令行參數(shù)中指定插件資源id的前綴迅箩。一般選用0x71~0xff這個(gè)區(qū)間內(nèi)的值作為前綴溉愁。
2)把修改后的AAPT工具命名為aapt_mac,放在項(xiàng)目根目錄下饲趋。
3)修改gradle拐揭,通過腳本反射,把AAPT的路徑修改為該App根路徑下的aapt_mac奕塑。
public.xml固定id值
場(chǎng)景:多個(gè)插件都需要一個(gè)自定義控件堂污,把它放在宿主中,插件調(diào)用宿主的Java代碼和使用宿主的資源爵川。
問題:App每次打包后敷鸦,會(huì)隨著資源的增加息楔,同一個(gè)資源的id值也會(huì)發(fā)生變化寝贡。
方案:如果宿主App的某個(gè)資源id被插件使用扒披,那么為了避免下次因資源值變化而導(dǎo)致資源找不到,需要把這個(gè)資源id值寫死圃泡,這個(gè)固定的值要保存在public.xml文件中碟案,放在res/values/目錄下。
<resources>
<public type="string" name="house_name" id="0x7f092234">
</resources>
在gradle1.3版本之前是默認(rèn)支持public.xml的颇蜡,但之后不再支持了价说,所以要在build.gradle中添加相應(yīng)任務(wù)。
應(yīng)用:插件如何使用宿主中的固定資源风秤?把宿主打包成jar包被各插件compileOnly鳖目,在插件中使用StringConstant.house_name
。StringConstant類是根據(jù)public.xml自動(dòng)生成的缤弦。
方案2:修改R.java & resources.arsc文件
Android中的兩類資源AssetManager和Resources领迈,其中AssetManager直接通過文件名稱就可以獲取到具體資源,而Resources先在resources.arsc文件中通過id查找到資源文件名稱碍沐,然后再通過AssetManager來獲取資源狸捅。
優(yōu)化:resources.arsc中存放了很多冗余的資源。因?yàn)槲覀冮_發(fā)時(shí)引入的AppCompat包累提、Design包尘喝,這些包也要生成資源id。對(duì)插件而言每個(gè)插件包的resources.arsc文件中都會(huì)有一份相同的資源斋陪,這樣就冗余了朽褪。所以對(duì)于插件中AppCompat包、Design包資源會(huì)在resources.arsc中刪除鳍贾,只會(huì)在宿主的resources.arsc中存在鞍匾。
具體過程:
1)aapt會(huì)生成R.java文件,Hook processReleaseResources這個(gè)task骑科,在它之后將R.java文件中的0x7f修改為0x71橡淑。【注:R.java文件不能修改咆爽,只能重新建一份保存】
2)aapt還會(huì)生成一個(gè)后綴為ap_的壓縮包梁棠,里面有AndroidManifest.xml、res斗埂、asset符糊、resources.arsc文件,解壓取出resources.arsc呛凶,把里面的0x7f修改為0x71男娄。
3)刪除resources.arsc文件中的冗余的資源Id,如AppCompat庫(kù)。
4)Hook compileReleaseJavaWithJavac模闲,把所有class中的R$drawable.class建瘫、R$layout.class這樣的class刪除,因?yàn)樗鼈冎斜4娴馁Y源Id值還是以0x7f為前綴尸折。
5)將步驟1中新生成的R.java文件啰脚,執(zhí)行javac,生成R.class文件实夹。
疑惑:步驟4橄浓、5有必要嗎?在步驟1中亮航,不能將新生成的R.java替換舊的嗎荸实?
2.5 最簡(jiǎn)單的實(shí)現(xiàn)一個(gè)插件化
1)合并所有插件的dex,來解決插件的類加載問題缴淋。
BaseDexClassLoader :: pathList :: dexElements泪勒。dexElements類型是Element[ ]數(shù)組,即利用反射把宿主和插件中的Element[ ]合并到一起宴猾,替換dexElements的值圆存。
2)把插件中所有的資源統(tǒng)一性地合并到宿主的資源中〕鸲撸【可能導(dǎo)致資源id沖突】
3)預(yù)先在宿主的AndroidManifest文件中聲明插件的四大組件沦辙。
提示:AndroidManifest文件中可以聲明不存在的Activity類。AndroidManifest文件只做格式校驗(yàn)讹剔,不會(huì)進(jìn)行編譯油讯。
3. 插件化四大組件
3.1 Activity
3.1.1 動(dòng)態(tài)框架
上半場(chǎng):用Stub欺騙AMS
ActivityManagerNative :: gDefault :: mInstance :: Singleton :: IActivityManager
下半場(chǎng):?jiǎn)?dòng)真實(shí)Activity
ActivityThread :: sCurrentActivityThread :: mH :: mCallback
解決LaunchMode問題
問題:AMS會(huì)認(rèn)為每次要打開都是StubActivity,在AMS端有個(gè)棧延欠,會(huì)存放每次要打開的Activity陌兑,那么現(xiàn)在這個(gè)棧上就都是StubActivity了。插件中設(shè)置的singleTask由捎、singleTop和singleInstance都無效兔综。
解決:占位思想。事先為SingleTop狞玛、SingleTask软驰、SingleInstance這三種LaunchMode創(chuàng)建多個(gè)StubActivity,指定插件Activity與哪個(gè)StubActivity對(duì)應(yīng)關(guān)系心肪。
在插件AndroidManifest中設(shè)置的許多屬性都是無效的锭亏。
3.1.2 靜態(tài)代理that框架
每次都是啟動(dòng)宿主中的ProxyActivity,攜帶參數(shù):要打開頁(yè)面所在插件的路徑dexPath和要打開Activity的全路徑名硬鞍。在宿主ProxyActivity中反射插件中的要啟動(dòng)的Activity類慧瘤,但反射出來的Activity是一個(gè)普通的類戴已,不具有Activity的生命周期。所以要在ProxyActivity的聲明周期方法中調(diào)用插件Activity的相應(yīng)方法锅减,以此來同步Activity的聲明周期恭陡。同時(shí)ProxyActivity中通過反射調(diào)用setProxy(this)與PluginActivity建立雙向通信,在PluginActivity中持有ProxyActivity的引用命名為that上煤。由于插件中定義的Activity都是一個(gè)木偶,而非真正的Activity著淆,所以this.setContentView();
和this.findViewById();
就會(huì)運(yùn)行時(shí)報(bào)錯(cuò)誤劫狠,而改為that.setContentView();
和that.findViewById();
。
問題:為什么Hook之后會(huì)有生命周期呢永部?独泞??
消滅that關(guān)鍵字
基類中實(shí)現(xiàn)苔埋,但Activity的final方法不能覆寫只能使用that調(diào)用懦砂。
@Override
public View findViewById(int id){
return that.findViewById(id);
}
跳轉(zhuǎn)
宿主跳插件;宿主跳宿主组橄;插件跳宿主荞膘;插件跳插件。
只有在跳插件時(shí)玉工,才會(huì)使用ProxyActivity羽资。
接口簡(jiǎn)化
在靜態(tài)代理中使用面向接口的編程思想來減少反射的使用。
解決LaunchMode問題
維護(hù)一個(gè)atyStack集合遵班,它持有所有打開的插件Activity屠升。
switch(launchMode){
case Standard:
正常存入集合atyStack中;
break;
case SingleTop:
判斷atyStack倒數(shù)第二個(gè)元素是否即將打開的插件Activity狭郑,如果是則移除腹暖,并調(diào)用其finish()方法;
break;
case SingleTask:
移除這個(gè)元素以及在它之上的元素翰萨,并調(diào)用finish()方法脏答;
break;
case SingleInstance:
只把這個(gè)元素移除,并調(diào)用finish()方法亩鬼;
break;
}
注意:與原生不同以蕴,這種方法是重新創(chuàng)建一個(gè)Activity,再finish掉之前的Activity辛孵,而不是復(fù)用丛肮。并且,如果所有的Activity都是插件Activity那這種方案是OK的魄缚,如果宿主中也有Activity宝与,并且不受ProxyActivity的管理焚廊,那宿主中的Activity不會(huì)遵守該種方案。
3.2 Service
3.2.1 動(dòng)態(tài)框架
問題:可以使用一個(gè)StubActivity來“欺騙AMS”【不考慮LaunchMode】习劫,而對(duì)于同一個(gè)Service調(diào)用多次startService并不會(huì)啟動(dòng)多個(gè)Service實(shí)例咆瘟。所以只用一個(gè)StubService是應(yīng)付不了多個(gè)插件Service的。
解決方案:預(yù)先占位诽里√徊停考慮到一個(gè)App中Service的數(shù)量不會(huì)超過10個(gè),所以在宿主中創(chuàng)建StubService1谤狡、StubService2等灸眼,并且它們與插件中的Service一一對(duì)應(yīng)。
startService的解決方案
首先墓懂,把插件和宿主的dex合并焰宣,這樣可以加載插件中的類;其次捕仔,“欺騙AMS”匕积。
Hook上半場(chǎng):
ActivityManagerNative :: gDefault :: mInstance :: Singleton
Hook IActivityManager【將PluginService切換回StubService】
Hook下半場(chǎng):
ActivityThread :: sCurrentActivityThread :: mH :: mCallBack
需要截獲handleMessage方法中的case CREATE_SERVICE
【將StubService切換回PluginService】
bindService的解決方案
與startService類似,但有兩點(diǎn)需要注意:
1)在Hook上半場(chǎng)時(shí)榜跌,對(duì)于unbindService不需要“欺騙AMS”闪唆,因?yàn)閡nbindService(_)需要一個(gè)ServiceConnection類型的參數(shù),跟intent沒有關(guān)系钓葫,所以不需要“欺騙AMS”苞氮。AMS會(huì)根據(jù)ServiceConnection參數(shù)找到對(duì)應(yīng)的Service。
2)在Hook下半場(chǎng)時(shí)瓤逼,不再需要將StubService切換回PluginService笼吟。因?yàn)樵趕tartService下半場(chǎng)Hook中,在CREATE_SERVICE時(shí)已做了切換處理霸旗,handleCreateService方法會(huì)把啟動(dòng)的PluginService放在mServices集合中贷帮。當(dāng)handleBindService和handleUnbindService時(shí)會(huì)從mService集合中找到PluginService進(jìn)行綁定和解綁。
3.2.2 靜態(tài)方案
與Activity靜態(tài)方案類似诱告。注意:要在ProxyService的onStartCommand和onBind方法中需要先反射實(shí)例化RemoteService對(duì)象撵枢,調(diào)用其mRemoteService.onCreate方法,然后再調(diào)用其mRemoteService.onStartCommand和mRemoteService.onBind精居。
單純的靜態(tài)方案也不能實(shí)現(xiàn)用一個(gè)StubService就能對(duì)應(yīng)多個(gè)插件的Service锄禽。可以通過Hook一部分代碼 + 靜態(tài)代理來實(shí)現(xiàn)靴姿∥值【純Hook當(dāng)然也可以,只不過使用靜態(tài)代碼會(huì)少Hook一些】
思路:將所有啟動(dòng)的Service放到一個(gè)集合中佛吓,每次從intent中取出真正要啟動(dòng)的Service宵晚,在該集合中查找垂攘,如果不存在則create service,存在則返回淤刃。當(dāng)service結(jié)束時(shí)晒他,要從該集合中刪除。
3.3 BroadcastReceiver
3.3.1 動(dòng)態(tài)方案
動(dòng)態(tài)廣播的解決方案
不需要跟AMS打交道逸贾,只要合并插件的dex陨仅,保證宿主能加載插件中的廣播類,反射調(diào)用其onReceive方法即可铝侵。
靜態(tài)廣播的解決方案
問題:不能使用插樁方案灼伤,因?yàn)閺V播必須指定IntentFilter,而IntentFilter中的action參數(shù)是隨意設(shè)置的哟沫。
方案1:把靜態(tài)廣播轉(zhuǎn)換為動(dòng)態(tài)廣播
將插件中聲明的靜態(tài)廣播【安裝App時(shí)會(huì)注冊(cè)在PMS中】轉(zhuǎn)換為動(dòng)態(tài)廣播注冊(cè)到AMS中。
具體措施:
1)反射PMS讀取插件AndroidManifest文件中聲明的靜態(tài)廣播锌介。
2)使用插件的ClassLoader加載靜態(tài)廣播嗜诀,實(shí)例化為一個(gè)對(duì)象,然后作為動(dòng)態(tài)廣播注冊(cè)到AMS中孔祸。
注意:該方案喪失了靜態(tài)廣播不需要啟動(dòng)App就可以被啟動(dòng)的特性隆敢。
方案2:占位StubReceiver
占位StubReceiver,該靜態(tài)廣播會(huì)預(yù)定義多個(gè)Action崔慧,每個(gè)Action都會(huì)對(duì)應(yīng)一個(gè)插件中的靜態(tài)廣播拂蝎。
宿主中占位的靜態(tài)廣播:
<receiver
android:name=".HostReceiver"
android:enabled="true"
android:exported="true">
<intent-filter><action android:name="stub1" /></intent-filter>
<intent-filter><action android:name="stub2" /></intent-filter>
<intent-filter><action android:name="stub3" /></intent-filter>
......
</receiver>
插件中定義的靜態(tài)廣播:
<receiver
android:name=".PluginReceiver"
android:enabled="true"
android:exported="true">
<intent-filter><action android:name="realReceiver1" /></intent-filter>
<meta-data
android:name="oldAction"
android:value="stub1" />
</receiver>
注意:同樣需要把插件中的靜態(tài)廣播作為動(dòng)態(tài)廣播手注冊(cè)到AMS中。
使用流程:
1)啟動(dòng)HostReceiver惶室,攜帶action=stub1温自。
2)在HostReceiver的onReceiver()方法中,得到action=stub1皇钞。
3)解析插件AndroidManifest中receiver的action和meta-data信息悼泌,將其保存在map中,如:map.put("stub1","realReceiver1")夹界。
4)根據(jù)action=stub1馆里,從map中獲取到真正的realReceiver1,發(fā)射實(shí)例化并sendBroadcast()可柿。
3.3.2 靜態(tài)框架
最簡(jiǎn)單鸠踪,可以實(shí)現(xiàn)一個(gè)StubReceiver對(duì)應(yīng)多個(gè)插件的Receiver。但that框架只能支持動(dòng)態(tài)廣播复斥,不支持靜態(tài)廣播营密。
4. 插件化相關(guān)知識(shí)
4.1 基于Fragment的插件化
原理:一個(gè)App中只有一個(gè)Activity來承載所有的Fragment。Fragment不同于四大組件目锭,它就是一個(gè)簡(jiǎn)單的類卵贱,不需要與AMS進(jìn)行交互滥沫。在這個(gè)唯一的Activity中需要管理所有插件的ClassLoader來加載相應(yīng)插件中的Fragment,并且還要將宿主和插件資源合并在一起键俱。
缺陷:對(duì)四大組件未能實(shí)現(xiàn)插件化兰绣。
三種跳轉(zhuǎn)場(chǎng)景:
1)宿主跳出插件的Fragment
2)從插件的Fragment跳本插件的Fragment【Fragment進(jìn)出棧】
3)從插件的Fragment跳宿主或其它插件的Fragment
4.2 插件的混淆
proguard工具不僅做混淆编振,還會(huì)把項(xiàng)目中用不到的方法刪除掉缀辩。【踪央?臀玄??】
插件不支持加固畅蹂,宿主可以加固健无,但插件支持簽名。
混淆的規(guī)則:
1液斜、四大組件和Application要在AndroidManifest中聲明累贤,不能混淆。
2少漆、R文件不能混淆臼膏,因?yàn)橛袝r(shí)會(huì)通過反射獲取資源。
3示损、support的v4渗磅、v7包中的類不能混淆,系統(tǒng)的東西检访,不能隨意動(dòng)始鱼。
4、實(shí)現(xiàn)了Serializable的類不能混淆脆贵,否則反序列化會(huì)出錯(cuò)风响。
5、泛型不能混淆丹禀。
6状勤、自定義View不能混淆,否則Layout布局中使用自定義View時(shí)會(huì)找不到双泪。
7持搜、反射的類不能混淆。
宿主和插件都會(huì)引用midlib基礎(chǔ)庫(kù)焙矛,那么混淆時(shí)如何對(duì)midlib進(jìn)行處理呢葫盼?
方案1:不混淆公共庫(kù)midlib
插件中compileOnly midlib庫(kù),compileOnly不會(huì)混淆村斟。并在宿主中keep midlib中的所有類贫导。
方案2:混淆公共庫(kù)midlib
具體過程:
1)插件中compile midlib庫(kù)抛猫。
2)multidex手動(dòng)拆包,把插件拆分成兩個(gè)包孩灯,插件中的代碼都放在主dex中闺金,而其他代碼放在classes2.dex中【包括midlib和其他compile的庫(kù),這些庫(kù)都會(huì)在宿主中同時(shí)存在一份】峰档。
3)gradle配置
dexOptions{
additionalParameters += '--main-dex-list=maindexlist.txt'
}
4)在插件中增加maindexlist.txt文件败匹,里面包括要將哪些文件保留在主dex中。如:
ljg/aaa/a.class
ljg/aaa/b.class
技巧:可以使用腳本生成maindexlist.txt文件讥巡,掃描插件項(xiàng)目的src/main/java/目錄下的所有Java文件掀亩,將文件后綴java替換為class,然后填充到maindexlist.txt欢顷。
問題:使用上述技巧槽棍,導(dǎo)致匿名內(nèi)部類放在classes2.dex中。
解決:預(yù)先為插件中的每個(gè)類抬驴,生成10個(gè)內(nèi)部類炼七。【因?yàn)閮?nèi)部類的命令是有規(guī)律的怎爵,User$1特石,User$2盅蝗,......】
5)如果midlib中有A鳖链,B,C三個(gè)類墩莫,而宿主中只用到了A芙委,B兩個(gè)類,插件中用到了C類狂秦,那么在宿主混淆時(shí)會(huì)將C類移除灌侣。所以,需要在插件和宿主的proguard-rule.pro中增加-dontshrink
裂问。這樣在混淆過程中即使沒有用到的類也會(huì)保留侧啼。
6)對(duì)插件打一個(gè)混淆包,會(huì)生成一個(gè)mapping.txt文件堪簿,里面含有midlib庫(kù)中類的對(duì)應(yīng)關(guān)系痊乾。將其中的這部分規(guī)則復(fù)制保存到mapping_plugin.txt中,并復(fù)制到宿主根目錄下椭更,與proguard-rule.pro平級(jí)哪审。然后對(duì)宿主proguard-rule.pro文件中增加-applymapping mapping_plugin.txt
。
7)移除插件中冗余的dex虑瀑,用一個(gè)空的classes2.dex替換插件中的classes2.dex湿滓。具體操作如下:
A. 反編譯滴须。java -jar apktool.jar d --no-src -f plugin.apk
解壓apk,這樣才能替換apk里面的classes2.dex叽奥。
B. 重新打包扔水。java -jar apktool.jar b plugin
C. 重新簽名。jarsigner -verbose -keystore keystore.jks ......
D. 對(duì)生成的簽名包執(zhí)行對(duì)齊操作而线。zipalign -v 4 plugin_sign.apk plugin_ok.apk
可以把混淆公共庫(kù)midlib這整套流程集成到gradle中铭污。
4.3 增量更新
流程如下:
1)通過bsdiff old.apk和new.apk生成patch.diff文件。
2)宿主中添加libApkPatchLibrary.so膀篮,在加載插件之前嘹狞, 使用PatchUtils.patch,將下發(fā)的patch.diff文件與現(xiàn)有的插件進(jìn)行合并誓竿,生成new.apk磅网,宿主加載該插件。
問題:在App兩個(gè)正式版本之間筷屡,可能會(huì)有多個(gè)插件版本涧偷,那么就需要維護(hù)多個(gè)增量包。有的用戶插件升級(jí)到了3.0.0.2毙死,而有的用戶沒有升級(jí)燎潮。
解決:App根據(jù)自己的插件版本號(hào),去服務(wù)端請(qǐng)求合適自己的增量包扼倘。
4.4 so的插件化
4.4.1 so知識(shí)的簡(jiǎn)介
Android支持的三種CPU類型:x86确封、arm、mips≡倬眨現(xiàn)在手機(jī)基本上都是arm爪喘,而arm又分為32位和64位。armeabi/armeabi-v7a是32位纠拔,其中armeabi是相當(dāng)老的版本秉剑,缺少對(duì)浮點(diǎn)數(shù)計(jì)算的硬件支持。arm64-v8a是64位稠诲,主要用于Android5.0之后侦鹏。
問題:通常我們是生成多種CPU類型的so,然后放到j(luò)niLibs不同目錄下臀叙。其實(shí)這是不必要的略水,因?yàn)閍rm體系是向下兼容的,比如:32位的so匹耕,是可以在64位系統(tǒng)上運(yùn)行的聚请。
原理:Android啟動(dòng)App時(shí)都會(huì)創(chuàng)建一個(gè)虛擬機(jī),Android64位系統(tǒng)加載32位的so或App時(shí),會(huì)在創(chuàng)建一個(gè)64位虛擬機(jī)的同時(shí)還創(chuàng)建一個(gè)32位的虛擬機(jī)來兼容32位的App應(yīng)用驶赏。
結(jié)論:App中只保留一個(gè)armeabi-v7a版本的so就足夠了炸卑。
4.4.2 so的加載流程
手機(jī)支持CPU的種類存放在abiList集合中,如有:arm64-v8a煤傍、armeabi-v7a盖文、armeabi。按照此順序變量jniLib目錄蚯姆,如果這個(gè)目錄下有arm64-v8a子目錄五续,并且里面有so文件,那么接下來將加載arm64-v8a下的所有so文件龄恋,就不再加載armeabi-v7a和armeabi中的so了疙驾。
所以,32位的arm手機(jī)肯定能加載到armeabi-v7a下的so文件郭毕。而64位的arm手機(jī)它碎,想要加載armeabi-v7a下的so文件,必須不能在arm64-v8a下方任何so文件显押,并且armeabi-v7a下必須有so文件扳肛。如果所有的so文件都是從服務(wù)器下發(fā)的,那么需要建一個(gè)簡(jiǎn)單的so文件乘碑,放在armeabi-v7a目錄下占位挖息。
4.4.3 so的加載方法
1)System.loadLibrary("ljg") 只能加載jniLibs目錄下的so文件∈薹簦【src/main/jniLibs與src/main/java平級(jí)】
2)System.load方法套腹,可以加載任意路徑下的so文件,需要傳入so文件的完整路徑轿衔。
ClassLoader與so的關(guān)系:
classLoader = new DexClassLoader(dexpath, fileRelease.getAbsolutePath(), null, getClassLoader());
其中沉迹,第三個(gè)參數(shù)null睦疫,是apk中so文件的路徑害驹。如果有多個(gè)so路徑,用逗號(hào)連接成字符串蛤育。
優(yōu)化:動(dòng)態(tài)加載so宛官,把非及時(shí)需要的so由服務(wù)器下發(fā)來減小apk的體積。
4.4.4 基于System.loadLibrary的so插件化
宿主在解析每個(gè)插件時(shí)瓦糕,為每個(gè)插件創(chuàng)建一個(gè)DexClassLoader底洗,先解析出每個(gè)插件apk中的so文件,解壓到某個(gè)位置咕娄,將其路徑用逗號(hào)拼接成字符串亥揖,放到DexClassLoader構(gòu)造函數(shù)的第三個(gè)參數(shù)中。這樣宿主和插件中都可以通過System.loadLibrary("xxx")來加載各自src/main/jniLibs中的so文件。
插件的DexClassLoader中包含so的路徑了费变,所以插件中就可通過loadLibrary("xxx")來加載so摧扇。
4.4.5 基于System.load的so插件化
插件中的so,可以交給插件自己來處理挚歧,不必通過DexClassLoader扛稽。插件把自身的jniLibs下的so復(fù)制到某個(gè)位置,然后通過System.load(libPath + "/" + soFileName)動(dòng)態(tài)加載滑负。
4.5 自定義Gradle
1)自定義Gradle插件庫(kù)的名字必須是buildSrc在张,還在buildSrc的build.gradle文件中配置:
apply plugin: 'groovy'
dependencies {
compile gradleApi()
compile localGroovy()
}
2)定義MyPlugin.groovy類
public class MyPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.task('testXXX') << {
println "hello gradle plugin"
}
}
}
3)創(chuàng)建自定義Gradle插件的入口,在buildSrc/resources/META-INF.gradle-plugins/下新建文件com.ljg.define.pluginTest.properties文件矮慕,在該文件中聲明:
implementation-class=com.ljg.MyPlugin
4)在build.gradle文件中引用【注意引用的名稱是入口的文件名】
apply plugin: 'com.ljg.define.pluginTest'
Extension動(dòng)態(tài)設(shè)置
在buildSrc目錄中定義類MyExtension
class MyExtension {
String message
}
在上面2)定義的MyPlugin類中應(yīng)用
public class MyPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.extensions.create('ljgTestPlugin', MyExtension)
project.task('testXXX') << {
println project.ljgTestPlugin.message
}
}
}
創(chuàng)建了一個(gè)名為ljgTestPlugin的Extension帮匾,它的類型是MyExtension。在build.gradle文件中引用痴鳄”俦罚【注意引入的名字是ljgTestPlugin】
apply plugin: 'com.ljg.define.pluginTest'
ljgTestPlugin {
message = 'hello xxx'
}
afterEvaluate應(yīng)用
public class MyPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.afterEvaluate() {
def preBuild = project.tasks['preBuild']
preBuild.doFirst {
println 'hook before preReleaseBuild'
}
preBuild.doLast {
println 'hook after preReleaseBuild'
}
}
}
}
preBuild、preDebugBuild夏跷、processReleaseResources哼转、compileReleaseJavaWithJavac等等,這些都是App打包的原生Task槽华。Gradle會(huì)先創(chuàng)建project的所有任務(wù)的有向圖壹蔓,然后調(diào)用project的afterEvaluate方法,所以當(dāng)我們想獲取preBuild這樣的task時(shí)猫态,就只能在afterEvaluate方法中獲取佣蓉。
提示:可以學(xué)習(xí)gradle-small的源碼來提升編寫Gradle的能力。