插件化框架android-pluginmgr全解析

博文出處:插件化框架android-pluginmgr全解析,歡迎大家關(guān)注我的博客踢星,謝謝统刮!

0x00 前言:插件化的介紹

閱讀須知:閱讀本文的童鞋最好是有過插件化框架使用經(jīng)歷或者對插件化框架有過了解的故源。前方高能污抬,大牛繞道。

最近一直在關(guān)注 Android 插件化方面,所以今天的主題就確定是 Android 中比較熱門的“插件化”了印机。所謂的插件化就是下載 apk 到指定目錄矢腻,不需要安裝該 apk ,就能利用某個已安裝的 apk (即“宿主”)調(diào)用起該未安裝 apk 中的 Activity 射赛、Service 等組件(即“插件”)多柑。

Android 插件化的發(fā)展到目前為止也有一段時間了,從一開始任主席的 dynamic-load-apk 到今天要分析的 android-pluginmgr 再到360的 DroidPlugin 楣责,也代表著插件化的思想從頂部的應(yīng)用層向下到 Framework 層滲入竣灌。最早插件化的思想是 dynamic-load-apk 實(shí)現(xiàn)的, dynamic-load-apk 在“宿主” ProxyActivity 的生命周期中利用接口回調(diào)了“插件” PluginActivity 的“生命周期”秆麸,以此來間接實(shí)現(xiàn) PluginActivity 的“生命周期”初嘹。也就是說,其實(shí)插件中的 “PluginActivity” 并不具有真正 Activity 的性質(zhì)蛔屹,實(shí)質(zhì)就是一個普通類削樊,只是利用接口回調(diào)了類中的生命周期方法而已。比接口回調(diào)更好的方案就是利用 ActivityThread 兔毒、Instrumentation 等去動態(tài)地 Hook 即將創(chuàng)建的 ProxyActivity ,也就是說表面上創(chuàng)建的是 ProxyActivity 甸箱,其實(shí)實(shí)際上是創(chuàng)建了 PluginActivity 育叁。這種思想相比于 dynamic-load-apk 而言,插件中 Activity 已經(jīng)是實(shí)質(zhì)上的 Activity 芍殖,具備了生命周期方法豪嗽。今天我們要解析的 android-pluginmgr 插件化框架就是基于這種思想的。最后就是像 DroidPlugin 這種插件化框架豌骏,改動了 ActivityManagerService 龟梦、 PackageManagerService 等 Android 源碼,以此來實(shí)現(xiàn)插件化窃躲〖品。總之,并沒有哪種插件化框架是最好的蒂窒,一切都是要根據(jù)自身實(shí)際情況而決定的躁倒。

熟悉插件化的童鞋都知道,插件化要解決的有三個基本難題:

  1. 插件中 ClassLoader 的問題洒琢;
  2. 插件中的資源文件訪問問題秧秉;
  3. 插件中 Activity 組件的生命周期問題。

基本上衰抑,解決了上面三個問題象迎,就可以算是一個合格的插件化框架了。但是要注意的是呛踊,插件化遠(yuǎn)遠(yuǎn)不止這三個問題砾淌,比如還有插件中 .so 文件加載啦撮,支持 Service 插件化等問題。

好了拇舀,講了這么多廢話逻族,接下來我們就來分析 android-pluginmgr 的源碼吧。

0x01 PluginManager.init

注:本文分析的 android-pluginmgr 為 master 分支骄崩,版本為0.2.2聘鳞;

android-pluginmgr的簡單用法

我們先簡單地來看一下 android-pluginmgr 框架的用法(來自于 android-pluginmgrREADME.md ):

  1. declare permission in your AndroidManifest.xml:

     <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    
  2. regist an activity:

     <activity android:name="androidx.pluginmgr.DynamicActivity" />
    
  3. init PluginMgr in your application:

       @Override
       public void onCreate(){
          PluginManager.init(this);
          //...
       }
    
  4. load plugin from plug apk:

       PluginManager pluginMgr = PluginManager.getSingleton();
       File myPlug = new File("/mnt/sdcard/Download/myplug.apk");
       PlugInfo plug = pluginMgr.loadPlugin(myPlug).iterator().next();
    
  5. start activity:

       mgr.startMainActivity(context, plug);
    

基本的用法就像以上這五步,另外需要注意的是要拂,“插件”中所需要的權(quán)限都要在“宿主”的 AndroidManifest.xml 中進(jìn)行申明抠璃。

PluginManager.init(this)源碼

下面我們來分析下 PluginManager.init(this); 的源碼:

/**
 * 初始化插件管理器,請不要傳入易變的Context,那將造成內(nèi)存泄露!
 *
 * @param context Application上下文
 */
public static void init(Context context) {
    if (SINGLETON != null) {
        Trace.store("PluginManager have been initialized, YOU needn't initialize it again!");
        return;
    }
    Trace.store("init PluginManager...");
    SINGLETON = new PluginManager(context);
}

