一、定義&理解
插件:Plug-in又稱add-in、addon或add-on,也稱外掛,它是一種遵循一定規(guī)范的應(yīng)用程序接口編寫出來的程序。
我的理解就是:插件是一個(gè)程序的輔助或者擴(kuò)展功能模塊,對程序來說插件可有可無,但它能給予程序一定的額外功能。
二、背景
android插件化的概念始于2012年,出自于免安裝的想法。發(fā)展到后來,android應(yīng)用程序更多的需要依賴一些第三方庫,比如地圖sdk、分享sdK、支付sdk等等,導(dǎo)致安裝包變得越來越大,單是依賴這些sdk,安裝包可能就會額外的增加20-30M的大小;當(dāng)需要新增功能時(shí),不得不重新更新整個(gè)安裝包。所以這時(shí)插件化的需求就變得更為更為突出。
插件化主要是解決的是減少應(yīng)用程序大小、免安裝擴(kuò)展功能。一般具一定規(guī)模的程序應(yīng)用插件化才有意義。
三、原理
插件本身是一個(gè)獨(dú)立的apk文件,要實(shí)現(xiàn)插件動態(tài)加載運(yùn)行,需要了解apk的運(yùn)行機(jī)制。java虛擬機(jī)運(yùn)行的是class文件,使用ClassLoader加載;而Android虛擬機(jī)運(yùn)行的是dex文件使用的是DexClassLoader,而資源是存在xml文件中,所以就會涉及到資源文件的加載過程,所以要實(shí)現(xiàn)插件的動態(tài)加載運(yùn)行,首先就需要解決類文件、資源、庫等的加載。
1、類加載
android運(yùn)行的是dex文件對應(yīng)的類加載器是PathClassLoader和DexClassLoader,PathClassLoader和DexClassLoader均繼承自BaseDexClassLoader,BaseDexClassLoader又繼承自ClassLoader。
我們來看下DexClassLoader基類BaseDexClassLoader的構(gòu)造函數(shù):
/**
* @param dexPath 目標(biāo)類所在的apk或jar文件路徑,類加載器將從這個(gè)apk或jar文件路徑查找目標(biāo)類
* @param optimizedDirectory 從apk中解壓出dex文件,經(jīng)過odex優(yōu)化之后,將解壓和優(yōu)化后的dex文件存放到optimizedDirectory 目錄,下次直接從這個(gè)目錄中的dex文件加載目標(biāo)類。注意從API26開始此參數(shù)已經(jīng)廢棄
* @param librarySearchPath 目標(biāo)類所使用的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的構(gòu)造函數(shù):
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
從PathClassLoader的注釋(自己看下PathClassLoader源碼的注釋)可以知道PathClassLoader是用于加載系統(tǒng)和應(yīng)用內(nèi)部類的。
問題2:為什么PathClassLoader是用于加載系統(tǒng)和應(yīng)用內(nèi)部的?有什么依據(jù)?
DexClassLoader的構(gòu)造函數(shù):
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
// 注意API26開始o(jì)ptimizedDirectory設(shè)置為空,API26之前是new File(optimizedDirectory)
super(dexPath, null, librarySearchPath, parent);
}
從DexClassLoader的注釋自己看下DexClassLoader源碼的注釋)可以知道,DexClassLoader可以加載從指定路徑(包括Sd卡)加載apk、jar、dex文件,實(shí)現(xiàn)動態(tài)加載功能,所以插件化類的加載就是通過DexClassLoader實(shí)現(xiàn)。
我們來看下類加載的過程:
# ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 首先檢查類是否已經(jīng)加載過(可以防止重復(fù)加載,提高效率)
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.
//父類加載失敗,則嘗試調(diào)用自身的findClass方法加載
c = findClass(name);
}
}
return c;
}
通過上面類加載的過程可以知道類的加載優(yōu)先使用父類的ClassLoader進(jìn)行加載,如果父類加載失敗,才通過自身去加載,這個(gè)過程即使有個(gè)術(shù)語,叫雙親委派。為什么要這樣做呢?主要是為了防止重復(fù)加載,提高效率。(這里回答了問題1)
2、資源加載
android中四大組件獲取資源一般都是通過getResources()得到Resources對象,通過Resources對象來獲取各種資源(文本、字體、顏色等),而Resources中獲取各種資源實(shí)際上都是通過AssetManager來實(shí)現(xiàn)的。我們來看下getString的代碼實(shí)現(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的實(shí)現(xiàn)類是ResourceImpl類,getAssets()返回的是AssetManager,所以也就證實(shí)了資源的加載實(shí)際是通過AssetManager來加載的。
3、android四大組件加載
上面講述了類和資源的加載原理,由于Android中的四大組件均有相應(yīng)的生命周期,由系統(tǒng)管理,所以如果單純的只是通過類加載器來加載一個(gè)組件,是無法實(shí)現(xiàn)生命周期相關(guān)的功能。
要怎么實(shí)現(xiàn)插件中的組件也具有相應(yīng)的生命周期呢?下面我們以Activity為實(shí)例來講解
首先先要知道什么時(shí)候、如何加載Activity并創(chuàng)建Activity實(shí)例,并且什么時(shí)候創(chuàng)建Resources對象、如何將Resources與Activity建立關(guān)系的。
我們通過Activity的創(chuàng)建過程來一步一步來揭開謎底:
#ActivityThread.java
//啟動Activity
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
//...省略
// 生成Context的實(shí)現(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的實(shí)現(xiàn)類是ContextImpl類
- Activity實(shí)例是在Instrumentation中的newActivity中通過ClassLoader的實(shí)現(xiàn)類加載創(chuàng)建的。
- 加載Activity使用的ClassLoader是PathClassLoader。(回答了問題2)
問題3:資源對象是什么時(shí)候創(chuàng)建的?
問題4:如何與Activity建立聯(lián)系的呢?
我們先看下Activity中g(shù)etResouces()的代碼實(shí)現(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方法進(jìn)行綁定的(回答了問題4)。
既然知道了Reources是通過Context的獲取的,那么我們看下Context實(shí)現(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實(shí)例
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實(shí)現(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時(shí)創(chuàng)建的,而且是通過Resources的資源加載是通過AssetManager實(shí)現(xiàn)的,而資源文件目錄是通過AssetManager的addAssetPath來實(shí)現(xiàn)的。(回答了問題3)
結(jié)合上面的代碼分析,知道了Activity是什么時(shí)候、在哪里、如何創(chuàng)建的;頁知道了Activity中資源是怎么創(chuàng)建和建立關(guān)系的,那有什么方案可以解決插件中Activity的生命周期的問題呢?
目前市面主要有兩個(gè)方案:通過代理或通過hook。
問題5:什么是hook?
3.1、代理
問題6:代理的思想是什么或者如何實(shí)現(xiàn)插件Activity的生命周期?
代理的思想是在主工程放一個(gè)ProxyActivity,啟動插件的Activity時(shí)先啟動ProxyActivity,再在ProxyActivity中創(chuàng)建插件Activity,并同步生命周期給插件的Activity(解答了問題6),看下其原理圖:
缺點(diǎn)
- 插件中的Activity必須繼承PluginActivity,開發(fā)侵入性強(qiáng)。
- 如果想支持Activity的singleTask,singleInstance等啟動模式,需要自己管理Activity棧,實(shí)現(xiàn)起來很繁瑣。
- 插件中需要小心處理Context,容易出錯(cuò)。
- 如果想把之前的模塊改造成插件需要很多額外的工作。
3.2、Hook:
Hook:使用技術(shù)手段在運(yùn)行時(shí)動態(tài)的將額外代碼依附給現(xiàn)進(jìn)程,從而實(shí)現(xiàn)替換現(xiàn)有處理邏輯或插入額外功能的目的
可以理解為Hook通過代理、反射等機(jī)制實(shí)現(xiàn)代碼的注入,從而實(shí)現(xiàn)對原來功能的改寫,以達(dá)到預(yù)想中的效果
此處回答了問題5
我們再看下Activity的整體啟動流程圖:
結(jié)合Activity創(chuàng)建過程的代碼分析和上圖的流程中,我們可以通過hook步驟1和步驟10來實(shí)現(xiàn)插件Activity的生命周期等相關(guān)功能。代理的方式時(shí)因?yàn)榻胄蕴珡?qiáng),hook的方式需要考慮浸入性的問題,其次我們在開發(fā)應(yīng)用的時(shí)候,如果Activity不再M(fèi)anifest中注冊,當(dāng)正常方式啟動Activity時(shí)就會拋出找不到Activity的錯(cuò)誤信息,所以
通過hook的方式啟動插件Activity需要解決如下問題:
a、插件Activity如何繞開Manifest中注冊的檢測
b、如何創(chuàng)建Activity實(shí)例,并同步生命周期
我們通過VirtualApk插件化框架來看其實(shí)現(xiàn)方案:
a、預(yù)先在Manifest中注冊各種啟動模式的Activity占坑,啟動時(shí)hook第1步,將Intent根據(jù)啟動模式替換成預(yù)先在Manifest占坑的Activity,這樣就解決了Manifest中注冊的檢測
b、hook第10步,使用插件的ClassLoader反射創(chuàng)建插件Activity,之后Activity的生命周期回調(diào)都通知給插件Activity,這樣就解決了創(chuàng)建Activity并同步生命周期的問題
3.2.1、替換系統(tǒng)Instrumentation
VirtualApk在初始化時(shí)會調(diào)用hookInstrumentationAndHandler(),該方法hook了系統(tǒng)的Instrumentation(該類與Activity啟動相關(guān)):
protected void hookInstrumentationAndHandler() {
try {
// 獲取ActivityThread對象
ActivityThread activityThread = ActivityThread.currentActivityThread();
// 獲取ActivityThread中的Instrumentation
Instrumentation baseInstrumentation = activityThread.getInstrumentation();
// 創(chuàng)建自定義的VAInstrumentation,具有復(fù)寫父類一些函數(shù)且具有代理功能
final VAInstrumentation instrumentation = createInstrumentation(baseInstrumentation);
// 反射的機(jī)制替換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
復(fù)寫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,解決啟動時(shí)是否
//在Manifest中注冊的校驗(yàn)
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時(shí),先處理隱身啟動,如果匹配到插件Activity則替換為顯示啟動;接著通過markIntentIfNeeded將待啟動的插件Activity替換為預(yù)先在Manifest占坑的Activity,并將插件Activity信息保存在Intent中。其中有個(gè)dispatchStubActivity函數(shù),會根據(jù)Activity的launchMode選擇具體啟動哪個(gè)StubActivity。VirtualAPK為了支持Activity的launchMode在主工程的AndroidManifest中對于每種啟動模式的Activity都預(yù)埋了多個(gè)坑位。
3.2.3、hook創(chuàng)建Activity
上一步欺騙了系統(tǒng),讓系統(tǒng)以為啟動的是一個(gè)正常的Activity。這一步切換回插件的Activity,調(diào)用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獲取對應(yīng)的插件
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(component);
// 插件對象為空時(shí),使用默認(rèn)的創(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);
//設(shè)置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沒有實(shí)現(xiàn)類,所以這里會報(bào)ClassNotFoundException 異常,在處理異常中取插件Activity的信息,使用插件ClassLoader反射創(chuàng)建插件Activity
3.2.4、設(shè)置Activity的資源Resources對象
插件Activity的構(gòu)造創(chuàng)建之后,還需要做一些額外的操作方才可以正常運(yùn)行,比如設(shè)置訪問資源所使用的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獲取對應(yīng)的插件對象
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
//反射設(shè)置Resources對象為插件Resources
Reflector.with(base).field("mResources").set(plugin.getResources());
Reflector reflector = Reflector.with(activity);
// 反射設(shè)置Context為插件的Context, 其實(shí)就是主工程的Context
reflector.field("mBase").set(plugin.createPluginContext(activity.getBaseContext()));
// 反射設(shè)置Application為插件的Application,其實(shí)就是主工程的Application
reflector.field("mApplication").set(plugin.getApplication());
//設(shè)置橫豎屏
// set screenOrientation
ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
activity.setRequestedOrientation(activityInfo.screenOrientation);
}
//重新設(shè)置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等對象替換成了插件的相應(yīng)對象,保證插件Activity在調(diào)用涉及到Context的方法時(shí)能夠正確運(yùn)行。
至此便實(shí)現(xiàn)了插件Activity的啟動,且此插件Activity中并不需要什么額外的處理,和常規(guī)的Activity一樣。那問題來了,之后的onResume,onStop等生命周期怎么辦呢?答案是所有和Activity相關(guān)的生命周期函數(shù),系統(tǒng)都會調(diào)用插件中的Activity。原因在于AMS在處理Activity時(shí),通過一個(gè)token表示具體Activity對象,而這個(gè)token正是和啟動Activity時(shí)創(chuàng)建的對象對應(yīng)的,而這個(gè)Activity被我們替換成了插件中的Activity,所以之后AMS的所有調(diào)用都會傳給插件中的Activity。
其他組件
四大組件中Activity的支持是最復(fù)雜的,其他組件的實(shí)現(xiàn)原理要簡單很多,簡要概括如下:
- Service:Service和Activity的差別在于,Activity的生命周期是由用戶交互決定的,而Service的生命周期是我們通過代碼主動調(diào)用的,且Service實(shí)例和manifest中注冊的是一一對應(yīng)的。實(shí)現(xiàn)Service插件化的思路是通過在manifest中預(yù)埋StubService,hook系統(tǒng)startService等調(diào)用替換啟動的Service,之后在StubService中創(chuàng)建插件Service,并手動管理其生命周期。
- BroadCastReceiver:解析插件的manifest,將靜態(tài)注冊的廣播轉(zhuǎn)為動態(tài)注冊。
- ContentProvider:類似于Service的方式,對插件ContentProvider的所有調(diào)用都會通過一個(gè)在manifest中占坑的ContentProvider分發(fā)。
七、插件間、插件與主程序間的交互
上面解決了插件化的原理,那么插件與插件之間、插件與主工程之間如何相互調(diào)用、相互訪問資源呢?
相互調(diào)用對應(yīng)的是類的調(diào)用,而每個(gè)類都需要對應(yīng)的ClassLoader來加載,所以一般就分為兩種調(diào)用機(jī)制:
- 單ClassLoader機(jī)制:將插件中DexClassLoader的pathList合入主工程的pathList中,這樣主工程就可以調(diào)用插件的類和方法,插件調(diào)用另一個(gè)插件則需要借助主工程或者插件框架統(tǒng)一訪問接口來實(shí)現(xiàn)。單ClassLoader的弊端就是如果插件使用不同版本的庫就會出現(xiàn)問題。
- 多ClassLoader機(jī)制:每個(gè)插件對應(yīng)一個(gè)DexClassLoader,那么主工程調(diào)用插件的類和方法就需要借助插件的ClassLoader,一般需要進(jìn)行插件化框架進(jìn)行統(tǒng)一管理。
資源的訪問也是有兩種方式: - 合入式:將插件的資源路徑合入主工程的AssetManager中,因此生成的Resources可以同時(shí)訪問插件和主工程的資源,弊端是由于插件和主工程是獨(dú)立編譯的,所有會存在資源id沖突的情況(解決資源Id沖突需要通過更改編譯過程修改資源Id,資源id是由8位16進(jìn)制數(shù)表示,表示為0xPPTTNNNN。PP段用來區(qū)分包空間,默認(rèn)只區(qū)分了應(yīng)用資源和系統(tǒng)資源,TT段為資源類型,NNNN段在同一個(gè)APK中從0000遞增。其中系統(tǒng)的pp段的值是0x01,應(yīng)用pp段的值是0x7f;系統(tǒng)和應(yīng)用的tt段值都是0x04,剩下的NNNN值在0x0000至0xfffff之間)
- 獨(dú)立式:資源隔離,不存在沖突情況,但是資源共享比較麻煩,需要借助統(tǒng)一接口進(jìn)行管理,會存在資源在不同插件重復(fù)存在的情況。
八、幾種成熟的插件框架
第一代:dynamic-load-apk最早使用ProxyActivity這種靜態(tài)代理技術(shù),由ProxyActivity去控制插件中PluginActivity的生命周期。該種方式缺點(diǎn)明顯,插件中的activity必須繼承PluginActivity,開發(fā)時(shí)要小心處理context。而DroidPlugin通過Hook系統(tǒng)服務(wù)的方式啟動插件中的Activity,使得開發(fā)插件的過程和開發(fā)普通的app沒有什么區(qū)別,但是由于hook過多系統(tǒng)服務(wù),異常復(fù)雜且不夠穩(wěn)定。
第二代:為了同時(shí)達(dá)到插件開發(fā)的低侵入性(像開發(fā)普通app一樣開發(fā)插件)和框架的穩(wěn)定性,在實(shí)現(xiàn)原理上都是趨近于選擇盡量少的hook,并通過在manifest中預(yù)埋一些組件實(shí)現(xiàn)對四大組件的插件化。另外各個(gè)框架根據(jù)其設(shè)計(jì)思想都做了不同程度的擴(kuò)展,其中Small更是做成了一個(gè)跨平臺,組件化的開發(fā)框架。
第三代:VirtualApp比較厲害,能夠完全模擬app的運(yùn)行環(huán)境,能夠?qū)崿F(xiàn)app的免安裝運(yùn)行和雙開技術(shù)。Atlas是阿里今年開源出來的一個(gè)結(jié)合組件化和熱修復(fù)技術(shù)的一個(gè)app基礎(chǔ)框架,其廣泛的應(yīng)用與阿里系的各個(gè)app,其號稱是一個(gè)容器化框架。