一邓嘹、定義&理解
插件:Plug-in又稱add-in膜赃、addon或add-on,也稱外掛,它是一種遵循一定規(guī)范的應用程序接口編寫出來的程序茫负。
我的理解就是:插件是一個程序的輔助或者擴展功能模塊破停,對程序來說插件可有可無翅楼,但它能給予程序一定的額外功能。
二真慢、背景
android插件化的概念始于2012年毅臊,出自于免安裝的想法。發(fā)展到后來黑界,android應用程序更多的需要依賴一些第三方庫管嬉,比如地圖sdk、分享sdK朗鸠、支付sdk等等蚯撩,導致安裝包變得越來越大,單是依賴這些sdk烛占,安裝包可能就會額外的增加20-30M的大星蟛蕖;當需要新增功能時,不得不重新更新整個安裝包呀癣。所以這時插件化的需求就變得更為更為突出。
插件化主要是解決的是減少應用程序大小弦赖、免安裝擴展功能项栏。一般具一定規(guī)模的程序應用插件化才有意義。
三蹬竖、原理
插件本身是一個獨立的apk文件沼沈,要實現(xiàn)插件動態(tài)加載運行,需要了解apk的運行機制币厕。java虛擬機運行的是class文件列另,使用ClassLoader加載;而Android虛擬機運行的是dex文件使用的是DexClassLoader旦装,而資源是存在xml文件中页衙,所以就會涉及到資源文件的加載過程,所以要實現(xiàn)插件的動態(tài)加載運行阴绢,首先就需要解決類文件店乐、資源、庫等的加載呻袭。
1眨八、類加載
android運行的是dex文件對應的類加載器是PathClassLoader和DexClassLoader,PathClassLoader和DexClassLoader均繼承自BaseDexClassLoader左电,BaseDexClassLoader又繼承自ClassLoader廉侧。
我們來看下DexClassLoader基類BaseDexClassLoader的構造函數(shù):
/**
* @param dexPath 目標類所在的apk或jar文件路徑,類加載器將從這個apk或jar文件路徑查找目標類
* @param optimizedDirectory 從apk中解壓出dex文件篓足,經過odex優(yōu)化之后段誊,將解壓和優(yōu)化后的dex文件存放到optimizedDirectory 目錄,下次直接從這個目錄中的dex文件加載目標類纷纫。注意從API26開始此參數(shù)已經廢棄
* @param librarySearchPath 目標類所使用的c/c++庫路徑
* @param parent 父類ClassLoader
*/
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
// 解壓枕扫、加載dex、資源文件辱魁,保存存放dex烟瞧、資源文件、庫文件路徑等
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
if (reporter != null) {
reportClassLoaderChain();
}
}
問題1:為什么要傳父類的ClassLoader染簇,有什么用参滴?
PathClassLoader的構造函數(shù):
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
從PathClassLoader的注釋(自己看下PathClassLoader源碼的注釋)可以知道PathClassLoader是用于加載系統(tǒng)和應用內部類的。
問題2:為什么PathClassLoader是用于加載系統(tǒng)和應用內部的锻弓?有什么依據(jù)砾赔?
DexClassLoader的構造函數(shù):
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
// 注意API26開始optimizedDirectory設置為空,API26之前是new File(optimizedDirectory)
super(dexPath, null, librarySearchPath, parent);
}
從DexClassLoader的注釋自己看下DexClassLoader源碼的注釋)可以知道,DexClassLoader可以加載從指定路徑(包括Sd卡)加載apk暴心、jar妓盲、dex文件,實現(xiàn)動態(tài)加載功能专普,所以插件化類的加載就是通過DexClassLoader實現(xiàn)悯衬。
我們來看下類加載的過程:
# ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 首先檢查類是否已經加載過(可以防止重復加載,提高效率)
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 未加載過檀夹,則優(yōu)先從父類的ClassLoader加載
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 在從根ClassLoader加載筋粗,android中返回空
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
//父類加載失敗,則嘗試調用自身的findClass方法加載
c = findClass(name);
}
}
return c;
}
通過上面類加載的過程可以知道類的加載優(yōu)先使用父類的ClassLoader進行加載炸渡,如果父類加載失敗娜亿,才通過自身去加載,這個過程即使有個術語蚌堵,叫雙親委派买决。為什么要這樣做呢?主要是為了防止重復加載辰斋,提高效率策州。(這里回答了問題1)
2、資源加載
android中四大組件獲取資源一般都是通過getResources()得到Resources對象宫仗,通過Resources對象來獲取各種資源(文本够挂、字體、顏色等)藕夫,而Resources中獲取各種資源實際上都是通過AssetManager來實現(xiàn)的孽糖。我們來看下getString的代碼實現(xiàn)過程:
#Resources.java
public String getString(@StringRes int id) throws NotFoundException {
return getText(id).toString();
}
public CharSequence getText(@StringRes int id) throws NotFoundException {
CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
if (res != null) {
return res;
}
throw new NotFoundException("String resource ID #0x"
+ Integer.toHexString(id));
}
通過上面的代碼知道Resources的實現(xiàn)類是ResourceImpl類,getAssets()返回的是AssetManager毅贮,所以也就證實了資源的加載實際是通過AssetManager來加載的办悟。
3、android四大組件加載
上面講述了類和資源的加載原理滩褥,由于Android中的四大組件均有相應的生命周期病蛉,由系統(tǒng)管理,所以如果單純的只是通過類加載器來加載一個組件瑰煎,是無法實現(xiàn)生命周期相關的功能铺然。
要怎么實現(xiàn)插件中的組件也具有相應的生命周期呢?下面我們以Activity為實例來講解
首先先要知道什么時候酒甸、如何加載Activity并創(chuàng)建Activity實例魄健,并且什么時候創(chuàng)建Resources對象、如何將Resources與Activity建立關系的插勤。
我們通過Activity的創(chuàng)建過程來一步一步來揭開謎底:
#ActivityThread.java
//啟動Activity
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
//...省略
// 生成Context的實現(xiàn)類
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
//...省略
}
//...省略
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);
//...省略
}
#Instrumentation.java
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
return (Activity)cl.loadClass(className).newInstance();
}
// ContextImpl.java類加載器
@Override
public ClassLoader getClassLoader() {
return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader());
}
#ClassLoader.java
public static ClassLoader getSystemClassLoader() {
return SystemClassLoader.loader;
}
static private class SystemClassLoader {
public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}
private static ClassLoader createSystemClassLoader() {
String classPath = System.getProperty("java.class.path", ".");
String librarySearchPath = System.getProperty("java.library.path", "");
return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}
通過上面的代碼我們知道:
- Context的實現(xiàn)類是ContextImpl類
- Activity實例是在Instrumentation中的newActivity中通過ClassLoader的實現(xiàn)類加載創(chuàng)建的沽瘦。
- 加載Activity使用的ClassLoader是PathClassLoader革骨。(回答了問題2)
問題3:資源對象是什么時候創(chuàng)建的?
問題4:如何與Activity建立聯(lián)系的呢析恋?
我們先看下Activity中getResouces()的代碼實現(xiàn):
#Activity.java
@Override
public Resources getResources() {
return getResourcesInternal();
}
private Resources getResourcesInternal() {
if (mResources == null) {
if (mOverrideConfiguration == null) {
mResources = super.getResources();
} else {
final Context resContext = createConfigurationContext(mOverrideConfiguration);
mResources = resContext.getResources();
}
}
return mResources;
}
通過getResouces()方法知道良哲,Activity中Resources對象是通過Context獲取的,而上面Activity的創(chuàng)建過程中Context是通過Activity的attach方法進行綁定的(回答了問題4)绿满。
既然知道了Reources是通過Context的獲取的臂外,那么我們看下Context實現(xiàn)類ContextImpl中Resources的創(chuàng)建過程:
#ActivityThread.java
private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
//...省略
ContextImpl appContext = ContextImpl.createActivityContext(
this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);
//...省略
}
#ContextImpl.java
static ContextImpl createActivityContext(ActivityThread mainThread,
LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
Configuration overrideConfiguration) {
//...省略
//創(chuàng)建ContextImpl實例
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName,
activityToken, null, 0, classLoader);
//...省略
// 創(chuàng)建ResourcesManager 對象
final ResourcesManager resourcesManager = ResourcesManager.getInstance();
// Create the base resources for which all configuration contexts for this Activity
// will be rebased upon.
//通過ResourcesManager 對象生成Resources對象并賦值給Context中的Resources
context.setResources(resourcesManager.createBaseActivityResources(activityToken,
packageInfo.getResDir(),
splitDirs,
packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfiguration,
compatInfo,
classLoader));
//...省略
}
#ResourcesManager.java
public @Nullable Resources createBaseActivityResources(@NonNull IBinder activityToken,
@Nullable String resDir,
@Nullable String[] splitResDirs,
@Nullable String[] overlayDirs,
@Nullable String[] libDirs,
int displayId,
@Nullable Configuration overrideConfig,
@NonNull CompatibilityInfo compatInfo,
@Nullable ClassLoader classLoader) {
//...省略
//獲取或者創(chuàng)建Resources對象
return getOrCreateResources(activityToken, key, classLoader);
//...省略
}
private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
//...省略
ResourcesImpl resourcesImpl = createResourcesImpl(key);
//...省略
}
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
daj.setCompatibilityInfo(key.mCompatInfo);
// 創(chuàng)建AssetManager對象
final AssetManager assets = createAssetManager(key);
if (assets == null) {
return null;
}
final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
final Configuration config = generateConfig(key, dm);
//創(chuàng)建Resources實現(xiàn)類對象
final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
if (DEBUG) {
Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
}
return impl;
}
@VisibleForTesting
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
AssetManager assets = new AssetManager();
// resDir can be null if the 'android' package is creating a new Resources object.
// This is fine, since each AssetManager automatically loads the 'android' package
// already.
if (key.mResDir != null) {
//添加資源文件目錄
if (assets.addAssetPath(key.mResDir) == 0) {
Log.e(TAG, "failed to add asset path " + key.mResDir);
return null;
}
//...省略
}
通過上面的代碼,我們知道Activity中的Resouces對象是在生成ContextImpl時創(chuàng)建的喇颁,而且是通過Resources的資源加載是通過AssetManager實現(xiàn)的,而資源文件目錄是通過AssetManager的addAssetPath來實現(xiàn)的嚎货。(回答了問題3)
結合上面的代碼分析橘霎,知道了Activity是什么時候、在哪里殖属、如何創(chuàng)建的姐叁;頁知道了Activity中資源是怎么創(chuàng)建和建立關系的,那有什么方案可以解決插件中Activity的生命周期的問題呢洗显?
目前市面主要有兩個方案:通過代理或通過hook外潜。
問題5:什么是hook?
3.1、代理
問題6:代理的思想是什么或者如何實現(xiàn)插件Activity的生命周期挠唆?
代理的思想是在主工程放一個ProxyActivity处窥,啟動插件的Activity時先啟動ProxyActivity,再在ProxyActivity中創(chuàng)建插件Activity玄组,并同步生命周期給插件的Activity(解答了問題6)滔驾,看下其原理圖:
缺點
- 插件中的Activity必須繼承PluginActivity,開發(fā)侵入性強俄讹。
- 如果想支持Activity的singleTask哆致,singleInstance等啟動模式,需要自己管理Activity棧患膛,實現(xiàn)起來很繁瑣摊阀。
- 插件中需要小心處理Context,容易出錯踪蹬。
- 如果想把之前的模塊改造成插件需要很多額外的工作胞此。
3.2、Hook:
Hook:使用技術手段在運行時動態(tài)的將額外代碼依附給現(xiàn)進程延曙,從而實現(xiàn)替換現(xiàn)有處理邏輯或插入額外功能的目的
可以理解為Hook通過代理豌鹤、反射等機制實現(xiàn)代碼的注入,從而實現(xiàn)對原來功能的改寫枝缔,以達到預想中的效果
此處回答了問題5
我們再看下Activity的整體啟動流程圖:
結合Activity創(chuàng)建過程的代碼分析和上圖的流程中布疙,我們可以通過hook步驟1和步驟10來實現(xiàn)插件Activity的生命周期等相關功能蚊惯。代理的方式時因為浸入性太強,hook的方式需要考慮浸入性的問題灵临,其次我們在開發(fā)應用的時候截型,如果Activity不再Manifest中注冊,當正常方式啟動Activity時就會拋出找不到Activity的錯誤信息儒溉,所以
通過hook的方式啟動插件Activity需要解決如下問題:
a宦焦、插件Activity如何繞開Manifest中注冊的檢測
b、如何創(chuàng)建Activity實例顿涣,并同步生命周期
我們通過VirtualApk插件化框架來看其實現(xiàn)方案:
a波闹、預先在Manifest中注冊各種啟動模式的Activity占坑,啟動時hook第1步涛碑,將Intent根據(jù)啟動模式替換成預先在Manifest占坑的Activity精堕,這樣就解決了Manifest中注冊的檢測
b、hook第10步蒲障,使用插件的ClassLoader反射創(chuàng)建插件Activity歹篓,之后Activity的生命周期回調都通知給插件Activity,這樣就解決了創(chuàng)建Activity并同步生命周期的問題
3.2.1揉阎、替換系統(tǒng)Instrumentation
VirtualApk在初始化時會調用hookInstrumentationAndHandler()庄撮,該方法hook了系統(tǒng)的Instrumentation(該類與Activity啟動相關):
protected void hookInstrumentationAndHandler() {
try {
// 獲取ActivityThread對象
ActivityThread activityThread = ActivityThread.currentActivityThread();
// 獲取ActivityThread中的Instrumentation
Instrumentation baseInstrumentation = activityThread.getInstrumentation();
// 創(chuàng)建自定義的VAInstrumentation,具有復寫父類一些函數(shù)且具有代理功能
final VAInstrumentation instrumentation = createInstrumentation(baseInstrumentation);
// 反射的機制替換ActivityThread中的Instrumentation
Reflector.with(activityThread).field("mInstrumentation").set(instrumentation);
Handler mainHandler = Reflector.with(activityThread).method("getHandler").call();
Reflector.with(mainHandler).field("mCallback").set(instrumentation);
this.mInstrumentation = instrumentation;
Log.d(TAG, "hookInstrumentationAndHandler succeed : " + mInstrumentation);
} catch (Exception e) {
Log.w(TAG, e);
}
}
上面代碼毙籽,創(chuàng)建自定義的Instrumentation洞斯,通過反射替換了ActivityThread中的Instrumentation
3.2.2、使用自定義統(tǒng)Instrumentation啟動Activity
復寫3.2.1自定義Instrumentation的execStartActivity惧财,
@Override
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Fragment target, Intent intent, int requestCode, Bundle options) {
injectIntent(intent);
return mBase.execStartActivity(who, contextThread, token, target, intent, requestCode, options);
}
protected void injectIntent(Intent intent) {
// 處理隱身啟動巡扇,如果匹配到插件Activity,則隱身啟動替換為顯示啟動
mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
// null component is an implicitly intent
if (intent.getComponent() != null) {
Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(), intent.getComponent().getClassName()));
// resolve intent with Stub Activity if needed
//如果是插件Activity,將Intent中的ClassName替換為占坑中的Activity,解決啟動時是否
//在Manifest中注冊的校驗
this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
}
}
public void markIntentIfNeeded(Intent intent) {
if (intent.getComponent() == null) {
return;
}
String targetPackageName = intent.getComponent().getPackageName();
String targetClassName = intent.getComponent().getClassName();
// search map and return specific launchmode stub activity
if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
intent.putExtra(Constants.KEY_IS_PLUGIN, true);
intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
dispatchStubActivity(intent);
}
}
execStartActivity時垮衷,先處理隱身啟動厅翔,如果匹配到插件Activity則替換為顯示啟動;接著通過markIntentIfNeeded將待啟動的插件Activity替換為預先在Manifest占坑的Activity搀突,并將插件Activity信息保存在Intent中刀闷。其中有個dispatchStubActivity函數(shù),會根據(jù)Activity的launchMode選擇具體啟動哪個StubActivity仰迁。VirtualAPK為了支持Activity的launchMode在主工程的AndroidManifest中對于每種啟動模式的Activity都預埋了多個坑位甸昏。
3.2.3、hook創(chuàng)建Activity
上一步欺騙了系統(tǒng)徐许,讓系統(tǒng)以為啟動的是一個正常的Activity施蜜。這一步切換回插件的Activity,調用VAInstrumentation的newActivity:
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
try {
cl.loadClass(className);
Log.i(TAG, String.format("newActivity[%s]", className));
} catch (ClassNotFoundException e) {
ComponentName component = PluginUtil.getComponent(intent);
if (component == null) {
return newActivity(mBase.newActivity(cl, className, intent));
}
String targetClassName = component.getClassName();
Log.i(TAG, String.format("newActivity[%s : %s/%s]", className, component.getPackageName(), targetClassName));
//根據(jù)component獲取對應的插件
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(component);
// 插件對象為空時雌隅,使用默認的創(chuàng)建方式
if (plugin == null) {
// Not found then goto stub activity.
boolean debuggable = false;
try {
Context context = this.mPluginManager.getHostContext();
debuggable = (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
} catch (Throwable ex) {
}
if (debuggable) {
throw new ActivityNotFoundException("error intent: " + intent.toURI());
}
Log.i(TAG, "Not found. starting the stub activity: " + StubActivity.class);
return newActivity(mBase.newActivity(cl, StubActivity.class.getName(), intent));
}
//使用插件的ClassLoader
Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
activity.setIntent(intent);
//設置activity的資源對象
// for 4.1+
Reflector.QuietReflector.with(activity).field("mResources").set(plugin.getResources());
//返回創(chuàng)建的Activity
return newActivity(activity);
}
return newActivity(mBase.newActivity(cl, className, intent));
}
由于Manifest中注冊的占坑Activity沒有實現(xiàn)類翻默,所以這里會報ClassNotFoundException 異常缸沃,在處理異常中取插件Activity的信息,使用插件ClassLoader反射創(chuàng)建插件Activity
3.2.4修械、設置Activity的資源Resources對象
插件Activity的構造創(chuàng)建之后趾牧,還需要做一些額外的操作方才可以正常運行,比如設置訪問資源所使用的Resources對象肯污、Context等:
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle, PersistableBundle persistentState) {
injectActivity(activity);
mBase.callActivityOnCreate(activity, icicle, persistentState);
}
protected void injectActivity(Activity activity) {
final Intent intent = activity.getIntent();
if (PluginUtil.isIntentFromPlugin(intent)) {
Context base = activity.getBaseContext();
try {
//根據(jù)Intent獲取對應的插件對象
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
//反射設置Resources對象為插件Resources
Reflector.with(base).field("mResources").set(plugin.getResources());
Reflector reflector = Reflector.with(activity);
// 反射設置Context為插件的Context, 其實就是主工程的Context
reflector.field("mBase").set(plugin.createPluginContext(activity.getBaseContext()));
// 反射設置Application為插件的Application,其實就是主工程的Application
reflector.field("mApplication").set(plugin.getApplication());
//設置橫豎屏
// set screenOrientation
ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
activity.setRequestedOrientation(activityInfo.screenOrientation);
}
//重新設置Intent
// for native activity
ComponentName component = PluginUtil.getComponent(intent);
Intent wrapperIntent = new Intent(intent);
wrapperIntent.setClassName(component.getPackageName(), component.getClassName());
activity.setIntent(wrapperIntent);
} catch (Exception e) {
Log.w(TAG, e);
}
}
}
這段代碼主要是將Activity中的Resource翘单,Context等對象替換成了插件的相應對象,保證插件Activity在調用涉及到Context的方法時能夠正確運行蹦渣。
至此便實現(xiàn)了插件Activity的啟動哄芜,且此插件Activity中并不需要什么額外的處理,和常規(guī)的Activity一樣柬唯。那問題來了忠烛,之后的onResume,onStop等生命周期怎么辦呢权逗?答案是所有和Activity相關的生命周期函數(shù),系統(tǒng)都會調用插件中的Activity冤议。原因在于AMS在處理Activity時斟薇,通過一個token表示具體Activity對象,而這個token正是和啟動Activity時創(chuàng)建的對象對應的恕酸,而這個Activity被我們替換成了插件中的Activity堪滨,所以之后AMS的所有調用都會傳給插件中的Activity。
其他組件
四大組件中Activity的支持是最復雜的蕊温,其他組件的實現(xiàn)原理要簡單很多袱箱,簡要概括如下:
- Service:Service和Activity的差別在于,Activity的生命周期是由用戶交互決定的义矛,而Service的生命周期是我們通過代碼主動調用的发笔,且Service實例和manifest中注冊的是一一對應的。實現(xiàn)Service插件化的思路是通過在manifest中預埋StubService凉翻,hook系統(tǒng)startService等調用替換啟動的Service了讨,之后在StubService中創(chuàng)建插件Service,并手動管理其生命周期制轰。
- BroadCastReceiver:解析插件的manifest前计,將靜態(tài)注冊的廣播轉為動態(tài)注冊。
- ContentProvider:類似于Service的方式垃杖,對插件ContentProvider的所有調用都會通過一個在manifest中占坑的ContentProvider分發(fā)男杈。
七、插件間调俘、插件與主程序間的交互
上面解決了插件化的原理伶棒,那么插件與插件之間旺垒、插件與主工程之間如何相互調用、相互訪問資源呢?
相互調用對應的是類的調用苞冯,而每個類都需要對應的ClassLoader來加載袖牙,所以一般就分為兩種調用機制:
- 單ClassLoader機制:將插件中DexClassLoader的pathList合入主工程的pathList中,這樣主工程就可以調用插件的類和方法舅锄,插件調用另一個插件則需要借助主工程或者插件框架統(tǒng)一訪問接口來實現(xiàn)鞭达。單ClassLoader的弊端就是如果插件使用不同版本的庫就會出現(xiàn)問題。
- 多ClassLoader機制:每個插件對應一個DexClassLoader皇忿,那么主工程調用插件的類和方法就需要借助插件的ClassLoader畴蹭,一般需要進行插件化框架進行統(tǒng)一管理。
資源的訪問也是有兩種方式: - 合入式:將插件的資源路徑合入主工程的AssetManager中鳍烁,因此生成的Resources可以同時訪問插件和主工程的資源叨襟,弊端是由于插件和主工程是獨立編譯的,所有會存在資源id沖突的情況(解決資源Id沖突需要通過更改編譯過程修改資源Id幔荒,資源id是由8位16進制數(shù)表示糊闽,表示為0xPPTTNNNN。PP段用來區(qū)分包空間爹梁,默認只區(qū)分了應用資源和系統(tǒng)資源右犹,TT段為資源類型,NNNN段在同一個APK中從0000遞增姚垃。其中系統(tǒng)的pp段的值是0x01,應用pp段的值是0x7f;系統(tǒng)和應用的tt段值都是0x04,剩下的NNNN值在0x0000至0xfffff之間)
- 獨立式:資源隔離念链,不存在沖突情況,但是資源共享比較麻煩积糯,需要借助統(tǒng)一接口進行管理掂墓,會存在資源在不同插件重復存在的情況。
八看成、幾種成熟的插件框架
第一代:dynamic-load-apk最早使用ProxyActivity這種靜態(tài)代理技術君编,由ProxyActivity去控制插件中PluginActivity的生命周期。該種方式缺點明顯绍昂,插件中的activity必須繼承PluginActivity啦粹,開發(fā)時要小心處理context。而DroidPlugin通過Hook系統(tǒng)服務的方式啟動插件中的Activity窘游,使得開發(fā)插件的過程和開發(fā)普通的app沒有什么區(qū)別唠椭,但是由于hook過多系統(tǒng)服務,異常復雜且不夠穩(wěn)定忍饰。
第二代:為了同時達到插件開發(fā)的低侵入性(像開發(fā)普通app一樣開發(fā)插件)和框架的穩(wěn)定性贪嫂,在實現(xiàn)原理上都是趨近于選擇盡量少的hook,并通過在manifest中預埋一些組件實現(xiàn)對四大組件的插件化艾蓝。另外各個框架根據(jù)其設計思想都做了不同程度的擴展力崇,其中Small更是做成了一個跨平臺斗塘,組件化的開發(fā)框架。
第三代:VirtualApp比較厲害亮靴,能夠完全模擬app的運行環(huán)境馍盟,能夠實現(xiàn)app的免安裝運行和雙開技術。Atlas是阿里今年開源出來的一個結合組件化和熱修復技術的一個app基礎框架茧吊,其廣泛的應用與阿里系的各個app贞岭,其號稱是一個容器化框架。