可以看到在 init(Context context) 中主要創(chuàng)建了一個 SINGLETON 單例,所以我們就要追蹤 PluginManager 構(gòu)造器的源碼了:

/**
 * 插件管理器私有構(gòu)造器
 *
 * @param context Application上下文
 */
private PluginManager(Context context) {
    if (!isMainThread()) {
        throw new IllegalThreadStateException("PluginManager must init in UI Thread!");
    }
    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
    );
    DelegateActivityThread delegateActivityThread = DelegateActivityThread.getSingleton();
    Instrumentation originInstrumentation = delegateActivityThread.getInstrumentation();
    if (!(originInstrumentation instanceof PluginInstrumentation)) {
        PluginInstrumentation pluginInstrumentation = new PluginInstrumentation(originInstrumentation);
        delegateActivityThread.setInstrumentation(pluginInstrumentation);
    }
}

在構(gòu)造器中做的事情有點(diǎn)多脱惰,我們一步步來看下搏嗡。一開始得到插件 dex opt 輸出路徑 dexOutputPath 和私有目錄中存儲插件的路徑 dexInternalStoragePath 。這些路徑都是在 Global 類中事先定義好的:

/**
 * 私有目錄中保存插件文件的文件夾名
 */
public static final String PRIVATE_PLUGIN_OUTPUT_DIR_NAME = "plugins-file";

/**
 * 私有目錄中保存插件odex的文件夾名
 */
public static final String PRIVATE_PLUGIN_ODEX_OUTPUT_DIR_NAME = "plugins-opt";

但是根據(jù)常量定義的名稱來看拉一,總感覺作者在 context.getDir() 時把這兩個路徑搞反了 \(╯-╰)/采盒。

之后在構(gòu)造器中創(chuàng)建了 DelegateActivityThread 類的單例:

public final class DelegateActivityThread {

    private static DelegateActivityThread SINGLETON = new DelegateActivityThread();

    private Reflect activityThreadReflect;

    public DelegateActivityThread() {
        activityThreadReflect = Reflect.on(ActivityThread.currentActivityThread());
    }

    public static DelegateActivityThread getSingleton() {
        return SINGLETON;
    }

    public Application getInitialApplication() {
        return activityThreadReflect.get("mInitialApplication");
    }

    public Instrumentation getInstrumentation() {
        return activityThreadReflect.get("mInstrumentation");
    }

    public void setInstrumentation(Instrumentation newInstrumentation) {
        activityThreadReflect.set("mInstrumentation", newInstrumentation);
    }

}

DelegateActivityThread 類的主要作用就是使用反射包裝了當(dāng)前的 ActivityThread ,并且一開始在 DelegateActivityThread 中使用 PluginInstrumentation 替換原始的 Instrumentation 蔚润。其實(shí) Activity 的生命周期調(diào)用都是通過 Instrumentation 來完成的磅氨。我們來看看 PluginInstrumentation 的構(gòu)造器相關(guān)代碼:

public class PluginInstrumentation extends DelegateInstrumentation
{

    /**
     * 當(dāng)前正在運(yùn)行的插件
     */
    private PlugInfo currentPlugin;

    /**
     * @param mBase 真正的Instrumentation
     */
    public PluginInstrumentation(Instrumentation mBase) {
        super(mBase);
    }

    ...

}

可以看到 PluginInstrumentation 是繼承自 DelegateInstrumentation 類的,而 DelegateInstrumentation 本質(zhì)上就是 Instrumentation 嫡纠。 DelegateInstrumentation 類中的方法都是直接調(diào)用 Instrumentation 類的:

public class DelegateInstrumentation extends Instrumentation {

    private Instrumentation mBase;

    /**
     * @param mBase 真正的Instrumentation
     */
    public DelegateInstrumentation(Instrumentation mBase) {
        this.mBase = mBase;
    }

    @Override
    public void onCreate(Bundle arguments) {
        mBase.onCreate(arguments);
    }

    @Override
    public void start() {
        mBase.start();
    }

    @Override
    public void onStart() {
        mBase.onStart();
    }

    ...
}

好了烦租,在 PluginManager.init() 方法中大概做的就是這些邏輯了。

0x02 PluginManager.loadPlugin

看完了上面的 PluginManager.init() 之后除盏,下一步就是調(diào)用 pluginManager.loadPlugin 去加載插件叉橱。一起來看看相關(guān)源碼:

/**
 * 加載指定插件或指定目錄下的所有插件
 * <p>
 * 都使用文件名作為Id
 *
 * @param pluginSrcDirFile - apk或apk目錄
 * @return 插件集合
 * @throws Exception
 */
