手写插件化

插件化技术也就是说用户只需安装宿主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动态加载的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容