插件化技術(shù)也就是說用戶只需安裝宿主apk,其它業(yè)務(wù)模塊打包成獨立的插件apk動態(tài)下發(fā),然后通過宿主app加載運行。其天然的就解決了部分包體積大小的問題,畢竟只需將核心業(yè)務(wù)模塊打包到宿主app,隨之附帶的還有插件apk的熱更新能力,通過網(wǎng)絡(luò)可以隨時下載更新插件apk,避免宿主APP的頻繁發(fā)版。
市面上的框架原理都差不多,構(gòu)建插件apk路徑的DexClassLoader,后續(xù)通過DexClassLoader加載插件類即可。普通類相對來說容易解決,加載即用。像四大組件比如Acitvity這種具有生命周期的組件則需要通過站樁方案轉(zhuǎn)發(fā)生命周期,當(dāng)然還有插件apk資源加載的問題。
插件化是一個聽起來很厲害、很高大上的技術(shù),但只要了解其中原理之后,自己擼一下也是很容易實現(xiàn)的,不過簡單的實現(xiàn)和穩(wěn)定在線上運行又是兩碼事了。看的再多不如手寫一個,寫個demo踩趟坑基本就懂了,下面以加載插件Activity為例。
首先需要構(gòu)建一個DexClassLoader,加載插件apk dex文件中的class。
創(chuàng)建HostActivity作為宿主,為了方便將插件apk拷貝到應(yīng)用私有目錄的cache文件夾中,在宿主HostActivity.onCreate()中初始化DexClassLoader。
private var pluginClassLoader: PluginClassLoader? = null
private var pluginActivity: PluginActivity? = null
private var apkPath: String? = null
private fun initCurrentActivity() {
apkPath = "${cacheDir.absolutePath}${File.separator}plugin-debug.apk"
pluginClassLoader = PluginClassLoader(
dexPath = apkPath ?: "",
optimizedDirectory = cacheDir.absolutePath,
librarySearchPath = null,
classLoader
)
val activityName = intent.getStringExtra("ActivityName") ?: ""
pluginActivity = pluginClassLoader?.loadActivity(activityName, this)
}
跳轉(zhuǎn)插件Activity統(tǒng)一修改為跳轉(zhuǎn)到HostActivity,如此便沒有校驗manifest的問題,在intent中傳入插件activity全類名,通過DexClassLoader加載插件activity并實例化。
class PluginClassLoader(
dexPath: String,
optimizedDirectory: String,
librarySearchPath: String?,
parent: ClassLoader
) : DexClassLoader(dexPath, optimizedDirectory, librarySearchPath, parent) {
fun loadActivity(activityName: String, host: HostActivity): PluginActivity? {
try {
return (loadClass(activityName)?.newInstance() as PluginActivity?).apply {
this?.bindHost(host)
}
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
}
插件基類PluginActivity實現(xiàn)接口PluginLifecycle同步HostActivity生命周期。
PluginActivity
open class PluginActivity : PluginLifecycle {
private var host: HostActivity? = null
fun bindHost(host: HostActivity) {
this.host = host
}
override fun onCreate(savedInstanceState: Bundle?) {
}
override fun onStart() {
}
override fun onResume() {
}
override fun onRestart() {
}
override fun onPause() {
}
override fun onStop() {
}
override fun onDestroy() {
}
override fun onSaveInstanceState(outState: Bundle) {
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
}
}
PluginLifecycle
interface PluginLifecycle {
fun onCreate(savedInstanceState: Bundle?)
fun onStart()
fun onResume()
fun onRestart()
fun onPause()
fun onStop()
fun onDestroy()
fun onSaveInstanceState(outState: Bundle)
fun onRestoreInstanceState(savedInstanceState: Bundle)
}
HostActivity宿主在生命周期回調(diào)中調(diào)用插件PluginActivity對應(yīng)方法
class HostActivity : AppCompatActivity() {
private var pluginClassLoader: PluginClassLoader? = null
private var pluginActivity: PluginActivity? = null
private var apkPath: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
initCurrentActivity()
super.onCreate(savedInstanceState)
pluginActivity?.onCreate(savedInstanceState)
}
private fun initCurrentActivity() {
apkPath = "${cacheDir.absolutePath}${File.separator}plugin-debug.apk"
pluginClassLoader = PluginClassLoader(
dexPath = apkPath ?: "",
optimizedDirectory = cacheDir.absolutePath,
librarySearchPath = null,
classLoader
)
val activityName = intent.getStringExtra("ActivityName") ?: ""
pluginActivity = pluginClassLoader?.loadActivity(activityName, this)
}
override fun onStart() {
super.onStart()
pluginActivity?.onStart()
}
override fun onResume() {
super.onResume()
pluginActivity?.onResume()
}
override fun onRestart() {
super.onRestart()
pluginActivity?.onRestart()
}
override fun onPause() {
super.onPause()
pluginActivity?.onPause()
}
override fun onStop() {
super.onStop()
pluginActivity?.onStop()
}
override fun onDestroy() {
super.onDestroy()
pluginActivity?.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
pluginActivity?.onSaveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
pluginActivity?.onRestoreInstanceState(savedInstanceState)
}
}
插件Activity編寫時繼承PluginActivity,此方案本質(zhì)上運行在系統(tǒng)中的是HostActivity,只不過我們開發(fā)時編寫的代碼在插件Activity中。將HostActivity生命周期轉(zhuǎn)發(fā)給PluginActivity,讓插件類同步感知生命周期;插件使用到Activity方法時也需要將調(diào)用轉(zhuǎn)發(fā)給HostActivity進(jìn)行真正的調(diào)用(雙向奔赴了屬于是),畢竟PluginActivity不是一個真正的Activity,比如設(shè)置布局的setContentView()方法。
PluginActivity
fun setContentView(@LayoutRes layoutResID: Int) {
host?.setContentView(layoutResID)
}
這個host在DexClassLoader加載插件activity時進(jìn)行了綁定,也就是宿主HostActivity,插件類需要使用Activity方法時都由host進(jìn)行轉(zhuǎn)發(fā)。
基類差不多寫好了,都放到base module,然后新建plugin module,app和plugin都依賴base module,下面是目錄結(jié)構(gòu)。

ActivityKtx粗略封裝一下跳轉(zhuǎn)插件Activity方法
fun Activity.jumpPluginActivity(activityName: String, pluginName: String? = "") {
startActivity(Intent(this, HostActivity::class.java).apply {
putExtra("ActivityName", activityName)
putExtra("PluginName", pluginName)
})
}
接下來在Plugin module中編寫插件Activity
LoginActivity
class LoginActivity : PluginActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
}
}
代碼很簡單,在onCreate時調(diào)用setContentView設(shè)置布局。然后run plugin,將生成的plugin-debug.apk復(fù)制到應(yīng)用私有目錄,對應(yīng)到之前初始化PluginClassLoader的路徑??梢杂肁S自帶的Devices File Explorer upload到data/user/0/package/cache目錄。

