整個插件化框架歷史部分參考了包建強在 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,號稱是零反射,而且框架自身也可實現動態化,看了代碼之後發現,其實本質上仍是使用了代理分發生命週期實現四大組件動態化,而後抽象接口來實現框架的動態化。後面有機會能夠對其作一下分析。java
這基本上就是插件化框架的歷史,從 2012 至今,能夠說插件化技術基本成型了,主要就是代理和 hook 系統兩種方式(這裏沒有統計熱修復的發展,熱修復其實和插件化仍是有些想通的地方,後面的文章會對熱修復進行介紹)。若是看將來的話,斗膽預測,插件化技術的原理,應該不會有太大的變更了。android
在插件化中有一些專有名詞,若是是第一次接觸可能不太瞭解,這裏解釋一下。
宿主
負責加載插件的 apk,通常來講就是已經安裝的應用自己。
StubActivity
宿主中的佔位 Activity,註冊在宿主 Manifest 文件中,負責加載插件 Activity。
PluginActivity
插件 Activity,在插件 apk 中,沒有註冊在 Manifest 文件中,須要 StubActivity 來加載。git
在學習和開發插件化的時候,咱們須要動態去加載插件 apk,因此開發過程當中通常須要有兩個 apk,一個是宿主 apk,一個是插件 apk,對應的就須要有宿主項目和插件項目。
在 CommonTec 這裏建立了 app 做爲宿主項目,plugin 爲插件項目。爲了方便,咱們直接把生成的插件 apk 放到宿主 apk 中的 assets 中,apk 啓動時直接放到內部存儲空間中方便加載。
這樣的項目結構,咱們調試問題時的流程就是下面這樣:
修改插件項目 -> 編譯生成插件 apk -> 拷貝插件 apk 到宿主 assets -> 修改宿主項目 -> 編譯生成宿主 apk -> 安裝宿主 apk -> 驗證問題
若是每次咱們修改一個很小的問題,都經歷這麼長的流程,那麼耐心很快就耗盡了。最好是能夠直接編譯宿主 apk 的時候自動打包插件 apk 並拷貝到宿主 assets 目錄下,這樣咱們無論修改什麼,都直接編譯宿主項目就行了。如何實現呢?還記得咱們以前講解過的 gradle 系列麼?如今就是學以至用的時候了。
首先在 plugin 項目的 build.gradle 添加下面的代碼:github
project.afterEvaluate {
project.tasks.each {
if (it.name == "assembleDebug") {
it.doLast {
copy {
from new File(project.getBuildDir(), 'outputs/apk/debug/plugin-debug.apk').absolutePath
into 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 添加下面的代碼:apache
project.afterEvaluate {
project.tasks.each {
if (it.name == 'mergeDebugAssets') {
it.dependsOn ':plugin:assembleDebug'
}
}
}
複製代碼
找到宿主打包的 mergeDebugAssets 任務,依賴插件項目的打包,這樣每次編譯宿主項目的時候,會先編譯插件項目,而後拷貝插件 apk 到宿主 apk 的 assets 目錄下,之後每次修改,只要編譯宿主項目就能夠了。架構
ClassLoader 是插件化中必需要掌握的,由於插件是未安裝的 apk,系統不會處理其中的類,因此須要咱們本身來處理。app
BootstrapClassLoader
負責加載 JVM 運行時的核心類,好比 JAVA_HOME/lib/rt.jar 等等框架
ExtensionClassLoader
負責加載 JVM 的擴展類,好比 JAVA_HOME/lib/ext 下面的 jar 包ide
AppClassLoader
負責加載 classpath 裏的 jar 包和目錄函數
在這裏,咱們統稱 dex 文件,包含 dex 的 apk 文件以及 jar 文件爲 dex 文件 PathClassLoader 用來加載系統類和應用程序類,能夠加載已經安裝的 apk 目錄下的 dex 文件
DexClassLoader 用來加載 dex 文件,能夠從存儲空間加載 dex 文件。
咱們在插件化中通常使用的是 DexClassLoader。
每個 ClassLoader 中都有一個 parent 對象,表明的是父類加載器,在加載一個類的時候,會先使用父類加載器去加載,若是在父類加載器中沒有找到,本身再進行加載,若是 parent 爲空,那麼就用系統類加載器來加載。經過這樣的機制能夠保證系統類都是由系統類加載器加載的。 下面是 ClassLoader 的 loadClass 方法的具體實現。
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) {
// 先從父類加載器中進行加載
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;
}
複製代碼
要加載插件中的類,咱們首先要建立一個 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").absolutePath
nativeLibDir = File(filesDir, "pluginlib").absolutePath
dexOutPath = File(filesDir, "dexout").absolutePath
// 生成 DexClassLoader 用來加載插件類
pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
}
複製代碼
插件化,就是從插件中加載咱們想要的類並運行,若是這個類是一個普通類,那麼使用上面說到的 DexClassLoader 就能夠直接加載了,若是這個類是特殊的類,好比說 Activity 等四大組件,那麼就須要一些特殊的處理,由於四大組件是須要和系統進行交互的。插件化中,四大組件須要解決的難點以下:
咱們以前說到 Activity 插件化的難點,咱們先來理順一下爲何會有這兩個問題。
由於插件是動態加載的,因此插件的四大組件不可能註冊到宿主的 Manifest 文件中,而沒有在 Manifest 中註冊的四大組件是不能和系統直接進行交互的。
可能有些同窗會問,那爲何不能直接把插件的 Activity 註冊到宿主 Manifest 裏呢?這樣是能夠,不過就失去了插件化的動態特性,若是每次插件中新增 Activity 都要修改宿主 Manifest 而且從新打包,那就和直接寫在宿主中沒什麼區別了。
咱們再來講一下爲何沒有註冊的 Activity 不能和系統交互
這裏的不能直接交互的含義有兩個
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 的。
這裏對 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 構造函數生成插件資源。具體代碼以下:
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)
}
複製代碼
前期準備的知識點差很少介紹完了,咱們接着就看看具體的實現方法。
手動調用生命週期原理以下圖:
咱們手動調用插件 Activity 生命週期時,須要在正確的時機去調用,如何在正確的時機調用呢?那就是啓動一個真正的 Activity,咱們俗稱佔坑 Activity(StubActivity),而後在 StubActivity 的生命週期裏調用插件 Activity 對應的生命週期,這樣就間接的啓動了插件 Activity。
在 StubActivity 中調用 插件 Activity 生命週期的方法有兩種,一種是直接反射其生命週期方法,粗暴簡單,惟一的缺點就是反射的效率問題。另一種方式就是生成一個接口,接口裏對應的是生命週期方法,讓插件 Activity 實現這個接口,在 StubActivity 裏就能直接調用接口方法了,從而避免了反射的效率低下問題。
具體的代碼實如今CommonTec項目裏能夠找到,這裏貼一下主要的實現(這裏的實現和 CommonTec 裏的可能會有些區別,CommonTec 裏有些代碼作了一些封裝,這裏主要作原理的解釋)。
具體的實現見 反射調用生命週期,下面列出了重點代碼。
class StubReflectActivity : Activity() {
protected var activityClassLoader: ClassLoader? = null
protected var activityName = ""
private var pluginPath = ""
private var nativeLibDir: String? = null
private var dexOutPath: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
nativeLibDir = File(filesDir, "pluginlib").absolutePath
dexOutPath = File(filesDir, "dexout").absolutePath
pluginPath = intent.getStringExtra("pluginPath")
activityName = intent.getStringExtra("activityName")
// 建立插件 ClassLoader
activityClassLoader = 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)
}
}
複製代碼
具體的實現見 接口調用生命週期,下面列出了重點代碼。 經過接口調用 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? = null
override 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? = null
protected var activityName = ""
private var pluginPath = ""
private var activity: IPluginActivity? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
nativeLibDir = File(filesDir, "pluginlib").absolutePath
dexOutPath = File(filesDir, "dexout").absolutePath
pluginPath = intent.getStringExtra("pluginPath")
activityName = intent.getStringExtra("activityName")
// 生成插件 ClassLoader
activityClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
// 加載插件 Activity 類並轉化成 IPluginActivity 接口
activity = activityClassLoader?.loadClass(activityName)?.newInstance() as IPluginActivity?
activity?.attach(this)
// 經過接口直接調用對應的生命週期方法
activity?.onCreate(savedInstanceState)
}
}
複製代碼
因爲手動調用生命週期的方式,會重寫大量的 Activity 生命週期方法,因此咱們只要重寫 getResources 方法,返回插件的資源實例就能夠了。下面是具體代碼。
open class StubBaseActivity : Activity() {
protected var activityClassLoader: ClassLoader? = null
protected var activityName = ""
private var pluginPath = ""
private var pluginAssetManager: AssetManager? = null
private var pluginResources: Resources? = null
private var pluginTheme: Resources.Theme? = null
private var nativeLibDir: String? = null
private var dexOutPath: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
nativeLibDir = File(filesDir, "pluginlib").absolutePath
dexOutPath = File(filesDir, "dexout").absolutePath
pluginPath = 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 {
// 生成 AssetManager
pluginAssetManager = 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)
}
}
複製代碼
上面講了如何經過手動調用插件 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 = componentName
return clazz.newInstance() as Activity?
}
private fun injectIntent(intent: Intent?) {
var component: ComponentName? = null
var oldComponent = intent?.component
if (component == null || component.packageName == realContext.packageName) {
// 替換 intent 中的類名爲佔位 Activity 的類名,這樣系統在 Manifest 中查找的時候就能夠找到 Activity
component = ComponentName("com.zy.commontec", "com.zy.commontec.activity.hook.HookStubActivity")
intent?.component = component
intent?.putExtra(KEY_COMPONENT, oldComponent)
}
}
fun execStartActivity( who: Context, contextThread: IBinder, token: IBinder, target: Activity, intent: Intent, requestCode: Int ): Instrumentation.ActivityResult? {
// 啓動 activity 的時候會調用這個方法,在這個方法裏替換 Intent 中的 ClassName 爲已經註冊的宿主 Activity
injectIntent(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 實例來進行對應的生命週期的調用。
由於咱們 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?.intent
val base = activity?.baseContext
try {
// 反射替換 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 activity
val 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 比起 Activity 要簡單很多,Service 沒有太複雜的生命週期須要處理,相似的 onCreate 或者 onStartCommand 能夠直接經過代理分發。能夠直接在宿主 app 裏添加一個佔位 Service,而後在對應的生命週期裏調用插件 Service 的生命週期方法便可。
class StubService : Service() {
var serviceName: String? = null
var pluginService: Service? = null
companion object {
var pluginClassLoader: ClassLoader? = null
fun startService(context: Context, classLoader: ClassLoader, serviceName: String) {
pluginClassLoader = classLoader
val 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 Service
pluginService?.onCreate()
return pluginService?.onStartCommand(intent, flags, startId) ?: res
}
override fun onDestroy() {
super.onDestroy()
pluginService?.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
}
複製代碼
動態廣播的處理也比較簡單,也沒有複雜的生命週期,也不須要在 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 BroadcastReceiver
val 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 插件化實現方法,就是相似 Service,在宿主 app 中註冊佔位 ContentProvider,而後轉發相應的操做到插件 ContentProvider 中。代碼以下:
class StubContentProvider : ContentProvider() {
private var pluginProvider: ContentProvider? = null
private 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/…