设置路由框架
背景
小爱语音唤醒指令需要打开设置的各个页面设置页面,需求列表如下 语音控制类功能&配置清单 。由于setting是基于Preference框架,换言之也是单Activity多Fragment框架,由Preference控制Fragment或者View的显示隐藏。
如果直接处理Uri会存在以下问题:
- 处理uri跳转必然会有大量与业务无关的逻辑,夹杂着页面标识判断,
- 参数从顶级页面传入层级过深的问题
- 增加业务的复杂性,降低代码可读性和维护性
因此,该框架目标在解决以上目的的同时希望可以达到:
- 集成简单,无需复杂配置
- 专注,只需处理关注的URI跳转,无需复杂判断
Preference构架

Preference是通过PreferenceController控制所有页面的扭转,因此我们只需要让Controller处理期支持的uri逻辑即可
框架调研
DRouter、 ARouter
https://github.com/alibaba/ARouter/blob/master/README_CN.md
https://github.com/didi/DRouter/wiki
DRouter、 ARouter实现比较类似,都是做了path和View、Fragment、Activity的映射,对于Fragment和View,框架做了初始化,通过路由创建实例并显示,但setting的单Activity多Fragment的Preference构架,Fragment、Controller扭转显示由系统根据xml创建,为了少的业务侵入,不太能改变实现方式,因此这两个框架思路并不适合。但这两个框架都在解决三个核心问题
- 路由表的创建
- 路由表的分发
- Uri行为关联
由于框架的限制,setting不太可能做到path和view的一一映射,但是可以做到路由表分发到具体的Fragment或者Controller,实现一个增强版本EventBus
思路参考:https://github.com/didi/DRouter/wiki/1.-Router#routerhandler%E5%AF%BC%E8%88%AA
框架设计
路由表的创建
- 路由表解析合并
📷 图示:路由表解析合并 diagram
📷 白板:路由表设计
- 路由行为关联
📷 图示:路由行为关联 diagram
路由分发
📷 图示:路由分发 diagram
代码实现
路由表生成
- 注解说明
注解有三个,@Module 用于生成模块的路由表,@Modules 用于生成所有模块的合并帮助类,@RouterProvider 用于生成具体映射
//Module.java
/**
* 模块名称注解,一个模块只需要注解一次
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Module {
String value();
}
//Modules.java
/**
* 模块列表注解,需要列举出所有包含RouterProvider注解的子模块,用于合并路由表
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Modules {
String[] value();
}
//RouterProvider.java
/**
* 用于生产路由表的注解类
* path 用于拦截外部跳转的 uri
* classPath 需要进行处理的类路径,目前只支持 controller 和 fragment,并且需要实现 IPageHandler
*/
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE})
public @interface RouterProvider {
String path() default "";
String classPath() default "";
}- 生成类
//CarSettings 模块映射表
package com.android.car.settings.router;
public final class Module_CarSettings {
public static void map(){
com.android.car.settings.router.manager.RouterTabs.map("carsettings://homepage/?subPage=energy_manage&tab=charge", "com.android.car.settings.miauto.energy.controller.ChargeConfigPreferenceController");
com.android.car.settings.router.manager.RouterTabs.map("carsettings://hudsetting", "com.android.car.settings.miauto.display.HudPhysicalTabLayPrefController");
com.android.car.settings.router.manager.RouterTabs.map("carsettings://homepage/?subPage=energy_manage", "com.android.car.settings.miauto.energy.EnergySettingFragment");
com.android.car.settings.router.manager.RouterTabs.map("carsettings://soundsetting/", "com.android.car.settings.miauto.volume.SoundSettingsFragment");
}
}
//CarSettings 模块映射表
package com.android.car.settings.router;
public final class Module_routerManager {
public static void map(){
com.android.car.settings.router.manager.RouterTabs.map("abc://www.baidu.com", "com.android.car.settings.miauto.display.HudPhysicalTabLayPrefController");
}
}
//合并帮助类
package com.android.car.settings.router;
import java.lang.Exception;
public final class RouterHelper {
public static void merge() {
try {
Class clazz_CarSettings=Class.forName("com.android.car.settings.router.Module_CarSettings");
java.lang.reflect.Method method_CarSettings=clazz_CarSettings.getDeclaredMethod("map");
method_CarSettings.setAccessible(true);
method_CarSettings.invoke(null);
}
catch(Exception e) {
e.printStackTrace();
}
try {
Class clazz_routerManager=Class.forName("com.android.car.settings.router.Module_routerManager");
java.lang.reflect.Method method_routerManager=clazz_routerManager.getDeclaredMethod("map");
method_routerManager.setAccessible(true);
method_routerManager.invoke(null);
}
catch(Exception e) {
e.printStackTrace();
}
}
}- 路由行为关联
controller创建时加入map
Router.java
// 与业务侵入的点
// 由于设置基于Preference框架,大部分逻辑会在controller 处理,因此在创建controller做拦截和映射,方便分发时直接处理
public synchronized void holdController(Object controller, Fragment controllerFragment) {
long startTime = SystemClock.uptimeMillis();
String controllerName = controller.getClass().getTypeName();
String fragmentName = controllerFragment.getClass().getTypeName();
// 如果RouterProvider注解的classPath 是Fragment 或者 controller,onAttach创建时记录实例,和路由映射
if (mControllerNameHash.contains(controllerName.hashCode()) || mControllerNameHash.contains(fragmentName.hashCode())) {
// 注解是controller需要记录controller实例和其所对应的Fragment
if (mControllerNameHash.contains(controllerName.hashCode())) {
mControllerObjects.put(controllerName, new WeakReference<>(controller));
mControllerInFragment.put(controllerName, new WeakReference<>(controllerFragment));
int fragmentHash = controllerFragment.hashCode();
HashSet<String> controllerNames = mFragmentControllers.get(fragmentHash);
if (controllerNames == null) {
controllerNames = new HashSet();
mFragmentControllers.put(fragmentHash, controllerNames);
}
controllerNames.add(controller.getClass().getTypeName());
}
// 注解是Fragment需要记录hash
mFragmentHash.add(controllerFragment.hashCode());
}
long endTime = SystemClock.uptimeMillis();
Log.i("Router", "holdController time=" + (endTime - startTime));
}路由表分发
activity创建监听Fragment生命周期
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
if (activity instanceof FragmentActivity) {
FragmentActivity fragmentActivity = ((FragmentActivity) activity);
FragmentManager fragmentManager = fragmentActivity.getSupportFragmentManager();
// 监控Activity 对应的Fragment生命周期
if (fragmentManager != null) {
fragmentManager.registerFragmentLifecycleCallbacks(buildFragmentLifeCallBack(), false);
}
}
}Fragment可见后分发uri
@Override
public void onFragmentResumed(@NonNull FragmentManager fm, @NonNull Fragment f) {
// 选择的分发时机为Resume
// 由于项目是Activity 多 Fragment 架构,因此分发过早会导致UI处理需要delay,Resume Fragment 可见,时机比较晚,业务侧如果时机不是特别靠后,无需额外处理
super.onFragmentResumed(fm, f);
long startTime = SystemClock.uptimeMillis();
boolean dealUri = false;
dealUri = dealUri || router.dispatcherUri(f);
/**
* Fragment 嵌套的情况下 fragmentManager.registerFragmentLifecycleCallbacks(buildFragmentLifeCallBack(), true);
* 只会回调子Fragment事件,false 只会回调父Fragment,因此这里通过父Fragment做分发,多重嵌套的情况下,这里没做处理,处理到二级
* 多重嵌套会导致Fragment生命周期的复杂性和不确定性,暂时不考虑
* */
List<Fragment> fragments = f.getChildFragmentManager().getFragments();
if (fragments.size() > 0) {
for (Fragment fragment : fragments) {
if (!fragment.isHidden()) {
boolean result = router.dispatcherUri(fragment);
dealUri = dealUri || result;
}
}
}
if (dealUri) {
router.postUri = null;
}
long endTime = SystemClock.uptimeMillis();
Log.i("Router", "dispatcherUri time=" + (endTime - startTime));
}处理跳转
@RouterProvider(path = "carsettings://soundsetting/")
class SoundSettingsFragment : TopLevelMenuFragment() ,IPageRouteHandler {
override fun getPreferenceScreenResId() = R.xml.miauto_sound_settings_fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requestPreferenceHighlight(getString(R.string.pk_sound_field_entry))
}
override fun handleJump(data: Uri?) {
var page = data?.getQueryParameter(CarSettingsJump.CarVoiceSetting.VOICE_SETTINGS_PAGE)
var key = "";
when(page){
CarSettingsJump.CarVoiceSetting.VOICE_PAGE_SEAT_EFFECT->{
key = getString(R.string.pk_sound_field_entry)
}
CarSettingsJump.CarVoiceSetting.VOICE_PAGE_SOUND_EFFECT->{
key = getString(R.string.pk_sound_effect_field_entry)
}
CarSettingsJump.CarVoiceSetting.VOICE_PAGE_BALANCE->{
key = getString(R.string.pk_sound_balance_entry)
}
}
if(key != ""){
performMenuClick(key)
}
}
}Uri匹配
Router.java
HashMap<String, String> classHashMap = RouterTabs.tabs();
Iterator<String> iterator = classHashMap.keySet().iterator();
LinkedList<String> routers = new LinkedList<>(); // 取出所有已经配置的路由表可能匹配的处理器
while (iterator.hasNext()) {
String saveUri = iterator.next();
if (TextUtils.isEmpty(saveUri)) {
break;
}
if (uri.contains(saveUri) || uri.startsWith(saveUri) || TextUtils.equals(uri, saveUri)) {
routers.add(saveUri);
}
}新增路由
添加路由注解
如果是子模块或者主模块,需要添加一次module注解,一般注解到模块Application里,目前子模块的Application生命周期暂时未实现,后续可以加入
// 由于系统编译暂时不支持kapt,因此用该方式做代理,后续直接注解到相应的类上即可
@Module("CarSettings") // 生成模块规则映射,统一模块只需要注册一次即可
@Modules({"CarSettings", "routerManager"}) // 生成子模块合并类,需要配置所有配置有注解模块的module,否则不会生成合并类,需要在主模块的代理类注册
public class RouterProviderDelegate {
}
@Module("CarSettings") // 生成模块规则映射,统一模块只需要注册一次即可
@Modules({"CarSettings", "routerManager"}) // 生成子模块合并类,需要配置所有配置有注解模块的module,否则不会生成合并类,需要在主模块的代理类注册
public class SettingsApplication extends Application {
}添加RouterProvider注解
// Hud 注解代理
/**
*path 自己关注的uri
*classPath 实现handleJump的类路径
*由于暂时不支持kapt,因此现有方案需要配置path,直接在实现类注解也是代码支持,不过make不支持kapt
*/
@RouterProvider(path = "carsettings://hudsetting", classPath = "com.android.car.settings.miauto.display.HudPhysicalTabLayPrefController")
public static class HudControllerDelegate {
}
// 关注的uri,一般为获取参数的上一级 如 carsettings://soundsetting/?subPage=seat_effect
@RouterProvider(path = "carsettings://soundsetting/")
class SoundSettingsFragment : TopLevelMenuFragment() ,IPageRouteHandler {
}实现IPageRouteHandler接口
实现IPageRouteHandler接口,目前只支持fragment和fragment依托的controller
class HudPhysicalTabLayPrefController(
context: Context?, preferenceKey: String?,
fragmentController: FragmentController?, uxRestrictions: CarUxRestrictions?
) : BaseSinglePropTabLayPrefController(context, preferenceKey, fragmentController, uxRestrictions),
KeyDownCallback, IPageRouteHandler {
override fun handleJump(uri: Uri) {
var stringTab: String? =
uri.getQueryParameter(CarSettingsJump.CarHUDSettings.HUD_SETTINGS_SCREEN_TAB) ?: return
//冷启下,需要等待tab绘制完成
mJumpRunnable = Runnable {
var index = -1
when (stringTab) {
CarSettingsJump.CarHUDSettings.SCREEN_TAB_HEIGHT_SWITCH -> index = 0
CarSettingsJump.CarHUDSettings.SCREEN_TAB_BRIGHTNESS_SWITCH -> index = 1
CarSettingsJump.CarHUDSettings.SCREEN_TAB_ANGLE_SWITCH -> index = 2
}
if (index >= 0) {
mSelectIndex = index
notifyAllTabs()
}
}
if (preference.adapter != null) {
mJumpRunnable?.run()
mJumpRunnable = null
}
}
}
@RouterProvider(path = "carsettings://soundsetting/")
class SoundSettingsFragment : TopLevelMenuFragment() ,IPageRouteHandler {
override fun handleJump(data: Uri?) {
var page = data?.getQueryParameter(CarSettingsJump.CarVoiceSetting.VOICE_SETTINGS_PAGE)
var key = "";
when(page){
CarSettingsJump.CarVoiceSetting.VOICE_PAGE_SEAT_EFFECT->{
key = getString(R.string.pk_sound_field_entry)
}
CarSettingsJump.CarVoiceSetting.VOICE_PAGE_SOUND_EFFECT->{
key = getString(R.string.pk_sound_effect_field_entry)
}
CarSettingsJump.CarVoiceSetting.VOICE_PAGE_BALANCE->{
key = getString(R.string.pk_sound_balance_entry)
}
}
if(key != ""){
performMenuClick(key)
}
}
}后续规划和现有问题
消除@Module,@Modules注解
apt支持传递参数,我们在gradle或者bp文件中配置相应的apt参数用于标识当前apt工作的模块,然后在apt中获取
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mMessager = processingEnv.getMessager();
module = processingEnv.getOptions().get("module");
String modulesNames = processingEnv.getOptions().get("modules");
if (modulesNames != null && !modulesNames.isEmpty()) {
modules = modulesNames.split(",");
}
}gradle配置
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
javaCompileOptions {
annotationProcessorOptions {
arguments = [module: 'CarSettings', modules : 'CarSettings,routerManager']
}
}
}Android.bp配置
//主module Android.bp
javacflags: ["-Amodule=CarSettings",-Amodules=CarSettings,routerManager],存在问题
在多个module都需要生成路由表时,android_library中配置javacflags可以正常获取参数,android_app则无法获取,但是在gradle下编译正常无问题
RouterHelper消除反射
RouterHelper的merge方法存在反射,影响少量性能,可以直接调用,processer处理如下
StringBuilder builder = new StringBuilder();
builder.append("package " + Constans.PACKAGE + ";\n\n");
builder.append("public class RouterHelper {\n\n")
.append("\tpublic static void merge(){\n");
for (String name : moduleNames) {
builder.append("\t\t" + Constans.PACKAGE + ".")
.append(Constans.MODULE_PREFIX)
.append(name)
.append(".map();\n");
}
builder.append("\t}\n}\n");
if (mFiler != null) {
try {
JavaFileObject file = this.mFiler.createSourceFile(Constans.PACKAGE + ".RouterHelper");
Writer writer = file.openWriter();
writer.append(builder.toString());
writer.flush();
writer.close();
} catch (IOException e) {
mMessager.printMessage(Diagnostic.Kind.ERROR, "RouterUriProcessor create RouterHelper.java Exception");
e.printStackTrace();
}生成类如下
//RouterHelper.java
public class RouterHelper {
public static void merge(){
com.android.car.settings.router.Module_CarSettings.map();
com.android.car.settings.router.Module_routerManager.map();
}
}make下编译存在找不到子模块类的情况,gradle无问题。transform可以解决该问题
--------make编译--------------------
warning: 警告: 来自注释处理程序 'org.jetbrains.kotlin.kapt3.base.ProcessorWrapper' 的受支持 source 版本 'RELEASE_8' 低于 -source '9'
info: 注: process() start : env.processingOver()=false
info: 注: RouterUriProcessor init
info: 注: RouterUriProcessor process start
info: 注: RouterUriProcessor process parser Annotations
info: 注: process() start : env.processingOver()=false
info: 注: RouterUriProcessor process start
info: 注: RouterUriProcessor process parser Annotations
info: 注: process() start : env.processingOver()=true
warning: 警告: 将不对在最后一个循环中创建的类型为 'com.android.car.settings.miauto.voiceassist.VoiceSearchWidgetProvider' 的文件进行注释处理。
info: 注: RouterUriProcessor process generaClass
info: 注: RouterUriProcessor create Module_CarSettings.java
info: 注: RouterUriProcessor parser RouterProvider path=carsettings://homepage/?subPage=energy_manage&tab=charge
classPath=com.android.car.settings.miauto.energy.controller.ChargeConfigPreferenceController
info: 注: RouterUriProcessor parser RouterProvider path=carsettings://soundsetting/
classPath=com.android.car.settings.miauto.volume.SoundSettingsFragment
info: 注: RouterUriProcessor parser RouterProvider path=carsettings://hudsetting
classPath=com.android.car.settings.miauto.display.HudPhysicalTabLayPrefController
info: 注: RouterUriProcessor parser RouterProvider path=carsettings://homepage/?subPage=energy_manage
classPath=com.android.car.settings.miauto.energy.EnergySettingFragment
warning: 警告: 将不对在最后一个循环中创建的类型为 'com.android.car.settings.router.Module_CarSettings' 的文件进行注释处理。
info: 注: RouterUriProcessor create RouterHelper.java
warning: 警告: 将不对在最后一个循环中创建的类型为 'com.android.car.settings.router.RouterHelper' 的文件进行注释处理。
info: 注: RouterUriProcessor process end
error: /media/huang/D1/code/enuma-dev/qssi/out/soong/.intermediates/packages/apps/Car/Settings/CarSettings/android_common/kapt/gen/sources/com/android/car/settings/router/RouterHelper.java:4: 错误: 找不到符号
import com.android.car.settings.router.Module_routerManager;统一的DispatcherActivity
目前Activity的配置分散在各个具体处理scheme的xml配置中,后续如果支持Uri业务过多,或者统一加拦截会存在不方便性,可以所有的uri跳转统一走DispatcherActivity,然后通过DispatcherActivity分发到具体的Activity,具体的Activity再分发到执行的controller
支持uri优先级和拦截器
由于第一版本框架较为初级和简单,为了解耦和方便配置,无太多增强功能,后续可以实现
支持uri参数解析
override fun handleJump(data: Uri?) {
var page = data?.getQueryParameter(CarSettingsJump.CarVoiceSetting.VOICE_SETTINGS_PAGE)
var key = "";
when(page){
CarSettingsJump.CarVoiceSetting.VOICE_PAGE_SEAT_EFFECT->{
key = getString(R.string.pk_sound_field_entry)
}
CarSettingsJump.CarVoiceSetting.VOICE_PAGE_SOUND_EFFECT->{
key = getString(R.string.pk_sound_effect_field_entry)
}
CarSettingsJump.CarVoiceSetting.VOICE_PAGE_BALANCE->{
key = getString(R.string.pk_sound_balance_entry)
}
}
if(key != ""){
performMenuClick(key)
}
}目前Router没有处理query参数,handleJump都需要处理自己的参数,这一块工作可以交给apt处理,使业务更专注