最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

淺談Android插件化

 更新時間:2021年09月24日 10:21:02   作者:QiShare  
插件化技術(shù)最初源于免安裝運(yùn)行 Apk的想法,這個免安裝的 Apk 就可以理解為插件,而支持插件的 app 我們一般叫 宿主,下面就跟著小編一起學(xué)習(xí)Android插件化吧,希望能幫助到你

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

1.1 插件化起源

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

想必大家都知道,在 Android 系統(tǒng)中,應(yīng)用是以 Apk 的形式存在的,應(yīng)用都需要安裝才能使用。但實(shí)際上 Android 系統(tǒng)安裝應(yīng)用的方式相當(dāng)簡單,其實(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)用

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

  • 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í)行對應(yīng)的組件而已。

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

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

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

  • 減少安裝Apk的體積、按需下載模塊
  • 動態(tài)更新插件
  • 宿主和插件分開編譯,提升開發(fā)效率
  • 解決方法數(shù)超過65535的問題

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

嗯,理想很美好不是嘛?

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

  • 組件化:是將一個App分成多個模塊,每個模塊都是一個組件(module),開發(fā)過程中可以讓這些組件相互依賴或獨(dú)立編譯、調(diào)試部分組件,但是這些組件最終會合并成一個完整的Apk去發(fā)布到應(yīng)用市場。
  • 插件化:是將整個App拆分成很多模塊,每個模塊都是一個Apk(組件化的每個模塊是一個lib),最終打包的時候?qū)⑺拗鰽pk和插件Apk分開打包,只需發(fā)布宿主Apk到應(yīng)用市場,插件Apk通過動態(tài)按需下發(fā)到宿主Apk。

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

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

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

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

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

  • 如何加載并執(zhí)行插件 Apk 中的代碼(ClassLoader Injection
  • 讓系統(tǒng)能調(diào)用插件 Apk 中的組件(Runtime Container
  • 正確識別插件 Apk 中的資源(Resource Injection

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

三、ClassLoader Injection

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

3.1 java 中的 ClassLoader

  • BootstrapClassLoader 負(fù)責(zé)加載 JVM 運(yùn)行時的核心類,比如 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 文件是一種對class文件優(yōu)化的產(chǎn)物,在Android中應(yīng)用打包時會把所有class文件進(jìn)行合并、優(yōu)化(把不同的class文件重復(fù)的東西只保留一份),然后生成一個最終的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 文件,可以從存儲空間加載 dex 文件。
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

我們在插件化中一般使用的是 DexClassLoader。

3.3 雙親委派機(jī)制

每一個 ClassLoader 中都有一個 parent 對象,代表的是父類加載器,在加載一個類的時候,會先使用父類加載器去加載,如果在父類加載器中沒有找到,自己再進(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 如何加載插件中的類

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

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

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

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

    // 從 assets 中拿出插件 apk 放到內(nèi)部存儲空間
    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)
    }

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

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

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

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

四、Runtime Container

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

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

這里的不能直接交互的含義有兩個

  • 系統(tǒng)會檢測 Activity 是否注冊 如果我們啟動一個沒有在 Manifest 中注冊的 Activity,會發(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?

這個 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í)一個 Activity 主要的工作,都是在其生命周期方法中調(diào)用了,既然上一步系統(tǒng)檢測了 Manifest 注冊文件,啟動 Activity 被拒絕,那么其生命周期方法也肯定不會被調(diào)用了。從而插件 Activity 也就不能正常運(yùn)行了。

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

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

  • 運(yùn)行時容器技術(shù)(ProxyActivity代理)
  • 預(yù)埋StubActivity,hook系統(tǒng)啟動Activity的過程

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

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

  • pluginName
  • pluginApkPath
  • pluginActivityName

等,其實(shí)最重要的就是 pluginApkPath pluginActivityName,當(dāng) ContainerActivity 啟動時,我們就加載插件的 ClassLoader、Resource,并反射 pluginActivityName 對應(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 的生命周期方法來完成第一步,而第二步我們需要定義一個 PluginActivity然后在編寫插件 Apk 中的 Activity 組件時,不再讓其集成 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)看懂了,雖然真正搞的時候還有很多小坑,但大概原理就是這么簡單,啟動插件組件需要依賴容器,容器負(fù)責(zé)加載插件組件并且完成雙向轉(zhuǎn)發(fā),轉(zhuǎn)發(fā)來自系統(tǒng)的生命周期回調(diào)至插件組件,同時轉(zhuǎn)發(fā)來自插件組件的系統(tǒng)調(diào)用至系統(tǒng)。

4.3 字節(jié)碼替換

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

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

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

Shadow 的做法是字節(jié)碼替換插件,這是一個非常棒的想法,簡單來說,Android 提供了一些 Gradle 插件開發(fā)套件,其中有一項(xiàng)功能叫 Transform Api,它可以介入項(xiàng)目的構(gòu)建過程,在字節(jié)碼生成后、dex 文件生成前,對代碼進(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 等)都會被打包到 Apk 中,然后生成一個對應(yīng)的 R 類,其中包含對所有資源的引用 id。

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

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

我們要做的就是在上面 ContainerActivity#onCreate 中加載插件 Apk 的時候,用這兩個方法創(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 一下,編寫一個新的 Resources 類,用這樣的方式完成自動代理:

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

    // ...
}

然后我們在 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;
    }
}

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

到此這篇關(guān)于淺談Android插件化的文章就介紹到這了,更多相關(guān)Android插件化內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論

慈溪市| 南通市| 巴彦淖尔市| 北京市| 平邑县| 禄劝| 剑川县| 涞水县| 阿克苏市| 鱼台县| 扎赉特旗| 桂平市| 哈尔滨市| 中江县| 乳山市| 台安县| 绥化市| 西乌珠穆沁旗| 龙山县| 佛教| 顺昌县| 云阳县| 防城港市| 陇南市| 和平县| 增城市| 怀宁县| 福鼎市| 威信县| 哈巴河县| 田林县| 孝昌县| 亚东县| 承德市| 桐柏县| 新巴尔虎左旗| 稻城县| 墨竹工卡县| 会宁县| 丹阳市| 界首市|