public Collection<PlugInfo> loadPlugin(final File pluginSrcDirFile)
        throws Exception {
    if (pluginSrcDirFile == null || !pluginSrcDirFile.exists()) {
        Trace.store("invalidate plugin file or Directory :"
                + pluginSrcDirFile);
        return null;
    }
    if (pluginSrcDirFile.isFile()) {
        PlugInfo one = buildPlugInfo(pluginSrcDirFile, null, null);
        if (one != null) {
            savePluginToMap(one);
        }
        return Collections.singletonList(one);
    }
//        synchronized (this) {
//            pluginPkgToInfoMap.clear();
//        }
    File[] pluginApkFiles = pluginSrcDirFile.listFiles(this);
    if (pluginApkFiles == null || pluginApkFiles.length == 0) {
        throw new FileNotFoundException("could not find plugins in:"
                + pluginSrcDirFile);
    }
    for (File pluginApk : pluginApkFiles) {
        try {
            PlugInfo plugInfo = buildPlugInfo(pluginApk, null, null);
            if (plugInfo != null) {
                savePluginToMap(plugInfo);
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
    return pluginPkgToInfoMap.values();
}

loadPlugin 代碼的注釋中,我們可以知道加載的插件可以是一個也可以是一個文件夾下的多個者蠕。因?yàn)闀鶕?jù)傳入的 pluginSrcDirFile 參數(shù)去判斷是文件還是文件夾窃祝,其實(shí)道理都是一樣的,無非就是多了一個 for 循環(huán)而已蠢棱。在這里要注意一下锌杀,PluginManager 是實(shí)現(xiàn)了 FileFilter 接口的,因此在加載多個插件時泻仙,調(diào)用 listFiles(this) 會過濾當(dāng)前文件夾下非 apk 文件:

@Override
public boolean accept(File pathname) {
    return !pathname.isDirectory() && pathname.getName().endsWith(".apk");
}

好了糕再,我們在 loadPlugin() 的代碼中會注意到,無論是加載單個插件還是多個插件都會調(diào)用 buildPlugInfo() 方法玉转。顧名思義突想,就是根據(jù)傳入的插件文件去加載:

private PlugInfo buildPlugInfo(File pluginApk, String pluginId,
                               String targetFileName) throws Exception {
    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());
    //Copy Plugin to Private Dir
    if (!pluginApk.getAbsolutePath().equals(privateFile.getAbsolutePath())) {
        copyApkToPrivatePath(pluginApk, privateFile);
    }
    String dexPath = privateFile.getAbsolutePath();
    //Load Plugin Manifest
    PluginManifestUtil.setManifestInfo(context, dexPath, info);
    //Load Plugin Res
    try {
        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);
    } catch (Exception e) {
        throw new RuntimeException("Unable to create Resources&Assets for "
                + info.getPackageName() + " : " + e.getMessage());
    }
    //Load  classLoader for Plugin
    PluginClassLoader pluginClassLoader = new PluginClassLoader(info, dexPath, dexOutputPath
            , getPluginLibPath(info).getAbsolutePath(), pluginParentClassLoader);
    info.setClassLoader(pluginClassLoader);
    ApplicationInfo appInfo = info.getPackageInfo().applicationInfo;
    Application app = makeApplication(info, appInfo);
    attachBaseContext(info, app);
    info.setApplication(app);
    Trace.store("Build pluginInfo => " + info);
    return info;
}

從上面的代碼中看到, buildPlugInfo() 方法中做的大致有四步:

  1. 復(fù)制插件 apk 到指定目錄;
  2. 加載插件 apk 的 AndroidManifest.xml 文件猾担;
  3. 加載插件 apk 中的資源文件袭灯;
  4. 為插件 apk 設(shè)置 ClassLoader。

復(fù)制插件 apk 到指定目錄

下面我們慢慢來分析绑嘹,第一步稽荧,會把傳入的插件 apk 復(fù)制到 dexInternalStoragePath 路徑下,也就是之前在 PluginManager 的構(gòu)造器中所指定的目錄工腋。這部分的代碼很簡單姨丈,就省略了。

加載插件 apk 的 AndroidManifest.xml 文件

第二步擅腰,根據(jù)代碼可知蟋恬,會使用 PluginManifestUtil.setManifestInfo() 去加載 AndroidManifest 里的信息,那就去看下相關(guān)的代碼實(shí)現(xiàn):

public static void setManifestInfo(Context context, String apkPath, PlugInfo info)
        throws XmlPullParserException, IOException {
    // 得到AndroidManifest文件
    ZipFile zipFile = new ZipFile(new File(apkPath), ZipFile.OPEN_READ);
    ZipEntry manifestXmlEntry = zipFile.getEntry(XmlManifestReader.DEFAULT_XML);
    // 解析AndroidManifest文件
    String manifestXML = XmlManifestReader.getManifestXMLFromAPK(zipFile,
            manifestXmlEntry);
    // 創(chuàng)建相應(yīng)的packageInfo
    PackageInfo pkgInfo = context.getPackageManager()
            .getPackageArchiveInfo(
                    apkPath,
                    PackageManager.GET_ACTIVITIES
                            | PackageManager.GET_RECEIVERS//
                            | PackageManager.GET_PROVIDERS//
                            | PackageManager.GET_META_DATA//
                            | PackageManager.GET_SHARED_LIBRARY_FILES//
                            | PackageManager.GET_SERVICES//
            // | PackageManager.GET_SIGNATURES//
            );
    if (pkgInfo == null || pkgInfo.activities == null) {
        throw new XmlPullParserException("No any activity in " + apkPath);
    }
    pkgInfo.applicationInfo.publicSourceDir = apkPath;
    pkgInfo.applicationInfo.sourceDir = apkPath;
    // 得到libDir趁冈,加載.so文件
    File libDir = PluginManager.getSingleton().getPluginLibPath(info);
    try {
        if (extractLibFile(zipFile, libDir)) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
                pkgInfo.applicationInfo.nativeLibraryDir = libDir.getAbsolutePath();
            }
        }
    } finally {
        zipFile.close();
    }
    info.setPackageInfo(pkgInfo);
    setAttrs(info, manifestXML);
}

