插件化原理(small)
ClassLoader
DexClassLoader 和 PathClassLoader
android 中的calssloader舀透,區(qū)別在于DexClassLoader多了一個optimize的優(yōu)化目錄,其可以加載外部的dex尊沸,zip,so等包些楣,而pathclassloader只能加載內(nèi)部的dex续崖,apk等包
而兩個都是繼承自BaseDexClassLoader ,而BaseDexClassLoader的主要工作是交給DexPathList是做,接下來讓我們看看這個DexPathList的構(gòu)造方法
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
if (definingContext == null) {
throw new NullPointerException("definingContext == null");
}
if (dexPath == null) {
throw new NullPointerException("dexPath == null");
}
if (optimizedDirectory != null) {
if (!optimizedDirectory.exists()) {
throw new IllegalArgumentException(
"optimizedDirectory doesn't exist: "
+ optimizedDirectory);
}
if (!(optimizedDirectory.canRead()
&& optimizedDirectory.canWrite())) {
throw new IllegalArgumentException(
"optimizedDirectory not readable/writable: "
+ optimizedDirectory);
}
}
this.definingContext = definingContext;
this.dexElements =
makeDexElements(splitDexPath(dexPath), optimizedDirectory);
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
dexPath就是我們需要加載插件的路徑鼻百,可以看到主要是由makeDexElements這個方法實(shí)現(xiàn)
private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
/*
* Open all files and load the (direct or contained) dex files
* up front.
*/
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
try {
zip = new ZipFile(file);
} catch (IOException ex) {
/*
* Note: ZipException (a subclass of IOException)
* might get thrown by the ZipFile constructor
* (e.g. if the file isn't actually a zip/jar
* file).
*/
System.logE("Unable to open zip file: " + file, ex);
}
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ignored) {
/*
* IOException might get thrown "legitimately" by
* the DexFile constructor if the zip file turns
* out to be resource-only (that is, no
* classes.dex file in it). Safe to just ignore
* the exception here, and let dex == null.
*/
}
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
該方法返回的是一個element的數(shù)組绞旅。
看到這里,我們先不用深入理解makeDexElements內(nèi)部的邏輯實(shí)現(xiàn)温艇,先思考一個問題因悲,何為插件化?
我們做插件的目的是有很多種勺爱,例如:減少包體積晃琳,熱更新 。。卫旱。
插件化意味著宿主和插件之間能夠進(jìn)行通信人灼,宿主可以調(diào)用插件里的對象,宿主可以訪問插件里的資源等等顾翼。
所以每個BaseDexClassLoader構(gòu)造完之后都會有一個dexElements投放,這就說明宿主的classloader有一個,我們插件內(nèi)部自己的classloader也會有一個适贸,說到這里已經(jīng)說明插件化類訪問的原理了灸芳。其核心就是分為以下步驟:
- 宿主的classloader通過反射拿到內(nèi)部的dexPathList數(shù)組
- 構(gòu)造一個我們插件的DexClassLoader(而不是PathClassLoader),然后通過反射拿到其中的dexPathList數(shù)組
- 將兩個數(shù)組進(jìn)行合并拜姿,然后通過反射設(shè)置會宿主的classloader中
事實(shí)上耗绿,Android官方的multidex就是這個原理。完成這些步驟以后砾隅,我們在宿主中就可以調(diào)用插件的類了误阻,但是工作還沒完,資源如何訪問晴埂?
Resources
設(shè)想一個問題究反,我們將兩個dexPathList進(jìn)行了合并,此時宿主可以調(diào)用插件,但是假設(shè)插件內(nèi)部根據(jù)一個id查找一個資源儒洛,會報(bào)ResourcesNotFind的異常精耐,為什么呢?我們來看看源碼琅锻,假設(shè)當(dāng)前處在插件中的某個activity卦停,根據(jù)id獲取獲取某個drawable并設(shè)置進(jìn)
imageview中
imageview.setImageDrawable(getResources().getDrawable(R.drawable.ic_launcher_background))
最終回到ContextThemeWrapper中:
@Override
public Resources getResources() {
return getResourcesInternal();
}
private Resources getResourcesInternal() {
//mResouces為空
if (mResources == null) {
if (mOverrideConfiguration == null) {
//將會調(diào)用super.getResources()
mResources = super.getResources();
} else {
final Context resContext = createConfigurationContext(mOverrideConfiguration);
mResources = resContext.getResources();
}
}
return mResources;
}
而super.getReources最終實(shí)現(xiàn)是ContextImpl.getResources()中
而ContextImpl是在ActivityThread中由系統(tǒng)執(zhí)行各個步驟時創(chuàng)建的,我們插件化的activity根本不會走這樣一套流程(如果走這套流程的話恼蓬,插件化就毫無意義啦~~)
所以惊完,拿到的ContextImpl則是宿主的。而這個ComtextImpl在Application到Activity的各個階段都會有所區(qū)別
具體在于处硬,Application的mBase成員是通過ContextImpl.createAppContext經(jīng)過attachBaseContext后創(chuàng)建的小槐,而Activity的mBase成員是通過ContextImpl.createActivityContext創(chuàng)建的,兩者的區(qū)別有興趣可以閱讀下源碼
無論以哪種方式荷辕,最終都會來到ResourcesManager.getOrCreateResources()方法創(chuàng)建資源對象凿跳,
而經(jīng)過層層判斷之后,又會來到createResourcesImpl()方法疮方,
而createResourcesImpl()內(nèi)部
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
final AssetManager assets = createAssetManager(key);
if (assets == null) {
return null;
}
final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
final Configuration config = generateConfig(key, dm);
final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
return impl;
}
而createAssetManager()方法:
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) {
...
return null;
}
}
...
...
return assets;
}
原來assets.addAssetPath()方法是把key.mResDir加進(jìn)去assetmanager中控嗜,這樣就可以訪問到資源,mResDir就是res文件
至此我們終于知道為啥在插件訪問不到資源了骡显。
看到這里有兩個實(shí)現(xiàn)方法
- 在插件的Activity重寫getResources方法疆栏,然后根據(jù)重新創(chuàng)建一個AssetManager曾掂,這樣插件內(nèi)的資源可能通過自己的AssetManager進(jìn)行資源
- 通過反射拿到宿主的AssetManager,然后調(diào)用內(nèi)部addAssetPath()將當(dāng)前插件的路徑傳進(jìn)去承边,相當(dāng)于進(jìn)行資源的合并遭殉。
這兩種方法都可行石挂,第一種會導(dǎo)致資源爆炸博助,宿主一份,插件一份痹愚,而且這里面的資源無法公用富岳。第二種則會導(dǎo)致資源id沖突,但是可以通過某些手段進(jìn)行控制(比如控制分配id的段達(dá)到防止資源id沖突)
而small用的是第二種拯腮,并且配合gradle介入資源id段(PP)的分配情況
具體原理則是:在gradle執(zhí)行到mergeAndroidResources這個task時窖式,將R.java,R.txt替換為small extention中配置的packageId字段动壤,并且替換完成后萝喘,重寫整個resources.arsc文件,將原來的arsc文件里面的索引的id替換成配置后的id琼懊。如原來生成的id為0x7F010001 替換成自定義 0x21010001
替換資源id這個方法阁簸,除了上述這個之外,還可以手動修改AAPT的源碼,然后重新編譯一個aapt工具
至此哼丈,資源也可以訪問了启妹。
四大組件
類和資源都可以訪問了,我們都知道四大組件要在宿主的AndroidManifest.xml中注冊才可以使用醉旦,否則會提示找不到該component饶米。以activity為例,如果將插件中的activity在宿主AndroidManifest中注冊,那插件化將毫無意義车胡,因?yàn)槊看斡行耡ctivity都需要更新宿主檬输,插件的思想也就無從談起。
有什么辦法可以做到不在宿主中注冊也可以調(diào)用呢匈棘?
Small使用hook褪猛,主要是hook住Instrumentation,ActivityThread和mH這幾個類。
App創(chuàng)建過程 說過 Instrumentation最初的目的是為了給UI測試預(yù)留的接口羹饰,沒想到可以被插件化玩出花樣來伊滋,可能谷歌一開始也沒想到。
步驟如下:
- Step 1
public static void hookInstrumentation() {
try {
Class at = Class.forName("android.app.ActivityThread");
Method atMethod = at.getDeclaredMethod("currentActivityThread",null);
atMethod.setAccessible(true);
Object activityThread = atMethod.invoke(null,null
);
Field instruFiled = at.getDeclaredField("mInstrumentation");
instruFiled.setAccessible(true);
Instrumentation instrumentation = (Instrumentation) instruFiled.get(activityThread);
TestInstrumentationWrapper wrapper = new TestInstrumentationWrapper(instrumentation);
instruFiled.set(activityThread, wrapper);
Log.d(TAG,"hook init success");
} catch (Throwable e) {
e.printStackTrace();
}
}
private static class TestInstrumentationWrapper extends Instrumentation {
private Instrumentation mBase;
public TestInstrumentationWrapper(Instrumentation base) {
mBase = base;
}
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent
intent,
int requestCode, Bundle options) {
Log.d(TAG, "TestInstrumentationWrapper hook 1");
//step1
return realExecStartActivity1(who, contextThread, token, target, intent, requestCode, options);
}
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, String target, Intent intent,
int requestCode, Bundle options) {
Log.d(TAG, "TestInstrumentationWrapper hook 2");
return realExecStartActivity2(who, contextThread, token, target, intent, requestCode, options);
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, String resultWho,
Intent intent, int requestCode, Bundle options, UserHandle user) {
Log.d(TAG, "TestInstrumentationWrapper hook 3");
return realExecStartActivity3(who,contextThread,token,resultWho,intent,requestCode,options,user);
}
@SuppressWarnings("NewApi")
private ActivityResult realExecStartActivity3(Context who, IBinder contextThread, IBinder token, String resultWho,
Intent intent, int requestCode, Bundle options, UserHandle user) {
ActivityResult activityResult = null;
try {
Class c = mBase.getClass();
Method execStartActivity = c.getDeclaredMethod("execStartActivity",
Context.class,
IBinder.class,
IBinder.class,
String.class,
Intent.class,
int.class,
Bundle.class,
UserHandle.class
);
activityResult = (ActivityResult) execStartActivity.invoke(mBase,
who,
contextThread,
token,
resultWho,
intent,
requestCode,
options,
user
);
} catch (Exception e) {
e.printStackTrace();
}
return activityResult;
}
private ActivityResult realExecStartActivity2(Context who, IBinder contextThread, IBinder token, String target,
Intent intent, int requestCode, Bundle options) {
ActivityResult activityResult = null;
try {
Class c = mBase.getClass();
Method execStartActivity = c.getDeclaredMethod("execStartActivity",
Context.class,
IBinder.class,
IBinder.class,
String.class,
Intent.class,
int.class,
Bundle.class
);
activityResult = (ActivityResult) execStartActivity.invoke(mBase,
who,
contextThread,
token,
target,
intent,
requestCode,
options
);
} catch (Exception e) {
e.printStackTrace();
}
return activityResult;
}
private ActivityResult realExecStartActivity1(Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
ActivityResult activityResult = null;
try {
Class c = mBase.getClass();
Method execStartActivity = c.getDeclaredMethod("execStartActivity",
Context.class,
IBinder.class,
IBinder.class,
Activity.class,
Intent.class,
int.class,
Bundle.class
);
activityResult = (ActivityResult) execStartActivity.invoke(mBase,
who,
contextThread,
token,
target,
intent,
requestCode,
options
);
} catch (Exception e) {
e.printStackTrace();
}
return activityResult;
}
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
//step2
//在這里實(shí)例化插件的activity
return super.newActivity(cl, className, intent);
}
// on Applicaiton
class App : Application(){
override fun onCreate() {
super.onCreate()
HookUtil.hookInstrumentation()
}
}
在MainActivity中通過intent啟動一個TestActivity队秩,運(yùn)行結(jié)果:
2019-03-23 15:36:09.820 6537-6537/com.example.simpleapp D/Hook: hook init success
2019-03-23 15:36:13.068 6537-6537/com.example.simpleapp D/Hook: TestInstrumentationWrapper hook 1
此時我們已經(jīng)hook住了startActivity過程笑旺,那么我可以在宿主中占坑一個ProxyActivity,在啟動插件activity的過程中馍资,重定向至ProxyActivity達(dá)到偷梁換柱的目的筒主。
step2:
有去有回,經(jīng)過上面已經(jīng)可以做到將插件的activity換了個皮變成宿主中的ProxyActivity,但是怎么將這個ProxyActivity換回來呢?
這涉及app啟動流程乌妙,主要是本地app進(jìn)程(ActivityThread)和系統(tǒng)SystemServer進(jìn)程(ActivityManagerService)進(jìn)行binder通信的過程,有興趣的看下之前寫過的 一篇文章 分析app創(chuàng)建流程
現(xiàn)在我們只需要知道使兔,Context.startActivity()最終會來到
ActivityStackSupervisor.realStartActivityLocked()
final boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app,
boolean andResume, boolean checkConfig) throws RemoteException {
...
app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken,
System.identityHashCode(r), r.info,
// TODO: Have this take the merged configuration instead of separate global
// and override configs.
mergedConfiguration.getGlobalConfiguration(),
mergedConfiguration.getOverrideConfiguration(), r.compat,
r.launchedFromPackage, task.voiceInteractor, app.repProcState, r.icicle,
r.persistentState, results, newIntents, !andResume,
mService.isNextTransitionForward(), profilerInfo);
}
而app.thread是ApplicationThread,它是一個ActivityThread的內(nèi)部類藤韵,可以理解為在ActivityManagerService這一側(cè)的ActivityThread代理對象虐沥,主要是通過binder與遠(yuǎn)端(app進(jìn)程)進(jìn)行調(diào)用,接著我們分析
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
int procState, Bundle state, PersistableBundle persistentState,
List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {
updateProcessState(procState, false);
ActivityClientRecord r = new ActivityClientRecord();
r.token = token;
r.ident = ident;
r.intent = intent;
r.referrer = referrer;
r.voiceInteractor = voiceInteractor;
r.activityInfo = info;
r.compatInfo = compatInfo;
r.state = state;
r.persistentState = persistentState;
r.pendingResults = pendingResults;
r.pendingIntents = pendingNewIntents;
r.startsNotResumed = notResumed;
r.isForward = isForward;
r.profilerInfo = profilerInfo;
r.overrideConfig = overrideConfig;
updatePendingConfiguration(curConfig);
sendMessage(H.LAUNCH_ACTIVITY, r);
}
主要是通過mH這個handler發(fā)送消息然后進(jìn)行處理泽艘,最終又會來到performLaunchActivity這個方法里面:
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
...
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
...
...
return activity;
}
就是利用Instrumentation.newActivity()方法通過反射調(diào)用實(shí)例化我們插件中的activity欲险,從而將插件中的activity交給系統(tǒng)托管。
而Small正是利用了這一點(diǎn)匹涮,核心原理大概講完了.而其余組件的
總結(jié)
當(dāng)然這里只是對核心原理進(jìn)行了一下簡略的描述天试,要想達(dá)到生產(chǎn)需求還要許多工作要做。
例如:正確區(qū)分宿主的activity和插件的activity然低,當(dāng)某個activity處在宿主中且已注冊時喜每,直接跳過插件化的步驟,交給系統(tǒng)處理即可雳攘。
再例如带兜,當(dāng)我們的需求需要在start多個相同的launchMode的activity時,需要在宿主占坑多少個這樣的proxy activity来农?
像上文提到的mH這個handler萧诫,我們其實(shí)可以hook住這個mH然后所有的分發(fā)事件兔跌。插件化的實(shí)現(xiàn)有很多種虚青,但無非都是在App創(chuàng)建流程中在ActivityThread绸栅,Instrumentation,ActivityManagerNative(AMS的本地代理對象)做文章,所以理解App創(chuàng)建流程對于插件化思想至關(guān)重要繁莹,說不定可以找到某個新奇的突破點(diǎn)進(jìn)行插件化檩互。
值得一提的是Android9開始對反射進(jìn)行限制,像反射調(diào)用ActivityThread里的currentActivityThread(),mH都被標(biāo)為淺灰名單咨演。
可能在日后的版本中插件化思想將不能使用了闸昨。。薄风。