0%

简陋Android插件化方案

由于一些不可描述的原因,需要把部分功能从app里独立出来,作为两个独立的apk应用。两个独立的apk进行交互非常麻烦。这里记录一下自己摸索的一个简陋但可行的实现方法。

插件代码的加载

首先,为了避免跨进程通信,所有代码都由宿主进程统一加载。

首先在插件应用留一个单例的类,在Application里实例化这个类,当作插件实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun createPluginInstance(context: Context): Pair<Context, Any>? {
return try {
val pluginContext = context.createPackageContext(
"soko.ekibun.bangumi.plugins",
Context.CONTEXT_IGNORE_SECURITY or Context.CONTEXT_INCLUDE_CODE
)
val pluginClass = pluginContext.classLoader.loadClass("soko.ekibun.bangumi.plugins.Plugin")
pluginContext to pluginClass.getDeclaredConstructor().let {
it.isAccessible = true
it.newInstance()
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}

然后在Activity.onCreate里用反射调用插件的setupPlugins函数:

1
2
3
4
5
6
7
8
9
10
11
12
fun setUpPlugins(activity: Activity): Boolean {
val pluginInstance = App.get(activity).pluginInstance ?: return false
return try {
val method =
pluginInstance.second.javaClass.getMethod("setUpPlugins", Activity::class.java, Context::class.java)
method.invoke(pluginInstance.second, activity, pluginInstance.first)
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}

插件就能获得当前的ActivityContext实例:

1
2
3
4
5
6
7
8
9
@Keep
fun setUpPlugins(activity: Activity, context: Context) {
App.init(activity.application, context)
try {
pluginList[activity.javaClass.name]?.setUpPlugins(WeakReference(activity))
} catch (e: Exception) {
Log.e("plugin", Log.getStackTraceString(e))
}
}

这里根据不同的Activity的名称,加载对应的类去处理。仿照Application模式,在App.init里创建一个全局的App实例,用来保存宿主和自己的上下文:

1
2
3
4
5
6
7
8
9
10
11
12
class App(val host: Context, val plugin: Context) {
val handler = android.os.Handler { true }

companion object {
val inited get() = ::app.isInitialized

lateinit var app: App
fun init(host: Context, plugin: Context) {
if (!inited) app = App(host, plugin)
}
}
}

到这里,就已经能运行插件的代码了,接下来是一些坑和解决的办法。

加载插件的布局和样式

createPackageContext创建的上下文是没有样式的,需要根据ActivityConfiguration去创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun createThemeContext(activityRef: WeakReference<Activity>): Context {
val themeContext = object : ContextThemeWrapper(app.plugin, R.style.AppTheme) {
override fun getApplicationContext(): Context {
return this
}

override fun getSystemService(name: String): Any? {
return when (name) {
Context.WINDOW_SERVICE -> activityRef.get()?.getSystemService(name)
else -> super.getSystemService(name)
}
}
}
activityRef.get()?.let { themeContext.applyOverrideConfiguration(it.resources.configuration) }
return themeContext
}

调用宿主的函数

和宿主加载插件一样,可以用反射来调用宿主的函数。直接使用反射有点麻烦,可以创建一个对应类相同的接口,通过Proxy.newProxyInstance进行调用,先写个Wrapper:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private fun getLoaderClasses(classLoader: ClassLoader, classes: Array<out Class<*>>): Array<Class<*>> {
return classes.map {
if (it.isPrimitive || it.classLoader == classLoader) it
else classLoader.loadClass(it.name)
}.toTypedArray()
}

private fun getMethod(clazz: Class<*>, name: String, vararg params: Class<*>): Method? {
val loaderParams = getLoaderClasses(clazz.classLoader!!, params)
var type = clazz
var ret: Method? = null
do {
try {
ret = type.getDeclaredMethod(name, *loaderParams)
} catch (e: NoSuchMethodException) {}
if (ret != null) break
type = type.superclass ?: break
} while (true)
ret?.isAccessible = true
return ret
}

@Suppress("UNCHECKED_CAST")
fun <T> proxyObject(obj: Any?, clazz: Class<T>): T? {
if (clazz.classLoader == null || obj == null || obj.javaClass == clazz || !clazz.isInterface)
return obj as? T
return Proxy.newProxyInstance(
clazz.classLoader, arrayOf(clazz)
) { _, method, args ->
getMethod(obj.javaClass, method.name, *method.parameterTypes)?.let {
it.invoke(obj, *(args ?: arrayOf()).mapIndexed { i, v ->
proxyObject(v, it.parameterTypes[i])
}.toTypedArray())
}?.let { proxyObject(it, method.returnType) }
} as? T
}

例如,若宿主的Application包含remoteAction变量,定义如下接口:

1
2
3
interface IApplication {
var remoteAction: (intent: Intent?, flags: Int, startId: Int) -> Unit
}

调用proxyObject(host, IApplication::class.java),就能像宿主一样访问变量了。

启动服务

服务必须要声明在AndroidManifest.xml里,用了个笨方法,先声明:

1
2
3
4
<service
android:name=".RemoteService"
android:exported="false">
</service>

服务里吧onStartCommand指向Application.remoteAction

1
2
3
4
5
6
7
8
9
10
11
class RemoteService : Service() {

override fun onBind(intent: Intent): IBinder? {
return null
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
(application as App).remoteAction(intent, flags, startId)
return super.onStartCommand(intent, flags, startId)
}
}

修改remoteAction变量,再启动RemoteService就能调用自己的代码了。