Activity的外掛化(三)

渣渣008發表於2017-12-13

上面一篇文章已經實現了Intent解析和targetClass的還原工作,這篇文章會來說說:

1. 外掛apk的解析

#### 2. ClassLoader的問題

3. 資源的問題

4. 外掛的下載,載入機制的問題

1.外掛apk的解析

說到這個我們很容易想到一般的apk的安裝過程也是需要解析apk的資訊的,下面貼一篇比較好的文章。 Android APK安裝過程分析

image.png
圖片取自上面的部落格,apk的安裝過程與pms有很大的關係,很多操作都是由pms完成的,有興趣的可以去了解。

private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
    final int installFlags = args.installFlags;
    final String installerPackageName = args.installerPackageName;
    final String volumeUuid = args.volumeUuid;
    final File tmpPackageFile = new File(args.getCodePath());
    final boolean forwardLocked = ((installFlags & PackageManager.INSTALL_FORWARD_LOCK) != 0);
    final boolean onExternal = (((installFlags & PackageManager.INSTALL_EXTERNAL) != 0)
            || (args.volumeUuid != null));
    boolean replace = false;
    int scanFlags = SCAN_NEW_INSTALL | SCAN_UPDATE_SIGNATURE;
    res.returnCode = PackageManager.INSTALL_SUCCEEDED;
    final int parseFlags = mDefParseFlags | PackageParser.PARSE_CHATTY
            | (forwardLocked ? PackageParser.PARSE_FORWARD_LOCK : 0)
            | (onExternal ? PackageParser.PARSE_EXTERNAL_STORAGE : 0);
    PackageParser pp = new PackageParser();
    pp.setSeparateProcesses(mSeparateProcesses);
    pp.setDisplayMetrics(mMetrics);
    final PackageParser.Package pkg;
    try {
        pkg = pp.parsePackage(tmpPackageFile, parseFlags);
    } catch (PackageParserException e) {
        res.setError("Failed parse during installPackageLI", e);
        return;
    }
}
複製程式碼

解析apk檔案呼叫了PackageParserparsePackage方法。 現在一般的外掛話框架也是這樣做的,解析apk也是通過呼叫這個方法,當然除了這個,還有一些開源的解析apk的框架例如:apk-parser

這裡要說明的是PackageParser這個類在不同的安卓SDK版本里面有不少的改動,所以需要做相容。 這裡比較一下滴滴的VirtualAPK和DroidPlugin的做法,他們的做法基本一樣,比較不同的是VirtualAPK框架把安卓framework把安卓裡面外掛化需要的類提取出來了,作為一個lib,外掛化框架VirtualAPK provide的形式依賴這個庫,就可以直接呼叫裡面的方法,有效避免了反射帶來的效能方面的損失。

image.png
DroidPlugin裡面的相容,下圖是VirtualAPK裡面的做法,抽出framework裡面的類,做成一個庫,外掛化框架provide形式依賴。
image.png

public final class PackageParserCompat {

    /**
    * 解析外掛apk
    *
    * @param context context
    * @param apk    外掛apk位置,必須是apk檔案
    * @param flags  flag
    * @return {@link PackageParser.Package}
    * @throws PackageParser.PackageParserException
    */
    public static final PackageParser.Package parsePackage(final Context context, final File apk, final int flags)
            throws PackageParser.PackageParserException {
        if (Build.VERSION.SDK_INT >= 24) {
            return PackageParserV24.parsePackage(context, apk, flags);
        } else if (Build.VERSION.SDK_INT >= 21) {
            return PackageParserLollipop.parsePackage(context, apk, flags);
        } else {
            return PackageParserLegacy.parsePackage(context, apk, flags);
        }
    }

    /**
    * 7.0及以後的相容處理
    */
    private static final class PackageParserV24 {
        static final PackageParser.Package parsePackage(Context context, File apk, int flags)
                throws PackageParser.PackageParserException {
            PackageParser parser = new PackageParser();
            PackageParser.Package pkg = parser.parsePackage(apk, flags);
            ReflectUtil.invokeNoException(PackageParser.class, null, "collectCertificates",
                    new Class[]{PackageParser.Package.class, int.class}, pkg, flags);
            return pkg;
        }
    }

    /**
    * 5.0,5.1,6.0的處理
    */
    private static final class PackageParserLollipop {
        static final PackageParser.Package parsePackage(final Context context, final File apk, final int flags)
                throws PackageParser.PackageParserException {
            PackageParser parser = new PackageParser();
            PackageParser.Package pkg = parser.parsePackage(apk, flags);
            try {
                parser.collectCertificates(pkg, flags);
            } catch (Throwable e) {
                // ignored
            }
            return pkg;
        }
    }

    /**
    * 低版本的處理
    */
    private static final class PackageParserLegacy {
        static final PackageParser.Package parsePackage(Context context, File apk, int flags) {
            PackageParser parser = new PackageParser(apk.getAbsolutePath());
            PackageParser.Package pkg = parser.parsePackage(apk, apk.getAbsolutePath(), context.getResources()
                    .getDisplayMetrics(), flags);
            ReflectUtil.invokeNoException(PackageParser.class, parser, "collectCertificates",
                    new Class[]{PackageParser.Package.class, int.class}, pkg, flags);
            return pkg;
        }
    }

}
複製程式碼

