设置路由框架

背景

小爱语音唤醒指令需要打开设置的各个页面设置页面,需求列表如下 语音控制类功能&配置清单 。由于setting是基于Preference框架,换言之也是单Activity多Fragment框架,由Preference控制Fragment或者View的显示隐藏。

如果直接处理Uri会存在以下问题:

  1. 处理uri跳转必然会有大量与业务无关的逻辑,夹杂着页面标识判断,
  2. 参数从顶级页面传入层级过深的问题
  3. 增加业务的复杂性,降低代码可读性和维护性

因此,该框架目标在解决以上目的的同时希望可以达到:

  1. 集成简单,无需复杂配置
  2. 专注,只需处理关注的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创建,为了少的业务侵入,不太能改变实现方式,因此这两个框架思路并不适合。但这两个框架都在解决三个核心问题

  1. 路由表的创建
  2. 路由表的分发
  3. Uri行为关联

由于框架的限制,setting不太可能做到path和view的一一映射,但是可以做到路由表分发到具体的Fragment或者Controller,实现一个增强版本EventBus

思路参考:https://github.com/didi/DRouter/wiki/1.-Router#routerhandler%E5%AF%BC%E8%88%AA

框架设计

路由表的创建

  1. 路由表解析合并

📷 图示:路由表解析合并 diagram

📷 白板:路由表设计

  1. 路由行为关联

📷 图示:路由行为关联 diagram

路由分发

📷 图示:路由分发 diagram

代码实现

路由表生成

  1. 注解说明

注解有三个,@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 "";
}
  1. 生成类
//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();
    }
  }
}
  1. 路由行为关联

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处理,使业务更专注