Android 插件化入門 (插樁原理)


插樁方式實現插件化

項目機構如下:app爲宿主application ,pluginstand爲中間連接作用,負責定義主app和插件之間組件傳遞規則。shop用來打包插件apk。
在這裏插入圖片描述

  • 主app和shop同時依賴 pluginstand
  • 爲了模擬實現網絡下載插件這裏採用文件拷貝的方式將apk存儲到內存卡,拷貝到/data/data/目錄下。
  • app 中,首先定義 PluginManager 用來解析下載下來的插件文件,代碼如下:
  • 原理就是通過解析 apk 文件格式來獲取想要的內容。
// 設置單利模式
object PluginManager {
	// 獲取 dexClassLoader 
    private var dexClassLoader: DexClassLoader? = null
	// 獲取資源
    private var resources: Resources? = null
	// 獲取文件包信息
    private var packageInfo: PackageInfo? = null
	
    fun loadPath(context: Context) {
        // context.getDir 保存位置 /data/user/0/com.lu.plugin/app_plugin
        // Context.MODE_PRIVATE:爲默認操作模式,代表該文件是私有數據,只能被應用本身訪問,在該模式下,寫入的內容會覆蓋原文件的內容,如果想把新寫入的內容追加到原文件中。可以使用Context.MODE_APPEND
        // Context.MODE_APPEND:模式會檢查文件是否存在,存在就往文件追加內容,否則就創建新文件。
        // Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用來控制其他應用是否有權限讀寫該文件。
        // MODE_WORLD_READABLE:表示當前文件可以被其他應用讀取;
        // MODE_WORLD_WRITEABLE:表示當前文件可以被其他應用寫入。
        val filesDir = context.getDir("plugin", Context.MODE_PRIVATE)
        val name = "shop-debug.apk"
        val path = File(filesDir, name).absolutePath
        val packageManager = context.packageManager
        // 未安裝的 Apk 文件,想要解析出 Apk 文件中的額外信息,PM 中,也有對應的 Api。
        // 非常的方便,直接使用 getPackageArchiveInfo() 即可
        packageInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES)
        // activity
        val dex = context.getDir("dex", Context.MODE_PRIVATE)
        // DexClassLoader:用於加載SD卡上的class.dex、jar、或apk文件。
        dexClassLoader = DexClassLoader(path, dex.absolutePath, null, context.classLoader)
        // resource
        try {
            val manager = AssetManager::class.java.newInstance()
            val addAssetPath =
                AssetManager::class.java.getMethod("addAssetPath", String::class.java)
            addAssetPath.invoke(manager, path)
            resources = Resources(
                manager,
                context.resources.displayMetrics,
                context.resources.configuration
            )
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    fun getResources(): Resources? {
        return resources
    }

    fun getDexClassLoader(): DexClassLoader? {
        return dexClassLoader
    }

    fun getPackageInfo(): PackageInfo? {
        return packageInfo
    }
}
  • MainActivity中負責下載和跳轉插件的頁面
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

    }
	// 下載插件 之前用Kotlin的文件拷貝會出現一些文件File文件讀取的文圖,還沒查找原因 
	// 改爲自己寫流的方式
    fun downLoadPlugin(view: View) {
//        val name = "shop-debug.apk"
//        val desFile = File(Environment.getExternalStorageDirectory(), name)
//
//        val targetFile = getDir("plugin", Context.MODE_APPEND)
//        val copyRecursively = desFile.copyRecursively(targetFile, true)
//        Log.e("main", copyRecursively.toString());
//        Log.e("path = ", targetFile.absolutePath)
//        if (copyRecursively) {
//            PluginManager.loadPath(this)
//        }

        val filesDir = getDir("plugin", Context.MODE_PRIVATE)
        val name = "shop-debug.apk"
        val filePath = File(filesDir, name).absolutePath
        val file = File(filePath)
        if (file.exists()) {
            file.delete()
        }
        var inputStream: InputStream? = null
        var outputStream: FileOutputStream? = null
        try {
            inputStream = FileInputStream(File(Environment.getExternalStorageDirectory(), name))
            outputStream = FileOutputStream(filePath)
            var len = 0
            val buffer = ByteArray(1024)
            while (inputStream.read(buffer).also { len = it } != -1) {
                outputStream.write(buffer, 0, len)
            }
            val f = File(filePath)
            if (f.exists()) {
                Toast.makeText(this, "dex overwrite", Toast.LENGTH_SHORT).show()
            }
            PluginManager.loadPath(this)
        } catch (e: IOException) {
            e.printStackTrace()
        } finally {
            try {
                outputStream?.close()
                inputStream?.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }
	// 具體跳轉 首先跳到 app 的 ProxyActivity 由它來執行具體的跳轉邏輯
    fun toShopPlugin(view: View) {
        val intent = Intent(this, ProxyActivity::class.java)
        //  PluginManager.getPackageInfo()!!.activities[0] 中的順序是按照 manifests 中的註冊順序由上到下的順序
        intent.putExtra(
            "className", PluginManager.getPackageInfo()!!.activities[0].name
        )
        startActivity(intent)
    }
}

  • ProxyActivity 具體代碼:由於插件apk是沒有進行安裝的,所有插件中最麻煩的就是如何獲取Context,所以我們把所有需要獲取 Context 的地方都需要重寫。這裏只是寫了一部分,包括生命週期也需要重寫。因爲插件的 activity 中是沒有 this 所謂的 context 的。

/**
 * 通過代理 activity 做具體跳轉
 */
class ProxyActivity : Activity() {
    private var pluginInterface: PluginInterface? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        init()
    }

    private fun init() {
        // 獲取要跳轉的 className 傳進來的 className 是在 apk 中的 manifest 文件解析出來的
        val className = intent.getStringExtra("className")
        className?.let {
            // 通過 classLoader 根據 className 獲取 class
            val aClass = classLoader!!.loadClass(className)
            // 獲取構造方法
            val constructor = aClass.getConstructor()
            // 根據構造方法創建具體要跳轉的 activity
            val realClass = constructor.newInstance()
            // 跳轉的頁面必須也實現接口,用來傳遞數據
            pluginInterface = realClass as PluginInterface?
            pluginInterface?.pluginAttach(this)
            // bundle 中可以傳遞數據
            val bundle = Bundle()
            pluginInterface?.pluginOnCreate(bundle)
        }
    }

    override fun startActivity(intent: Intent) {
        val className = intent.getStringExtra("className")
        val intent1 = Intent(this, ProxyActivity::class.java)
        intent1.putExtra("className", className)
        super.startActivity(intent1)
    }

    //重寫加載類
    override fun getClassLoader(): ClassLoader? {
        return PluginManager.getDexClassLoader()
    }

    //重寫加載資源
    override fun getResources(): Resources? {
        return PluginManager.getResources()
    }
    
    override fun onStart() {
        super.onStart()
        pluginInterface?.pluginOnStart()
    }

    override fun onResume() {
        super.onResume()
        pluginInterface?.pluginOnResume()
    }

    override fun onStop() {
        super.onStop()
        pluginInterface?.pluginOnStop()
    }

    override fun onDestroy() {
        super.onDestroy()
        pluginInterface?.pluginOnDestroy()
    }
}
  • plugnstand 插件中存有接口如下:
interface PluginInterface {
    fun pluginAttach(proxyActivity: Activity)
    // 生命週期
    fun pluginOnCreate(saveInstanceState: Bundle?)
    fun pluginOnStart()
    fun pluginOnResume()
    fun pluginOnPause()
    fun pluginOnStop()
    fun pluginOnDestroy()
    fun pluginOnSaveInstanceState(outState: Bundle?)
    fun pluginOnTouchEvent(event: MotionEvent?): Boolean
    fun pluginOnBackPressed()
}
  • shop 插件中
  • 定義 BaseActivity,實現 PluginInterface 接口。that 爲插件中的 Context
open class BaseActivity : Activity(), PluginInterface {
    protected var that: Activity? = null
    override fun pluginAttach(proxyActivity: Activity) {
        that = proxyActivity
    }

    override fun setContentView(view: View?) {
        if (that != null) {
            that!!.setContentView(view)
        } else {
            super.setContentView(view)
        }
    }

    override fun setContentView(layoutResID: Int) {
        if (that != null) {
            that!!.setContentView(layoutResID)
        } else {
            super.setContentView(layoutResID)
        }
    }

    override fun startActivity(intent: Intent?) {
//        val intent = Intent()
        intent?.let {
            intent.putExtra("className", intent.component?.className)
            that?.startActivity(intent)
        }

    }

    override fun <T : View> findViewById(id: Int): T {
        return that!!.findViewById(id)
    }

    override fun pluginOnCreate(saveInstanceState: Bundle?) {

    }

    override fun pluginOnStart() {
    }

    override fun pluginOnResume() {
    }

    override fun pluginOnPause() {
    }

    override fun pluginOnStop() {
    }

    override fun pluginOnDestroy() {
    }

    override fun pluginOnSaveInstanceState(outState: Bundle?) {
    }

    override fun pluginOnTouchEvent(event: MotionEvent?): Boolean {
        return false
    }

    override fun pluginOnBackPressed() {
    }

}
  • shop的MainActiviy很簡單,就是一個圖片,有個跳轉的功能

class ShopMainActivity : BaseActivity() {

    override fun pluginOnCreate(saveInstanceState: Bundle?) {
        super.pluginOnCreate(saveInstanceState)
        setContentView(R.layout.activity_main)
        img.setOnClickListener {
            val intent = Intent(that, ShopSecondActivity::class.java)
           startActivity(intent)
        }
    }

}

  • 這樣一個插樁就完成了。service和broadCast也是一樣的。在 pluginstand 中定義規則,然後重寫需要重寫的東西。service記得註冊等

demo代碼下載連接

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