前言
之前所做的一個項目為一個嵌入到游戲中常柄,具備商城竞川,支付等功能的SDK鲁沥,由于游戲動態(tài)更新的問題瘫筐,SDK因此也需要具備動態(tài)更新的能力,否則每一次的SDK更新都要強制游戲發(fā)布新版本了喂链,本著該原則返十,限于部分歷史原因,項目中采用了一個比較老的插件化方案android-pluginmgr椭微,對于SDK的核心功能洞坑,全部抽離出放在插件中,通過這種方式可以實現(xiàn)對于核心功能的動態(tài)更新蝇率。
基礎(chǔ)使用
- 在 Application中初始化插件
@Override
public void onCreate(){
PluginManager.init(this);
//...
}
- 從Apk中加載插件
PluginManager mgr = PluginManager.getSingleton();
File myPlug = new File("/mnt/sdcard/Download/myplug.apk");
PlugInfo plug = pluginMgr.loadPlugin(myPlug).iterator().next();
從目錄中加載相應(yīng)的插件迟杂,通過PlugInfo來存儲插件信息。
- 啟動插件中的Activity
start activity: mgr.startMainActivity(context, plug);
Activity的啟動通過調(diào)用PluginManager的startMainActivity本慕。
- 插件驗證功能
PluginManager.getSingleton().setPluginOverdueVerifier(new PluginOverdueVerifier() {
@Override
public boolean isOverdue(File originPluginFile, File targetExistFile) {
//check If the plugin has expired
return true;
}
});
提供了一個回調(diào)排拷,我們可以實現(xiàn)這個回調(diào)中的方法來根據(jù)自己的需求做自定義的插件過期校驗。
源碼實現(xiàn)分析
PluginManager的初始化
1.線程的判斷
if (!isMainThread()) {
throw new IllegalThreadStateException("PluginManager must init in UI Thread!");
}
需要確保其初始化操作發(fā)生在主線程锅尘。
2.生成確定相應(yīng)的裝載優(yōu)化生成文件目錄
this.context = context;
//插件輸出路徑
File optimizedDexPath = context.getDir(Globals.PRIVATE_PLUGIN_OUTPUT_DIR_NAME, Context.MODE_PRIVATE);
dexOutputPath = optimizedDexPath.getAbsolutePath();
dexInternalStoragePath = context.getDir(
Globals.PRIVATE_PLUGIN_ODEX_OUTPUT_DIR_NAME, Context.MODE_PRIVATE
);
3.部分Hook替換操作
DelegateActivityThread delegateActivityThread = DelegateActivityThread.getSingleton();
Instrumentation originInstrumentation = delegateActivityThread.getInstrumentation();
if (!(originInstrumentation instanceof PluginInstrumentation)) {
PluginInstrumentation pluginInstrumentation = new PluginInstrumentation(originInstrumentation);
delegateActivityThread.setInstrumentation(pluginInstrumentation);
}
此處DelegateActivityThread的作用是通過反射拿到當(dāng)前的ActivityThread监氢,同時通過反射來獲取其內(nèi)部的Instrumentation和對Instrumentation進(jìn)行設(shè)置。
PluginInstrumentation 繼承自DelegateInstrumentation鉴象,DelegateInstrumentation持有了原有的Instrumentation忙菠,對于其中的大部分方法通過代理的方式,將其轉(zhuǎn)交給原有的Instrumention進(jìn)行處理纺弊,對于幾個Activity啟動相關(guān)的核心方法進(jìn)行了重寫。
插件裝載過程
if (pluginSrcDirFile.isFile()) {
PlugInfo one = buildPlugInfo(pluginSrcDirFile, null, null);
if (one != null) {
savePluginToMap(one);
}
return Collections.singletonList(one);
}
此處已經(jīng)省略了對于目錄的一些判空操作的代碼骡男,首先判斷給定文件路徑是為目錄還是一個文件淆游,如果是一個文件則進(jìn)行構(gòu)建,如果是一個目錄,則會對該目錄進(jìn)行遍歷犹菱,然后進(jìn)行單個文件執(zhí)行的操作拾稳。首先根據(jù)給定的文件,構(gòu)造出一個插件信息腊脱,然后將該插件信息存入到我們的內(nèi)存中存放PlugInfo的一個Map之中访得。
Map<String, PlugInfo> pluginPkgToInfoMap = new ConcurrentHashMap<String, PlugInfo>()
所以其核心操作就是buildPlugInfo。構(gòu)建過程則為創(chuàng)建一個PlugInfo對象出來陕凹,具體步驟為對插件進(jìn)行解析悍抑,來補充PlugInfo的相關(guān)屬性。
構(gòu)建插件信息
1.設(shè)置PlugInfo的文件路徑信息杜耙,傳入的插件位置和初始化時設(shè)置的路徑如果不一致搜骡,則進(jìn)行拷貝操作。
PlugInfo info = new PlugInfo();
info.setId(pluginId == null ? pluginApk.getName() : pluginId);
File privateFile = new File(dexInternalStoragePath,
targetFileName == null ? pluginApk.getName() : targetFileName);
info.setFilePath(privateFile.getAbsolutePath());
//如果文件不在相同的地方佑女,則進(jìn)行復(fù)制
if(!pluginApk.getAbsolutePath().equals(privateFile.getAbsolutePath())) {
copyApkToPrivatePath(pluginApk, privateFile);
}
2.裝載解析Manifest
String dexPath = privateFile.getAbsolutePath();
//Load Plugin Manifest
PluginManifestUtil.setManifestInfo(context, dexPath, info);
根據(jù)當(dāng)前的dex路徑來獲得到Manifest记靡,然后解析該文件,得到其中的Activity团驱,Service摸吠,Receiver,Provider信息嚎花,然后將這些信息分別用來設(shè)置到PlugInfo相應(yīng)的屬性中蜕便。
3.裝載資源文件
AssetManager am = AssetManager.class.newInstance();
am.getClass().getMethod("addAssetPath", String.class)
.invoke(am, dexPath);
info.setAssetManager(am);
Resources hotRes = context.getResources();
Resources res = new Resources(am, hotRes.getDisplayMetrics(),
hotRes.getConfiguration());
info.setResources(res);
通過反射獲取到執(zhí)行AssetManager的addAssetPath方法,將其設(shè)置到插件的路徑中贩幻,然后利用當(dāng)前的AssetManager來構(gòu)造一個Resource對象轿腺。將該對象設(shè)置到PlugInfo中。用來后續(xù)對插件中資源裝載時使用丛楚。
4.設(shè)置ClassLoader
PluginClassLoader pluginClassLoader = new PluginClassLoader(info, dexPath, dexOutputPath
, getPluginLibPath(info).getAbsolutePath(), pluginParentClassLoader);
info.setClassLoader(pluginClassLoader);
繼承自DexClassLoader寫的ClassLoader族壳,相比于DexClassLoader增加了一個PlugInfo屬性,同時在構(gòu)造函數(shù)中為其賦值趣些。
5.創(chuàng)建Application仿荆,設(shè)置Application信息
ApplicationInfo appInfo = info.getPackageInfo().applicationInfo;
Application app = makeApplication(info, appInfo);
attachBaseContext(info, app);
info.setApplication(app);
創(chuàng)建Application對象,attachBaseContext坏平,在這里為什么要用attachBaseContext呢拢操?這就設(shè)置到Context的一些問題了,先看下代碼中attachbaseContext中核心代碼舶替。
Field mBase = ContextWrapper.class.getDeclaredField("mBase");
mBase.setAccessible(true);
mBase.set(app, new PluginContext(context.getApplicationContext(), info));
Application繼承自ContextWrapper令境,其具備獲取資源問及那,獲取包管理器顾瞪,獲取應(yīng)用程序上下文等等舔庶,而這些方法的實現(xiàn)都是通過attachBaseContext方法為在ContextWrapper設(shè)置一個context的實現(xiàn)類抛蚁,attachBaseContext()方法其實是由系統(tǒng)來調(diào)用的,它會把ContextImpl對象作為參數(shù)傳遞到attachBaseContext()方法當(dāng)中惕橙,從而賦值給mBase對象瞧甩,之后ContextWrapper中的所有方法其實都是通過這種委托的機制交由ContextImpl去具體實現(xiàn)的。因此這里需要我們手動為Application設(shè)置上這個Context的實現(xiàn)類弥鹦。
到此為止肚逸,我們已經(jīng)完成了我們SDK的初始化過程和我們的插件的裝載過程。這個時候彬坏,我們可能需要對于我們插件中一些功能類的調(diào)用朦促,或者是啟動其中的Activity。
Activity的啟動
//從插件中查找當(dāng)前Activity信息
ActivityInfo activityInfo = plugInfo.findActivityByClassName(targetActivity);
//構(gòu)建創(chuàng)建Activiyt的相關(guān)對象
CreateActivityData createActivityData = new CreateActivityData(activityInfo.name, plugInfo.getPackageName());
intent.setClass(from, activitySelector.selectDynamicActivity(activityInfo));
//設(shè)置標(biāo)志啟動來自插件的Activity
intent.putExtra(Globals.FLAG_ACTIVITY_FROM_PLUGIN, createActivityData);
from.startActivity(intent);
根據(jù)目標(biāo)Activity從我們創(chuàng)建的PlugInfo中找到相關(guān)的Activity信息苍鲜。通過Activity名和插件的包名來創(chuàng)建一個Activity的信息思灰。selectDynamicActivity是我們在宿主類中設(shè)置的一個動態(tài)代理類,將其設(shè)置我們跳轉(zhuǎn)的一個目標(biāo)混滔。然后通過intent攜帶FLAG_ACTIVITY_FROM_PLUGIN
的標(biāo)記下的Activity的信息洒疚,這個時候通過當(dāng)前的Activity來啟動。啟動MainActivity則為對向其傳遞的Activity信息做一個改變坯屿,直接啟動油湖。
Activity的啟動后面實際上是通過Instrumentation中的execStartActivity
來執(zhí)行啟動新的Activity,Instrumentation中對于execStartActivity有許多的重載方法领跛。在這些方法執(zhí)行之前都會調(diào)用一個方法:replaceIntentTargetIfNeed
,replaceIntentTargetIfNeed()
用來對跳轉(zhuǎn)到插件Activity進(jìn)行相應(yīng)的處理乏德。在方法中進(jìn)行的處理如下:
//判斷是否啟動來自插件的Activity
if (!intent.hasExtra(Globals.FLAG_ACTIVITY_FROM_PLUGIN) && currentPlugin != null){
ComponentName componentName = intent.getComponent();
if (componentName != null){
//獲取包名和Activity名
String pkgName = componentName.getPackageName();
String activityName = componentName.getClassName();
if (pkgName != null){
CreateActivityData createActivityData = new CreateActivityData(activityName, currentPlugin.getPackageName());
ActivityInfo activityInfo = currentPlugin.findActivityByClassName(activityName);
if (activityInfo != null) {
intent.setClass(from, PluginManager.getSingleton().getActivitySelector().selectDynamicActivity(activityInfo));
intent.putExtra(Globals.FLAG_ACTIVITY_FROM_PLUGIN, createActivityData);
//為Intent設(shè)置額外的classLoader intent.setExtrasClassLoader(currentPlugin.getClassLoader());
}
}
}
}
如果Intent中沒有來自插件的標(biāo)識,然后當(dāng)前的插件信息不為null吠昭,則會根據(jù)插件信息提取出相關(guān)的信息喊括,然后對Intent
進(jìn)行一系列的設(shè)置。
在經(jīng)過一系列處理矢棚,和AMS
之間交互等之后郑什,最終會調(diào)用ActivityThread
的performLaunchActivity
來進(jìn)行Activity的創(chuàng)建和啟動,首先是通過相應(yīng)的類裝載器創(chuàng)建出Activity對象蒲肋,然后調(diào)用其相應(yīng)的生命周期函數(shù)蘑拯,這個過程都是系統(tǒng)自動執(zhí)行。在performLaunchActivity
中具體執(zhí)行的任務(wù)有以下幾個兜粘。
1.首先從intent中解析出目標(biāo)activity的啟動參數(shù)申窘。
2.通過Activity的無參構(gòu)造方法來new一個對象,對象就是在這里new出來孔轴,實際的調(diào)用是Instrumentation的newActivity
函數(shù)剃法,這個函數(shù)也是我們在Hook中要重寫的。
3.然后為該Activity設(shè)置上Application距糖,Context玄窝,Instrumentation等信息牵寺。然后通過Instrumentation的callActivityOnCreate調(diào)用Activity的onCreate函數(shù)悍引,使得其具備了生命周期恩脂。
此處我們的實現(xiàn)是通過我們本地的一個Activity作為樁,也就是說我們實際調(diào)用的Activity是我們本地的一個Activity趣斤,然后對其中一些步驟做Hook俩块,對于其中的一些信息的檢測,缺失處理浓领。
這個過程玉凯,我們要對newActivity()
進(jìn)行Hook,還要對callActivityOnCreate()
進(jìn)行Hook联贩,newActivity的實現(xiàn)代碼
CreateActivityData activityData = (CreateActivityData) intent.getSerializableExtra(Globals.FLAG_ACTIVITY_FROM_PLUGIN);
if (activityData != null && PluginManager.getSingleton().getPlugins().size() > 0) {
//這里找不到插件信息就會拋異常的,不用擔(dān)心空指針
PlugInfo plugInfo;
plugInfo =
PluginManager.getSingleton().tryGetPluginInfo(activityData.pluginPkg);
plugInfo.ensureApplicationCreated();
if (activityData.activityName != null){
className = activityData.activityName;
cl = plugInfo.getClassLoader();
}
}
return super.newActivity(cl, className, intent);
Activity的創(chuàng)建中漫仆,獲取Intent中的內(nèi)容,然后將其中的信息進(jìn)行解析泪幌,然后從中解析出相關(guān)屬性盲厌,配置給Activity,然后調(diào)用原有父類中的方法祸泪,這個Intent在發(fā)起的時候吗浩,我們告訴系統(tǒng)的是調(diào)用的是我們本地插的一個Activity,但是在實際創(chuàng)建的時候没隘,通過newActivity的時候懂扼,創(chuàng)建出的Activity是我們插件中的Activity。
Activity的創(chuàng)建之后右蒲,接下來需要調(diào)用其生命周期函數(shù)阀湿,然后這個過程需要我們對其再次進(jìn)行Hook,添加進(jìn)我們的相關(guān)操作瑰妄。對于其中的代碼陷嘴,我們逐步來分析。
lookupActivityInPlugin(activity);
該方法執(zhí)行的操作
ClassLoader classLoader = activity.getClass().getClassLoader();
if (classLoader instanceof PluginClassLoader){
currentPlugin = ((PluginClassLoader)classLoader).getPlugInfo();
}else{
currentPlugin = null;
}
執(zhí)行該方法之后翰撑,會為currentPlugin賦值罩旋。當(dāng)currentPlugin不為null時,也就是表明此時確定了該Activity是來自插件眶诈。
Context baseContext = activity.getBaseContext();
PluginContext pluginContext = new PluginContext(baseContext, currentPlugin);
在PluginContext中進(jìn)行了對于獲取資源涨醋,類裝載器等一些信息方法的重寫。對于其中的一些資源獲取逝撬,ClassLoader的獲取等浴骂,都是通過PlugInfo中的信息進(jìn)行設(shè)置。然后再通過反射的方式對這些原有的獲取方式進(jìn)行替換宪潮。
Reflect.on(activity).set("mResources", pluginContext.getResources());
Field field = ContextWrapper.class.getDeclaredField("mBase");
field.setAccessible(true);
field.set(activity, pluginContext);
Reflect.on(activity).set("mApplication", currentPlugin.getApplication());
獲取Activity的一些主題溯警,
ActivityInfo activityInfo = currentPlugin.findActivityByClassName(activity.getClass().getName());
int resTheme = activityInfo.getThemeResource();
if (resTheme != 0) {
boolean hasNotSetTheme = true;
Field mTheme = ContextThemeWrapper.class
.getDeclaredField("mTheme");
mTheme.setAccessible(true);
hasNotSetTheme = mTheme.get(activity) == null;
if (hasNotSetTheme) {
changeActivityInfo(activityInfo, activity);
activity.setTheme(resTheme);
}
}
如果當(dāng)前Activity未設(shè)置主題趣苏,則對Activity的信息進(jìn)行替換。調(diào)用了方法 changeActivityInfo
梯轻。
在Activity的啟動過程中食磕,對于Activity相關(guān)的內(nèi)容通過之前保存在插件信息中的內(nèi)容通過反射的方式進(jìn)行設(shè)置。
總結(jié)
該插件的實現(xiàn)比較簡單喳挑,通過該插件可以幫助我們回顧前兩篇講的App啟動彬伦,資源裝載,類裝載問題伊诵,該插件在2年前已經(jīng)停止更新維護(hù)单绑,其功能上相比現(xiàn)有的一些成熟方案,如Replugin曹宴,VirtualApk等存在很大進(jìn)步空間搂橙,但是由于其實現(xiàn)簡單,非常方便我們?nèi)チ私膺@一個技術(shù)的實現(xiàn)流程笛坦,對于后續(xù)插件化代碼閱讀非常有幫助区转。接下來是對于360 RePlugin的源碼分析。