Android每周一輪子:android-pluginmgr(插件化)

前言

之前所做的一個項目為一個嵌入到游戲中常柄,具備商城竞川,支付等功能的SDK鲁沥,由于游戲動態(tài)更新的問題瘫筐,SDK因此也需要具備動態(tài)更新的能力,否則每一次的SDK更新都要強制游戲發(fā)布新版本了喂链,本著該原則返十,限于部分歷史原因,項目中采用了一個比較老的插件化方案android-pluginmgr椭微,對于SDK的核心功能洞坑,全部抽離出放在插件中,通過這種方式可以實現(xiàn)對于核心功能的動態(tài)更新蝇率。

SDK設(shè)計

Github地址

基礎(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)行了重寫。

Instrumentation

插件裝載過程

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。

插件信息構(gòu)建

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)用ActivityThreadperformLaunchActivity來進(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è)置。

Activity啟動流程

總結(jié)

該插件的實現(xiàn)比較簡單喳挑,通過該插件可以幫助我們回顧前兩篇講的App啟動彬伦,資源裝載,類裝載問題伊诵,該插件在2年前已經(jīng)停止更新維護(hù)单绑,其功能上相比現(xiàn)有的一些成熟方案,如Replugin曹宴,VirtualApk等存在很大進(jìn)步空間搂橙,但是由于其實現(xiàn)簡單,非常方便我們?nèi)チ私膺@一個技術(shù)的實現(xiàn)流程笛坦,對于后續(xù)插件化代碼閱讀非常有幫助区转。接下來是對于360 RePlugin的源碼分析。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末弯屈,一起剝皮案震驚了整個濱河市蜗帜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌资厉,老刑警劉巖厅缺,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異宴偿,居然都是意外死亡湘捎,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進(jìn)店門窄刘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來窥妇,“玉大人,你說我怎么就攤上這事娩践』铘妫” “怎么了?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵翻伺,是天一觀的道長材泄。 經(jīng)常有香客問我,道長吨岭,這世上最難降的妖魔是什么拉宗? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上旦事,老公的妹妹穿的比我還像新娘魁巩。我一直安慰自己,他們只是感情好姐浮,可當(dāng)我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布谷遂。 她就那樣靜靜地躺著,像睡著了一般单料。 火紅的嫁衣襯著肌膚如雪埋凯。 梳的紋絲不亂的頭發(fā)上点楼,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天扫尖,我揣著相機與錄音,去河邊找鬼掠廓。 笑死换怖,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蟀瞧。 我是一名探鬼主播沉颂,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼悦污!你這毒婦竟也來了铸屉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤切端,失蹤者是張志新(化名)和其女友劉穎彻坛,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體踏枣,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡昌屉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了茵瀑。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片间驮。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖马昨,靈堂內(nèi)的尸體忽然破棺而出竞帽,到底是詐尸還是另有隱情,我是刑警寧澤鸿捧,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布屹篓,位于F島的核電站,受9級特大地震影響笛谦,放射性物質(zhì)發(fā)生泄漏抱虐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一饥脑、第九天 我趴在偏房一處隱蔽的房頂上張望恳邀。 院中可真熱鬧懦冰,春花似錦、人聲如沸谣沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乳附。三九已至内地,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赋除,已是汗流浹背阱缓。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留举农,地道東北人荆针。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像颁糟,于是被迫代替她去往敵國和親航背。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內(nèi)容