在代碼中歼争,一開始會通過 apk 得到 AndroidManifest.xml 文件。然后使用 XmlManifestReader 去讀取 AndroidManifest 中的信息渗勘。在 XmlManifestReader 中會使用 XmlPullParser 去解析 xml 沐绒, XmlManifestReader 相關(guān)的源碼就不貼出來了,想要進(jìn)一步了解的童鞋可以自己去看旺坠,點(diǎn)擊這里查看 XmlManifestReader 源碼洒沦。接下來根據(jù) apkPath 得到相應(yīng)的 pkgInfo ,并且若有 libDir 會去加載相應(yīng)的 .so 文件价淌。最后會調(diào)用 setAttrs(info, manifestXML) 這個方法:

private static void setAttrs(PlugInfo info, String manifestXML)
        throws XmlPullParserException, IOException {
    XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
    factory.setNamespaceAware(true);
    XmlPullParser parser = factory.newPullParser();
    parser.setInput(new StringReader(manifestXML));
    int eventType = parser.getEventType();
    String namespaceAndroid = null;
    do {
        switch (eventType) {
        case XmlPullParser.START_DOCUMENT: {
            break;
        }
        case XmlPullParser.START_TAG: {
            String tag = parser.getName();
            if (tag.equals("manifest")) {
                namespaceAndroid = parser.getNamespace("android");
            } else if ("activity".equals(parser.getName())) {
                addActivity(info, namespaceAndroid, parser);
            } else if ("receiver".equals(parser.getName())) {
                addReceiver(info, namespaceAndroid, parser);
            } else if ("service".equals(parser.getName())) {
                addService(info, namespaceAndroid, parser);
            }else if("application".equals(parser.getName())){
                parseApplicationInfo(info, namespaceAndroid, parser);
            }
            break;
        }
        case XmlPullParser.END_TAG: {
            break;
        }
        }
        eventType = parser.next();
    } while (eventType != XmlPullParser.END_DOCUMENT);
}

setAttrs(PlugInfo info, String manifestXML) 方法中,使用了 pull 方式去解析 manifest 瞒津,并且根據(jù) activity 蝉衣、 recevicer 、 service 等調(diào)用不同的 addXxxx() 方法巷蚪。這些方法其實(shí)本質(zhì)上是一樣的病毡,我們就挑 addActivity() 方法來看一下:

private static void addActivity(PlugInfo info, String namespace,
        XmlPullParser parser) throws XmlPullParserException, IOException {
    int eventType = parser.getEventType();
    String activityName = parser.getAttributeValue(namespace, "name");
    String packageName = info.getPackageInfo().packageName;
    activityName = getName(activityName, packageName);
    ResolveInfo act = new ResolveInfo();
    act.activityInfo = info.findActivityByClassNameFromPkg(activityName);
    do {
        switch (eventType) {
        case XmlPullParser.START_TAG: {
            String tag = parser.getName();
            if ("intent-filter".equals(tag)) {
                if (act.filter == null) {
                    act.filter = new IntentFilter();
                }
            } else if ("action".equals(tag)) {
                String actionName = parser.getAttributeValue(namespace,
                        "name");
                act.filter.addAction(actionName);
            } else if ("category".equals(tag)) {
                String category = parser.getAttributeValue(namespace,
                        "name");
                act.filter.addCategory(category);
            } else if ("data".equals(tag)) {
                // TODO parse data
            }
            break;
        }
        }
        eventType = parser.next();
    } while (!"activity".equals(parser.getName()));
    //
    info.addActivity(act);
}

addActivity() 代碼中的邏輯比較簡單,就是創(chuàng)建一個 ResolveInfo 類的對象 act 屁柏,把 Activity 相關(guān)的信息全部裝進(jìn)去啦膜,比如有 ActivityInfo 、 intent-filter 等淌喻。最后把 act 添加到 info 中僧家。其他的 addReceiveraddService 也是同一個邏輯。而 parseApplicationInfo 也是把 Application 的相關(guān)信息封裝到 info 中裸删。感興趣的同學(xué)可以看一下相關(guān)的源碼八拱,點(diǎn)擊這里查看。到這里,就把加載插件中 AndroidManifest.xml 的代碼分析完了肌稻。

