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

前言

以前所作的一個項目爲一個嵌入到遊戲中,具有商城,支付等功能的SDK,因爲遊戲動態更新的問題,SDK所以也須要具有動態更新的能力,不然每一次的SDK更新都要強制遊戲發佈新版本了,本着該原則,限於部分歷史緣由,項目中採用了一個比較老的插件化方案android-pluginmgr,對於SDK的核心功能,所有抽離出放在插件中,經過這種方式能夠實現對於核心功能的動態更新。android

SDK設計

Github地址git

基礎使用

  • 在 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();

從目錄中加載相應的插件,經過PlugInfo來存儲插件信息。github

  • 啓動插件中的Activity
start activity: mgr.startMainActivity(context, plug);

Activity的啓動經過調用PluginManager的startMainActivity。app

  • 插件驗證功能
PluginManager.getSingleton().setPluginOverdueVerifier(new PluginOverdueVerifier() {
          @Override
          public boolean isOverdue(File originPluginFile, File targetExistFile) {
              //check If the plugin has expired
              return true;
          }
      });

提供了一個回調,咱們能夠實現這個回調中的方法來根據本身的需求作自定義的插件過時校驗。ide

源碼實現分析

PluginManager的初始化

1.線程的判斷函數

if (!isMainThread()) {
            throw new IllegalThreadStateException("PluginManager must init in UI Thread!");
}

須要確保其初始化操做發生在主線程。源碼分析

2.生成肯定相應的裝載優化生成文件目錄優化

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替換操做ui

DelegateActivityThread delegateActivityThread = DelegateActivityThread.getSingleton();
Instrumentation originInstrumentation = delegateActivityThread.getInstrumentation();
if (!(originInstrumentation instanceof PluginInstrumentation)) {
            PluginInstrumentation pluginInstrumentation = new PluginInstrumentation(originInstrumentation);
            delegateActivityThread.setInstrumentation(pluginInstrumentation);
        }

此處DelegateActivityThread的做用是經過反射拿到當前的ActivityThread,同時經過反射來獲取其內部的Instrumentation和對Instrumentation進行設置。this

PluginInstrumentation 繼承自DelegateInstrumentation,DelegateInstrumentation持有了原有的Instrumentation,對於其中的大部分方法經過代理的方式,將其轉交給原有的Instrumention進行處理,對於幾個Activity啓動相關的核心方法進行了重寫。

Instrumentation

插件裝載過程

if (pluginSrcDirFile.isFile()) {
       PlugInfo one = buildPlugInfo(pluginSrcDirFile, null, null);
       if (one != null) {
           savePluginToMap(one);
       }
      return Collections.singletonList(one);
 }

此處已經省略了對於目錄的一些判空操做的代碼,首先判斷給定文件路徑是爲目錄仍是一個文件,若是是一個文件則進行構建,若是是一個目錄,則會對該目錄進行遍歷,而後進行單個文件執行的操做。首先根據給定的文件,構造出一個插件信息,而後將該插件信息存入到咱們的內存中存放PlugInfo的一個Map之中。

Map<String, PlugInfo> pluginPkgToInfoMap = new ConcurrentHashMap<String, PlugInfo>()

因此其核心操做就是buildPlugInfo。構建過程則爲建立一個PlugInfo對象出來,具體步驟爲對插件進行解析,來補充PlugInfo的相關屬性。

構建插件信息

1.設置PlugInfo的文件路徑信息,傳入的插件位置和初始化時設置的路徑若是不一致,則進行拷貝操做。

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());
//若是文件不在相同的地方,則進行復制
if(!pluginApk.getAbsolutePath().equals(privateFile.getAbsolutePath())) {
      copyApkToPrivatePath(pluginApk, privateFile);
}

2.裝載解析Manifest

String dexPath = privateFile.getAbsolutePath();
//Load Plugin Manifest
PluginManifestUtil.setManifestInfo(context, dexPath, info);

根據當前的dex路徑來得到到Manifest,而後解析該文件,獲得其中的Activity,Service,Receiver,Provider信息,而後將這些信息分別用來設置到PlugInfo相應的屬性中。

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);

經過反射獲取到執行AssetManager的addAssetPath方法,將其設置到插件的路徑中,而後利用當前的AssetManager來構造一個Resource對象。將該對象設置到PlugInfo中。用來後續對插件中資源裝載時使用。

4.設置ClassLoader

PluginClassLoader pluginClassLoader = new PluginClassLoader(info, dexPath, dexOutputPath
                , getPluginLibPath(info).getAbsolutePath(), pluginParentClassLoader);
 info.setClassLoader(pluginClassLoader);

繼承自DexClassLoader寫的ClassLoader,相比於DexClassLoader增長了一個PlugInfo屬性,同時在構造函數中爲其賦值。

5.建立Application,設置Application信息

ApplicationInfo appInfo = info.getPackageInfo().applicationInfo;
Application app = makeApplication(info, appInfo);
attachBaseContext(info, app);
info.setApplication(app);

建立Application對象,attachBaseContext,在這裏爲何要用attachBaseContext呢?這就設置到Context的一些問題了,先看下代碼中attachbaseContext中核心代碼。

Field mBase = ContextWrapper.class.getDeclaredField("mBase");
mBase.setAccessible(true);
mBase.set(app, new PluginContext(context.getApplicationContext(), info));

Application繼承自ContextWrapper,其具有獲取資源問及那,獲取包管理器,獲取應用程序上下文等等,而這些方法的實現都是經過attachBaseContext方法爲在ContextWrapper設置一個context的實現類,attachBaseContext()方法實際上是由系統來調用的,它會把ContextImpl對象做爲參數傳遞到attachBaseContext()方法當中,從而賦值給mBase對象,以後ContextWrapper中的全部方法其實都是經過這種委託的機制交由ContextImpl去具體實現的。所以這裏須要咱們手動爲Application設置上這個Context的實現類。

