溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Android插件化是怎樣的

發(fā)布時(shí)間:2021-09-24 14:37:53 來源:億速云 閱讀:154 作者:柒染 欄目:開發(fā)技術(shù)

本篇文章為大家展示了Android插件化是怎樣的,內(nèi)容簡(jiǎn)明扼要并且容易理解,絕對(duì)能使你眼前一亮,通過這篇文章的詳細(xì)介紹希望你能有所收獲。

    一、認(rèn)識(shí)插件化

    1.1 插件化起源

    插件化技術(shù)最初源于免安裝運(yùn)行 Apk的想法,這個(gè)免安裝的 Apk 就可以理解為插件,而支持插件的 app 我們一般叫 宿主。

    想必大家都知道,在 Android 系統(tǒng)中,應(yīng)用是以 Apk 的形式存在的,應(yīng)用都需要安裝才能使用。但實(shí)際上 Android 系統(tǒng)安裝應(yīng)用的方式相當(dāng)簡(jiǎn)單,其實(shí)就是把應(yīng)用 Apk 拷貝到系統(tǒng)不同的目錄下、然后把 so 解壓出來而已。

    常見的應(yīng)用安裝目錄有:

    • /system/app:系統(tǒng)應(yīng)用

    • /system/priv-app:系統(tǒng)應(yīng)用

    • /data/app:用戶應(yīng)用

    那可能大家會(huì)想問,既然安裝這個(gè)過程如此簡(jiǎn)單,Android 是怎么運(yùn)行應(yīng)用中的代碼的呢,我們先看 Apk 的構(gòu)成,一個(gè)常見的 Apk 會(huì)包含如下幾個(gè)部分:

    • classes.dex:Java 代碼字節(jié)碼

    • res:資源文件

    • lib:so 文件

    • assets:靜態(tài)資產(chǎn)文件

    • AndroidManifest.xml:清單文件

    其實(shí) Android 系統(tǒng)在打開應(yīng)用之后,也只是開辟進(jìn)程,然后使用 ClassLoader 加載 classes.dex 至進(jìn)程中,執(zhí)行對(duì)應(yīng)的組件而已。

    那大家可能會(huì)想一個(gè)問題,既然 Android 本身也是使用類似反射的形式加載代碼執(zhí)行,憑什么我們不能執(zhí)行一個(gè) Apk 中的代碼呢?

    1.2 插件化優(yōu)點(diǎn)

    插件化讓 Apk 中的代碼(主要是指 Android 組件)能夠免安裝運(yùn)行,這樣能夠帶來很多收益:

    • 減少安裝Apk的體積、按需下載模塊

    • 動(dòng)態(tài)更新插件

    • 宿主和插件分開編譯,提升開發(fā)效率

    • 解決方法數(shù)超過65535的問題

    想象一下,你的應(yīng)用擁有 Native 應(yīng)用一般極高的性能,又能獲取諸如 Web 應(yīng)用一樣的收益。

    嗯,理想很美好不是嘛?

    1.3 與組件化的區(qū)別

    • 組件化:是將一個(gè)App分成多個(gè)模塊,每個(gè)模塊都是一個(gè)組件(module),開發(fā)過程中可以讓這些組件相互依賴或獨(dú)立編譯、調(diào)試部分組件,但是這些組件最終會(huì)合并成一個(gè)完整的Apk去發(fā)布到應(yīng)用市場(chǎng)。

    • 插件化:是將整個(gè)App拆分成很多模塊,每個(gè)模塊都是一個(gè)Apk(組件化的每個(gè)模塊是一個(gè)lib),最終打包的時(shí)候?qū)⑺拗鰽pk和插件Apk分開打包,只需發(fā)布宿主Apk到應(yīng)用市場(chǎng),插件Apk通過動(dòng)態(tài)按需下發(fā)到宿主Apk。

    二、插件化的技術(shù)難點(diǎn)

    想讓插件的Apk真正運(yùn)行起來,首先要先能找到插件Apk的存放位置,然后我們要能解析加載Apk里面的代碼。

    但是光能執(zhí)行Java代碼是沒有意義的,在Android系統(tǒng)中有四大組件是需要在系統(tǒng)中注冊(cè)的,具體來說是在 Android 系統(tǒng)的 ActivityManagerService (AMS) 和 PackageManagerService (PMS) 中注冊(cè)的,而四大組件的解析和啟動(dòng)都需要依賴 AMS 和 PMS,如何欺騙系統(tǒng),讓他承認(rèn)一個(gè)未安裝的 Apk 中的組件,如何讓宿主動(dòng)態(tài)加載執(zhí)行插件Apk中 Android 組件(即 ActivityService、BroadcastReceiver、ContentProvider、Fragment)等是插件化最大的難點(diǎn)。

    另外,應(yīng)用資源引用(特指 R 中引用的資源,如 layout、values 等)也是一大問題,想象一下你在宿主進(jìn)程中使用反射加載了一個(gè)插件 Apk,代碼中的 R 對(duì)應(yīng)的 id 卻無法引用到正確的資源,會(huì)產(chǎn)生什么后果。

    總結(jié)一下,其實(shí)做到插件化的要點(diǎn)就這幾個(gè):

    • 如何加載并執(zhí)行插件 Apk 中的代碼(ClassLoader Injection

    • 讓系統(tǒng)能調(diào)用插件 Apk 中的組件(Runtime Container

    • 正確識(shí)別插件 Apk 中的資源(Resource Injection

    當(dāng)然還有其他一些小問題,但可能不是所有場(chǎng)景下都會(huì)遇到,我們后面再單獨(dú)說。

    三、ClassLoader Injection

    ClassLoader 是插件化中必須要掌握的,因?yàn)槲覀冎?code>Android 應(yīng)用本身是基于魔改的 Java 虛擬機(jī)的,而由于插件是未安裝的 apk,系統(tǒng)不會(huì)處理其中的類,所以需要使用 ClassLoader 加載 Apk,然后反射里面的代碼。

    3.1 java 中的 ClassLoader

    • BootstrapClassLoader 負(fù)責(zé)加載 JVM 運(yùn)行時(shí)的核心類,比如 JAVA_HOME/lib/rt.jar 等等

    • ExtensionClassLoader 負(fù)責(zé)加載 JVM 的擴(kuò)展類,比如 JAVA_HOME/lib/ext 下面的 jar 包

    • AppClassLoader 負(fù)責(zé)加載 classpath 里的 jar 包和目錄

    3.2 android 中的 ClassLoader

    Android系統(tǒng)中ClassLoader是用來加載dex文件的,有包含 dex 的 apk 文件以及 jar 文件,dex 文件是一種對(duì)class文件優(yōu)化的產(chǎn)物,在Android中應(yīng)用打包時(shí)會(huì)把所有class文件進(jìn)行合并、優(yōu)化(把不同的class文件重復(fù)的東西只保留一份),然后生成一個(gè)最終的class.dex文件

    • PathClassLoader 用來加載系統(tǒng)類和應(yīng)用程序類,可以加載已經(jīng)安裝的 apk 目錄下的 dex 文件

    public class PathClassLoader extends BaseDexClassLoader {
        public PathClassLoader(String dexPath, ClassLoader parent) {
            super(dexPath, null, null, parent);
        }
    
        public PathClassLoader(String dexPath, String libraryPath,
                ClassLoader parent) {
            super(dexPath, null, libraryPath, parent);
        }
    }
    • DexClassLoader 用來加載 dex 文件,可以從存儲(chǔ)空間加載 dex 文件。

    public class DexClassLoader extends BaseDexClassLoader {
        public DexClassLoader(String dexPath, String optimizedDirectory,
                String libraryPath, ClassLoader parent) {
            super(dexPath, new File(optimizedDirectory), libraryPath, parent);
        }
    }

    我們?cè)诓寮幸话闶褂玫氖?DexClassLoader

    3.3 雙親委派機(jī)制

    每一個(gè) ClassLoader 中都有一個(gè) parent 對(duì)象,代表的是父類加載器,在加載一個(gè)類的時(shí)候,會(huì)先使用父類加載器去加載,如果在父類加載器中沒有找到,自己再進(jìn)行加載,如果 parent 為空,那么就用系統(tǒng)類加載器來加載。通過這樣的機(jī)制可以保證系統(tǒng)類都是由系統(tǒng)類加載器加載的。 下面是 ClassLoader loadClass 方法的具體實(shí)現(xiàn)。

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
                // First, check if the class has already been loaded
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    try {
                        if (parent != null) {
                            // 先從父類加載器中進(jìn)行加載
                            c = parent.loadClass(name, false);
                        } else {
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                        // ClassNotFoundException thrown if class not found
                        // from the non-null parent class loader
                    }
    
                    if (c == null) {
                        // 沒有找到,再自己加載
                        c = findClass(name);
                    }
                }
                return c;
        }

    3.4 如何加載插件中的類

    要加載插件中的類,我們首先要?jiǎng)?chuàng)建一個(gè) DexClassLoader先看下 DexClassLoader 的構(gòu)造函數(shù)需要哪些參數(shù)。

    public class DexClassLoader extends BaseDexClassLoader {
        public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
            // ...
        }
    }

    構(gòu)造函數(shù)需要四個(gè)參數(shù): dexPath 是需要加載的 dex / apk / jar 文件路徑 optimizedDirectory 是 dex 優(yōu)化后存放的位置,在 ART 上,會(huì)執(zhí)行 oat 對(duì) dex 進(jìn)行優(yōu)化,生成機(jī)器碼,這里就是存放優(yōu)化后的 odex 文件的位置 librarySearchPath 是 native 依賴的位置 parent 就是父類加載器,默認(rèn)會(huì)先從 parent 加載對(duì)應(yīng)的類

    創(chuàng)建出 DexClassLaoder 實(shí)例以后,只要調(diào)用其 loadClass(className) 方法就可以加載插件中的類了。具體的實(shí)現(xiàn)在下面:

    // 從 assets 中拿出插件 apk 放到內(nèi)部存儲(chǔ)空間
        private fun extractPlugin() {
            var inputStream = assets.open("plugin.apk")
            File(filesDir.absolutePath, "plugin.apk").writeBytes(inputStream.readBytes())
        }
    
        private fun init() {
            extractPlugin()
            pluginPath = File(filesDir.absolutePath, "plugin.apk").absolutePath
            nativeLibDir = File(filesDir, "pluginlib").absolutePath
            dexOutPath = File(filesDir, "dexout").absolutePath
            // 生成 DexClassLoader 用來加載插件類
            pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
        }

    Android插件化是怎樣的

    3.5 執(zhí)行插件類的方法

    通過反射來執(zhí)行類的方法

    val loadClass = pluginClassLoader.loadClass(activityName)
    loadClass.getMethod("test",null).invoke(loadClass)

    我們稱這個(gè)過程叫做 ClassLoader 注入。完成注入后,所有來自宿主的類使用宿主的 ClassLoader 進(jìn)行加載,所有來自插件 Apk 的類使用插件 ClassLoader 進(jìn)行加載,而由于 ClassLoader 的雙親委派機(jī)制,實(shí)際上系統(tǒng)類會(huì)不受 ClassLoader 的類隔離機(jī)制所影響,這樣宿主 Apk 就可以在宿主進(jìn)程中使用來自于插件的組件類了。

    四、Runtime Container

    我們之前說到 Activity 插件化最大的難點(diǎn)是如何欺騙系統(tǒng),讓他承認(rèn)一個(gè)未安裝的 Apk 中的組件。 因?yàn)椴寮莿?dòng)態(tài)加載的,所以插件的四大組件不可能注冊(cè)到宿主的 Manifest 文件中,而沒有在 Manifest 中注冊(cè)的四大組件是不能和系統(tǒng)直接進(jìn)行交互的。 如果直接把插件的 Activity 注冊(cè)到宿主 Manifest 里就失去了插件化的動(dòng)態(tài)特性,因?yàn)槊看尾寮行略?Activity 都要修改宿主 Manifest 并且重新打包,那就和直接寫在宿主中沒什么區(qū)別了。

    4.1 為什么沒有注冊(cè)的 Activity 不能和系統(tǒng)交互

    這里的不能直接交互的含義有兩個(gè)

    • 系統(tǒng)會(huì)檢測(cè) Activity 是否注冊(cè) 如果我們啟動(dòng)一個(gè)沒有在 Manifest 中注冊(cè)的 Activity,會(huì)發(fā)現(xiàn)報(bào)如下 error:

    android.content.ActivityNotFoundException: Unable to find explicit activity class {com.zyg.commontec/com.zyg.plugin.PluginActivity}; have you declared this activity in your AndroidManifest.xml?

    這個(gè) log 在 Instrumentation checkStartActivityResult 方法中可以看到:

    public class Instrumentation {
        public static void checkStartActivityResult(int res, Object intent) {
            if (!ActivityManager.isStartResultFatalError(res)) {
                return;
            }
    
            switch (res) {
                case ActivityManager.START_INTENT_NOT_RESOLVED:
                case ActivityManager.START_CLASS_NOT_FOUND:
                    if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
                        throw new ActivityNotFoundException(
                                "Unable to find explicit activity class "
                                + ((Intent)intent).getComponent().toShortString()
                                + "; have you declared this activity in your AndroidManifest.xml?");
                    throw new ActivityNotFoundException(
                            "No Activity found to handle " + intent);
                    ...
            }
        }
    }
    • Activity 的生命周期無法被調(diào)用,其實(shí)一個(gè) Activity 主要的工作,都是在其生命周期方法中調(diào)用了,既然上一步系統(tǒng)檢測(cè)了 Manifest 注冊(cè)文件,啟動(dòng) Activity 被拒絕,那么其生命周期方法也肯定不會(huì)被調(diào)用了。從而插件 Activity 也就不能正常運(yùn)行了。

    4.2 運(yùn)行時(shí)容器技術(shù)

    由于Android中的組件(ActivityService,BroadcastReceiverContentProvider)是由系統(tǒng)創(chuàng)建的,并且由系統(tǒng)管理生命周期。 僅僅構(gòu)造出這些類的實(shí)例是沒用的,還需要管理組件的生命周期。其中以Activity最為復(fù)雜,不同框架采用的方法也不盡相同。插件化如何支持組件生命周期的管理。 大致分為兩種方式:

    • 運(yùn)行時(shí)容器技術(shù)(ProxyActivity代理)

    • 預(yù)埋StubActivity,hook系統(tǒng)啟動(dòng)Activity的過程

    我們的解決方案很簡(jiǎn)單,即運(yùn)行時(shí)容器技術(shù),簡(jiǎn)單來說就是在宿主 Apk 中預(yù)埋一些空的 Android 組件,以 Activity 為例,我預(yù)置一個(gè) ContainerActivity extends Activity 在宿主中,并且在 AndroidManifest.xml 中注冊(cè)它。

    它要做的事情很簡(jiǎn)單,就是幫助我們作為插件 Activity 的容器,它從 Intent 接受幾個(gè)參數(shù),分別是插件的不同信息,如:

    • pluginName

    • pluginApkPath

    • pluginActivityName

    等,其實(shí)最重要的就是 pluginApkPath pluginActivityName,當(dāng) ContainerActivity 啟動(dòng)時(shí),我們就加載插件的 ClassLoaderResource,并反射 pluginActivityName 對(duì)應(yīng)的 Activity 類。當(dāng)完成加載后,ContainerActivity 要做兩件事:

    • 轉(zhuǎn)發(fā)所有來自系統(tǒng)的生命周期回調(diào)至插件 Activity

    • 接受 Activity 方法的系統(tǒng)調(diào)用,并轉(zhuǎn)發(fā)回系統(tǒng)

    我們可以通過復(fù)寫 ContainerActivity 的生命周期方法來完成第一步,而第二步我們需要定義一個(gè) PluginActivity然后在編寫插件 Apk 中的 Activity 組件時(shí),不再讓其集成 android.app.Activity,而是集成自我們的 PluginActivity

    public class ContainerActivity extends Activity {
        private PluginActivity pluginActivity;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            String pluginActivityName = getIntent().getString("pluginActivityName", "");
            pluginActivity = PluginLoader.loadActivity(pluginActivityName, this);
            if (pluginActivity == null) {
                super.onCreate(savedInstanceState);
                return;
            }
    
            pluginActivity.onCreate();
        }
    
        @Override
        protected void onResume() {
            if (pluginActivity == null) {
                super.onResume();
                return;
            }
            pluginActivity.onResume();
        }
    
        @Override
        protected void onPause() {
            if (pluginActivity == null) {
                super.onPause();
                return;
            }
            pluginActivity.onPause();
        }
    
        // ...
    }
    public class PluginActivity {
        private ContainerActivity containerActivity;
    
        public PluginActivity(ContainerActivity containerActivity) {
            this.containerActivity = containerActivity;
        }
    
        @Override
        public <T extends View> T findViewById(int id) {
            return containerActivity.findViewById(id);
        }
        // ...
    }
    
    // 插件 `Apk` 中真正寫的組件
    public class TestActivity extends PluginActivity {
        // ......
    }

    是不是感覺有點(diǎn)看懂了,雖然真正搞的時(shí)候還有很多小坑,但大概原理就是這么簡(jiǎn)單,啟動(dòng)插件組件需要依賴容器,容器負(fù)責(zé)加載插件組件并且完成雙向轉(zhuǎn)發(fā),轉(zhuǎn)發(fā)來自系統(tǒng)的生命周期回調(diào)至插件組件,同時(shí)轉(zhuǎn)發(fā)來自插件組件的系統(tǒng)調(diào)用至系統(tǒng)。

    4.3 字節(jié)碼替換

    該方式雖然能夠很好的實(shí)現(xiàn)啟動(dòng)插件Activity的目的,但是由于開發(fā)式侵入性很強(qiáng),插件中的Activity必須繼承PluginActivity,如果想把之前的模塊改造成插件需要很多額外的工作。

    class TestActivity extends Activity {}
    ->
    class TestActivity extends PluginActivity {}

    有沒有什么辦法能讓插件組件的編寫與原來沒有任何差別呢?

    Shadow 的做法是字節(jié)碼替換插件,這是一個(gè)非常棒的想法,簡(jiǎn)單來說,Android 提供了一些 Gradle 插件開發(fā)套件,其中有一項(xiàng)功能叫 Transform Api,它可以介入項(xiàng)目的構(gòu)建過程,在字節(jié)碼生成后、dex 文件生成前,對(duì)代碼進(jìn)行某些變換,具體怎么做的不說了,可以自己看文檔。

    實(shí)現(xiàn)的功能嘛,就是用戶配置 Gradle 插件后,正常開發(fā),依然編寫:

    class TestActivity extends Activity {}

    然后完成編譯后,最后的字節(jié)碼中,顯示的卻是:

    class TestActivity extends PluginActivity {}

    到這里基本的框架就差不多結(jié)束了。

    五、Resource Injection

    最后要說的是資源注入,其實(shí)這一點(diǎn)相當(dāng)重要,Android 應(yīng)用的開發(fā)其實(shí)崇尚的是邏輯與資源分離的理念,所有資源(layout、values 等)都會(huì)被打包到 Apk 中,然后生成一個(gè)對(duì)應(yīng)的 R 類,其中包含對(duì)所有資源的引用 id。

    資源的注入并不容易,好在 Android 系統(tǒng)給我們留了一條后路,最重要的是這兩個(gè)接口:

    PackageManager#getPackageArchiveInfo:根據(jù) Apk 路徑解析一個(gè)未安裝的 Apk 的 PackageInfo
    PackageManager#getResourcesForApplication:根據(jù) ApplicationInfo 創(chuàng)建一個(gè) Resources 實(shí)例

    我們要做的就是在上面 ContainerActivity#onCreate 中加載插件 Apk 的時(shí)候,用這兩個(gè)方法創(chuàng)建出來一份插件資源實(shí)例。具體來說就是先用 PackageManager#getPackageArchiveInfo 拿到插件 Apk 的 PackageInfo,有了 PacakgeInfo 之后我們就可以自己組裝一份 ApplicationInfo,然后通過 PackageManager#getResourcesForApplication 來創(chuàng)建資源實(shí)例,大概代碼像這樣:

    PackageManager packageManager = getPackageManager();
    PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(
        pluginApkPath,
        PackageManager.GET_ACTIVITIES
        | PackageManager.GET_META_DATA
        | PackageManager.GET_SERVICES
        | PackageManager.GET_PROVIDERS
        | PackageManager.GET_SIGNATURES
    );
    packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath;
    packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath;
    
    Resources injectResources = null;
    try {
        injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);
    } catch (PackageManager.NameNotFoundException e) {
        // ...
    }

    拿到資源實(shí)例后,我們需要將宿主的資源和插件資源 Merge 一下,編寫一個(gè)新的 Resources 類,用這樣的方式完成自動(dòng)代理:

    public class PluginResources extends Resources {
        private Resources hostResources;
        private Resources injectResources;
    
        public PluginResources(Resources hostResources, Resources injectResources) {
            super(injectResources.getAssets(), injectResources.getDisplayMetrics(), injectResources.getConfiguration());
            this.hostResources = hostResources;
            this.injectResources = injectResources;
        }
    
        @Override
        public String getString(int id, Object... formatArgs) throws NotFoundException {
            try {
                return injectResources.getString(id, formatArgs);
            } catch (NotFoundException e) {
                return hostResources.getString(id, formatArgs);
            }
        }
    
        // ...
    }

    然后我們?cè)?ContainerActivity 完成插件組件加載后,創(chuàng)建一份 Merge 資源,再復(fù)寫 ContainerActivity#getResources將獲取到的資源替換掉:

    public class ContainerActivity extends Activity {
        private Resources pluginResources;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            // ...
            pluginResources = new PluginResources(super.getResources(), PluginLoader.getResources(pluginApkPath));
            // ...
        }
    
        @Override
        public Resources getResources() {
            if (pluginActivity == null) {
                return super.getResources();
            }
            return pluginResources;
        }
    }

    這樣就完成了資源的注入。

    上述內(nèi)容就是Android插件化是怎樣的,你們學(xué)到知識(shí)或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識(shí)儲(chǔ)備,歡迎關(guān)注億速云行業(yè)資訊頻道。

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI