浅谈Android模块化设计(路由框架ARouter源码分析)

2017年3月26日 12点热度 0条评论 来源: Magician锋

首先着重分析下Arouter的源码设计,主要研究这个框架的原因,一个是它算比较新的框架,功能较为全面和强大,并且不断在维护。其次和作者是一个公司的,可以有更深层的交流。

先放一篇作者的演讲既要,这个也是分析源码时候思路的参考和本文很多图片的来源。这篇文章中,也很好的说明了为什么要设计路由的原因以及所带来的好处。

ARouter整体结构


首先来看下整个Arouter的大致代码结构。

整个代码结构应该是分成三个module。

  • 图中API,基本是路由框架在初始化,和工作过程中的相关代码。

    • Launcher 层中主要是ARouter实例的实现和各种对外提供链式调用的API。
    • Service 是对外跳转过程中,系统定义的一些Service能力,如拦截,降级,替换跳转路径等。CallBack 是某些服务的回调,业务根据回调进行相应的处理,如跳转失败时候的处理等。Template是一些模版代码,基本都是接口,在注解编译器生成代码,或者业务需要时,实例化接口,实现相应的功能。
    • Ware House那一层,主要处理,框架运行过程中,缓存,线程,日志,异常等相关的操作。
    • Logistics Center 主要是路由机制的初始化,和跳转过程处理参数相关流程。
    • 这里图中,有个关键的对象没有画出,Postcard,在跳转过程中,这个对象用于携带一些参数,和相关属性,无论是跳到目标页面后,还是发生拦截的过程中,框架处理的流程中,均会用到这些信息。
  • Compiler 是个注解处理器。

    • Router Processor 用于处理路由信息的注解,生成路由表的信息。
    • Interceptor Processor 用于处理拦截器相关的信息。
    • Autowire Processor 用于处理跳转目标对象中自动绑定的变量,当URL携带参数跳转时候,可以直接对变量传值。
  • 还有一个Annotaiton的module没有画在图中,那个主要定义一些注解,声明在需要被注解的Activity或者变量中,然后通过注解处理器,可以自动生成路由表等信息。

模块化后的代码结构


下面看下结合ARouter框架进行改造后的代码结构。

  • root 和 group 对应了目标页面
  • interceptor 是跳转目标页面的过程中,所进行统一拦截操作,会进行一些统一的处理动作
  • provider 这个是控制控制反转的provider节点,同时通过povider,也提供对外的一种 service 能力,service 中定义一些方法,可以让别的页面,通过路由进行跨模块调用

和一些较为简单的模块化框架相比,ARouter中 引入了组的概念,将同一模块的模块的目标页面分成不同小组,然后通过一个根节点统一管理,这样的好处,是可以根据需要,按需加载模块。具体可以参考作者的一段话,图就不引用了。

ARouter在初始化的时候只会一次性地加载所有的root结点,而不会加载任何一个Group结点,这样就会极大地降低初始化时加载结点的数量。因为每个模块中可能有N个分组,每个分组中可能有N个页面,如果一次性地将所有的页面全部加载进来,那么整个复杂度可能不只是O(N^2),但是每个模块都只加载其根节点,从算法的角度考虑可能就是复杂度为O(N)的方案,也就是有多少个模块就只需要加载多少个结点。下图中的三个圈中体现的就是ARouter初始化时加载的状况。那么什么时候加载分组结点呢?其实就是当某一个分组下的某一个页面第一次被访问的时候,整个分组的全部页面都会被加载进去,这就是ARouter的按需加载。其实在整个APP运行的周期中,并不是所有的页面都需要被访问到,可能只有20%的页面能够被访问到,所以这时候使用按需加载的策略就显得非常重要了,这样就会减轻很大的内存压力。

工作流程

再看下经过 ARouter 模块化后的代码是如何工作的。主要步骤在上图中已经有了说明,初始化时候,会加载对应的映射文件到缓存中,然后通过URL跳转目标页面时候,会通过映射文件的规则,经由Arouter 模块进行跳转,拦截,操作服务中方法等动作,从而达到模块间解耦。

注解处理器对路由的处理过程

主要用 Router Processor 为例,简要分析下注解处理器,生成一个URL路由映射关系的流程。在如下的示例代码中,@Route的path中,会和当前目标页面Test1Activity 进行匹配,被 @Autowired 所注解的变量,可以直接通过跳转的URL被赋值。

/** * https://m.aliyun.com/test/activity1?name=老王&age=23&boy=true&high=180 */
@Route(path = "/test/activity1")
public class Test1Activity extends AppCompatActivity { 

    @Autowired
    String name;

    @Autowired
    int age;