到此爲止,咱們已經完成了咱們SDK的初始化過程和咱們的插件的裝載過程。這個時候,咱們可能須要對於咱們插件中一些功能類的調用,或者是啓動其中的Activity。

插件信息構建

Activity的啓動

//從插件中查找當前Activity信息
ActivityInfo activityInfo = plugInfo.findActivityByClassName(targetActivity);

//構建建立Activiyt的相關對象
CreateActivityData createActivityData = new CreateActivityData(activityInfo.name, plugInfo.getPackageName());
intent.setClass(from, activitySelector.selectDynamicActivity(activityInfo));

//設置標誌啓動來自插件的Activity
intent.putExtra(Globals.FLAG_ACTIVITY_FROM_PLUGIN, createActivityData);
from.startActivity(intent);

根據目標Activity從咱們建立的PlugInfo中找到相關的Activity信息。經過Activity名和插件的包名來建立一個Activity的信息。selectDynamicActivity是咱們在宿主類中設置的一個動態代理類,將其設置咱們跳轉的一個目標。而後經過intent攜帶FLAG_ACTIVITY_FROM_PLUGIN的標記下的Activity的信息,這個時候經過當前的Activity來啓動。啓動MainActivity則爲對向其傳遞的Activity信息作一個改變,直接啓動。

Activity的啓動後面其實是經過Instrumentation中的execStartActivity來執行啓動新的Activity,Instrumentation中對於execStartActivity有許多的重載方法。在這些方法執行以前都會調用一個方法:replaceIntentTargetIfNeed,replaceIntentTargetIfNeed()用來對跳轉到插件Activity進行相應的處理。在方法中進行的處理以下:

//判斷是否啓動來自插件的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設置額外的classLoader                   intent.setExtrasClassLoader(currentPlugin.getClassLoader());
                }
             }
          }
 }

若是Intent中沒有來自插件的標識,而後當前的插件信息不爲null,則會根據插件信息提取出相關的信息,而後對Intent進行一系列的設置。

在通過一系列處理,和AMS之間交互等以後,最終會調用ActivityThreadperformLaunchActivity來進行Activity的建立和啓動,首先是經過相應的類裝載器建立出Activity對象,而後調用其相應的生命週期函數,這個過程都是系統自動執行。在performLaunchActivity中具體執行的任務有如下幾個。

1.首先從intent中解析出目標activity的啓動參數。

2.經過Activity的無參構造方法來new一個對象,對象就是在這裏new出來,實際的調用是Instrumentation的newActivity函數,這個函數也是咱們在Hook中要重寫的。

3.而後爲該Activity設置上Application,Context,Instrumentation等信息。而後經過Instrumentation的callActivityOnCreate調用Activity的onCreate函數,使得其具有了生命週期。

此處咱們的實現是經過咱們本地的一個Activity做爲樁,也就是說咱們實際調用的Activity是咱們本地的一個Activity,而後對其中一些步驟作Hook,對於其中的一些信息的檢測,缺失處理。

這個過程,咱們要對newActivity()進行Hook,還要對callActivityOnCreate()進行Hook,newActivity的實現代碼

CreateActivityData activityData = (CreateActivityData) intent.getSerializableExtra(Globals.FLAG_ACTIVITY_FROM_PLUGIN);
if (activityData != null && PluginManager.getSingleton().getPlugins().size() > 0) {
            //這裏找不到插件信息就會拋異常的,不用擔憂空指針
     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的建立中,獲取Intent中的內容,而後將其中的信息進行解析,而後從中解析出相關屬性,配置給Activity,而後調用原有父類中的方法,這個Intent在發起的時候,咱們告訴系統的是調用的是咱們本地插的一個Activity,可是在實際建立的時候,經過newActivity的時候,建立出的Activity是咱們插件中的Activity。
Activity的建立以後,接下來須要調用其生命週期函數,而後這個過程須要咱們對其再次進行Hook,添加進咱們的相關操做。對於其中的代碼,咱們逐步來分析。

lookupActivityInPlugin(activity);

該方法執行的操做

ClassLoader classLoader = activity.getClass().getClassLoader();
 if (classLoader instanceof PluginClassLoader){
        currentPlugin = ((PluginClassLoader)classLoader).getPlugInfo();
  }else{
        currentPlugin = null;
 }

執行該方法以後,會爲currentPlugin賦值。當currentPlugin不爲null時,也就是代表此時肯定了該Activity是來自插件。

Context baseContext = activity.getBaseContext();
PluginContext pluginContext = new PluginContext(baseContext, currentPlugin);

在PluginContext中進行了對於獲取資源,類裝載器等一些信息方法的重寫。對於其中的一些資源獲取,ClassLoader的獲取等,都是經過PlugInfo中的信息進行設置。而後再經過反射的方式對這些原有的獲取方式進行替換。

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);
     }
}

若是當前Activity未設置主題,則對Activity的信息進行替換。調用了方法 changeActivityInfo

在Activity的啓動過程當中,對於Activity相關的內容經過以前保存在插件信息中的內容經過反射的方式進行設置。

Activity啓動流程

總結

該插件的實現比較簡單,經過該插件能夠幫助咱們回顧前兩篇講的App啓動,資源裝載,類裝載問題,該插件在2年前已經中止更新維護,其功能上相比現有的一些成熟方案,如Replugin,VirtualApk等存在很大進步空間,可是因爲其實現簡單,很是方便咱們去了解這一個技術的實現流程,對於後續插件化代碼閱讀很是有幫助。接下來是對於360 RePlugin的源碼分析。

相關文章
相關標籤/搜索