程式碼處理,對了,我最近在給VirtualAPK框架新增註釋,程式碼在這裡:VirtualAPK

2. ClassLoader的問題

ClassLoader問題之前已經提過了,外掛是不是能使用宿主的類,如果要能使用的話,外掛是需要繼承宿主的ClassLoader的,宿主是不是能載入到外掛的類,如果能,外掛的dex是要放到宿主的dex陣列裡面滴。 VirtualAPK在處理這個問題的時候分了兩種情況,一種是宿主有價值外掛類的能力,一種是沒有,實現方式那是相當的簡單。(PS:外掛是一直可以載入到宿主類的)

/**
    * 建立外掛ClassLoader
    *
    * @param context host的classLoader
    * @param apk    外掛檔案位置
    * @param libsDir native lib的資料夾,按照他的寫法和files,cache同級的app_valibs目錄
    * @param parent  host的ClassLoader
    * @return 外掛ClassLoader
    */
private static ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) {
    // data/data/name/app_dex
    File dexOutputDir = context.getDir(Constants.OPTIMIZE_DIR, Context.MODE_PRIVATE);
    String dexOutputPath = dexOutputDir.getAbsolutePath();
    DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent);
    if (Constants.COMBINE_CLASSLOADER) {
        try {
            DexUtil.insertDex(loader);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    return loader;
}
複製程式碼

這裡的處理,就很簡單了,如果宿主有載入外掛類的能力,那麼,把外掛的dex檔案放到宿主的dex列表裡面(至於為什麼要這樣做,可以瞭解一下安卓的BaseDexClassLoader)。

3. 資源的問題

關於資源的問題和程式碼的問題差不多,如果想要共享資源,那麼需要把外掛的資源新增到宿主的資源裡面,當然也可以外掛就自己管理自己的資源,每個外掛一個資源管理器。

這裡需要注意的是如果把外掛的資源放到宿州的資源裡面那麼需要解決一個問題,檔案描述的明白點就是,宿主是一個正常的apk檔案,資源id是0x7f開頭,外掛也是apk檔案,資源id也是0x7f開頭,那麼這樣的話,外掛和宿主的資源id不是就存在重複的可能了麼,答案是肯定的,這種可能很大呢,為了解決這個問題也有兩種解決方案:(這裡只討論修改aapt的方法) 1.修改aapt,讓外掛資源id不再以0x7f開頭 2.使用gradle plugin外掛修改資源產物.ap_裡面的資源id,然後重新壓縮回去

VirtualAPK在處理這個問題上也是分兩種情況,一種是把外掛的資源新增到宿州的資源裡面,一種是外掛自己管理自己的資源,當然實現的程式碼依舊非常簡單。

/**
    * 建立外掛的AssetManager
    *
    * @param context 宿主context
    * @param apk    外掛apk檔案
    * @return 外掛AssetManager
    */
private static AssetManager createAssetManager(Context context, File apk) {
    try {
        AssetManager am = AssetManager.class.newInstance();
        ReflectUtil.invoke(AssetManager.class, am, "addAssetPath", apk.getAbsolutePath());
        return am;
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

/**
    * 建立外掛的資源管理器
    *
    * @param context 宿主host
    * @param apk    外掛apk檔案
    * @return 外掛Resources
    */
@WorkerThread
private static Resources createResources(Context context, File apk) {
    if (Constants.COMBINE_RESOURCES) {
        Resources resources = ResourcesManager.createResources(context, apk.getAbsolutePath());
        ResourcesManager.hookResources(context, resources);
        return resources;
    } else {
        Resources hostResources = context.getResources();
        AssetManager assetManager = createAssetManager(context, apk);
        return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
    }
}
複製程式碼

這裡就分兩種情況,如果資源要合併的話,就把外掛資源放入宿主裡面,不然外掛就使用自己的資源。

4. 外掛的下載,載入機制的問題

外掛的下載要考慮一些問題,首先是後臺的設計,根據選擇的外掛化框架的不同,有不同的處理。 這些問題是我的一些關於外掛化問題的一些思考。

image.png
外掛的後臺也要考慮到這些問題。 外掛的載入,外掛載入是需要釋放dex優化dex檔案的,那麼這個過程的管理升級的一些問題。

5. 後續

後續就不說Activity外掛化相關的事情了,等把程式碼補充完成之後,就開始其他三大元件的外掛化講解了。 關於程式碼:整理中

其實這個系統的外掛化基本都是基於VirtualAPK講解的,個人更喜歡360團隊的外掛化框架Replugin,等這系列結束之後會去研究他。 個人打算寫一個外掛化的後臺PluginServer,這個後臺是為Replugin服務的,當然,也會考慮到我上面說的一些問題,版本的問題,爭取做一個通用的外掛化平臺的後臺。

相關文章