如此便算是模擬下載插件apk,下面回到宿主app。
MainActivity點擊按鈕跳轉(zhuǎn)插件Activity,調(diào)用前面封裝的jumpPluginActivity()
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<TextView>(R.id.tv).setOnClickListener {
jumpPluginActivity("com.chenxuan.plugin.LoginActivity")
}
}
}
不出意外跳轉(zhuǎn)會崩潰,因為LoginActivity設(shè)置布局使用到的lauout資源文件在插件apk中,調(diào)用HostActivity.setContentView()時,HostActivity運行在宿主app中,資源無法引用到。
下面解決資源問題,HostActivity中反射創(chuàng)建AssetManager,調(diào)用其addAssetPath()方法指定資源路徑,然后構(gòu)造資源類Resources,重寫getResources()方法返回插件資源。
HostActivity
private var pluginClassLoader: PluginClassLoader? = null
private var pluginActivity: PluginActivity? = null
private var apkPath: String? = null
private var pluginResources: Resources? = null
override fun onCreate(savedInstanceState: Bundle?) {
initCurrentActivity()
initActivityResource()
super.onCreate(savedInstanceState)
pluginActivity?.onCreate(savedInstanceState)
}
override fun getResources(): Resources {
return pluginResources ?: super.getResources()
}
private fun initActivityResource() {
try {
val pluginAssetManager = AssetManager::class.java.newInstance()
val addAssetPathMethod = pluginAssetManager.javaClass
.getMethod("addAssetPath", String::class.java)
addAssetPathMethod.invoke(pluginAssetManager, apkPath)
pluginResources = Resources(
pluginAssetManager,
super.getResources().displayMetrics,
super.getResources().configuration
)
} catch (e: Exception) {
e.printStackTrace()
}
}
run app,點擊按鈕跳轉(zhuǎn)。


沒啥問題,正常加載插件Activity。到這即使是作為一個demo還是略顯粗糙的,Activity的方法還是有很多的,后續(xù)還需完善插件Activity的能力,搬磚式的將各種調(diào)用轉(zhuǎn)發(fā)給HostActivity。而且四大組件還有其它三個要處理,即使是Activity,其啟動模式不同也需要對應(yīng)的站樁Activity。不過擼完原理肯定是拿捏了,加載資源包也是輕而易舉,畢竟很多皮膚包的實現(xiàn)原理也是這樣下發(fā)資源包apk動態(tài)加載的。