Android 外掛化框架 DynamicLoadApk 原始碼分析

WngShhng發表於2019-03-02

DynamicLoadApk 應該算是 Android 外掛化諸多框架中資歷比較老的一個了。它的專案地址在:dynamic-load-apk。該專案執行之後的效果是,使用 Gradle 編譯出外掛包和宿主包,都是以 APK 的形式。安裝宿主包之後,通過 ADB 將外掛包 push 到手機中。啟動宿主包時,它會自動進行掃描將外掛載入到應用中。點選外掛之後,進入到外掛的應用介面。

印象中最初接觸的外掛都是以單獨安裝的形式存在的,比如我可以做一個基礎的應用,然後在該應用的基礎上開發外掛。使用者可以對外掛進行選擇,然後下載並安裝,以讓自己的應用具有更豐富的功能。外掛化也算是一種比較實用的技術,畢竟我們使用 Chrome 和 AS 的時候不是一樣要載入外掛。只是比較反感的是去修改底層的程式碼,容易給系統帶來不穩定因素不說,技術到了一些人手裡,你知道他用來幹什麼。外掛化挺好,但真的要去推廣這項技術,還是看好 Google 官方去進行規範。

技術要服務於產品,好的產品不一定要高超的技術,技術並不是最重要的,重要的是你究竟想要表達什麼。這就像國內很多人只注重數理化,不注重人文學科。相比於國內的技術精英,我還是比較贊同 Google 站在整個生態的角度去考慮技術演進。前些日子社群裡對外掛化的討論:移動開發的羅曼蒂克消亡史。好吧,我自己的理解是,這從來就不是什麼羅曼蒂克。

DynamicLoadApk 外掛化的實現方式還是挺有意思的,它使用純 Java 實現,沒有涉及 Native 層的程式碼,下面我理了下 DynamicLoadApk 的 Demo 程式的整個執行過程。後續的文章我們就圍繞這張圖進行,

DynamicLoadApk 的外掛化執行過程

首先是掃描檔案路徑並載入 APK,這裡需要解析 APK 檔案的資訊,它是本質上是通過 PMS 實現的;

    public DLPluginPackage loadApk(final String dexPath, boolean hasSoLib) {
        mFrom = DLConstants.FROM_EXTERNAL;

        // 通過 PMS 獲取包資訊,這裡獲取了 Activity 和 Service 的資訊
        PackageInfo packageInfo = mContext.getPackageManager().getPackageArchiveInfo(dexPath,
                PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES);
        if (packageInfo == null) {
            return null;
        }

        DLPluginPackage pluginPackage = preparePluginEnv(packageInfo, dexPath);
        if (hasSoLib) {
            copySoLib(dexPath);
        }

        return pluginPackage;
    }
複製程式碼

然後,它通過呼叫 preparePluginEnv() 方法來建立 AssetManager, DexClassLoader 和 Resource 等。我們的外掛類載入各種資源和類的時候使用的就是這哥仨:

    private DLPluginPackage preparePluginEnv(PackageInfo packageInfo, String dexPath) {

        DLPluginPackage pluginPackage = mPackagesHolder.get(packageInfo.packageName);
        if (pluginPackage != null) {
            return pluginPackage;
        }
        DexClassLoader dexClassLoader = createDexClassLoader(dexPath);
        AssetManager assetManager = createAssetManager(dexPath);
        Resources resources = createResources(assetManager);
        // create pluginPackage
        pluginPackage = new DLPluginPackage(dexClassLoader, resources, packageInfo);
        mPackagesHolder.put(packageInfo.packageName, pluginPackage);
        return pluginPackage;
    }

    private DexClassLoader createDexClassLoader(String dexPath) {
        File dexOutputDir = mContext.getDir("dex", Context.MODE_PRIVATE);
        dexOutputPath = dexOutputDir.getAbsolutePath();
        DexClassLoader loader = new DexClassLoader(dexPath, dexOutputPath, mNativeLibDir, mContext.getClassLoader());
        return loader;
    }

    private AssetManager createAssetManager(String dexPath) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, dexPath);
            return assetManager;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private Resources createResources(AssetManager assetManager) {
        Resources superRes = mContext.getResources();
        Resources resources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
        return resources;
    }
複製程式碼

然後點選外掛的時候要啟動外掛的 Activity,

    PluginItem item = mPluginItems.get(position);
    DLPluginManager pluginManager = DLPluginManager.getInstance(this);
    pluginManager.startPluginActivity(this, new DLIntent(item.packageInfo.packageName, item.launcherActivityName));
複製程式碼

