手寫插件化

插件化技術也就是說用戶只需安裝宿主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,下面是目錄結構。



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目錄。



如此便算是模擬下載插件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,點擊按鈕跳轉。




沒啥問題,正常加載插件Activity。到這即使是作爲一個demo還是略顯粗糙的,Activity的方法還是有很多的,後續還需完善插件Activity的能力,搬磚式的將各種調用轉發給HostActivity。而且四大組件還有其它三個要處理,即使是Activity,其啓動模式不同也需要對應的站樁Activity。不過擼完原理肯定是拿捏了,加載資源包也是輕而易舉,畢竟很多皮膚包的實現原理也是這樣下發資源包apk動態加載的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章