    @Autowired(name = "boy")
    boolean girl;

    @Autowired
    TestParcelable pac;

    @Autowired
    TestObj obj;

关于注解处理器,可以参考 Android APT(编译时代码生成)最佳实践 了解基本用法。

在注解处理器中,和路由信息相关的主要有两个比较重要的数据结构,分别表示来组的关系,和根结点。

    private Map<String, Set<RouteMeta>> groupMap = new HashMap<>(); // ModuleName and routeMeta.
    private Map<String, String> rootMap = new TreeMap<>();  // Map of root metas, used for generate class file in order.

首先在 process 方法中拿到 被Route注解的元素。

 Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
    try {
        logger.info(">>> Found routes, start... <<<");
        this.parseRoutes(routeElements);

    } catch (Exception e) {
        logger.error(e);
    }

然后交给 parseRoutes 流程完成解析,并生成代码。
生成代码的流程用到来 javapoet 这个第三方库来生成路由映射关系的代码,而不是靠纯的硬编码的方式。

类似参数类型名,参数名称,方法等都可以通过api的方式来生成,最后组装成代码。

目标生成路由表结构如下

public class ARouter$$Group$$test implements IRouteGroup {  @Override public void loadInto(Map<String, RouteMeta> atlas) { atlas.put("/test/activity1", RouteMeta.build(RouteType.ACTIVITY, Test1Activity.class, "/test/activity1", "test", new java.util.HashMap<String, Integer>(){ { put("name", 18); put("boy", 0); put("age", 3); put("url", 18); }}, -1, -2147483648)); atlas.put("/test/activity2", RouteMeta.build(RouteType.ACTIVITY, Test2Activity.class, "/test/activity2", "test", new java.util.HashMap<String, Integer>(){ { put("key1", 18); }}, -1, -2147483648)); atlas.put("/test/activity3", RouteMeta.build(RouteType.ACTIVITY, Test3Activity.class, "/test/activity3", "test", new java.util.HashMap<String, Integer>(){ { put("name", 18); put("boy", 0); put("age", 3); }}, -1, -2147483648)); atlas.put("/test/activity4", RouteMeta.build(RouteType.ACTIVITY, Test4Activity.class, "/test/activity4", "test", null, -1, -2147483648)); atlas.put("/test/webview", RouteMeta.build(RouteType.ACTIVITY, TestWebview.class, "/test/webview", "test", null, -1, -2147483648)); } }

具体在 parseRoutes 解析过程中的流程如下。

  • 生成对应的参数类型。
ParameterizedTypeName inputMapTypeOfGroup = ParameterizedTypeName.get(
                    ClassName.get(Map.class),
                    ClassName.get(String.class),
                    ClassName.get(RouteMeta.class)
            );
  • 创建参数名称。
ParameterSpec groupParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "atlas").build();
  • 创建loadinfo方法。
MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
                        .addAnnotation(Override.class)
                        .addModifiers(PUBLIC)
                        .addParameter(groupParamSpec);
  • 补全方法体的内容。
 loadIntoMethodOfGroupBuilder.addStatement(
                            "atlas.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, " + (StringUtils.isEmpty(mapBody) ? null : ("new java.util.HashMap<String, Integer>(){ {" + mapBodyBuilder.toString() + "}}")) + ", " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",
                            routeMeta.getPath(),
                            routeMetaCn,
                            routeTypeCn,
                            ClassName.get((TypeElement) routeMeta.getRawType()),
                            routeMeta.getPath().toLowerCase(),
                            routeMeta.getGroup().toLowerCase());
  • 将类进行组装,并输出成类文件。
String groupFileName = NAME_OF_GROUP + groupName;
                JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                        TypeSpec.classBuilder(groupFileName)
                                .addJavadoc(WARNING_TIPS)
                                .addSuperinterface(ClassName.get(type_IRouteGroup))
                                .addModifiers(PUBLIC)
                                .addMethod(loadIntoMethodOfGroupBuilder.build())
                                .build()
                ).build().writeTo(mFiler);

其他注解的处理大致类似,中间可能加入了一些特别的处理逻辑。

通过类似的方式,结合各种注解,在代码编译阶段,可以预先生成路由关系等信息的类,生成的位置如下,然后这些类会被一同打包到 apk 中。

这些类中包含了路由信息(包括根结点和组的对应关系),拦截器,以及Provider的信息。

初始化加载映射文件

这是在ARouter 模块初始化时候所进行动作,主要是将注解编译器生成的映射文件中路由关系等信息,加载到内存中。然后在后续执行跳转操作时候,直接查询内存中的路由规则进行跳转。主要通过如下方法实现:

ARouter.init(getApplication());

这个方法实际调用了LogisticsCenterinit的静态方法。
主要流程是先通过包名去扫描注解编译器生成的映射文件,拿到一个类名对应的集合,然后通过反射的方式,实例化接口,通过loadinfo方法,将映射文件中,定义的路由关系信息,载入到warehouse中数据结构中,完成了在内存中缓存一份的操作。

 // These class was generate by arouter-compiler.
            List<String> classFileNames = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);

            //
            for (String className : classFileNames) {
                if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
                    // This one of root elements, load root.
                    ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
                } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
                    // Load interceptorMeta
                    ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
                } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
                    // Load providerIndex
                    ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
                }
            }

经过上述流程后,在Warehouse对应静态变量中,就存在了一份对应的缓存数据。

路由跳转的过程

 ARouter.getInstance()
        .build("/test/activity2")
        .navigation();

上述是一个简单的跳转过程。结合源码分析下过程。
这里首先看下一个关键的类Postcard

// Base
    private Uri uri;
    private Object tag;             // A tag prepare for some thing wrong.
    private Bundle mBundle;         // Data to transform
    private int flags = -1;         // Flags of route
    private int timeout = 300;      // Navigation timeout, TimeUnit.Second
    private IProvider provider;     // It will be set value, if this postcard was provider.
    private boolean greenChannel;

    // Animation
    private Bundle optionsCompat;    // The transition animation of activity
    private int enterAnim;
    private int exitAnim;

这个类继承了RouteMeta这个基类。

    private RouteType type;         // Type of route
    private Element rawType;        // Raw type of route
    private Class<?> destination;   // Destination
    private String path;            // Path of route
    private String group;           // Group of route
    private int priority = -1;      // The smaller the number, the higher the priority
    private int extra;              // Extra data
    private Map<String, Integer> paramsType;  // Param type

在这两个类中,有很多和跳转相关的成员变量。在跳转的过程中,通过Arouter类中提供的Api完成参数填充,实际是先创建了一个Postcard对象,随后在_Arouter这个类中进行逻辑处理,取到Postcard对象中参数,拿到映射关系,然后完成跳转。

具体看下这个过程:

 ARouter.getInstance()
        .build("/test/activity2")
        .navigation();
  • 创建Postcard对象。

调用ArouterbuildApi,最后走到了如下流程,在这一步流程中,最重要的就是完成将path填入,方便后续流程通过path在映射关系中找到需要跳转的Activity。

 protected Postcard build(String path, String group) {
        if (TextUtils.isEmpty(path) || TextUtils.isEmpty(group)) {
            throw new HandlerException(Consts.TAG + "Parameter is invalid!");
        } else {
            PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
            if (null != pService) {
                path = pService.forString(path);
            }
            return new Postcard(path, group);
        }
    }
  • 完成跳转。

navigation的流程中完成跳转,这是一个在Postcard中的方法。最终会走到_ARouter中的 navigation方法,在这个流程中,完成路由跳转的流程。

public Object navigation(Context mContext, Postcard postcard, int requestCode, NavigationCallback callback) {
        return _ARouter.getInstance().navigation(mContext, postcard, requestCode, callback);
    }

在这个方法中,会完成一步重要的操作,LogisticsCenter.completion(postcard);完成Postcard数据填入。主要是将之前的 path ,在之前的缓存关系中找到相应的Activity,填入到Postcarddestination变量中。

在随后的流程中,会处理一些降级,拦截,绿色通道等处理。最终走到Android原生的跳转流程, 在这个流程中,拿到destination的值,即为可以跳转的对象,然后完成跳转。

case ACTIVITY:
                // Build intent
                final Intent intent = new Intent(currentContext, postcard.getDestination());
                intent.putExtras(postcard.getExtras());

                // Set flags.
                int flags = postcard.getFlags();
                if (-1 != flags) {
                    intent.setFlags(flags);
                } else if (!(currentContext instanceof Activity)) {    // Non activity, need less one flag.
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }

                // Navigation in main looper.
                new Handler(Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        if (requestCode > 0) {  // Need start for result
                            ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());
                        } else {
                            ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle());
                        }

                        if ((0 != postcard.getEnterAnim() || 0 != postcard.getExitAnim()) && currentContext instanceof Activity) {    // Old version.
                            ((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
                        }
                    }
                });

                break;

至此一次简单的跳转就就结束了,当然在跳转过程中,还可以通过其他Api完成类似设置参数等复杂的操作,完成更多复杂的功能。

    原文作者:Magician锋
    原文地址: https://blog.csdn.net/weijianfeng1990912/article/details/66475978
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。