這裡會把要啟動的包名和啟動類名包裝到 DLIntent 中。DLIntent 是 Intent 的子類。啟動外掛的進一步的邏輯在 DLPluginManager 的 startPluginActivity() 方法中。按照上文的描述,這裡主要做了以下四件事情:

  1. 判斷要啟動的 Activity 是否是外掛 Activity:因為要啟動的類也可能不是外掛類,所以我們需要分成兩種情況來進行處理,普通的 Activity 直接呼叫 Context.startActivity() 外掛 Activity 需要呼叫代理 Activity 來執行。
  2. 判斷包名,獲取外掛相關資訊:這裡就算是一個安全的校驗吧,主要是從之前解析的 APK 資訊中進行校驗。
  3. 使用外掛的 DexClassLoader 載入啟動類:先要使用類載入器載入外掛的 Activity 到記憶體中,外掛 Activity 的資訊會作為 Intent 的引數一起傳遞給代理 Activity。
  4. 使用 DLIntent.setClass() 啟動代理類:要啟動的代理類可能是 DLProxyFragmentActivity 和 DLProxyActivity,所以這裡我們先使用 getProxyActivityClass() 得到代理類。該方法中使用了 Class 的 isAssignableFrom() 方法來判斷某個例項是否是指定型別的。比如 DLBasePluginActivity.class.isAssignableFrom(clazz) 表示 clazz 是否是 DLBasePluginActivity 型別的。
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public int startPluginActivityForResult(Context context, DLIntent dlIntent, int requestCode) {
        // 1.判斷要啟動的 Activity 是否是外掛 Activity
        if (mFrom == DLConstants.FROM_INTERNAL) {
            dlIntent.setClassName(context, dlIntent.getPluginClass());
            performStartActivityForResult(context, dlIntent, requestCode);
            return DLPluginManager.START_RESULT_SUCCESS;
        }

        // 2.判斷包名,獲取外掛相關資訊
        String packageName = dlIntent.getPluginPackage();
        if (TextUtils.isEmpty(packageName)) {
            throw new NullPointerException("disallow null packageName.");
        }

        DLPluginPackage pluginPackage = mPackagesHolder.get(packageName);
        if (pluginPackage == null) {
            return START_RESULT_NO_PKG;
        }

        // 3.使用外掛的 DexClassLoader 載入啟動類
        final String className = getPluginActivityFullPath(dlIntent, pluginPackage);
        Class<?> clazz = loadPluginClass(pluginPackage.classLoader, className);
        if (clazz == null) {
            return START_RESULT_NO_CLASS;
        }

        Class<? extends Activity> activityClass = getProxyActivityClass(clazz);
        if (activityClass == null) {
            return START_RESULT_TYPE_ERROR;
        }

        // 4.使用 DLIntent.setClass() 啟動代理類,並傳入外掛類和包資訊
        dlIntent.putExtra(DLConstants.EXTRA_CLASS, className);
        dlIntent.putExtra(DLConstants.EXTRA_PACKAGE, packageName);
        dlIntent.setClass(mContext, activityClass);
        performStartActivityForResult(context, dlIntent, requestCode);
        return START_RESULT_SUCCESS;
    }

    private Class<? extends Activity> getProxyActivityClass(Class<?> clazz) {
        Class<? extends Activity> activityClass = null;
        if (DLBasePluginActivity.class.isAssignableFrom(clazz)) {
            activityClass = DLProxyActivity.class;
        } else if (DLBasePluginFragmentActivity.class.isAssignableFrom(clazz)) {
            activityClass = DLProxyFragmentActivity.class;
        }

        return activityClass;
    }

    private void performStartActivityForResult(Context context, DLIntent dlIntent, int requestCode) {
        if (context instanceof Activity) {
            ((Activity) context).startActivityForResult(dlIntent, requestCode);
        } else {
            context.startActivity(dlIntent);
        }
    }
複製程式碼

這裡需要注意下,我們的外掛 Activity 是需要繼承 DLBasePluginActivity 或者 DLProxyFragmentActivity。這兩個類中重寫了 Activity 的許多生命週期方法。在代理 Activity 啟動之後,代理 Activity 會被傳遞到前面兩個基類中。比如,當外掛類想要獲取 AssetsManager 的時候,會呼叫到這兩個基類的 getAssetsManager(),然後基類通過代理類得到之前我們建立的 AssetsManager.

按照上述流程,代理類被正常啟動。啟動之後它會建立 DLProxyImpl 例項,並在 onCreate() 方法中呼叫 DLProxyImpl 的 onCreate() 方法:

    public void onCreate(Intent intent) {
        intent.setExtrasClassLoader(DLConfigs.sPluginClassloader);

        mPackageName = intent.getStringExtra(DLConstants.EXTRA_PACKAGE);
        mClass = intent.getStringExtra(DLConstants.EXTRA_CLASS);

        mPluginManager = DLPluginManager.getInstance(mProxyActivity);
        mPluginPackage = mPluginManager.getPackage(mPackageName);
        mAssetManager = mPluginPackage.assetManager;
        mResources = mPluginPackage.resources;

        initializeActivityInfo();
        handleActivityInfo();
        launchTargetActivity();
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    protected void launchTargetActivity() {
        try {
            Class<?> localClass = getClassLoader().loadClass(mClass);
            Constructor<?> localConstructor = localClass.getConstructor(new Class[] {});
            Object instance = localConstructor.newInstance(new Object[] {});
            mPluginActivity = (DLPlugin) instance;
            ((DLAttachable) mProxyActivity).attach(mPluginActivity, mPluginManager);
            mPluginActivity.attach(mProxyActivity, mPluginPackage);

            Bundle bundle = new Bundle();
            bundle.putInt(DLConstants.FROM, DLConstants.FROM_EXTERNAL);
            mPluginActivity.onCreate(bundle);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
複製程式碼

這裡的主要邏輯在上述兩個方法中。第一個方法中會根據包名從 DLPluginManager 中獲取包的類載入器,然後使用該載入其載入器載入外掛類,反射觸發其構造方法,獲取例項。然後呼叫代理 Activity 的 attach() 方法將該外掛類複製給代理類。然後當 AMS 回撥代理類的各個生命週期的時候,代理類呼叫外掛類的各個生命週期。(這裡會使用類載入器再次載入外掛類,其實這是沒必要的,我們可以直接使用 Intent 將外掛類的 Class 通過序列化的方式傳遞過來,然後直接觸發其構造方法即可,無需再次執行類載入邏輯。)

好了,以上就是 DynamicLoadApk 的原理,其實本質就是:外掛類作為一個普通的類被呼叫,它不歸 AMS 負責。當我們啟動外掛的時候,實際啟動的是代理類,當 AMS 回撥代理類的生命週期的時候,代理類再呼叫外掛類的各個生命週期方法。只是,對資源和類載入的部分需要注意下,因為我們需要進行自定義配置來把它們的路徑指向我們的外掛包。

相關文章