加載插件 apk 中的資源文件

再回到 buildPlugInfo() 的代碼中去清蚀,接下來就是第三步,加載插件中的資源文件了爹谭。

為了方便枷邪,我們把相關(guān)的代碼復(fù)制到這里來:

try {
    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);
} catch (Exception e) {
    throw new RuntimeException("Unable to create Resources&Assets for "
            + info.getPackageName() + " : " + e.getMessage());
}

首先通過反射得到 AssetManager 的對象 am,然后通過反射其 addAssetPath 方法傳入 dexPath 參數(shù)來加載插件的資源文件诺凡,接下來就得到相應(yīng)插件的 Resource 對象 res 了东揣。這樣就實(shí)現(xiàn)了訪問插件中的資源文件了。那么到底 addAssetPath 這個方法有什么魔力呢绑洛?我們查看一下 Android 相關(guān)的源代碼(android/content/res/AssetManager.java):

/**
 * Add an additional set of assets to the asset manager.  This can be
 * either a directory or ZIP file.  Not for use by applications.  Returns
 * the cookie of the added asset, or 0 on failure.
 * {@hide}
 */
public final int addAssetPath(String path) {
    synchronized (this) {
        int res = addAssetPathNative(path);
        makeStringBlocks(mStringBlocks);
        return res;
    }
}

查看方法的注釋我們知道救斑,這個 addAssetPath() 方法就是用來添加額外的資源文件到 AssetManager 中去的,但是已經(jīng)被 hide 了真屯。所以我們只能通過反射的方式來執(zhí)行了脸候。這樣就解決了加載插件中的資源文件的問題了。

其實(shí)绑蔫,大多數(shù)插件化框架都是通過反射 addAssetPath() 的方式來解決加載插件資源問題运沦,基本上已經(jīng)成為了標(biāo)準(zhǔn)方案了。

為插件 apk 設(shè)置 ClassLoader

終于到了最后一個步驟了配深,如何為插件設(shè)置 ClassLoader 呢携添?其實(shí)解決的方案就是通過 DexClassLoader 。我們先來看 buildPlugInfo() 中的代碼:

PluginClassLoader pluginClassLoader = new PluginClassLoader(info, dexPath, dexOutputPath
        , getPluginLibPath(info).getAbsolutePath(), pluginParentClassLoader);
info.setClassLoader(pluginClassLoader);
ApplicationInfo appInfo = info.getPackageInfo().applicationInfo;
Application app = makeApplication(info, appInfo);
attachBaseContext(info, app);
info.setApplication(app);
Trace.store("Build pluginInfo => " + info);

在代碼中創(chuàng)建了 pluginClassLoader 對象篓叶,而 PluginClassLoader 正是繼承自 DexClassLoader 的烈掠,將 dexPathdexOutputPath 等參數(shù)傳入后缸托,就可以去加載插件中的類了左敌。 基本上所有的插件化框架都是通過 DexClassLoder 來作為插件 apk 的 ClassLoader 的。

之后在 makeApplication(info, appInfo) 就使用 PluginClassLoader 利用反射去創(chuàng)建插件的 Application 了:

/**
 * 構(gòu)造插件的Application
 *
 * @param plugInfo 插件信息
 * @param appInfo 插件ApplicationInfo
 * @return 插件App
 */
private Application makeApplication(PlugInfo plugInfo, ApplicationInfo appInfo) {
    String appClassName = appInfo.className;
    if (appClassName == null) {
        //Default Application
        appClassName = Application.class.getName();
    }
        try {
            return (Application) plugInfo.getClassLoader().loadClass(appClassName).newInstance();
        } catch (Throwable e) {
            throw new RuntimeException("Unable to create Application for "
                    + plugInfo.getPackageName() + ": "
                    + e.getMessage());
        }
}

創(chuàng)建完插件的 Application 之后俐镐, 再調(diào)用 attachBaseContext(info, app) 方法把 Application 的 mBase 屬性替換成 PluginContext 對象矫限,PluginContext 類繼承自 LayoutInflaterProxyContext ,里面封裝了一些插件的信息佩抹,比如有插件資源叼风、插件 ClassLoader 等。值得一提的是棍苹,在插件中 PluginContext 可以得到“宿主”的 Context 无宿,也就是所謂的“破殼”。具體可查看 PluginContext 的源碼廊勃。

private void attachBaseContext(PlugInfo info, Application app) {
    try {
        Field mBase = ContextWrapper.class.getDeclaredField("mBase");
        mBase.setAccessible(true);
        mBase.set(app, new PluginContext(context.getApplicationContext(), info));
    } catch (Throwable e) {
        e.printStackTrace();
    }
}

講到這里基本上把 buildPlugInfo() 中的邏輯講完了懈贺, pluginManager.loadPlugin 剩下的代碼都比較簡單经窖,相信大家一看就懂了。

0x03 PluginManager.startActivity

startActivity

