• 严选 Android 路由框架优化(下篇)


    3 router 框架优化

    3.1 apt 生成代码量过大问题优化

    思考框架本身,其实可以发现仅有 router 映射表是需要根据注解编译生成的,其他的全部代码都是固定代码,完全可以 sdk 中直接编码提供。反过来思考为何当初 sdk 开发需要编写繁重的 apt 生成代码,去生成这些固定的逻辑,可以发现 htrouterdispatch-process 工程是一个纯 java 工程,部分纯 java 类的提供在 htrouterdispatch。由于无法引用 Android 类,同时期望业务层接口能完美隐藏内部实现,为此和 Android 相关的类,索性全部由 apt 生成。

    apply plugin: 'java' // 使用 apply plugin: 'com.android.library' 编译报错
    
    sourceCompatibility = JavaVersion.VERSION_1_7
    targetCompatibility = JavaVersion.VERSION_1_7
    
    dependencies {
        compile project (':htrouterdispatch')
        compile 'com.google.auto.service:auto-service:1.0-rc2'
        compile 'com.squareup:javapoet:1.0.0'
    }
    

    为了解决这里的问题,我们可以稍微降低对实现封装的隐藏程度,修改初始化接口,需要业务层将 router 映射表显式的传入。修改后就能发现仅有 HTRouterTable 里面的映射表接口需要 apt 生成,而其余的代码均可通过直接编码。

    HTRouterManager.init();
    →
    HTRouterManager.init(HTRouterTable.pageRouters(),
                    HTRouterTable.methodRouters(),
                    HTRouterTable.interceptors());
    

    HTRouterTable.methodRouters() 和 HTRouterTable.interceptors() 先忽略,后续解释

    新建了一个 Android Library htrouter,引用工程 htrouterdispatch,app 工程修改引用 htrouter

    经过优化,router 跳转的逻辑代码可通过直接编码方式实现,普通 Android 开发也能轻松修改其中的逻辑,同时 apt 生成的类从 6 个直接减少至 1 个 HTRouterTable。若出现业务层代码编译错误导致 apt 生成失败,最终导致编译器提示 HTRouterTable not found,可仅需注释掉初始化代码即可。

    /**
     * 用于用户启动Activity或者通过URL获得可以跳转的目标
     */
    public final class HTRouterTable {
      public static final String HT_URL_PARAMS_KEY = "ht_url_params_map";
    
      private static final List<HTRouterEntry> PAGE_ROUTERS = new LinkedList<HTRouterEntry>();
    
      private static final List<HTInterceptorEntry> INTERCEPTORS = new LinkedList<HTInterceptorEntry>();
    
      private static final List<HTMethodRouterEntry> METHOD_ROUTERS = new LinkedList<HTMethodRouterEntry>();
    
      public static List<HTRouterEntry> pageRouters() {
        if (PAGE_ROUTERS.isEmpty()) {
          PAGE_ROUTERS.add(new HTRouterEntry("com.netease.yanxuan.module.home.category.activity.CategoryPushActivity", "yanxuan://homepage_categoryl2", 0, 0, false));
          ...
        }
        return PAGE_ROUTERS;
      }
    
      public static List<HTInterceptorEntry> interceptors() {
        if (INTERCEPTORS.isEmpty()) {
          PAGE_ROUTERS.add(new HTRouterEntry("com.netease.yanxuan.module.home.recommend.activity.TagActivity", "yanxuan://tag", 0, 0, false));
          ...
        }
        return INTERCEPTORS;
      }
    
      public static List<HTMethodRouterEntry> methodRouters() {
        if (METHOD_ROUTERS.isEmpty()) {
           {
            List<Class> paramTypes = new ArrayList<Class>();
            paramTypes.add(Context.class);
            paramTypes.add(String.class);
            paramTypes.add(int.class);
            METHOD_ROUTERS.add(new HTMethodRouterEntry("http://www.you.163.com/jumpA", "com.netease.hearttouch.example.JumpUtil", "jumpA", paramTypes));
          }
          ...
        }
        return METHOD_ROUTERS;
      }
    }
    

    3.2 拦截器优化

    3.2.1 优化前临时方案

    针对登录拦截需求,当时的临时解决方案如下:

    1. 路由注解添加 needLogin 字段
    2. 并修改 apt 生成代码,使 HTRouterEntry 记录 needLogin 信息
    3. 提供 RouterUtil.startActivity 将目标页面的跳转构建成一个 runnable 传入,在登录成功回调中执行 runnable
    @HTRouter(url = {PreemptionActivateActivity.ROUTER_URL}, needLogin = true)
    public class PreemptionActivateActivity extends Activity {
        ...
    }
    
    public static boolean startActivity(final Context context, final String schemeUrl,
                                        final Intent sourceIntent, final boolean isFinish) {
    
        return doStartActivity(context, schemeUrl, new Runnable() {
            @Override
            public void run() {
                HTRouterManager.startActivity(context, schemeUrl, sourceIntent, isFinish);
            }
        });
    }
    
    private static boolean doStartActivity(final Context context, final String schemeUrl,
                                     final Runnable runnable) {
    
        if (HTRouterManager.isUrlRegistered(schemeUrl)) {
            HTRouterEntry entry = HTRouterManager.findRouterEntryByUrl(schemeUrl);
            if (entry == null) {
                return false;
            }
    
            if (entry.isNeedLogin() && !UserInfo.isLogin()) {
                LoginActivity.setOnLoginResultListener(new OnLoginResultListener() {
                    @Override
                    public void onLoginSuccess() {
                        runnable.run();
                    }
    
                    @Override
                    public void onLoginFail() {
                        // do nothing
                    }
                });
                LoginActivity.start(context);
            }
    
            return true;
        }
    
        return false;
    }

    可以发现这种处理方式并不通用,同时需要业务层代码全部修改调用方式,未修改的接口还是可能出现以未登录态进入需要登录的页面(这种情况也确实在后面发生过,后来我们要求前端跳转之前,先通过 jsbridge 唤起登录页面(⊙﹏⊙)b)。我们需要一种通用规范的方式处理拦截逻辑,同时能适用各种场景,也能规避业务层的错误。

    3.2.2 拦截器优化和设计

    为避免业务层绕过拦截器直接调用到 HTRouterManager,将 HTRouterManager.startActivity 等接口修改为 package 引用范围,此外新定义 HTRouterCall 作为对外接口类。

    public class HTRouterCall implements IRouterCall {
        ...
    }
    
    public interface IRouterCall {
        // 继续路由跳转
        void proceed();
        // 继续路由跳转
        void cancel();
        // 获取路由参数
        HTRouterParams getParams();
    }
    

    定义拦截器 interface 如下:

    public interface IRouterInterceptor {
        void intercept(IRouterCall call);
    }

    总结拦截的需求场景,归纳拦截场景为 3 种:

    1. 全局拦截 → 全局拦截器

      全局拦截器,通过静态接口设置添加

       public static void addGlobalInterceptors(IRouterInterceptor... interceptors) {
           Collections.addAll(sGlobalInterceptors, interceptors);
       }
      

      登录拦截需求可以理解是一个全局的需求,全部的 Activity 跳转都需要判断是否需要唤起登录页面。

       public class LoginRouterInterceptor implements IRouterInterceptor {
      
           @Override
           public void intercept(final IRouterCall call) {
               HTDroidRouterParams params = (HTDroidRouterParams) call.getParams();
               HTRouterEntry entry = HTRouterManager.findRouterEntryByUrl(params.url);
               if (entry == null) {
                   call.cancel();
                   return;
               }
      
               if (entry.isNeedLogin() && !UserInfo.isLogin()) {
                   LoginActivity.setOnLoginResultListener(new OnLoginResultListener() {
                       @Override
                       public void onLoginSuccess() {
                           call.proceed();
                       }
      
                       @Override
                       public void onLoginFail() {
                           call.cancel();
                       }
                   });
                   LoginActivity.start(params.getContext());
               } else {
                   call.proceed();
               }
           }
       }
      

      登录拦截效果

    2. 业务页面固定拦截 → 注解拦截器

      上面剩余的 7 个 switch-case 拦截,可以理解为特定业务页面唤起都必须进入的一个拦截处理,分别定义 7 个拦截器类,同样通过注解的方式标记。

      以 yanxuan://category 为例子

       @HTRouter(url = {"yanxuan://category", "yanxuan://categoryl2"})
       public class CategoryL2Activity extends Activity {
           ...
       }

      对应的注解拦截器

       @HTRouter(url = {"yanxuan://category"})
       public class CategoryL2Intercept implements IRouterInterceptor {
      
           @Override
           public void intercept(IRouterCall call) {
               HTRouterParams routerParams = call.getParams();
               Uri uri = Uri.parse(routerParams.url);
      
               // routerParams.url 添加额外参数
               Uri.Builder builder = uri.buildUpon();
               ...
               routerParams.url = builder.build().toString();
      
               call.proceed();
           }
       }

      apt 生成拦截器初始化代码

       public static List<HTInterceptorEntry> interceptors() {
           if (INTERCEPTORS.isEmpty()) {
               ...
               INTERCEPTORS.add(new HTInterceptorEntry("yanxuan://category", new CategoryL2Intercept()));
               ...
           }
           return INTERCEPTORS;
       }
      

      HTRouterTable

    1. 业务页面动态拦截

      比如 onClick 方法内执行路由跳转时,需要弹窗提示用户是否继续跳转,其他场景跳转并不需要这个弹窗,这种场景的拦截器我们认为是动态拦截

       HTRouterCall.newBuilder(data.schemeUrl)
           .context(mContext)
           .interceptors(new IRouterInterceptor() {
               @Override
               public void intercept(final IRouterCall call) {
                   Log.i("TEST", call.toString());
                   AlertDialog dialog = new AlertDialog.Builder(mContext)
                           .setTitle("alert")
                           .setMessage("是否继续")
                           .setPositiveButton("继续", new DialogInterface.OnClickListener() {
                               @Override
                               public void onClick(DialogInterface dialog, int which) {
                                   call.proceed();
                               }
                           })
                           .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                               @Override
                               public void onClick(DialogInterface dialog, int which) {
                                   call.cancel();
                               }
                           }).create();
                   dialog.show();
               }
           })
           .build()
           .start();

                  

                   优先级:动态拦截器 > 注解拦截器 > 全局拦截器

    3.3 sdk 页面 router 支持

    我们接入了七鱼、HTImagePick 等 sdk,这些 sdk 也有自己的页面,而这部分页面并不能通过前面的路由方式打开,其原因如下:

      1. 我们不能修改他们的代码
      2. apt 处理的注解仅能针对引入 apt 的 app 工程
      3. 对应的页面唤起需要通过 sdk 提供的特殊接口唤起

         public static void openYsf(Context context, String url, String title, String custom) {
             ConsultSource source = new ConsultSource(url, title, custom);
             Unicorn.openServiceActivity(context, // 上下文
                     title, // 聊天窗口的标题
                     source // 咨询的发起来源,包括发起咨询的url,title,描述信息等
             );
         }
        

        七鱼客服页面唤起

    • public void openImagePick(Context context, ArrayList<PhotoInfo> photoInfos, boolean multiSelectMode, int maxPhotoNum, String title) { HTPickParamConfig paramConfig = new HTPickParamConfig(HTImageFrom.FROM_LOCAL, null, photoInfos, multiSelectMode, maxPhotoNum, title); HTImagePicker.INSTANCE.start(context, paramConfig, this); }

    基于此,只需要提供对方法的 router 调用,就能支持 sdk 中的页面路由跳转。具体用法示例如下

      1. 通过 HTMethodRouter 注解标记跳转方法(非静态方法需实现 getInstance 单例)

         public class JumpUtil {
        
             private static final String TAG = "JumpUtil";
             private static JumpUtil sInstance = null;
        
             public static JumpUtil getInstance() {
                 if (sInstance == null) {
                     synchronized (JumpUtil.class) {
                         if (sInstance == null) {
                             sInstance = new JumpUtil();
                         }
                     }
                 }
                 return sInstance;
             }
        
             private JumpUtil() {
             }
        
             @HTMethodRouter(url = {"http://www.you.163.com/jumpA"}, needLogin = true)
             public void jumpA(Context context, String str, int i) {
                 String msg = "jumpA called: str=" + str + "; i=" + i;
                 Log.i(TAG, msg);
                 if (context != null) {
                     Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
                 }
             }
        
             @HTMethodRouter(url = {"http://www.you.163.com/jumpB"})
             public static void jumpB(Context context, String str, int i) {
                 String msg = "jumpB called: str=" + str + "; i=" + i;
                 Log.i(TAG, msg);
                 if (context != null) {
                     Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
                 }
             }
        
             @HTMethodRouter(url = {"http://www.you.163.com/jumpC"})
             public void jumpC() {
                 Log.i(TAG, "jumpC called");
             }
         }
        
      2. 方法路由触发逻辑

        除了设置动画、是否关闭当前页面等参数,这里方法路由的调用方式和页面路由完全一致,同样支持 needLogin 字段,同样支持全局拦截器、注解拦截器、动态拦截器

         // JUMPA 按钮点击
         public void onMethodRouter0(View v) {
             HTRouterCall.call(MainActivity.this, "http://www.you.163.com/jumpA?a=lilei&b=10");
         }
        
         // JUMPB 按钮点击
         public void onMethodRouter1(View v) {
             HTRouterCall.call(MainActivity.this, "http://www.you.163.com/jumpB?a=hanmeimei&b=10");
         }
        
         // JUMPC 按钮点击
         public void onMethodRouter2(View v) {
             HTRouterCall.call(MainActivity.this, "http://www.you.163.com/jumpC");
         }
        
      3. 结果示例

              

    3.4 main dex 优化处理

    这里的处理逻辑较为简单,仅需修改类引用为类名字符串,后续跳转时通过反射获取类

    public static List<HTRouterEntry> routers() {
        if (ROUTERS.isEmpty()) {
            ...
            ROUTERS.add(new HTRouterEntry("com.netease.yanxuan.module.subject.SubjectActivity", "yanxuan://subject", 0, 0, false));
            ...
        }
        return ROUTERS;
    }
    

    4 总结

    通过优化拦截器,解决登录拦截问题,优化子模块和全局代码划分;通过提供方法路由,解决 sdk 页面的路由跳转问题;通过区分路由表生成代码和其他跳转逻辑,优化 apt 代码生成逻辑的复杂性和和维护性;通过修改路由表对类的直接引用,解决 main-dex 问题。

    除此之外,路由框架并未对 module 子工程的 Activity 做路由集成,严选当前也没做更进一步的业务组件化。后续有需求进一步补充文章。

    相关阅读:严选 Android 路由框架优化(上篇)

    本文来自网易云社区,经作者张云龙授权发布。

    原文地址:严选 Android 路由框架优化(下篇)

    更多网易研发、产品、运营经验分享请访问网易云社区

  • 相关阅读:
    Python 函数 I
    jmeter-将上一个接口的返回值作为下一个接口的参数
    Python 文件的操作
    Python 基础数据类型 VI
    Pyhton 基础数据类型 V (补充)
    Python 基础数据类型 IV (集合)
    Python 基础数据类型 III (字典)
    难道是你?
    是你啦
    checkWeb
  • 原文地址:https://www.cnblogs.com/163yun/p/9284079.html
Copyright © 2020-2023  润新知