From:Android插件化開發(fā)指南
目錄
- 預備知識
1.1 簡介
?插件化的用途
?插件化的發(fā)展史
1.2 Binder原理
1.3 Activity工作原理
?App啟動流程 / App內(nèi)部頁面跳轉(zhuǎn)
1.4 PMS
1.5 ClassLoader
1.6 反射
1.7 代理模式 - 插件化知識
2.1 加載外部類
2.2 插件的Application
2.3 訪問插件中的類
?方案1:把插件dex合并到宿主dex
?方案2:為每個插件創(chuàng)建ClassLoader
?方案3:Hook App原生的ClassLoader
2.4 訪問插件中的資源
?2.4.1 資源簡介
??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 最簡單的實現(xiàn)一個插件化 - 插件化四大組件
3.1 Activity
?3.1.1 動態(tài)框架
??上半場:用Stub欺騙AMS
??下半場:啟動真實Activity
??解決LaunchMode問題
?3.1.2 靜態(tài)代理that框架
??解決LaunchMode問題
3.2 Service
?3.2.1 動態(tài)框架
??startService的解決方案
??bindService的解決方案
?3.2.2 靜態(tài)方案
3.3 BroadcastReceiver
?3.3.1 動態(tài)方案
??動態(tài)廣播的解決方案
??靜態(tài)廣播的解決方案
???方案1:把靜態(tài)廣播轉(zhuǎn)換為動態(tài)廣播
???方案2:占位StubReceiver
?3.3.2 靜態(tài)框架 - 插件化相關(guān)知識
4.1 基于Fragment的插件化
4.2 插件的混淆
?方案1:不混淆公共庫midlib
?方案2:混淆公共庫midlib
4.3 增量更新
4.4 so的插件化
?4.4.1 so知識的簡介
?4.4.2 so的加載流程
?4.4.3 so的加載方法
?4.4.4 基于System.loadLibrary的so插件化
?4.4.5 基于System.load的so插件化
4.5 自定義Gradle
?Extension動態(tài)設(shè)置
?afterEvaluate應用
1. 預備知識
1.1 簡介
插件化的用途
游戲平臺,按需下載?!倔w積 & 更新】PC是,而Android采用服務(wù)器動態(tài)下發(fā)腳本。
動態(tài)更新:增加新功能或完整的模塊,80%用于修復線上bug。
換膚:用于游戲領(lǐng)域,王者榮耀的換膚,上線新英雄,調(diào)整數(shù)據(jù)。
ABTest,數(shù)據(jù)驅(qū)動產(chǎn)品。
獨立性 & 開發(fā)效率【組件化???】
插件化的未來:虛擬機技術(shù) — — 應用雙開。
國內(nèi)對RN等相關(guān)技術(shù)的需求遠大于插件化;GooglePlay不允許插件化App的存在。
插件化的發(fā)展史
2012.07 —— 大眾點評,基于Fragment。
2013.03 —— 淘寶Atlas,未開源。
2014.03 —— 任玉剛,that框架靜態(tài)代理,非Hook。
2014.11 —— 提出StubActivity欺騙AMS。
2014.12 —— Android Studio1.0,可以借助Gradle。
2015.08 —— 360手機助手張勇,DroidPlugin。
2015.12 —— 林光亮Small框架。
此時,插件化中遇到的技術(shù)難題都已解決?!鹃_始關(guān)注:熱修復技術(shù)和RN】
2017.06 —— 360手機助手RePlugin。
可見,一項技術(shù)5年時間內(nèi)由雛形到成熟。
1.2 Binder原理
- Client、Service、ServiceManager三者關(guān)系。
- AIDL
- Binder、IBinder、IInterface、Stub.asInterface()、asBinder()、onTransact()
- 問題:
1)類結(jié)構(gòu)層級設(shè)計原理?
2)跨進程與同進程是如何區(qū)分?
3)onTransact在同一進程如何被調(diào)用?
4)一次通信的過程,數(shù)據(jù)如何傳遞和解析?
1.3 Activity工作原理
App啟動流程 / App內(nèi)部頁面跳轉(zhuǎn)
- Launcher在一個不同的進程。
- App安裝時,Android系統(tǒng)中的PackageManagerService從apk包中的AndroidManifest文件中讀取信息。
- Launcher、App、AMS關(guān)系。
- 啟動流程?!続ctivityThread中有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中哪個類扮演Stub,哪個類扮演Proxy?
ServiceManager.getService("xxx") // 連接池?
一個應用中Context的個數(shù) = Service個數(shù)+Activity個數(shù)+1(Application的)
getApplicationContext在ContextImpl中實現(xiàn),返回的就是在ActivityThread main()方法中初始化的Application對象。
1.4 PMS
PMS加載包的信息,將其封裝在LoadedApk這個類對象中,然后可以從中取出AndroidManifest中的信息。
結(jié)束安裝的時候,都會把安裝信息保存在xml文件中,當Android系統(tǒng)再次啟動時,會重新安裝所有的apk,就可以直接讀取之前保存的xml文件。
Android的5個安裝目錄:data/app-private、data/app、system/app、vender/app、system/framework。
Android系統(tǒng)重啟后,會重新安裝所有的App,這是由PMS類完成,并且App首次安裝到手機上也是由PMS完成。
PMS中的一個類PackageParse,用來解析AndroidManifest文件,通過反射調(diào)用generatePackageInfo()來獲取插件中的四大組件。
涉及到AIDL:IPackageManager
1.5 ClassLoader
DexClassLoader可以根據(jù)optimizedDirectory加載需要的【dex、apk、jar】文件,并創(chuàng)建一個DexFile對象,也可以從外部SD卡加載。
對于App而言,Apk文件中有一個classes.dex,他是Apk的主dex,通過PathClassLoader加載,它的父類是BaseDexClassLoader。MultiDex把一個dex文件拆分成多個dex文件,每個dex的方法數(shù)量不超過65536個,classes.des主dex由PathClassLoader加載,其它classes2.dex等會在App啟動后使用DexClassLoader加載。
可以讓classes.dex中只保留App啟動時所需要的類以及首頁的代碼,從而確保App進入首頁時間最少。
如何手動指定classes2.dex中包含哪些類的代碼?
gradle配置:
dexOptions{
additionalParameters += '--main-dex-list=maindexlist.txt'
}
增加maindexlist.txt文件,里面包括要將哪些文件保留在主dex中,注意是class文件。如:
ljg/aaa/a.class
ljg/aaa/b.class
后面有一個詳細的例子。
1.6 反射
- 如何反射一個泛型類
- 網(wǎng)絡(luò)數(shù)據(jù)解析 & Json2Bean是如何利用反射實現(xiàn)的?
- setAccessible(true)的本質(zhì)?【跳過校驗】
- jOOR庫,在Android中不支持反射final類型的字段,因為:Android的Field類中沒有定義final字段。
1.7 代理模式
- 動態(tài)代理的原理?
- 生成的代理類是什么樣子的?
- PMS是系統(tǒng)服務(wù),為什么沒有辦法Hook?
只能Hook App自己進程的東西,Hook永遠只在Client端,若在Service端那就是病毒了。所以,App只能對App所在的進程進程Hook,所影響的范圍也僅限于App本身。
Java動態(tài)代理只能代理接口,不能代理類, 為什么?如何破?
Java動態(tài)代理是由Java內(nèi)部的反射機制來實現(xiàn)的,而cglib動態(tài)代理底層則是借助asm來實現(xiàn)的。https://blog.csdn.net/u010111422/article/details/69062338
在Hook過程中,什么時候用靜態(tài)代理【暴露類】,什么時候用動態(tài)代理【暴露接口】
Hook AMS => ActivityManagerNative :: gDefault :: Singleton :: mInstance
Hook ActivityThread => sCurrentActivityThread :: mH :: mCallBack
2. 插件化知識
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);
利用反射可以不用引起其對象,調(diào)用其類中的方法時也要通過反射。如果使用接口編程,在反射出對象后,可以直接類型轉(zhuǎn)換為該接口對象,從而可以直接調(diào)用類中的方法,不再通過反射。
2.2 插件的Application
插件Application的onCreate是沒有機會調(diào)用的,除非我們在宿主自定義的Application的onCreate方法中利用反射來執(zhí)行插件們的onCreate方法。因此,插件Application沒有生命周期,它就是一個普通的類。
2.3 訪問插件中的類
方案1:把插件dex合并到宿主dex
BaseDexClassLoader :: pathList :: dexElements[ ]
方案2:為每個插件創(chuàng)建ClassLoader
為每個插件創(chuàng)建一個ClassLoader,把LoadedApk類中mClassLoader替換為插件的ClassLoader。
ActivityThread :: currentActivityThread :: mPackages
mPackages中緩存dex文件。
為插件創(chuàng)建loadedApk,然后mPackages.put(packageName, loadedApk)
loadedApk :: mClassLoader 賦值為插件的ClassLoader。
缺陷:Hook的點太多
方案3:Hook App原生的ClassLoader
修改App原生的ClassLoader【mPackageInfo :: mClassLoader】。構(gòu)建一個SuperClassLoader類,它內(nèi)部有一個mClassLoaderList變量,即持有所有插件ClassLoader的集合。于是SuperClassLoader的loadClass()方法,會先嘗試使用宿主的ClassLoader【即系統(tǒng)的】加載類,如果不能加載,就遍歷插件的ClassLoader。
注意:使用該方案加載插件中的類時,不能再使用Class.forName()方法來反射插件中的類了,因為Class.forName會使用BootClassLoader來加載類,這個類并沒有被Hook。應該使用:getClassLoader().loadClass()來反射類。
2.4 訪問插件中的資源
2.4.1 資源簡介
將插件放在宿主的assets目錄中,App啟動時會把assets目錄中的東西加載到內(nèi)存中?!綼ssets目錄不編譯】
AssetManager
AssetManager的addAssetPath方法可以解決資源的插件化。由于apk下載后不會解壓到本地,所以無法直接獲取到assets的絕對路徑。只能通過AssetManager類的open方法來獲取assets目錄下的文件資源。AssetManager中的addAssetPath方法,App啟動時會把當前apk的路徑傳遞進去,從而能夠訪問當前apk的所有資源。傳插件的路徑時,就能訪問插件中的資源了。
Resources
Resources是外暴露的類 => 調(diào)用AssetManager中的方法 => 訪問resources.arsc文件。resources.arsc在打包時生成。
2.4.2 資源訪問
方案1:在宿主Activity中創(chuàng)建插件的AssetManager
宿主中讀取插件里的資源:
1)反射創(chuàng)建AssetManager對象,調(diào)用addAssetPath方法,把插件的路徑添加到這個AssetManager對象中,這個對象只為該插件服務(wù)。并根據(jù)該AssetManager對象創(chuàng)建相應的Resources和Theme對象。
2)重寫Activity的getAsset()、getResources()和getTheme()方法,返回新創(chuàng)建的插件對象。【如果沒有則默認讀取宿主中的資源】
3)宿主中加載外部插件,生成該插件的ClassLoader。通過反射獲取插件中的類,從而讀取插件中的資源。
// 插件中被調(diào)用的方法
public String getStringF(Context context){
return context.getResources().getString(R.string.hello);
}
注意:反射調(diào)用插件中的getStringF方法時,傳入的context是宿主中的MainActivity.this,因為宿主Activity的getResources已經(jīng)被覆寫,此時返回的是該插件的AssetManager所創(chuàng)建的Resources對象。
當宿主需要某個插件中的資源時,才會loadResource,即利用反射為某插件生成AssetManager對象和與其相關(guān)的Resources、Theme,再反射調(diào)用addAssetPath方法。宿主默認是加載自己的資源。
將插件中的getStringF()移到宿主中去定義了,插件不做任何事
R.java中的內(nèi)部類:

R.java中的string類:

該R.java會存在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。
插件如何訪問插件中的資源呢?插件不能自動加載自身的資源,因為該插件中的資源并沒有addAssetPath到資源池中。所以,跟宿主訪問一樣,一樣需要反射AssetManager并調(diào)用addAssetPath,同時還要覆寫getAsset()、getResources()和getTheme()方法。
總結(jié):該方案不會合并宿主和插件的資源,進入到哪個插件,就為這個插件創(chuàng)建AssetManager和Resource對象,AssetManager通過反射調(diào)用addAssetPath方法,把插件自己的資源添加進去,當宿主進入到一個插件的時候,就把AssetManager切換為該插件的AssetManager,所以插件就只能加載到插件中的資源了。
方案2:宿主插件共用AssetManager
構(gòu)建一個超級AssetManager對象,在addAssetPath時,添加宿主和所有插件的資源。該Resources為全局變量?!舅拗骱筒寮绾喂蚕頂?shù)據(jù)???】
注意:插件Activity中必須覆寫getResources()方法,返回超級Resources全局變量。
public Resources getResources() {
return PluginManager.mSuperResources;
}
方案2會存在資源id沖突問題,如何解決呢?在下一節(jié)介紹。
2.4.3 資源id沖突
背景:把宿主和插件的資源合并到一起,通過AssetManager的addAssetPath來實現(xiàn),此方案會產(chǎn)生資源id沖突。
原因:宿主App和各插件App都是各自打包。
思路:Hook App打包過程中的aapt階段。
Android打包流程:
1、aapt。為res目錄的資源生成R.java文件,同時為AndroidManifest.xml生成Manifest.java文件。
2、aidl。把項目中定義的aidl文件生成相應的Java代碼。
3、javac。自己編寫的代碼+aapt生成的Java文件+aidl生成的Java文件,編譯成class文件。
4、proguard?;煜耐瑫r生成proguardMapping.txt文件。
5、dex。自己項目中生成的class文件+第三方庫的class文件,轉(zhuǎn)換為dex文件。
6、aapt。打包,把res目錄下的資源、assets目錄下的文件,打包成一個.ap_文件。
7、apkbuilder。將所有的dex文件+.ap_文件+AndroidManifest.xml打包為.apk文件。
8、jarsigner。對apk進行簽名。
9、zipalign。對要發(fā)布的apk文件進行對齊操作,以便運行時節(jié)省內(nèi)存。
方案1:修改AAPT構(gòu)建工具
資源id的定義格式:public static final int fade_in=0x7f050023;該十六進制由三部分組成:PackageId【7f】+ TypeId 【05】+ EntryId【0023】
PackageId:apk包的id,默認為0x7f。
TypeId:資源類型值,如:layout、id、string、drawable。
具體過程:
1)修改AAPT這個Android SDK工具,在AAPT的命令行參數(shù)中指定插件資源id的前綴。一般選用0x71~0xff這個區(qū)間內(nèi)的值作為前綴。
2)把修改后的AAPT工具命名為aapt_mac,放在項目根目錄下。
3)修改gradle,通過腳本反射,把AAPT的路徑修改為該App根路徑下的aapt_mac。
public.xml固定id值
場景:多個插件都需要一個自定義控件,把它放在宿主中,插件調(diào)用宿主的Java代碼和使用宿主的資源。
問題:App每次打包后,會隨著資源的增加,同一個資源的id值也會發(fā)生變化。
方案:如果宿主App的某個資源id被插件使用,那么為了避免下次因資源值變化而導致資源找不到,需要把這個資源id值寫死,這個固定的值要保存在public.xml文件中,放在res/values/目錄下。
<resources>
<public type="string" name="house_name" id="0x7f092234">
</resources>
在gradle1.3版本之前是默認支持public.xml的,但之后不再支持了,所以要在build.gradle中添加相應任務(wù)。
應用:插件如何使用宿主中的固定資源?把宿主打包成jar包被各插件compileOnly,在插件中使用StringConstant.house_name。StringConstant類是根據(jù)public.xml自動生成的。
方案2:修改R.java & resources.arsc文件
Android中的兩類資源AssetManager和Resources,其中AssetManager直接通過文件名稱就可以獲取到具體資源,而Resources先在resources.arsc文件中通過id查找到資源文件名稱,然后再通過AssetManager來獲取資源。
優(yōu)化:resources.arsc中存放了很多冗余的資源。因為我們開發(fā)時引入的AppCompat包、Design包,這些包也要生成資源id。對插件而言每個插件包的resources.arsc文件中都會有一份相同的資源,這樣就冗余了。所以對于插件中AppCompat包、Design包資源會在resources.arsc中刪除,只會在宿主的resources.arsc中存在。
具體過程:
1)aapt會生成R.java文件,Hook processReleaseResources這個task,在它之后將R.java文件中的0x7f修改為0x71?!咀ⅲ篟.java文件不能修改,只能重新建一份保存】
2)aapt還會生成一個后綴為ap_的壓縮包,里面有AndroidManifest.xml、res、asset、resources.arsc文件,解壓取出resources.arsc,把里面的0x7f修改為0x71。
3)刪除resources.arsc文件中的冗余的資源Id,如AppCompat庫。
4)Hook compileReleaseJavaWithJavac,把所有class中的R$drawable.class、R$layout.class這樣的class刪除,因為它們中保存的資源Id值還是以0x7f為前綴。
5)將步驟1中新生成的R.java文件,執(zhí)行javac,生成R.class文件。
疑惑:步驟4、5有必要嗎?在步驟1中,不能將新生成的R.java替換舊的嗎?
2.5 最簡單的實現(xiàn)一個插件化
1)合并所有插件的dex,來解決插件的類加載問題。
BaseDexClassLoader :: pathList :: dexElements。dexElements類型是Element[ ]數(shù)組,即利用反射把宿主和插件中的Element[ ]合并到一起,替換dexElements的值。
2)把插件中所有的資源統(tǒng)一性地合并到宿主的資源中。【可能導致資源id沖突】
3)預先在宿主的AndroidManifest文件中聲明插件的四大組件。
提示:AndroidManifest文件中可以聲明不存在的Activity類。AndroidManifest文件只做格式校驗,不會進行編譯。
3. 插件化四大組件
3.1 Activity
3.1.1 動態(tài)框架
上半場:用Stub欺騙AMS
ActivityManagerNative :: gDefault :: mInstance :: Singleton :: IActivityManager
下半場:啟動真實Activity
ActivityThread :: sCurrentActivityThread :: mH :: mCallback
解決LaunchMode問題
問題:AMS會認為每次要打開都是StubActivity,在AMS端有個棧,會存放每次要打開的Activity,那么現(xiàn)在這個棧上就都是StubActivity了。插件中設(shè)置的singleTask、singleTop和singleInstance都無效。
解決:占位思想。事先為SingleTop、SingleTask、SingleInstance這三種LaunchMode創(chuàng)建多個StubActivity,指定插件Activity與哪個StubActivity對應關(guān)系。
在插件AndroidManifest中設(shè)置的許多屬性都是無效的。
3.1.2 靜態(tài)代理that框架
每次都是啟動宿主中的ProxyActivity,攜帶參數(shù):要打開頁面所在插件的路徑dexPath和要打開Activity的全路徑名。在宿主ProxyActivity中反射插件中的要啟動的Activity類,但反射出來的Activity是一個普通的類,不具有Activity的生命周期。所以要在ProxyActivity的聲明周期方法中調(diào)用插件Activity的相應方法,以此來同步Activity的聲明周期。同時ProxyActivity中通過反射調(diào)用setProxy(this)與PluginActivity建立雙向通信,在PluginActivity中持有ProxyActivity的引用命名為that。由于插件中定義的Activity都是一個木偶,而非真正的Activity,所以this.setContentView(); 和this.findViewById();就會運行時報錯誤,而改為that.setContentView(); 和that.findViewById();。
問題:為什么Hook之后會有生命周期呢???
消滅that關(guān)鍵字
基類中實現(xiàn),但Activity的final方法不能覆寫只能使用that調(diào)用。
@Override
public View findViewById(int id){
return that.findViewById(id);
}
跳轉(zhuǎn)
宿主跳插件;宿主跳宿主;插件跳宿主;插件跳插件。
只有在跳插件時,才會使用ProxyActivity。
接口簡化
在靜態(tài)代理中使用面向接口的編程思想來減少反射的使用。
解決LaunchMode問題
維護一個atyStack集合,它持有所有打開的插件Activity。
switch(launchMode){
case Standard:
正常存入集合atyStack中;
break;
case SingleTop:
判斷atyStack倒數(shù)第二個元素是否即將打開的插件Activity,如果是則移除,并調(diào)用其finish()方法;
break;
case SingleTask:
移除這個元素以及在它之上的元素,并調(diào)用finish()方法;
break;
case SingleInstance:
只把這個元素移除,并調(diào)用finish()方法;
break;
}
注意:與原生不同,這種方法是重新創(chuàng)建一個Activity,再finish掉之前的Activity,而不是復用。并且,如果所有的Activity都是插件Activity那這種方案是OK的,如果宿主中也有Activity,并且不受ProxyActivity的管理,那宿主中的Activity不會遵守該種方案。
3.2 Service
3.2.1 動態(tài)框架
問題:可以使用一個StubActivity來“欺騙AMS”【不考慮LaunchMode】,而對于同一個Service調(diào)用多次startService并不會啟動多個Service實例。所以只用一個StubService是應付不了多個插件Service的。
解決方案:預先占位??紤]到一個App中Service的數(shù)量不會超過10個,所以在宿主中創(chuàng)建StubService1、StubService2等,并且它們與插件中的Service一一對應。
startService的解決方案
首先,把插件和宿主的dex合并,這樣可以加載插件中的類;其次,“欺騙AMS”。
Hook上半場:
ActivityManagerNative :: gDefault :: mInstance :: Singleton
Hook IActivityManager【將PluginService切換回StubService】
Hook下半場:
ActivityThread :: sCurrentActivityThread :: mH :: mCallBack
需要截獲handleMessage方法中的case CREATE_SERVICE【將StubService切換回PluginService】
bindService的解決方案
與startService類似,但有兩點需要注意:
1)在Hook上半場時,對于unbindService不需要“欺騙AMS”,因為unbindService(_)需要一個ServiceConnection類型的參數(shù),跟intent沒有關(guān)系,所以不需要“欺騙AMS”。AMS會根據(jù)ServiceConnection參數(shù)找到對應的Service。
2)在Hook下半場時,不再需要將StubService切換回PluginService。因為在startService下半場Hook中,在CREATE_SERVICE時已做了切換處理,handleCreateService方法會把啟動的PluginService放在mServices集合中。當handleBindService和handleUnbindService時會從mService集合中找到PluginService進行綁定和解綁。
3.2.2 靜態(tài)方案
與Activity靜態(tài)方案類似。注意:要在ProxyService的onStartCommand和onBind方法中需要先反射實例化RemoteService對象,調(diào)用其mRemoteService.onCreate方法,然后再調(diào)用其mRemoteService.onStartCommand和mRemoteService.onBind。
單純的靜態(tài)方案也不能實現(xiàn)用一個StubService就能對應多個插件的Service。可以通過Hook一部分代碼 + 靜態(tài)代理來實現(xiàn)?!炯僅ook當然也可以,只不過使用靜態(tài)代碼會少Hook一些】
思路:將所有啟動的Service放到一個集合中,每次從intent中取出真正要啟動的Service,在該集合中查找,如果不存在則create service,存在則返回。當service結(jié)束時,要從該集合中刪除。
3.3 BroadcastReceiver
3.3.1 動態(tài)方案
動態(tài)廣播的解決方案
不需要跟AMS打交道,只要合并插件的dex,保證宿主能加載插件中的廣播類,反射調(diào)用其onReceive方法即可。
靜態(tài)廣播的解決方案
問題:不能使用插樁方案,因為廣播必須指定IntentFilter,而IntentFilter中的action參數(shù)是隨意設(shè)置的。
方案1:把靜態(tài)廣播轉(zhuǎn)換為動態(tài)廣播
將插件中聲明的靜態(tài)廣播【安裝App時會注冊在PMS中】轉(zhuǎn)換為動態(tài)廣播注冊到AMS中。
具體措施:
1)反射PMS讀取插件AndroidManifest文件中聲明的靜態(tài)廣播。
2)使用插件的ClassLoader加載靜態(tài)廣播,實例化為一個對象,然后作為動態(tài)廣播注冊到AMS中。
注意:該方案喪失了靜態(tài)廣播不需要啟動App就可以被啟動的特性。
方案2:占位StubReceiver
占位StubReceiver,該靜態(tài)廣播會預定義多個Action,每個Action都會對應一個插件中的靜態(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)廣播作為動態(tài)廣播手注冊到AMS中。
使用流程:
1)啟動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ā)射實例化并sendBroadcast()。
3.3.2 靜態(tài)框架
最簡單,可以實現(xiàn)一個StubReceiver對應多個插件的Receiver。但that框架只能支持動態(tài)廣播,不支持靜態(tài)廣播。
4. 插件化相關(guān)知識
4.1 基于Fragment的插件化
原理:一個App中只有一個Activity來承載所有的Fragment。Fragment不同于四大組件,它就是一個簡單的類,不需要與AMS進行交互。在這個唯一的Activity中需要管理所有插件的ClassLoader來加載相應插件中的Fragment,并且還要將宿主和插件資源合并在一起。
缺陷:對四大組件未能實現(xiàn)插件化。
三種跳轉(zhuǎn)場景:
1)宿主跳出插件的Fragment
2)從插件的Fragment跳本插件的Fragment【Fragment進出?!?br>
3)從插件的Fragment跳宿主或其它插件的Fragment
4.2 插件的混淆
proguard工具不僅做混淆,還會把項目中用不到的方法刪除掉?!????】
插件不支持加固,宿主可以加固,但插件支持簽名。
混淆的規(guī)則:
1、四大組件和Application要在AndroidManifest中聲明,不能混淆。
2、R文件不能混淆,因為有時會通過反射獲取資源。
3、support的v4、v7包中的類不能混淆,系統(tǒng)的東西,不能隨意動。
4、實現(xiàn)了Serializable的類不能混淆,否則反序列化會出錯。
5、泛型不能混淆。
6、自定義View不能混淆,否則Layout布局中使用自定義View時會找不到。
7、反射的類不能混淆。
宿主和插件都會引用midlib基礎(chǔ)庫,那么混淆時如何對midlib進行處理呢?
方案1:不混淆公共庫midlib
插件中compileOnly midlib庫,compileOnly不會混淆。并在宿主中keep midlib中的所有類。
方案2:混淆公共庫midlib
具體過程:
1)插件中compile midlib庫。
2)multidex手動拆包,把插件拆分成兩個包,插件中的代碼都放在主dex中,而其他代碼放在classes2.dex中【包括midlib和其他compile的庫,這些庫都會在宿主中同時存在一份】。
3)gradle配置
dexOptions{
additionalParameters += '--main-dex-list=maindexlist.txt'
}
4)在插件中增加maindexlist.txt文件,里面包括要將哪些文件保留在主dex中。如:
ljg/aaa/a.class
ljg/aaa/b.class
技巧:可以使用腳本生成maindexlist.txt文件,掃描插件項目的src/main/java/目錄下的所有Java文件,將文件后綴java替換為class,然后填充到maindexlist.txt。
問題:使用上述技巧,導致匿名內(nèi)部類放在classes2.dex中。
解決:預先為插件中的每個類,生成10個內(nèi)部類?!疽驗閮?nèi)部類的命令是有規(guī)律的,User$1,User$2,......】
5)如果midlib中有A,B,C三個類,而宿主中只用到了A,B兩個類,插件中用到了C類,那么在宿主混淆時會將C類移除。所以,需要在插件和宿主的proguard-rule.pro中增加-dontshrink。這樣在混淆過程中即使沒有用到的類也會保留。
6)對插件打一個混淆包,會生成一個mapping.txt文件,里面含有midlib庫中類的對應關(guān)系。將其中的這部分規(guī)則復制保存到mapping_plugin.txt中,并復制到宿主根目錄下,與proguard-rule.pro平級。然后對宿主proguard-rule.pro文件中增加-applymapping mapping_plugin.txt。
7)移除插件中冗余的dex,用一個空的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. 對生成的簽名包執(zhí)行對齊操作。zipalign -v 4 plugin_sign.apk plugin_ok.apk
可以把混淆公共庫midlib這整套流程集成到gradle中。
4.3 增量更新
流程如下:
1)通過bsdiff old.apk和new.apk生成patch.diff文件。
2)宿主中添加libApkPatchLibrary.so,在加載插件之前, 使用PatchUtils.patch,將下發(fā)的patch.diff文件與現(xiàn)有的插件進行合并,生成new.apk,宿主加載該插件。
問題:在App兩個正式版本之間,可能會有多個插件版本,那么就需要維護多個增量包。有的用戶插件升級到了3.0.0.2,而有的用戶沒有升級。
解決:App根據(jù)自己的插件版本號,去服務(wù)端請求合適自己的增量包。
4.4 so的插件化
4.4.1 so知識的簡介
Android支持的三種CPU類型:x86、arm、mips?,F(xiàn)在手機基本上都是arm,而arm又分為32位和64位。armeabi/armeabi-v7a是32位,其中armeabi是相當老的版本,缺少對浮點數(shù)計算的硬件支持。arm64-v8a是64位,主要用于Android5.0之后。
問題:通常我們是生成多種CPU類型的so,然后放到j(luò)niLibs不同目錄下。其實這是不必要的,因為arm體系是向下兼容的,比如:32位的so,是可以在64位系統(tǒng)上運行的。
原理:Android啟動App時都會創(chuàng)建一個虛擬機,Android64位系統(tǒng)加載32位的so或App時,會在創(chuàng)建一個64位虛擬機的同時還創(chuàng)建一個32位的虛擬機來兼容32位的App應用。
結(jié)論:App中只保留一個armeabi-v7a版本的so就足夠了。
4.4.2 so的加載流程
手機支持CPU的種類存放在abiList集合中,如有:arm64-v8a、armeabi-v7a、armeabi。按照此順序變量jniLib目錄,如果這個目錄下有arm64-v8a子目錄,并且里面有so文件,那么接下來將加載arm64-v8a下的所有so文件,就不再加載armeabi-v7a和armeabi中的so了。
所以,32位的arm手機肯定能加載到armeabi-v7a下的so文件。而64位的arm手機,想要加載armeabi-v7a下的so文件,必須不能在arm64-v8a下方任何so文件,并且armeabi-v7a下必須有so文件。如果所有的so文件都是從服務(wù)器下發(fā)的,那么需要建一個簡單的so文件,放在armeabi-v7a目錄下占位。
4.4.3 so的加載方法
1)System.loadLibrary("ljg") 只能加載jniLibs目錄下的so文件?!緎rc/main/jniLibs與src/main/java平級】
2)System.load方法,可以加載任意路徑下的so文件,需要傳入so文件的完整路徑。
ClassLoader與so的關(guān)系:
classLoader = new DexClassLoader(dexpath, fileRelease.getAbsolutePath(), null, getClassLoader());
其中,第三個參數(shù)null,是apk中so文件的路徑。如果有多個so路徑,用逗號連接成字符串。
優(yōu)化:動態(tài)加載so,把非及時需要的so由服務(wù)器下發(fā)來減小apk的體積。
4.4.4 基于System.loadLibrary的so插件化
宿主在解析每個插件時,為每個插件創(chuàng)建一個DexClassLoader,先解析出每個插件apk中的so文件,解壓到某個位置,將其路徑用逗號拼接成字符串,放到DexClassLoader構(gòu)造函數(shù)的第三個參數(shù)中。這樣宿主和插件中都可以通過System.loadLibrary("xxx")來加載各自src/main/jniLibs中的so文件。
插件的DexClassLoader中包含so的路徑了,所以插件中就可通過loadLibrary("xxx")來加載so。
4.4.5 基于System.load的so插件化
插件中的so,可以交給插件自己來處理,不必通過DexClassLoader。插件把自身的jniLibs下的so復制到某個位置,然后通過System.load(libPath + "/" + soFileName)動態(tài)加載。
4.5 自定義Gradle
1)自定義Gradle插件庫的名字必須是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動態(tài)設(shè)置
在buildSrc目錄中定義類MyExtension
class MyExtension {
String message
}
在上面2)定義的MyPlugin類中應用
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)建了一個名為ljgTestPlugin的Extension,它的類型是MyExtension。在build.gradle文件中引用?!咀⒁庖氲拿质莑jgTestPlugin】
apply plugin: 'com.ljg.define.pluginTest'
ljgTestPlugin {
message = 'hello xxx'
}
afterEvaluate應用
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會先創(chuàng)建project的所有任務(wù)的有向圖,然后調(diào)用project的afterEvaluate方法,所以當我們想獲取preBuild這樣的task時,就只能在afterEvaluate方法中獲取。
提示:可以學習gradle-small的源碼來提升編寫Gradle的能力。