在加載好插件 apk 之后梭灿,就可以使用插件了画侣。和平常無異,我們使用 PluginManager.startActivity 來啟動插件中的 Activity 堡妒。其實(shí) PluginManager 有很多 startActivity 的方法:

startActivity截圖

但是終于都會調(diào)用 startActivity(Context from, PlugInfo plugInfo, ActivityInfo activityInfo, Intent intent) 這個方法:

private DynamicActivitySelector activitySelector = DefaultActivitySelector.getDefault();

...

/**
 * 啟動插件的指定Activity
 *
 * @param from         fromContext
 * @param plugInfo     插件信息
 * @param activityInfo 要啟動的插件activity信息
 * @param intent       通過此Intent可以向插件傳參, 可以為null
 */
public void startActivity(Context from, PlugInfo plugInfo, ActivityInfo activityInfo, Intent intent) {
    if (activityInfo == null) {
        throw new ActivityNotFoundException("Cannot find ActivityInfo from plugin, could you declare this Activity in plugin?");
    }
    if (intent == null) {
        intent = new Intent();
    }
    CreateActivityData createActivityData = new CreateActivityData(activityInfo.name, plugInfo.getPackageName());
    intent.setClass(from, activitySelector.selectDynamicActivity(activityInfo));
    intent.putExtra(Globals.FLAG_ACTIVITY_FROM_PLUGIN, createActivityData);
    from.startActivity(intent);
}

我們先來看代碼配乱, CreateActivityData 類是用來存儲一個將要創(chuàng)建的插件 Activity 的數(shù)據(jù),實(shí)現(xiàn)了 Serializable 接口皮迟,因此可以被序列化搬泥。總之伏尼, CreateActivityData 會存儲將要創(chuàng)建的插件 Activity 的類名和包名忿檩,再把它放入 intent 中。之后爆阶, intent 設(shè)置要創(chuàng)建的 Activity 為 activitySelector.selectDynamicActivity(activityInfo) 燥透,activitySelectorDefaultActivitySelector 類的對象,那么這 DefaultActivitySelector 到底是什么東西呢辨图?一起來看看 DefaultActivitySelector 的源碼:

public class DefaultActivitySelector implements DynamicActivitySelector {

    private static DynamicActivitySelector DEFAULT = new DefaultActivitySelector();

    @Override
    public Class<? extends Activity> selectDynamicActivity(ActivityInfo pluginActivityInfo) {
        return DynamicActivity.class;
    }

    public static DynamicActivitySelector getDefault() {
        return DEFAULT;
    }
}

其實(shí)很簡單班套,不管傳入的 pluginActivityInfo 參數(shù)是什么,返回的都是 DynamicActivity.class 故河。也就是我們在介紹 android-pluginmgr 簡單用法時吱韭,第二步在 AndroidManifest 中注冊的那個 DynamicActivity
看到這里的代碼鱼的,我們一定可以猜到什么理盆。因?yàn)檫@里的 intent 中設(shè)置即將啟動的 Activity 仍然為 DynamicActivity ,所以在后面的代碼中肯定會去動態(tài)地替換掉 DynamicActivity凑阶。

動態(tài)Hook

之前在 PluginManager.init(this) 源碼這一小節(jié)中介紹了熏挎,當(dāng)前 ActivityThreadInstrumentation 已經(jīng)被替換成了 PluginInstrumentation。所以在創(chuàng)建 Activity 的時候會去調(diào)用 PluginInstrumentation 里面的方法晌砾。這樣就可以在里面“做手腳”,實(shí)現(xiàn)了動態(tài)去替換 Activity 的思路烦磁。我們先來看一下 PluginInstrumentation 中部分方法的源碼:

private void replaceIntentTargetIfNeed(Context from, Intent intent)
{
    if (!intent.hasExtra(Globals.FLAG_ACTIVITY_FROM_PLUGIN) && currentPlugin != null)
    {
        ComponentName componentName = intent.getComponent();
        if (componentName != null)
        {
            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.setExtrasClassLoader(currentPlugin.getClassLoader());
                }
            }
        }
    }
}

@Override
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Fragment fragment, Intent intent, int requestCode)
{
    replaceIntentTargetIfNeed(who, intent);
    return super.execStartActivity(who, contextThread, token, fragment, intent, requestCode);
}

@Override
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Fragment fragment, Intent intent, int requestCode, Bundle options)
{
    replaceIntentTargetIfNeed(who, intent);
    return super.execStartActivity(who, contextThread, token, fragment, intent, requestCode, options);
}

@Override
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode)
{
    replaceIntentTargetIfNeed(who, intent);
    return super.execStartActivity(who, contextThread, token, target, intent, requestCode);
}

@Override
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options)
{
    replaceIntentTargetIfNeed(who, intent);
    return super.execStartActivity(who, contextThread, token, target, intent, requestCode, options);
}

我們發(fā)現(xiàn)养匈,在所有的 execStartActivity() 方法執(zhí)行前,都加上了 replaceIntentTargetIfNeed(Context from, Intent intent) 這個方法都伪,在方法里面 intent.setClass 中設(shè)置的還是 DynamicActivity.class 呕乎,把插件信息都檢查了一遍。

