文章目錄
插樁方式實現插件化
項目機構如下: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記得註冊等