日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > Android >内容正文

Android

【Android 修炼手册】常用技术篇 -- Android 插件化解析

發布時間:2023/12/15 Android 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【Android 修炼手册】常用技术篇 -- Android 插件化解析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

預備知識

  • 了解 android 基本開發
  • 了解 android 四大組件基本原理
  • 了解 ClassLoader 相關知識
  • 看完本文可以達到什么程度

  • 了解插件化常見的實現原理
  • 閱讀前準備工作

  • clone CommonTec 項目
  • 文章概覽

    ?

    ?

    一、插件化框架歷史

    整個插件化框架歷史部分參考了包建強在 2016GMTC 全球開發大會上的演講
    2012 年 AndroidDynamicLoader 給予 Fragment 實現了插件化框架,可以動態加載插件中的 Fragment 實現頁面的切換。
    2013 年 23Code 提供了一個殼,可以在殼里動態化下載插件然后運行。
    2013 年 阿里技術沙龍上,伯奎做了 Atlas 插件化框架的分享,說明那時候阿里已經在做插件化的運用和開發了。
    2014 年 任玉剛開源了 dynamic-load-apk,通過代理分發的方式實現了動態化,如果看過 Android 開發藝術探索這本書,應該會對這個方式有了解。
    2015 年 張勇 發布了 DroidPlugin,使用 hook 系統方式實現插件化。
    2015 年 攜程發布 DynamicApk
    2015 - 2016 之間(這塊時間不太確定),Lody 發布了 VirtualApp,可以直接運行未安裝的 apk,基本上還是使用 hook 系統的方式實現的,不過里面的實現要精致很多,實現了自己的一套 AMS 來管理插件 Activity 等等。
    2017 年阿里推出 Atlas
    2017 年 360 推出 RePlugin
    2017 年滴滴推出 VirtualApk
    2019 年騰訊推出了 Shadow,號稱是零反射,并且框架自身也可實現動態化,看了代碼以后發現,其實本質上還是使用了代理分發生命周期實現四大組件動態化,然后抽象接口來實現框架的動態化。后面有機會可以對其做一下分析。

    這基本上就是插件化框架的歷史,從 2012 至今,可以說插件化技術基本成型了,主要就是代理和 hook 系統兩種方式(這里沒有統計熱修復的發展,熱修復其實和插件化還是有些想通的地方,后面的文章會對熱修復進行介紹)。如果看未來的話,斗膽預測,插件化技術的原理,應該不會有太大的變動了。

    二、名詞解釋

    在插件化中有一些專有名詞,如果是第一次接觸可能不太了解,這里解釋一下。
    宿主
    負責加載插件的 apk,一般來說就是已經安裝的應用本身。
    StubActivity
    宿主中的占位 Activity,注冊在宿主 Manifest 文件中,負責加載插件 Activity。
    PluginActivity
    插件 Activity,在插件 apk 中,沒有注冊在 Manifest 文件中,需要 StubActivity 來加載。

    三、使用 gradle 簡化插件開發流程

    在學習和開發插件化的時候,我們需要動態去加載插件 apk,所以開發過程中一般需要有兩個 apk,一個是宿主 apk,一個是插件 apk,對應的就需要有宿主項目和插件項目。
    在 CommonTec 這里創建了 app 作為宿主項目,plugin 為插件項目。為了方便,我們直接把生成的插件 apk 放到宿主 apk 中的 assets 中,apk 啟動時直接放到內部存儲空間中方便加載。
    這樣的項目結構,我們調試問題時的流程就是下面這樣:
    修改插件項目 -> 編譯生成插件 apk -> 拷貝插件 apk 到宿主 assets -> 修改宿主項目 -> 編譯生成宿主 apk -> 安裝宿主 apk -> 驗證問題
    如果每次我們修改一個很小的問題,都經歷這么長的流程,那么耐心很快就耗盡了。最好是可以直接編譯宿主 apk 的時候自動打包插件 apk 并拷貝到宿主 assets 目錄下,這樣我們不管修改什么,都直接編譯宿主項目就好了。如何實現呢?還記得我們之前講解過的 gradle 系列么?現在就是學以致用的時候了。
    首先在 plugin 項目的 build.gradle 添加下面的代碼:

    project.afterEvaluate {project.tasks.each {if (it.name == "assembleDebug") {it.doLast {copy {from new File(project.getBuildDir(), 'outputs/apk/debug/plugin-debug.apk').absolutePathinto new File(project.getRootProject().getProjectDir(), 'app/src/main/assets')rename 'plugin-debug.apk', 'plugin.apk'}}}} } 復制代碼

    這段代碼是在 afterEvaluate 的時候,遍歷項目的 task,找到打包 task 也就是 assembleDebug,然后在打包之后,把生成的 apk 拷貝到宿主項目的 assets 目錄下,并且重命名為 plugin.apk。 然后在 app 項目的 build.gradle 添加下面的代碼:

    project.afterEvaluate {project.tasks.each {if (it.name == 'mergeDebugAssets') {it.dependsOn ':plugin:assembleDebug'}} } 復制代碼

    找到宿主打包的 mergeDebugAssets 任務,依賴插件項目的打包,這樣每次編譯宿主項目的時候,會先編譯插件項目,然后拷貝插件 apk 到宿主 apk 的 assets 目錄下,以后每次修改,只要編譯宿主項目就可以了。

    四、ClassLoader

    ClassLoader 是插件化中必須要掌握的,因為插件是未安裝的 apk,系統不會處理其中的類,所以需要我們自己來處理。

    4.1 java 中的 ClassLoader

    BootstrapClassLoader
    負責加載 JVM 運行時的核心類,比如 JAVA_HOME/lib/rt.jar 等等

    ExtensionClassLoader
    負責加載 JVM 的擴展類,比如 JAVA_HOME/lib/ext 下面的 jar 包

    AppClassLoader
    負責加載 classpath 里的 jar 包和目錄

    4.2 android 中的 ClassLoader

    在這里,我們統稱 dex 文件,包含 dex 的 apk 文件以及 jar 文件為 dex 文件 PathClassLoader 用來加載系統類和應用程序類,可以加載已經安裝的 apk 目錄下的 dex 文件

    DexClassLoader 用來加載 dex 文件,可以從存儲空間加載 dex 文件。

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

    4.3 雙親委派機制

    每一個 ClassLoader 中都有一個 parent 對象,代表的是父類加載器,在加載一個類的時候,會先使用父類加載器去加載,如果在父類加載器中沒有找到,自己再進行加載,如果 parent 為空,那么就用系統類加載器來加載。通過這樣的機制可以保證系統類都是由系統類加載器加載的。 下面是 ClassLoader 的 loadClass 方法的具體實現。

    protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {try {if (parent != null) {// 先從父類加載器中進行加載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;} 復制代碼

    4.4 如何加載插件中的類

    要加載插件中的類,我們首先要創建一個 DexClassLoader,先看下 DexClassLoader 的構造函數需要那些參數。

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

    構造函數需要四個參數:
    dexPath 是需要加載的 dex / apk / jar 文件路徑
    optimizedDirectory 是 dex 優化后存放的位置,在 ART 上,會執行 oat 對 dex 進行優化,生成機器碼,這里就是存放優化后的 odex 文件的位置
    librarySearchPath 是 native 依賴的位置
    parent 就是父類加載器,默認會先從 parent 加載對應的類

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

    // 從 assets 中拿出插件 apk 放到內部存儲空間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").absolutePathnativeLibDir = File(filesDir, "pluginlib").absolutePathdexOutPath = File(filesDir, "dexout").absolutePath// 生成 DexClassLoader 用來加載插件類pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)} 復制代碼

    五、插件化需要解決的難點

    插件化,就是從插件中加載我們想要的類并運行,如果這個類是一個普通類,那么使用上面說到的 DexClassLoader 就可以直接加載了,如果這個類是特殊的類,比如說 Activity 等四大組件,那么就需要一些特殊的處理,因為四大組件是需要和系統進行交互的。插件化中,四大組件需要解決的難點如下:

    • Activity
  • 生命周期如何調用
  • 如何使用插件中的資源
    • Service
  • 生命周期如何調用
    • BroadcastReceiver
  • 靜態廣播和動態廣播的注冊
    • ContentProvider
  • 如何注冊插件 Provider 到系統
  • 六、Activity 的插件化實現

    6.1 難點分析

    我們之前說到 Activity 插件化的難點,我們先來理順一下為什么會有這兩個問題。
    因為插件是動態加載的,所以插件的四大組件不可能注冊到宿主的 Manifest 文件中,而沒有在 Manifest 中注冊的四大組件是不能和系統直接進行交互的。
    可能有些同學會問,那為什么不能直接把插件的 Activity 注冊到宿主 Manifest 里呢?這樣是可以,不過就失去了插件化的動態特性,如果每次插件中新增 Activity 都要修改宿主 Manifest 并且重新打包,那就和直接寫在宿主中沒什么區別了。
    我們再來說一下為什么沒有注冊的 Activity 不能和系統交互
    這里的不能直接交互的含義有兩個

  • 系統會檢測 Activity 是否注冊 如果我們啟動一個沒有在 Manifest 中注冊的 Activity,會發現報如下 error:
  • android.content.ActivityNotFoundException: Unable to find explicit activity class {com.zy.commontec/com.zy.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 的生命周期無法被調用 其實一個 Activity 主要的工作,都是在其生命周期方法中調用了,既然上一步系統檢測了 Manifest 注冊文件,啟動 Activity 被拒絕,那么其生命周期方法也肯定不會被調用了。從而插件 Activity 也就不能正常運行了。
  • 其實上面兩個問題,最終都指向同一個難點,那就是插件中的 Activity 的生命周期如何被調用。 解決問題之前我們先看一下正常系統是如何啟動一個 Activity 的。
    這里對 Activity 的啟動流程進行一些簡單的介紹,具體的流程代碼就不分析了,因為分析的話大概又能寫一篇文章了,而且其實關于 Activity 的啟動過程也有不少文章有分析了。這里放一張簡圖說明一下:

    ?

    整個調用路徑如下:

    Activity.startActivity -> Instrumentation.execStartActivity -> Binder -> AMS.startActivity -> ActivityStarter.startActivityMayWait -> startActivityLocked -> startActivityUnChecked -> ActivityStackSupervisor.resumeFocusedStackTopActivityLocked -> ActivityStatk.resumeTopAcitivityUncheckLocked -> resumeTopActivityInnerLocked -> ActivityStackSupervisor.startSpecificActivityLocked -> realStartActivityLocked -> Binder -> ApplictionThread.scheduleLauchActivity -> H -> ActivityThread.scheduleLauchActivity -> handleLaunchActivity -> performLaunchActivity -> Instrumentation.newActivity 創建 Activity -> callActivityOnCreate 一系列生命周期 復制代碼

    其實我們可以把 AMS 理解為一個公司的背后大 Boss,Activity 相當于小職員,沒有權限直接和大 Boss 說話,想做什么事情都必須經過秘書向上匯報,然后秘書再把大 Boss AMS 的命令傳達下來。而且大 Boss 那里有所有職員的名單,如果想要混入非法職員時不可能的。而我們想讓沒有在大 Boss 那里注冊的編外人員執行任務,只有兩種方法,一種是正式職員領取任務,再分發給編外人員,另一種就是欺騙 Boss,讓 Boss 以為這個職員是已經注冊的。

    對應到實際的解決方法就是:

  • 我們手動去調用插件 Activity 的生命周期
  • 欺騙系統,讓系統以為 Activity 是注冊在 Manifest 中的
  • 說完生命周期的問題,再來看一下資源的問題
    在 Activity 中,基本上都會展示界面,而展示界面基本上都要用到資源。
    在 Activity 中,有一個 mResources 變量,是 Resources 類型。這個變量可以理解為代表了整個 apk 的資源。

    ?

    在宿主中調用的 Activity,mResources 自然代表了宿主的資源,所以需要我們對插件的資源進行特殊的處理。
    我們先看一下如何生成代表插件資源的 Resources 類。
    首先要生成一個 AssetManager 實例,然后通過其 addAssetPath 方法添加插件的路徑,這樣 AssetManager 中就包含了插件的資源。然后通過 Resources 構造函數生成插件資源。具體代碼如下:

    private fun handleResources() {try {// 首先通過反射生成 AssetManager 實例pluginAssetManager = AssetManager::class.java.newInstance()// 然后調用其 addAssetPath 把插件的路徑添加進去。val addAssetPathMethod = pluginAssetManager?.javaClass?.getMethod("addAssetPath", String::class.java)addAssetPathMethod?.invoke(pluginAssetManager, pluginPath)} catch (e: Exception) {}// 調用 Resources 構造函數生成實例pluginResources = Resources(pluginAssetManager, super.getResources().displayMetrics, super.getResources().configuration) } 復制代碼

    前期準備的知識點差不多介紹完了,我們接著就看看具體的實現方法。

    6.2 手動調用 Activity 生命周期

    手動調用生命周期原理如下圖:

    ?

    我們手動調用插件 Activity 生命周期時,需要在正確的時機去調用,如何在正確的時機調用呢?那就是啟動一個真正的 Activity,我們俗稱占坑 Activity(StubActivity),然后在 StubActivity 的生命周期里調用插件 Activity 對應的生命周期,這樣就間接的啟動了插件 Activity。
    在 StubActivity 中調用 插件 Activity 生命周期的方法有兩種,一種是直接反射其生命周期方法,粗暴簡單,唯一的缺點就是反射的效率問題。另外一種方式就是生成一個接口,接口里對應的是生命周期方法,讓插件 Activity 實現這個接口,在 StubActivity 里就能直接調用接口方法了,從而避免了反射的效率低下問題。

    具體的代碼實現在CommonTec項目里可以找到,這里貼一下主要的實現(這里的實現和 CommonTec 里的可能會有些區別,CommonTec 里有些代碼做了一些封裝,這里主要做原理的解釋)。

    6.2.1 通過反射調用 Activity 生命周期

    具體的實現見 反射調用生命周期,下面列出了重點代碼。

    class StubReflectActivity : Activity() {protected var activityClassLoader: ClassLoader? = nullprotected var activityName = ""private var pluginPath = ""private var nativeLibDir: String? = nullprivate var dexOutPath: String? = nulloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)nativeLibDir = File(filesDir, "pluginlib").absolutePathdexOutPath = File(filesDir, "dexout").absolutePathpluginPath = intent.getStringExtra("pluginPath")activityName = intent.getStringExtra("activityName")// 創建插件 ClassLoaderactivityClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)}// 以 onCreate 方法為例,其他 onStart 等生命周期方法類似fun onCreate(savedInstanceState: Bundle?) {// 獲取插件 Activity 的 onCreate 方法并調用getMethod("onCreate", Bundle::class.java)?.invoke(activity, savedInstanceState)}fun getMethod(methodName: String, vararg params: Class<*>): Method? {return activityClassLoader?.loadClass(activity)?.getMethod(methodName, *params)} } 復制代碼

    6.2.2 通過接口調用 Activity 生命周期

    具體的實現見 接口調用生命周期,下面列出了重點代碼。 通過接口調用 Activity 生命周期的前提是要定義一個接口 IPluginActivity

    interface IPluginActivity {fun attach(proxyActivity: Activity)fun onCreate(savedInstanceState: Bundle?)fun onStart()fun onResume()fun onPause()fun onStop()fun onDestroy() } 復制代碼

    然后在插件 Activity 中實現這個接口

    open class BasePluginActivity : Activity(), IPluginActivity {var proxyActivity: Activity? = nulloverride fun attach(proxyActivity: Activity) {this.proxyActivity = proxyActivity}override fun onCreate(savedInstanceState: Bundle?) {if (proxyActivity == null) {super.onCreate(savedInstanceState)}}// ... } 復制代碼

    在 StubActivity 通過接口調用插件 Activity 生命周期

    class StubInterfaceActivity : StubBaseActivity() {protected var activityClassLoader: ClassLoader? = nullprotected var activityName = ""private var pluginPath = ""private var activity: IPluginActivity? = nulloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)nativeLibDir = File(filesDir, "pluginlib").absolutePathdexOutPath = File(filesDir, "dexout").absolutePathpluginPath = intent.getStringExtra("pluginPath")activityName = intent.getStringExtra("activityName")// 生成插件 ClassLoaderactivityClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)// 加載插件 Activity 類并轉化成 IPluginActivity 接口activity = activityClassLoader?.loadClass(activityName)?.newInstance() as IPluginActivity?activity?.attach(this)// 通過接口直接調用對應的生命周期方法activity?.onCreate(savedInstanceState)} } 復制代碼

    6.2.3 資源處理方式

    ?

    ?

    由于手動調用生命周期的方式,會重寫大量的 Activity 生命周期方法,所以我們只要重寫 getResources 方法,返回插件的資源實例就可以了。下面是具體代碼。

    open class StubBaseActivity : Activity() {protected var activityClassLoader: ClassLoader? = nullprotected var activityName = ""private var pluginPath = ""private var pluginAssetManager: AssetManager? = nullprivate var pluginResources: Resources? = nullprivate var pluginTheme: Resources.Theme? = nullprivate var nativeLibDir: String? = nullprivate var dexOutPath: String? = nulloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)nativeLibDir = File(filesDir, "pluginlib").absolutePathdexOutPath = File(filesDir, "dexout").absolutePathpluginPath = intent.getStringExtra("pluginPath")activityName = intent.getStringExtra("activityName")activityClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)handleResources()}override fun getResources(): Resources? {// 這里返回插件的資源,這樣插件 Activity 中使用的就是插件資源了return pluginResources ?: super.getResources()}override fun getAssets(): AssetManager {return pluginAssetManager ?: super.getAssets()}override fun getClassLoader(): ClassLoader {return activityClassLoader ?: super.getClassLoader()}private fun handleResources() {try {// 生成 AssetManagerpluginAssetManager = AssetManager::class.java.newInstance()// 添加插件 apk 路徑val addAssetPathMethod = pluginAssetManager?.javaClass?.getMethod("addAssetPath", String::class.java)addAssetPathMethod?.invoke(pluginAssetManager, pluginPath)} catch (e: Exception) {}// 生成插件資源pluginResources = Resources(pluginAssetManager, super.getResources().displayMetrics, super.getResources().configuration)} } 復制代碼

    6.3 hook 系統相關實現的方式欺騙系統,讓系統調用生命周期

    6.3.1 hook Instrumentation

    上面講了如何通過手動調用插件 Activity 的生命周期方法來啟動插件 Activity,現在來看一下欺騙系統的方法。
    上面簡單介紹了 Activity 的啟動流程,我們可以看到,其實 Android 系統的運行是很巧妙的,AMS 是系統服務,應用通過 Binder 和 AMS 進行交互,其實和我們日常開發中客戶端和服務端交互有些類似,只不過這里使用了 Binder 做為交互方式,關于 Binder,可以簡單看看這篇文章。我們暫時只要知道通過 Binder 應用可以和 AMS 進行對話就行。
    這種架構的設計方式,也為我們提供了一些機會。理論上來說,我們只要在啟動 Activity 的消息到達 AMS 之前把 Activity 的信息就行修改,然后再消息回來以后再把信息恢復,就可以達到欺騙系統的目的了。
    在這個流程里,有很多 hook 點可以進行,而且不同的插件化框架對于 hook 點的選擇也不同,這里我們選擇 hook Instrumentation 的方式進行介紹(原因是個人感覺這種方式要簡單一點)。
    簡化以后的流程如下:

    ?

    Instrumentation 相當于 Activity 的管理者,Activity 的創建,以及生命周期的調用都是 AMS 通知以后通過 Instrumentation 來調用的。
    我們上面說到,AMS 相當于一個公司的背后大 Boss,而 Instrumentation 相當于秘書,Activity 相當于小職員,沒有權限直接和大 Boss 說話,想做什么事情都必須經過秘書向上匯報,然后 Instrumentation 再把大 Boss AMS 的命令傳達下來。而且大 Boss 那里有所有職員的名單,如果想要混入非法職員時不可能的。不過在整個過程中,由于 java 的語言特性,大 Boss 在和秘書 Instrumentation 對話時,不會管秘書到底是誰,只會確認這個人是不是秘書(是否是 Instrumentation 類型)。我們加載插件中的 Activity,相當于讓一個不在 Boss 名單上的編外職員去申請執行任務。在正常情況下,大 Boss 會檢查職員的名單,確認職員的合法性,一定是通過不了的。但是上有政策,下有對策,我們悄悄的替換了秘書,在秘書和 Boss 匯報時,把職員名字改成大 Boss 名單中的職員,在 Boss 安排工作以后,秘書再把名字換回來,讓編外職員去執行任務。
    而我們 hook 的方式就是替換調 Instrumentation,修改 Activity 類名,達到隱瞞 AMS 的效果。

    hook 方式原理圖

    ?

    接下來看看具體的代碼實現。 具體的實現見 hook 實現插件化,下面主要講解重點代碼。
    替換 Instrumentation 之前,首先我們要實現一個我們自己的 Instrumentation,具體實現如下:

    class AppInstrumentation(var realContext: Context, var base: Instrumentation, var pluginContext: PluginContext) :Instrumentation() {private val KEY_COMPONENT = "commontec_component"companion object {fun inject(activity: Activity, pluginContext: PluginContext) {// hook 系統,替換 Instrumentation 為我們自己的 AppInstrumentation,Reflect 是從 VirtualApp 里拷貝的反射工具類,使用很流暢~var reflect = Reflect.on(activity)var activityThread = reflect.get<Any>("mMainThread")var base = Reflect.on(activityThread).get<Instrumentation>("mInstrumentation")var appInstrumentation = AppInstrumentation(activity, base, pluginContext)Reflect.on(activityThread).set("mInstrumentation", appInstrumentation)Reflect.on(activity).set("mInstrumentation", appInstrumentation)}}override fun newActivity(cl: ClassLoader, className: String, intent: Intent): Activity? {// 創建 Activity 的時候會調用這個方法,在這里需要返回插件 Activity 的實例val componentName = intent.getParcelableExtra<ComponentName>(KEY_COMPONENT)var clazz = pluginContext.classLoader.loadClass(componentName.className)intent.component = componentNamereturn clazz.newInstance() as Activity?}private fun injectIntent(intent: Intent?) {var component: ComponentName? = nullvar oldComponent = intent?.componentif (component == null || component.packageName == realContext.packageName) {// 替換 intent 中的類名為占位 Activity 的類名,這樣系統在 Manifest 中查找的時候就可以找到 Activitycomponent = ComponentName("com.zy.commontec", "com.zy.commontec.activity.hook.HookStubActivity")intent?.component = componentintent?.putExtra(KEY_COMPONENT, oldComponent)}}fun execStartActivity(who: Context,contextThread: IBinder,token: IBinder,target: Activity,intent: Intent,requestCode: Int): Instrumentation.ActivityResult? {// 啟動 activity 的時候會調用這個方法,在這個方法里替換 Intent 中的 ClassName 為已經注冊的宿主 ActivityinjectIntent(intent)return Reflect.on(base).call("execStartActivity", who, contextThread, token, target, intent, requestCode).get()}// ... } 復制代碼

    在 AppInstrumentation 中有兩個關鍵點,execStartActivity 和 newActivity。
    execStartActivity 是在啟動 Activity 的時候必經的一個過程,這時還沒有到達 AMS,所以,在這里把 Activity 替換成宿主中已經注冊的 StubActivity,這樣 AMS 在檢測 Activity 的時候就認為已經注冊過了。newActivity 是創建 Activity 實例,這里要返回真正需要運行的插件 Activity,這樣后面系統就會基于這個 Activity 實例來進行對應的生命周期的調用。

    6.3.2 hook 系統的資源處理方式

    因為我們 hook 了 Instrumentation 的實現,還是把 Activity 生命周期的調用交給了系統,所以我們的資源處理方式和手動調用生命周期不太一樣,這里我們生成 Resources 以后,直接反射替換掉 Activity 中的 mResource 變量即可。下面是具體代碼。

    class AppInstrumentation(var realContext: Context, var base: Instrumentation, var pluginContext: PluginContext) : Instrumentation() {private fun injectActivity(activity: Activity?) {val intent = activity?.intentval base = activity?.baseContexttry {// 反射替換 mResources 資源Reflect.on(base).set("mResources", pluginContext.resources)Reflect.on(activity).set("mResources", pluginContext.resources)Reflect.on(activity).set("mBase", pluginContext)Reflect.on(activity).set("mApplication", pluginContext.applicationContext)// for native activityval componentName = intent!!.getParcelableExtra<ComponentName>(KEY_COMPONENT)val wrapperIntent = Intent(intent)wrapperIntent.setClassName(componentName.packageName, componentName.className)activity.intent = wrapperIntent} catch (e: Exception) {}}override fun callActivityOnCreate(activity: Activity?, icicle: Bundle?) {// 在這里進行資源的替換injectActivity(activity)super.callActivityOnCreate(activity, icicle)} }public class PluginContext extends ContextWrapper {private void generateResources() {try {// 反射生成 AssetManager 實例assetManager = AssetManager.class.newInstance();// 調用 addAssetPath 添加插件路徑Method method = assetManager.getClass().getMethod("addAssetPath", String.class);method.invoke(assetManager, pluginPath);// 生成 Resources 實例resources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());} catch (Exception e) {e.printStackTrace();}} } 復制代碼

    講完上面兩種方法,我們這里對比一下這兩種方法的優缺點:

    實現方法優點缺點
    手動調用1. 比較穩定,不需要 hook 系統實現 2. 實現相對簡單,不需要對系統內部實現做過多了解通過反射效率太低,通過接口需要實現的方法數量很多
    hook 系統1. 不需要實現大量接口方法 2. 由于最終還是交給系統去處理,各種處理相對比較完整1. 需要適配不同的系統及設備 2. 對開發者要求比較高,需要對系統實現有深入的了解

    七、Service 的插件化實現

    Service 比起 Activity 要簡單不少,Service 沒有太復雜的生命周期需要處理,類似的 onCreate 或者 onStartCommand 可以直接通過代理分發??梢灾苯釉谒拗?app 里添加一個占位 Service,然后在對應的生命周期里調用插件 Service 的生命周期方法即可。

    class StubService : Service() {var serviceName: String? = nullvar pluginService: Service? = nullcompanion object {var pluginClassLoader: ClassLoader? = nullfun startService(context: Context, classLoader: ClassLoader, serviceName: String) {pluginClassLoader = classLoaderval intent = Intent(context, StubService::class.java)intent.putExtra("serviceName", serviceName)context.startService(intent)}}override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {val res = super.onStartCommand(intent, flags, startId)serviceName = intent?.getStringExtra("serviceName")pluginService = pluginClassLoader?.loadClass(serviceName)?.newInstance() as ServicepluginService?.onCreate()return pluginService?.onStartCommand(intent, flags, startId) ?: res}override fun onDestroy() {super.onDestroy()pluginService?.onDestroy()}override fun onBind(intent: Intent?): IBinder? {return null} } 復制代碼

    八、BroadcastReceiver 的插件化實現

    動態廣播的處理也比較簡單,也沒有復雜的生命周期,也不需要在 Manifest 中進行注冊,使用的時候直接注冊即可。所以只要通過 ClassLoader 加載插件 apk 中的廣播類然后直接注冊就好。

    class BroadcastUtils {companion object {private val broadcastMap = HashMap<String, BroadcastReceiver>()fun registerBroadcastReceiver(context: Context, classLoader: ClassLoader, action: String, broadcastName: String) {val receiver = classLoader.loadClass(broadcastName).newInstance() as BroadcastReceiverval intentFilter = IntentFilter(action)context.registerReceiver(receiver, intentFilter)broadcastMap[action] = receiver}fun unregisterBroadcastReceiver(context: Context, action: String) {val receiver = broadcastMap.remove(action)context.unregisterReceiver(receiver)}} } 復制代碼

    靜態廣播稍微麻煩一點,這里可以解析 Manifest 文件找到其中靜態注冊的 Broadcast 并進行動態注冊,這里就不對 Manifest 進行解析了,知道其原理即可。

    九、ContentProvider 的插件化實現

    其實在日常開發中對于插件化中的 ContentProvider 使用還是比較少的,這里只介紹一種比較簡單的 ContentProvider 插件化實現方法,就是類似 Service,在宿主 app 中注冊占位 ContentProvider,然后轉發相應的操作到插件 ContentProvider 中。代碼如下:

    class StubContentProvider : ContentProvider() {private var pluginProvider: ContentProvider? = nullprivate var uriMatcher: UriMatcher? = UriMatcher(UriMatcher.NO_MATCH)override fun insert(uri: Uri?, values: ContentValues?): Uri? {loadPluginProvider()return pluginProvider?.insert(uri, values)}override fun query(uri: Uri?, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? {loadPluginProvider()if (isPlugin1(uri)) {return pluginProvider?.query(uri, projection, selection, selectionArgs, sortOrder)}return null}override fun onCreate(): Boolean {uriMatcher?.addURI("com.zy.stubprovider", "plugin1", 0)uriMatcher?.addURI("com.zy.stubprovider", "plugin2", 0)return true}override fun update(uri: Uri?, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {loadPluginProvider()return pluginProvider?.update(uri, values, selection, selectionArgs) ?: 0}override fun delete(uri: Uri?, selection: String?, selectionArgs: Array<out String>?): Int {loadPluginProvider()return pluginProvider?.delete(uri, selection, selectionArgs) ?: 0}override fun getType(uri: Uri?): String {loadPluginProvider()return pluginProvider?.getType(uri) ?: ""}private fun loadPluginProvider() {if (pluginProvider == null) {pluginProvider = PluginUtils.classLoader?.loadClass("com.zy.plugin.PluginContentProvider")?.newInstance() as ContentProvider?}}private fun isPlugin1(uri: Uri?): Boolean {if (uriMatcher?.match(uri) == 0) {return true}return false} } 復制代碼

    這里面需要處理的就是,如何轉發對應的 Uri 到正確的插件 Provider 中呢,解決方案是在 Uri 中定義不同的插件路徑,比如 plugin1 的 Uri 對應就是 content://com.zy.stubprovider/plugin1,plugin2 對應的 uri 就是 content://com.zy.stubprovider/plugin2,然后在 StubContentProvider 中根據對應的 plugin 分發不同的插件 Provider。

    十、總結

    本文介紹了插件化的相關實現,主要集中在 Activity 的實現上。重點如下:

    ?

    最后推薦大家在學習插件化的同時,也去學習一些四大組件以及 Binder 的系統實現~

    推薦學習資料

    插件化歷史
    www.infoq.cn/article/and…
    Activity 啟動流程
    blog.csdn.net/AndrLin/art…
    gityuan.com/2016/03/12/…

    【Android 修煉手冊】系列內容 每周更新
    歡迎關注下面賬號,獲取更新:
    Github
    知乎
    掘金

    總結

    以上是生活随笔為你收集整理的【Android 修炼手册】常用技术篇 -- Android 插件化解析的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。