在這之后陨晶,會去執(zhí)行 PluginInstrumentation.newActivity 方法來創(chuàng)建即將要啟動的Activity 猬仁。也正是在這里帝璧,對之前的 DynamicActivity 進(jìn)行 Hook ,達(dá)到啟動插件 Activity 的目的湿刽。

@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException
{
    CreateActivityData activityData = (CreateActivityData) intent.getSerializableExtra(Globals.FLAG_ACTIVITY_FROM_PLUGIN);
    //如果activityData存在,那么說明將要創(chuàng)建的是插件Activity
    if (activityData != null && PluginManager.getSingleton().getPlugins().size() > 0) {
        //這里找不到插件信息就會拋異常的,不用擔(dān)心空指針
        PlugInfo plugInfo;
        try
        {
            Log.d(getClass().getSimpleName(), "+++ Start Plugin Activity => " + activityData.pluginPkg + " / " + activityData.activityName);
            // 得到插件信息類
            plugInfo = PluginManager.getSingleton().tryGetPluginInfo(activityData.pluginPkg);
            // 在該方法中會調(diào)用插件的Application.onCreate()
            plugInfo.ensureApplicationCreated();
        }
        catch (PluginNotFoundException e)
        {
            PluginManager.getSingleton().dump();
            throw new IllegalAccessException("Cannot get plugin Info : " + activityData.pluginPkg);
        }
        if (activityData.activityName != null)
        {
            // 在這里替換了className的烁,變成了插件Activity的className
            className = activityData.activityName;
            // 替換classloader
            cl = plugInfo.getClassLoader();
        }
    }
    return super.newActivity(cl, className, intent);
}

newActivity() 方法中,先拿到了插件信息 plugInfo 诈闺,然后會確保插件的 Application 已經(jīng)創(chuàng)建渴庆。然后在第25行會去替換掉 classNamecl 。這樣雅镊,原本要創(chuàng)建的是 DynamicActivity 就變成了插件的 Activity 了襟雷,從而實(shí)現(xiàn)了創(chuàng)建插件 Activity 的目的,并且這個 Activity 是真實(shí)的 Activity 組件仁烹,具備生命周期的耸弄。

也許有童鞋會有疑問,如果直接在 startActivity 中設(shè)置要啟動的 Activity 為插件 Activity 卓缰,這樣不行嗎计呈?答案是肯定的,因?yàn)檫@樣就會拋出一個異常:ActivityNotFoundException:...have you declared this activity in your AndroidManifest.xml?我相信這個異常大家很熟悉的吧僚饭,在剛開始學(xué)習(xí) Android 時震叮,大家都會犯的一個錯誤。所以鳍鸵,我想我們也明白了為什么要花這么大的一個功夫去動態(tài)地替換要創(chuàng)建的 Activity 苇瓣,就是為了繞過這個 ActivityNotFoundException 異常,達(dá)到去“欺騙” Android 系統(tǒng)的效果偿乖。

既然創(chuàng)建好了击罪,那么就來看看 PluginInstrumentation 里調(diào)用相關(guān)生命周期的方法:

@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
    lookupActivityInPlugin(activity);
    if (currentPlugin != null) {
        //初始化插件Activity
        Context baseContext = activity.getBaseContext();
        PluginContext pluginContext = new PluginContext(baseContext, currentPlugin);
        try {
            try {
                //在許多設(shè)備上,Activity自身hold資源
                Reflect.on(activity).set("mResources", pluginContext.getResources());

            } catch (Throwable ignored) {
            }

            Field field = ContextWrapper.class.getDeclaredField("mBase");
            field.setAccessible(true);
            field.set(activity, pluginContext);
            try {
                Reflect.on(activity).set("mApplication", currentPlugin.getApplication());
            } catch (ReflectException e) {
                Trace.store("Application not inject success into : " + activity);
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }

        ActivityInfo activityInfo = currentPlugin.findActivityByClassName(activity.getClass().getName());
        if (activityInfo != null) {
            //根據(jù)AndroidManifest.xml中的參數(shù)設(shè)置Theme
            int resTheme = activityInfo.getThemeResource();
            if (resTheme != 0) {
                boolean hasNotSetTheme = true;
                try {
                    Field mTheme = ContextThemeWrapper.class
                            .getDeclaredField("mTheme");
                    mTheme.setAccessible(true);
                    hasNotSetTheme = mTheme.get(activity) == null;
                } catch (Exception e) {
                    e.printStackTrace();
                }
                if (hasNotSetTheme) {
                    changeActivityInfo(activityInfo, activity);
                    activity.setTheme(resTheme);
                }
            }

        }

        // 如果是三星手機(jī)贪薪,則使用包裝的LayoutInflater替換原LayoutInflater
        // 這款手機(jī)在解析內(nèi)置的布局文件時有各種錯誤
        if (android.os.Build.MODEL.startsWith("GT")) {
            Window window = activity.getWindow();
            Reflect windowRef = Reflect.on(window);
            try {
                LayoutInflater originInflater = window.getLayoutInflater();
                if (!(originInflater instanceof LayoutInflaterWrapper)) {
                    windowRef.set("mLayoutInflater", new LayoutInflaterWrapper(originInflater));
                }
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }
    super.callActivityOnCreate(activity, icicle);
}

/**
 * 檢查跳轉(zhuǎn)目標(biāo)是不是來自插件
 *
 * @param activity Activity
 */
private void lookupActivityInPlugin(Activity activity) {
    ClassLoader classLoader = activity.getClass().getClassLoader();
    if (classLoader instanceof PluginClassLoader) {
        currentPlugin = ((PluginClassLoader) classLoader).getPlugInfo();
    } else {
        currentPlugin = null;
    }
}

callActivityOnCreate() 中先去檢查了創(chuàng)建的 Activity 是否來自于插件媳禁。如果是,那么會給 Activity 設(shè)置 Context 画切、 設(shè)置主題等竣稽;如果不是,則直接執(zhí)行父類方法霍弹。在 super.callActivityOnCreate(activity, icicle) 中會去調(diào)用 Activity.onCreate()方法毫别。其他的生命周期方法作者沒有特殊處理,這里就不講了典格。

分析到這岛宦,我們終于把 android-pluginmgr 插件化實(shí)現(xiàn)的方案完整地梳理了一遍。當(dāng)然耍缴,不同的插件化框架會有不同的實(shí)現(xiàn)方案砾肺,具體的仍然需要自己專心研究挽霉。另外我們發(fā)現(xiàn)該框架還沒有實(shí)現(xiàn)啟動插件 Service 的功能,如果想要了解变汪,可以參考下其他插件化框架侠坎。

0x04 總結(jié)

上面亂七八糟的流程講了一遍,可能還有一些童鞋不太懂疫衩,所以在這里給出一張 android-pluginmgr 的流程圖硅蹦。不懂的童鞋可以根據(jù)這張圖再好好看一下源碼,相信你會恍然大悟的闷煤。

android-pluginmgr流程圖

最后童芹,如果對本文哪里有疑問的童鞋,歡迎留言鲤拿,一起交流假褪。

0x05 References

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末近顷,一起剝皮案震驚了整個濱河市生音,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌窒升,老刑警劉巖缀遍,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異饱须,居然都是意外死亡域醇,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門蓉媳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來譬挚,“玉大人,你說我怎么就攤上這事酪呻〖跣” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵玩荠,是天一觀的道長漆腌。 經(jīng)常有香客問我,道長阶冈,這世上最難降的妖魔是什么屉凯? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮眼溶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘晓勇。我一直安慰自己堂飞,他們只是感情好灌旧,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著绰筛,像睡著了一般枢泰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上铝噩,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天衡蚂,我揣著相機(jī)與錄音,去河邊找鬼骏庸。 笑死毛甲,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的具被。 我是一名探鬼主播玻募,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼一姿!你這毒婦竟也來了七咧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤叮叹,失蹤者是張志新(化名)和其女友劉穎艾栋,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蛉顽,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蝗砾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蜂林。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片遥诉。...
    茶點(diǎn)故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖噪叙,靈堂內(nèi)的尸體忽然破棺而出矮锈,到底是詐尸還是另有隱情,我是刑警寧澤睁蕾,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布苞笨,位于F島的核電站,受9級特大地震影響子眶,放射性物質(zhì)發(fā)生泄漏瀑凝。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一臭杰、第九天 我趴在偏房一處隱蔽的房頂上張望粤咪。 院中可真熱鬧,春花似錦渴杆、人聲如沸寥枝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽囊拜。三九已至某筐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間冠跷,已是汗流浹背南誊。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蜜托,地道東北人缴淋。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓四苇,卻偏偏與公主長得像晋被,于是被迫代替她去往敵國和親得封。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評論 2 355

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,133評論 25 707
  • 是時候來一波Android插件化了 是時候來一波Android插件化了前言Android開發(fā)演進(jìn)模塊化介紹插件化介...
    流水不腐小夏閱讀 4,790評論 3 51
  • 今天發(fā)現(xiàn)自己原來是這樣的呀。 聽到一個事情如果與當(dāng)下無關(guān)锅劝,我本能的會拒絕為它高興攒驰,去思考關(guān)聯(lián)性,過后我會主動想起這...
    靜默物語閱讀 167評論 0 0
  • title: “推薦書-指針的編程藝術(shù)date: 2016-03-27 16:17:22tags: 讀書筆記cat...
    jeffleefree閱讀 493評論 0 0
  • 埃及故爵,一個聽名字就無比神秘的國度玻粪,如同沙漠中蒙著面紗的少女,讓你忍不住的想要觀其真容诬垂。無論是哺育了人類文明...
    琳的blue閱讀 260評論 0 0