手写插件化
插件化技术也就是说用户只需安装宿主apk,其它业务模块打包成独立的插件apk动态下发,然后通过宿主app加载运行。其天然的就解决了部分包体积大小的问题,毕竟只需将核心业务模块打包到宿主app,随之附带的还有插件apk的热更新能力,通过网络可以随时下载更新插件apk,避免宿主APP的频繁发版。
市面上的框架原理都差不多,构建插件apk路径的DexClassLoader,后续通过DexClassLoader加载插件类即可。普通类相对来说容易解决,加载即用。像四大组件比如Acitvity这种具有生命周期的组件则需要通过站桩方案转发生命周期,当然还有插件apk资源加载的问题。
插件化是一个听起来很厉害、很高大上的技术,但只要了解其中原理之后,自己撸一下也是很容易实现的,不过简单的实现和稳定在线上运行又是两码事了。看的再多不如手写一个,写个demo踩趟坑基本就懂了,下面以加载插件Activity为例。
首先需要构建一个DexClassLoader,加载插件apk dex文件中的class。
创建HostActivity
作为宿主,为了方便将插件apk拷贝到应用私有目录的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)
}
跳转插件Activity统一修改为跳转到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
实现接口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宿主在生命周期回调中调用插件PluginActivity
对应方法
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,此方案本质上运行在系统中的是HostActivity,只不过我们开发时编写的代码在插件Activity中。将HostActivity生命周期转发给PluginActivity,让插件类同步感知生命周期;插件使用到Activity方法时也需要将调用转发给HostActivity进行真正的调用(双向奔赴了属于是),毕竟PluginActivity不是一个真正的Activity,比如设置布局的setContentView()方法。
PluginActivity
fun setContentView(@LayoutRes layoutResID: Int) {
host?.setContentView(layoutResID)
}
这个host在DexClassLoader加载插件activity时进行了绑定,也就是宿主HostActivity,插件类需要使用Activity方法时都由host进行转发。
基类差不多写好了,都放到base module,然后新建plugin module,app和plugin都依赖base module,下面是目录结构。
Project
ActivityKtx粗略封装一下跳转插件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时调用setContentView设置布局。然后run plugin,将生成的plugin-debug.apk复制到应用私有目录,对应到之前初始化PluginClassLoader的路径。可以用AS自带的Devices File Explorer upload到data/user/0/package/cache目录。
data/user/0/package/cache
如此便算是模拟下载插件apk,下面回到宿主app。
MainActivity
点击按钮跳转插件Activity,调用前面封装的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")
}
}
}
不出意外跳转会崩溃,因为LoginActivity
设置布局使用到的lauout资源文件在插件apk中,调用HostActivity.setContentView()时,HostActivity运行在宿主app中,资源无法引用到。
下面解决资源问题,HostActivity
中反射创建AssetManager,调用其addAssetPath()方法指定资源路径,然后构造资源类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,点击按钮跳转。
MainActivity
LoginActivity
没啥问题,正常加载插件Activity。到这即使是作为一个demo还是略显粗糙的,Activity的方法还是有很多的,后续还需完善插件Activity的能力,搬砖式的将各种调用转发给HostActivity。而且四大组件还有其它三个要处理,即使是Activity,其启动模式不同也需要对应的站桩Activity。不过撸完原理肯定是拿捏了,加载资源包也是轻而易举,毕竟很多皮肤包的实现原理也是这样下发资源包apk动态加载的。