一次讓你徹底掌握Android插件化架構(gòu)設(shè)計(jì)
插件化簡(jiǎn)介
宿主host 與 插件(免安裝:不需要安裝apk,下載即可)
-->插件加載
-->插件化中的組件支持(startActivity如何去啟動(dòng))
-->插件化中資源(布局文件圖片)的加載
插件化優(yōu)點(diǎn)
1減小apk的體積箩朴,按需求下載模塊
2動(dòng)態(tài)更新插件
3宿主和插件分開編譯,提升團(tuán)隊(duì)開發(fā)效率
4解決方法數(shù)查過65535問題
缺點(diǎn):
項(xiàng)目復(fù)雜度變高了辐益,難度變高,版本兼容問題(插件化的兼容)
組件化和插件化區(qū)別
組件化:是將一個(gè)APP分成多個(gè)模塊,每個(gè)模塊都是一個(gè)組件(module)颓遏,開發(fā)的過程中我們可以讓這些組件相互依賴或者單獨(dú)調(diào)試部分組件孵坚,但是最終發(fā)布的時(shí)候?qū)⑦@些組件合并成一個(gè)統(tǒng)一的APK粮宛。
插件化:是將整個(gè)APP拆分成很多模塊,每個(gè)模塊都是一個(gè)APK(組件化的每個(gè)模塊是一個(gè)Lib)最終打包的時(shí)候?qū)⑺拗鰽PK和插件APK分開打包卖宠,插件APK通過動(dòng)態(tài)下發(fā)到宿主APK巍杈。
插件化框架對(duì)比
選中DroidPlugin
因?yàn)?60大廠,四大組件全支持扛伍,插件不需在清單文件注冊(cè)筷畦,最重要的是幾乎全部支持Android特性。
插件化架構(gòu)的設(shè)計(jì)思路
面試題 簡(jiǎn)述Java類加載的過程
加載階段刺洒,虛擬機(jī)主要完成三件事:
1通過一個(gè)類的權(quán)限定名來獲取定義此類的二進(jìn)制字節(jié)流(class文件—>字節(jié)流)鳖宾;
2將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)域的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)(字節(jié)流-->對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu));
3在java堆中生成一個(gè)代表這個(gè)類的class對(duì)象逆航,作為方法區(qū)域數(shù)據(jù)的訪問入口鼎文。
驗(yàn)證:是否符合java字節(jié)碼編碼規(guī)范
初始化完就會(huì)得到class對(duì)象(一切反射的基石)
Android中類加載器的集成結(jié)構(gòu)
常用的類加載器
BootClassLoader:系統(tǒng)啟動(dòng)時(shí)用于加載系統(tǒng)常用類,ClassLoader內(nèi)部類因俐;
PathClassLoader:加載系統(tǒng)類和應(yīng)用程序類拇惋,一般不建議開發(fā)者使用周偎;
DexClassLoader:加載DEX文件及包含dex文件的apk或jar。也支持從SD卡進(jìn)行加載蚤假,這也意味著DexClassLoader可以在應(yīng)用未安裝的情況下加載dex相關(guān)文件栏饮。因此它是熱修復(fù)和插件化技術(shù)的基礎(chǔ)。
驗(yàn)證上述的Demo
使用一個(gè)dexclassloader:
記得給讀寫權(quán)限A籽觥袍嬉!
最后參數(shù)父類為什么用pathClassLoader而不是BaseDexClassLoader,之后再說(在下一節(jié))
面試題:雙親委派機(jī)制與自定義String類
流程:
DexClassLoader加載前問PathClassLoader你加載過嗎灶平,如果沒有伺通,PathClassLoader問BootClassLoader你加載過嗎,如果沒有逢享,BootClassLoader問是夠可以加載罐监,能加載自己就加載了。不能再向下回傳瞒爬。
雙親委派機(jī)制優(yōu)點(diǎn):
避免重復(fù)加載弓柱,若已經(jīng)加載直接從緩存中讀取。
更加安全侧但,避免開發(fā)者修改系統(tǒng)類矢空。
參數(shù)父類為什么用pathClassLoader而不是BaseDexClassLoader:根據(jù)ClassLoader繼承圖他們倆不是繼承關(guān)系,這個(gè)方法參數(shù)里parent不是父類的意思(父類是super)禀横,這里是優(yōu)先級(jí)屁药、上一層的意思。
當(dāng)發(fā)生Activity跳轉(zhuǎn)時(shí)自動(dòng)加載插件Activity
如何手動(dòng) 變 自動(dòng)
即:把插件的classloader由DexClassloader變成 pathClassLoader柏锄,讓其認(rèn)為是自己人酿箭。就會(huì)自動(dòng)加載了。
為什么行得通:DexClassloader和PathClassLoader都是調(diào)用的一樣的父類方法趾娃,區(qū)別就是8.0之前可以指定生成后的odex目錄缭嫡,8.0之后都是系統(tǒng)目錄了。
加載指定類的時(shí)序圖
BaseDexClassLoader有一個(gè)DexPathList屬性抬闷,調(diào)用findClass遍歷dexElements妇蛀。
搞清時(shí)序圖是為了Activity跳轉(zhuǎn)時(shí)能自動(dòng)加載。我們知道了pathClassLoader的dex都放到哪里了饶氏。然后把dexClassLoader放進(jìn)去讥耗。
App里的dex是用一個(gè)Element[ ] dexElements數(shù)組來存放的有勾。
*為什么是數(shù)組疹启?為了分包,一個(gè)app可以有多個(gè)dex蔼卡。
插件dex的處理
即上面說的流程
把dexClassLoader放進(jìn)pathClassLoader的Element[ ] dexElements數(shù)組
如果想改dexElements數(shù)組要用到Hook技術(shù)喊崖。
Hook與Hook技巧
Hook技巧:
1要掌握反射和代理模式挣磨;
2盡量Hook靜態(tài)變量或者單例對(duì)象;(因?yàn)殪o態(tài)變量不用實(shí)例化一個(gè)對(duì)象)
3盡量Hook public的對(duì)象和方法荤懂。(如果是private容易搞壞內(nèi)部結(jié)構(gòu))
插件化架構(gòu)的模塊關(guān)系
創(chuàng)建兩個(gè)module
如何加載插件茁裙、插入dexElements數(shù)組等復(fù)雜工作都會(huì)在PluginCore里去完成
編碼實(shí)現(xiàn)宿主與插件dex數(shù)組合并
插入dexElements數(shù)組在什么時(shí)候比較好呢?
應(yīng)用程序啟動(dòng)時(shí)候最好节仿。即Application
配置權(quán)限
插件管理器 (單例模式)
public class PluginManager{
private static PluginManager instance;
private Context context;
private PluginManager(Context context){
this.context = context;
}
public static PluginManager getInstance(Context context){
if(instance == null){
instance = new PluginManager(context);
}
return instance;
}
public void init(){
try{
loadApk();
}catch(Exception e){e.printStackTrace();}
}
//加載插件APK文件并且合并dexElements
private void loadApk() throws Exception{
//加載插件的apk
String pluginApkPath = context.getExternalFilesDir(null).getAbsolutePath()+"/pluginapp-debug.apk";
//即插件apk地址
String cachePath = context.getDir("cache_plugin",Context.MODE_PRIVATE).getAbsolutePath();
DexClassLoader dexClassLoader = new DexClassLoader(pluginApkPath ,cachePath ,null,context.getClassLoader());
//反射操作
Class<?> baseDexClassLoader = dexClassLoader.getClass().getSuperclass();
Field pathListField = baseDexClassLoader.getDeclareField("pathList")
pathListField.setAccessible(true);
//1獲取plugin的dexElements
Object pluginPathListObject = pathListField.get(dexClassLoader);
Class<?> pathListClass = pluginPathListObject .getClass();
Field dexElementsField = pathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object pluginDexElements = dexElementsField.get(pluginPathListObject);
Log.d("z","pluginDexElements: "+pluginDexElements );
//2獲取host的dexElements
ClassLoader pathClassLoader =context.getClassLoader();
Object hostPathListObject = pathClassLoader.get(pathClassLoader);
Object hostDexElements = dexElementsField.get(hostPathListObject )
Log.d("z","hostDexElements : "+hostDexElements );
//3合并
int pluginDexElementsLength = Array.getLength(pluginDexElements );
int hostDexElementsLength = Array.getLength(hostDexElements);
int newDexElementsLength = pluginDexElementsLength +hostDexElementsLength ;
Object newDexElements = Array.newInstance(hostDexElements.getClass().getComponentType(),newDexElementsLength);
for(int i=0;i<newDexElementsLength;i++){
if(i<pluginDexElementsLength){//plugin
Array.set(newDexElements,i,Array.get(pluginDexElements,i));
}else{//host
Array.set(newDexElements,i,Array.get(hostDexElements,i-pluginDexElementsLength));
}
}
dexElementsField.set(hostPathListObject,newDexElements);
Log.d("z","newDexElements: "+newDexElements);
}
}
把插件apk文件放到sdcard/Android/包名/files下
其他:
結(jié)果:
非常輕松地獲得插件的class
接下來是如何啟動(dòng)我們的組件晤锥。Activity如何跳轉(zhuǎn)的
Activity啟動(dòng)中的跨進(jìn)程訪問
我們?cè)噲D這樣來做跳轉(zhuǎn):
報(bào)錯(cuò)了:
提示沒有在宿主里注冊(cè)(雖然在插件自己的里面注冊(cè)了)
Activity1 --》Activity2 要走兩次跨進(jìn)程訪問
AMS會(huì)檢查我們的activity是否在清單文件里完成注冊(cè)。所以報(bào)錯(cuò)了
Hook在插件化架構(gòu)中的運(yùn)用
我們寫一個(gè)RegisteredActivity廊宪,里面什么都不需要矾瘾,只要在清單文件里注冊(cè)。
分析activity啟動(dòng)流程
IActivityManager 就是AMS對(duì)象
我們hook這個(gè)IActivityManager 或者直接拿到mInstance這個(gè)屬性 即可拿到AMS對(duì)象
Hook AMS
public class HookUtils{
//hook 我們的AMS對(duì)象箭启,對(duì)其startActivity攔截
//把里面intent處理
public static void hookAMS(Context context) throws Exception{
//1.獲取AMS對(duì)象
//1.1獲取靜態(tài)屬性ActivityManager.IActivityManager Singleton的靜態(tài)屬性值
//它是Singleton類型
Field iActivityManagerSingletonField = ActivityManager.class.getDeclaredField("IActivityManagerSingleton");
iActivityManagerSingletonField.setAccessible(true);
Object iActivityManagerSingletonObject = iActivityManagerSingletonField.get(null);
//1.2獲取Singleton的mInstance屬性值
Class<?> singletonClazz = Class.forName("android.util.Singleton");
Field mInstanceField = singletonClazz.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
Object AMSSubject = mInstanceField.get(iActivityManagerSingletonObject);
//2對(duì)AMS對(duì)象進(jìn)行代理
Class<?> IActivityManagerInterface=Class.forName("android.app.IActivityManager");
AMSInvocationHandler handler = new AMSInvocationHandler(context,AMSSubject);
Object AMSProxy = Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader()
,new Class[]{IActivityManagerInterface}
,handler
);
mInstanceField.set(iActivityManagerSingletonObject,AMSProxy);
//3InvocationHandler對(duì)AMS對(duì)象的方法進(jìn)行攔截
}
}
public class AMSInvocationHandler implements InvocationHandler{
private Context context;
private Object subject;
public AMSInvocationHandler (Context context,Object subject){
this.context = context;
this.subject = subject;
}
@Override
public Object invoke(Object proxy,Method method,Object[] args) throws Throwable{
if("startActivity".equals(method.getName())){
Log.d("z","AMSInvocationHandler startActivity invoke");
//要把PluginActivity替換成RegisteredActivity
//找到intent參數(shù)
for(int i = 0;i<args.length;i++){
Object arg = args[i];
if(arg instanceof Intent){
Intent intentNew = new Intent();
intentNew.setClass(context,RegisteredActivity.class);
//原來的保存
intentNew.putExtra("actionIntent",(Intent)arg);
args[i] = intentNew;
Log.d("z","AMSInvocationHandler new Intent");
break;
}
}
}
return method.invoke(subject,args);
}
}
在上面pluginManager 代碼段里加上
public void init(){
try{
loadApk();
HookUtils.hookAMS(context);
HookUtils.hookHandler();
}catch(Exception e){e.printStackTrace();}
}
Hook Handler思路
現(xiàn)在等AMS驗(yàn)證完后壕翩,我們什么時(shí)候把想要啟動(dòng)的拿出來?
dispatchMessage會(huì)判斷callback是否為空
如果不為空傅寡,會(huì)執(zhí)行callback的handlerMessage,然后在執(zhí)行子類的handlerMessage.
所以我們給callback一個(gè)值放妈,先執(zhí)行callback里handlerMessage,修改我們需要的內(nèi)容荐操。
ActivityThread有一個(gè)H 內(nèi)部類 繼承自handler芜抒,里面記錄了hangler的信息(activityInfo)。
Handler發(fā)消息淀零,looper輪詢到 后都會(huì)dispatchMessage挽绩,里面判斷mCallback是否為空。(一般都為空)
所以我們修改callback驾中,在里面做事唉堪。
編碼實(shí)現(xiàn)Hook Handler
在上面HookUtils里加入一個(gè)方法
//hook 獲取到 Handler的特定消息(LAUNCH_ACTIVITY)中的intent,進(jìn)行處理肩民。
//將intent對(duì)象里的RegisteredActivity替換成PluginActivity
public static void hookHandler() throws Exception{
//1.獲取到handler對(duì)象(mH屬性值)
//1.1獲取到ActivityThread對(duì)象
Class<?> activityThreadClazz= Class.forName("android.app.ActivityThread");
//拿到activityThread對(duì)象唠亚,他有一個(gè)靜態(tài)的屬性(sCurrentActivityThread)
Field sCurrentActivityThreadField = activityThreadClazz.getDeclaredField("sCurrentActivityThread");
sCurrentActivityThreadField.setAccessible(true);
Object activityThreadObject = sCurrentActivityThreadField.get(null);
//1.2獲取ActivityThread對(duì)象的mH屬性值
Field mHField = activityThreadClazz.getDeclaredField("mH");
mHField.setAccessible(true);
Object handler = mHField.get(activityThreadObject);
//2.給我們的Handler的mCallBack屬性進(jìn)行賦值
Field mCallbackField = Handler.class.getDeclaredField("mCallback");
mCallbackField .setAccessible(true);
//3.在callback里面將intent對(duì)象里的RegisteredActivity替換成PluginActivity
//創(chuàng)建了MyCallback
mCallbackField.set(handler,new MyCallback());
}
public class MyCallback implements Handler.Callback{
private static final int LAUNCH_ACTIVITY = 100;
@Override
public boolean handleMessage(Message msg){
switch(msg.what){
case LAUNCH_ACTIVITY:
Log.d("z","MyCallback handleMessage LAUNCH_ACTIVITY");
try{
Field intentField = msg.obj.getClass().getDeclaredField("intent");
intentField.setAcessible(true);
Intent intent = intentField.get(msg.obj);
//取出我們放入的actionIntent
Parcelable actionIntent = intent.getParcelableExtra("actionIntent");
if(actionIntent!= null){
//替換
Log.d("z","MyCallback intent replaced");
intentField.set(msg.obj,actionIntent);
}
}catch(Exception e){
e.printStackTrace();
}
break;
}
return false;//這里true直接結(jié)束了,return false則執(zhí)行子類的hangleMessage持痰。灶搜!
}
}
AMS版本適配
修改之前代碼
public static void hookAMS(Context context) throws Exception{
//1.獲取AMS對(duì)象
//1.1獲取靜態(tài)屬性ActivityManager.IActivityManager Singleton的靜態(tài)屬性值
//它是Singleton類型
Field iActivityManagerSingletonField =null;
if(Build.VERSION.SDK_INT>=BUILD.VERSION_O){
iActivityManagerSingletonField = ActivityManager.class.getDeclaredField("IActivityManagerSingleton");
}else{
//低版本拿ActivityManagerNative的gDefault
Class<?> ActivityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
iActivityManagerSingletonField = ActivityManagerNativeClazz.getDeclaredField("gDefault");
}
iActivityManagerSingletonField.setAccessible(true);
Object iActivityManagerSingletonObject = iActivityManagerSingletonField.get(null);
Handler版本適配
修改MyCallback
public class MyCallback implements Handler.Callback{
private static final int LAUNCH_ACTIVITY = 100;
private static final int EXECUTE_TRANSACTION= 159;
@Override
public boolean handleMessage(Message msg){
switch(msg.what){
case LAUNCH_ACTIVITY:
Log.d("z","MyCallback handleMessage LAUNCH_ACTIVITY");
try{
Field intentField = msg.obj.getClass().getDeclaredField("intent");
intentField.setAcessible(true);
Intent intent = intentField.get(msg.obj);
//取出我們放入的actionIntent
Parcelable actionIntent = intent.getParcelableExtra("actionIntent");
if(actionIntent!= null){
//替換
Log.d("z","MyCallback intent replaced");
intentField.set(msg.obj,actionIntent);
}
}catch(Exception e){
e.printStackTrace();
}
break;
//API 28
case EXECUTE_TRANSACTION:
try{
//Intent
//1獲取mActivityCallbacks集合
Object clientTransactionObject = msg.obj;
Class<?>clientTransactionClazz = clientTransactionObject.getClass();
Field mActivityCallbacksField = clientTransactionClazz.getDeclaredField("mActivityCallbacks");
mActivityCallbacksField.setAccessible(true);
List mActivityCallbacks = (List)mActivityCallbacksField.get(clientTransactionObject);
//2遍歷集合里的元素得到LaunchActivityItem
for(Object item:mActivityCallbacks ){
if("android.app.servertransaction.LaunchActivityItem".equals(item.getClass().getName())){
Field mIntentField = item.getClass().getDeclaredField("mIntent");
mIntentField.setAccessible(true);
Intent intent = (Intent)mIntentField.get(item);
Parcelable actionIntent =intent.getParcelableExtra("actionIntent");
if(actionIntent !=null){
Log.d("z","MyCallback handleMessage intent replaced");
//3替換LaunchActivityItem的Intent
mIntentField.set(item,actionIntent);
}
}
}
}catch(Exception e){
e.printStackTrace();
}
break;
}
return false;//這里true直接結(jié)束了,return false則執(zhí)行子類的hangleMessage工窍。割卖!
}
}
面試題 簡(jiǎn)述Activity啟動(dòng)流程
我們從Context的starstActivity說起,其實(shí)現(xiàn)時(shí)ContextImpl的startActivity,然后內(nèi)部通過Instrumentation來嘗試啟動(dòng)Activity患雏,它會(huì)調(diào)用AMS的startActivity方法鹏溯,這是一個(gè)跨進(jìn)程過程,當(dāng)AMS效驗(yàn)完成Activity的合法性后淹仑,會(huì)通過Application回調(diào)到我們的進(jìn)程丙挽,也是一次跨進(jìn)程過程肺孵,而ApplicationThread就是一個(gè)Binder,毀掉邏輯是在binder線程池中完成的颜阐,所以需要通過Handler H將其切換到UI線程平窘,第一個(gè)消息是LAUNCH_ACTIVITY,它對(duì)應(yīng)handleLaunchActivity凳怨,在這個(gè)方法里玩成了Activity的創(chuàng)建和啟動(dòng)瑰艘。
面試題raw目錄和assets目錄有什么區(qū)別
raw:Android 會(huì)自動(dòng)的為目錄中的所有資源文件生成一個(gè)ID,這意味著很容易就可以訪問到這個(gè)資源肤舞,甚至在xml中都是可以訪問的磅叛,使用ID訪問的速度是最快的
assets:不會(huì)生成ID,只能通過AssetManager訪問萨赁,xml中不能訪問弊琴,訪問速度會(huì)慢一些,不過操作更加方便杖爽。
插件化中的資源加載
Resources資源加載過程分析
我們是否可以new 一個(gè)Resource來加載資源呢敲董?
Activity構(gòu)建上下文時(shí)也會(huì)構(gòu)建Resources。
ActivityThread-->
創(chuàng)建上下文
ResourceManager如何創(chuàng)建的慰安?
實(shí)際上是ResourcesImpl 創(chuàng)建AssetManager腋寨,其中指定要去加載資源的路徑
所以我們new Resources()對(duì)象時(shí)指定AssetManager,并且AssetManager指定我們插件資源的路徑化焕,那么這個(gè)Resources對(duì)象就可以加載我們的資源了萄窜。
編碼實(shí)現(xiàn)插件資源加載
在PluginManager中寫入方法:
//獲取插件的Resources對(duì)象
public Resources loadResources() throws Exception{
String pluginApkPath = context.getExternalFilesDir(null).getAbsolutePath()+"/pluginapp-debug.apk";
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = AssetManager.class.getMethod("addAssetPath",String.class);
addAssetPathMethod.invoke(assetManager,pluginApkPath);
return new Resources(assetManager,context.getResources().getDisplayMetrics(),context.getResources().getConfiguration());
}
那在什么時(shí)候使用?
Plugin 和宿主在同一個(gè)Application之下
所以在宿主的Application里調(diào)用
修改Application,重寫父類的getResources()
在插件Activity里 重寫getResource撒桨,從Application里拿
運(yùn)行時(shí)查刻,插件會(huì)進(jìn)入到宿主的Application里找到資源
如果好多插件,Resource需要分組凤类。