之前有同事分享过shadow这个插件化框架,当时对这块还挺感兴趣的,但是因为有一些其他的问题一直没有深入,比如singleTask的Activity要先定义好多个占位,后面学习了compose,非常喜欢用compose构建界面,看到compose是单Activity的模式,看了一下页面注册路由的原理,发现是可以动态加载路由的,所以和大家分享下
首先需要简单的熟悉下这里路由的跳转逻辑及分工
NavHost 定义路由和页面的地方
NavController 导航的全局管理者,可以用它来进行页面跳转,维护着NavGraph路由索引图和回退栈NavBackStacks
NavGraph 定义导航时,需要收集各个节点的导航信息,并统一注册到导航图中
NavDestination 导航中的各个节点,携带了 route,arguments 等信息
Navigator 导航的具体执行者,NavController 基于导航图获取目标节点,并通过 Navigator 执行跳转
其实compose也是适配了Jetpack Navigatioin 导航框架,在他的基础上加了一些自己的东西,比如NavHost是注册页面路由的地方,相当于Activity在Manifest注册的情况
大概注册方式如下
NavHost(navController = navController,
startDestination = startDestination,) {
composable("route") {
MainPage()
}
}
然后我们看看composable这个方法,源码里面做了些什么事,首先会把route和arguments包装成一个NavDestination,在添加到NavGraphBuilder的list里面,再build的时候再添加到NavGraph里面
public fun NavGraphBuilder.composable(
route: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
content: @Composable (NavBackStackEntry) -> Unit
) {
addDestination(
ComposeNavigator.Destination(provider[ComposeNavigator::class], content).apply {
this.route = route
arguments.forEach { (argumentName, argument) ->
addArgument(argumentName, argument)
}
deepLinks.forEach { deepLink ->
addDeepLink(deepLink)
}
}
)
}
override fun build(): NavGraph = super.build().also { navGraph ->
navGraph.addDestinations(destinations)
...
}
所以我们想要动态添加路由,只需要获取NavGraph然后添加就行了,NavGraph又能从NavHostController中获取
现在我们来验证一下,模仿composable写一个加载路由的方法composablePlugin
@ExperimentalAnimationApi
fun NavGraphBuilder.composablePlugin(
graph: NavGraph,
route: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit
) {
val match = graph.matchDeepLink(NavDeepLinkRequest.Builder.fromUri(NavDestination.createRoute(route).toUri()).build())
if (match == null)
graph.addDestination(
AnimatedComposeNavigator.Destination(
provider[AnimatedComposeNavigator::class],
content
).apply {
this.route = route
arguments.forEach { (argumentName, argument) ->
addArgument(argumentName, argument)
}
deepLinks.forEach { deepLink ->
addDeepLink(deepLink)
}
}
)
}
上面我们已经验证可以动态加载路由了,现在我们该考虑如何动态加载一个插件apk了,然后再读取和配置里面的路由,再跳转到这个模块的页面
首先我们先建立两个App,一个是main一个就叫other,这里我写了一个简单的组件化用来注册路由的框架,具体大家可以看源码里的实现,大概就是每个module都需要建一个NavGraph的文件,然后在里面注册这个module所有页面的路由,大概如下
val navGraph = composeModules { controller ->
//插件包名
packageName = "com.ckenergy.compose.other"
composable(ComposeRouterMapper.Other.url) {
OtherPage {
controller.navigate1(ComposeRouterMapper.Second.url)
}
}
}
还要写一个动态加载apk的方法
fun loadApk(ctx: Context?, apkPath: String, packageName: String) {
if (ctx == null) {
Log.d(TAG, "ctx is null, apk cannot be loaded dynamically")
throw RuntimeException("ctx is null, apk cannot be loaded dynamically")
}
try {
val appCtx = ctx.applicationContext
val pluginDexClassLoader = DexClassLoader(
apkPath,
appCtx!!.getDir("dex2opt", Context.MODE_PRIVATE).absolutePath,
null,
appCtx!!.classLoader
)
val pluginPackageArchiveInfo =
appCtx!!.packageManager.getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES)!!
val pluginAssets = AssetManager::class.java.newInstance()
val addAssetPath: Method =
AssetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java)
addAssetPath.invoke(pluginAssets, apkPath)
val superResources: Resources? = ctx.resources
val pluginRes = Resources(
pluginAssets,
superResources?.displayMetrics,
superResources?.configuration
)
pluginInfoMap[packageName] = PluginInfo(packageName, appCtx, pluginAssets, pluginRes, pluginDexClassLoader, pluginPackageArchiveInfo)
Log.d(TAG, "dynamic loading of apk success")
} catch (e: Exception) {
Log.d(TAG, "dynamic loading of apk failed")
e.printStackTrace()
}
}
然后再查找apk里面的路由再加载
private fun addRoute(context: Context, controller: NavController) {
try {
val cls =
PluginManager.getPluginInfo(Constants.OTHER_PKG)?.pluginDexClassLoader!!.loadClass(
Constants.OTHER_PKG+".NavGraphKt"
)
val graph = cls.declaredMethods.first().invoke(null) as ModuleBuilder
NavGraphManager.composablePlugIn(controller, graph)
Toast.makeText(context, "add route success", Toast.LENGTH_SHORT).show()
} catch (e: ClassNotFoundException) {
e.printStackTrace()
} catch (e: InstantiationException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
加载完成后调用controller.navigate就可以直接跳转了
这里我们简单写两个按钮,一个加载路由,另外一个跳转,具体可以看源码,最后测试是可以跳转成功的
但是有一个资源获取的问题,插件里面getResource获取的是宿主的,这里我们可能要通过字节码插桩去hook compose包androidx.compose.ui.res.Resources的这个方法resources(),然后返回插件的resource
1、hook resource()方法
2、用ksp生成composeModules简化框架(重新写了一个路由框架KRouter)
最近工作太忙,本来两个月前就写好了这块代码了,想完善好再分享出来的,但是一直没有时间,所以先把自己的发现分享出来,有兴趣的可以完善一下,如果对上面的两个ToDo有想法的欢迎和我交流(2ckenergy@gmail。com),避免大家重复造轮子:)