草庐IT

手写插件化

折剑游侠 2023-03-28 原文

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

有关手写插件化的更多相关文章

  1. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

  2. ruby-on-rails - 无法使用 Rails 3.2 创建插件? - 2

    我对最新版本的Rails有疑问。我创建了一个新应用程序(railsnewMyProject),但我没有脚本/生成,只有脚本/rails,当我输入ruby./script/railsgeneratepluginmy_plugin"Couldnotfindgeneratorplugin.".你知道如何生成插件模板吗?没有这个命令可以创建插件吗?PS:我正在使用Rails3.2.1和ruby​​1.8.7[universal-darwin11.0] 最佳答案 随着Rails3.2.0的发布,插件生成器已经被移除。查看变更日志here.现在

  3. ruby-on-rails - 您希望看到哪些 Rails 插件? - 2

    您认为可以作为插件很好地存在于您的Rails应用程序中必须实现的哪些行为?您过去曾搜索过哪些插件功能但找不到?哪些现有的Rails插件可以改进或扩展,如何改进或扩展? 最佳答案 我希望在管理界面中看到一个引擎插件,它提供了应用程序中所有模型的仪表板摘要,以及可配置的事件图表。 关于ruby-on-rails-您希望看到哪些Rails插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questio

  4. ruby - vagrant 从 github 安装插件 - 2

    我们正在使用Vagrant进行部署,我们最终希望将此集群部署在Rackspace上。vagrant-rackspace插件是一个自然的选择,但它有一些错误,这些错误未包含在最新的0.1.1版本中(notablythatvagrantprovisiondoesn'twork)。我已经在我的personalfork中解决了这个问题通过合并其他人的工作来对存储库进行改造。是否可以从github安装vagrant插件?显而易见的事情没有奏效:[unix]$vagrantplugininstallvagrant-rackspace--plugin-sourcehttps://github.com

  5. IDEA使用LeetCode插件 - 2

    前言我们习惯用idea编写、调试代码,在LeetCode上刷题时,如果能够在IDEA编写代码,并且做好代码管理,是一件事半功倍的事情。对于后续复习题目,做笔记也会非常便利。本文目的在于介绍LeetCodeEditor的使用,以及配置工具类,最终目录结构如下:note:放置笔记src:放置代码leetcode.editor.cn:插件LeetCodeEditor自动生成utils:自定义的工具包,可用于自动化输入测试用例,定义题目需要的类(结构体)out:运行测试时自动生成LeetCodeEditorGitHub:https://github.com/shuzijun/leetcode-edit

  6. regex - Ruby 是否有类似于 Perl 6 语法的插件? - 2

    多年来,Perl一直是我首选的编程语言工具之一。Perl6语法看起来像是一个很棒的语言特性。我想知道是否有人开始为Ruby做这样的事情。 最佳答案 如果您想在Ruby中使用实际的Perl6语法,最好的选择是Cardinal,Parrot上的ruby​​编译器。它目前尚未完成并且非常缓慢,但我非常希望它最终成为一个可行的ruby​​实现。它目前大部分处于非事件状态,等待Parrot中的一些基础架构更改以支持改进的解析速度和其他功能。 关于regex-Ruby是否有类似于Perl6语法的插件

  7. ruby-on-rails - 你为 Rails 推荐哪个状态机插件? - 2

    关闭。这个问题不符合StackOverflowguidelines.它目前不接受答案。要求我们推荐或查找工具、库或最喜欢的场外资源的问题对于StackOverflow来说是偏离主题的,因为它们往往会吸引自以为是的答案和垃圾邮件。相反,describetheproblem以及迄今为止为解决该问题所做的工作。关闭9年前。Improvethisquestion我正在为Rails3/ActiveRecord项目寻找一个相对简单的状态机插件。我做了一些研究并提出了以下插件:转换:https://github.com/qoobaa/transitions从旧的ActiveRecord状态机库中提取

  8. ruby-on-rails - 使用模块扩展带有 "has_many"的插件中的模型 - 2

    我在引擎样式插件中有一些代码,其中包含一些模型。在我的应用程序中,我想扩展其中一个模型。通过在初始值设定项中包含一个模块,我已经设法将实例和类方法添加到相关模型中。但是我似乎无法添加关联、回调等。我收到“找不到方法”错误。/libs/qwerty/core.rbmoduleQwertymoduleCoremoduleExtensionsmoduleUser#InstanceMethodsGoHere#ClassMethodsmoduleClassMethodshas_many:hits,:uniq=>true#nomethodfoundbefore_validation_on_crea

  9. ruby - 用于 CSS3 跨浏览器兼容性的 SASS 插件? - 2

    是否有一个SASS扩展可以采用SASS样式表,找到中性属性(例如border-radius)并为其输出所有特定于供应商的属性(例如-webkit-border-radius等)自动?我真的不想手动创建所有混入,也不想手动编写代码。我确定一定有这样的扩展名,但我找不到它。帮忙? 最佳答案 有一个非常好的gem可以满足您的需求。它叫做Bourbon它不会用特定于供应商的css替换您的css,因为它可以像SASS一样工作。它基本上是一个正确生成跨浏览器css的mixin集合。 关于ruby-用

  10. ruby - vim 使用 AutoComplPop 插件崩溃 - 2

    我使用vim编辑ruby​​文件,但是当我输入“.”时它崩溃了。我发现它是由AutoComplPop插件引起的。我该怎么办? 最佳答案 我找到了一种使用autocomplpop和filetype=ruby来防止vim崩溃的方法。将以下行放入您的.vimrcletg:acp_behaviorRubyOmniMethodLength=-1这将防止在您键入“.”时触发autocomplpop。(期间)这不是解决办法。(我不是vim插件程序员)祝你好运! 关于ruby-vim使用AutoComp

随机推荐