【Android 修炼手册】常用技术篇 -- Android 插件化解析
預備知識
看完本文可以達到什么程度
閱讀前準備工作
文章概覽
?
?
一、插件化框架歷史
整個插件化框架歷史部分參考了包建強在 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 添加下面的代碼:
這段代碼是在 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
六、Activity 的插件化實現
6.1 難點分析
我們之前說到 Activity 插件化的難點,我們先來理順一下為什么會有這兩個問題。
因為插件是動態加載的,所以插件的四大組件不可能注冊到宿主的 Manifest 文件中,而沒有在 Manifest 中注冊的四大組件是不能和系統直接進行交互的。
可能有些同學會問,那為什么不能直接把插件的 Activity 注冊到宿主 Manifest 里呢?這樣是可以,不過就失去了插件化的動態特性,如果每次插件中新增 Activity 都要修改宿主 Manifest 并且重新打包,那就和直接寫在宿主中沒什么區別了。
我們再來說一下為什么沒有注冊的 Activity 不能和系統交互
這里的不能直接交互的含義有兩個
這個 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 的。
這里對 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 中,有一個 mResources 變量,是 Resources 類型。這個變量可以理解為代表了整個 apk 的資源。
?
在宿主中調用的 Activity,mResources 自然代表了宿主的資源,所以需要我們對插件的資源進行特殊的處理。
我們先看一下如何生成代表插件資源的 Resources 類。
首先要生成一個 AssetManager 實例,然后通過其 addAssetPath 方法添加插件的路徑,這樣 AssetManager 中就包含了插件的資源。然后通過 Resources 構造函數生成插件資源。具體代碼如下:
前期準備的知識點差不多介紹完了,我們接著就看看具體的實現方法。
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,具體實現如下:
在 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 插件化解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 家居用3000k还是4000k(PCho
- 下一篇: 洗衣机嗡嗡响是什么原因(如何正确选购洗衣