androidARouter

ARouter使用与原理分析

2020-11-11  本文已影响0人  邱穆

ARouter使用与原理分析

一、使用入门

见官方文档:ARouter

二、技术原理分析

ARouter采用注解与APT技术,自动生成代码,在运行时调用,那么这里也就分为两部分,生成的代码和ARouter源代码,我们也分为两部分学习。

2.1 调用过程和代码探析

2.1.1 调用过程

根据官方文档,基本的使用方法为下面这行代码:

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

根据这行代码,从Activity出发,调用时许过程如下图:

img

第一步:构建Postcard

Postcard顾名思义,为“跳转卡片”,ARouter.getInstance().build("/xxx/xxxx")返回的就是Postcard对象,主要用来携带bundle跳转参数。而ARouter只是对外统一的api接口,实现基本由_ARouter完成,所以构建Postcard也是由_ARouter。

第二步:Postcard.navigation()实现跳转

Postcard.navigation()其实也是调用_ARouter.getInstance().navigation()完成的,即最后也是由_ARouter完成,在_ARouter.navigation时,首先调用LogisticsCenter.completion(postcard)完善postcard的信息,而completion方法,则完成了path到activity的转换关系。完善后,再调用_navigation完成最终跳转。

第三步:LogisticsCenter.completion(postcard)将path映射到activity

看到这一步时,会发现大致看不懂,没关系,大致明白这里的作用,待会换个思路看源码就可以了。

第四步:_ARouter._navigation进行跳转

关键代码如下:

private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        final Context currentContext = null == context ? mContext : context;

        switch (postcard.getType()) {
            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);
                }

                // Set Actions
                String action = postcard.getAction();
                if (!TextUtils.isEmpty(action)) {
                    intent.setAction(action);
                }

                // Navigation in main looper.
                runInMainThread(new Runnable() {
                    @Override
                    public void run() {
                        startActivity(requestCode, currentContext, intent, postcard, callback);
                    }
                });

                break;
           ···············
                return null;
        }

        return null;
    }

可以很明显的看到,这一步是判断postcard的信息,然后建立intent进行跳转,也就是仍然还是通过startActivity()这个函数进行跳转,并没有太多的奥秘。重点是ARouter内部映射的实现原理和方法。

2.1.2 主体源码分析

刚才在调用过程中接触到了几个类,趁热打铁,先介绍下这几个类:

ARouter同样也定义了好几个注解类,其中最关键的就是@Route注解,而@Route注解是使用了APT技术来发挥作用的。APT的核心类-AbstractProcessor类就是编译时用来生成代码的。ARouter源码中有个RouteProcessor.java类,继承自AbstractProcessor类,就是用来处理@Route注解的,重写的process()方法,实现了对@Route注解的处理和自动生成文件代码的逻辑。

项目编译后,会生成代码,结构如图:

生成的代码结构

这里先说明下,目前我们的项目一共有4个Activity,分别是登录用的LoginActivity和LoginWeb,以及项目主体的MainActivity和ProfileActivity,所以目前的路由分组,前两个Activity分为一组,后两个Activity分为一组,如下图:

路由分组信息

ARouter默认用第一个斜杠后面的字符串表示分组信息。

于是ARouter生成的代码文件,Group有两个,后缀分别是我们的两个分组。我们分别学习这几个文件:

public class ARouter$$Root$$app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("app", ARouter$$Group$$app.class);
    routes.put("login", ARouter$$Group$$login.class);
  }
}

这个类中的loadInto()方法,是在ARouter初始化的时候,即ARouter.init(mApplication) -> LogisticsCenter.init()中被调用的。传入的routes即为Warehouse类的静态变量Warehouse.groupsIndex。也就是为路由分组这个Map设置了两个键值关系。

public class ARouter$$Group$$app implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/app/main", RouteMeta.build(RouteType.ACTIVITY, MainActivity.class, "/app/main", "app", null, -1, -2147483648));
    atlas.put("/app/profile", RouteMeta.build(RouteType.ACTIVITY, ProfileActivity.class, "/app/profile", "app", new java.util.HashMap<String, Integer>(){{put("login", 8); }}, -1, -2147483648));
  }
}
public class ARouter$$Group$$login implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/login/index", RouteMeta.build(RouteType.ACTIVITY, LoginActivity.class, "/login/index", "login", null, -1, -2147483648));
    atlas.put("/login/oauth", RouteMeta.build(RouteType.ACTIVITY, LoginWeb.class, "/login/oauth", "login", new java.util.HashMap<String, Integer>(){{put("url", 8); }}, -1, -2147483648));
  }
}

这个类的loadInto()方法,传入的是Warehouse类的另一个静态变量Warehouse.routes,但是此方法不是在ARouter初始化的时候调用,而是在页面跳转的时候,如果满足条件“要跳转的页面是其所在的分组首个要跳转的”才调用,加载完Warehouse.routes后继续跳转页面。这样也就实现了分组加载,避免加载过多,只加载该页面所属分组的映射关系,减少了不必要的内存耗费。

代码调用链:ARouter.getInstance().navigation() -> _ARouter.getInstance().navigation() -> LogisticsCenter.completion() -> IRouteGroup.loadInto(Warehouse.routes),这里也就接上了之前讲到的调用过程的第三步。

下面我们再来详细看一下_Arouter类的build()方法:

    /**
     * Build postcard by path and default group
     */
protected Postcard build(String path) {
    if (TextUtils.isEmpty(path)) {
        throw new HandlerException(Consts.TAG + "Parameter is invalid!");
    } else {
        PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
        if (null != pService) {
            path = pService.forString(path);
        }
        return build(path, extractGroup(path), true);
    }
}

一般情况下我们都不会自定义路径替换逻辑,即不会实现PathReplaceService,因此代码会走到最后一行:return build(path, extractGroup(path), true),extractGroup(path)这个方法的作用是提取出路径的分组部分,比如页面path为“/login/index”,则extractGroup(path)得到的结果是“login”,再继续看build(path, extractGroup(path), true)的代码:

    /**
     * Build postcard by path and group
     */
    protected Postcard build(String path, String group, Boolean afterReplace) {
        if (TextUtils.isEmpty(path) || TextUtils.isEmpty(group)) {
            throw new HandlerException(Consts.TAG + "Parameter is invalid!");
        } else {
            if (!afterReplace) {
                PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
                if (null != pService) {
                    path = pService.forString(path);
                }
            }
            return new Postcard(path, group);
        }
    }

afterReplace为true,因此ARouter.getInstance().build("/login/index")的运行结果就是new Postcard("/login/index", “/login”)。

那么ARouter.getInstance().build("/login/index")..navigation()就是:new Postcard("/login/index", “/login”).navigation()。

再来看Postcard做了什么处理:

public Postcard() {
        this(null, null);
    }

    public Postcard(String path, String group) {
        this(path, group, null, null);
    }

    public Postcard(String path, String group, Uri uri, Bundle bundle) {
        setPath(path);
        setGroup(group);
        setUri(uri);
        this.mBundle = (null == bundle ? new Bundle() : bundle);
    }

可以看到new Postcard("/login/index", “/login”)是得到一个Postcard对象,其成员变量path,group和mBundle都已经赋值。

再看Postcard的navigation()方法:

public Object navigation() {
        return navigation(null);
    }

    public Object navigation(Context context) {
        return navigation(context, null);
    }

    public Object navigation(Context context, NavigationCallback callback) {
        return ARouter.getInstance().navigation(context, this, -1, callback);
    }

ARouter的navigation()方法是由_ARouter实现的,方法较长,这里贴出简化后的代码:

protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
    try {
        LogisticsCenter.completion(postcard);
    } catch (NoRouteFoundException ex) {
        logger.warning(Consts.TAG, ex.getMessage());
        xxx处理
        return null;
    }
    xxxx处理
    return _navigation(context, postcard, requestCode, callback);
}

可见,通过LogisticsCenter.completion(postcard)补充了postcard后,传入了_navigation()方法。而_navigation()在之前的2.1.1中就有说明,就是根据postcard携带的信息构造出Intent对象等,然后调用ActivityCompat.startActivity()方法启动页面。最后再来看看按需加载的实现方法:

    public synchronized static void completion(Postcard postcard) {
        RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
        if (null == routeMeta) {    
            // 首次跳转此分组的页面时,Warehouse.routes未加载此分组,因此会走到这里
            // 此路由信息不存在,或者未加载,所以找不到
            Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());  // Load route meta.
            
            // 加载此分组的path和页面映射关系到Warehouse.routes中
            try {
                IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
                iGroupInstance.loadInto(Warehouse.routes);
            } catch (Exception e) {
                throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
            }

            completion(postcard);   // 再次调用本方法,补充postcard
            
        } else {
            // 非首次跳转此分组的页面,则会走到这里
            // 已找到路由信息,直接填充给postcard
            postcard.setDestination(routeMeta.getDestination());
            postcard.setType(routeMeta.getType());
            postcard.setPriority(routeMeta.getPriority());
            postcard.setExtra(routeMeta.getExtra());
            
            xxx其它信息的添加
        }
    }

2.1.3 映射和分组源码分析

刚才提到,Warehouse.java类包含了几个集合类型(主要是Map类型)的静态变量,这几个静态集合用来保存activity和其path对应关系,以及其分组关系等。

首先看如何分组的,Arouter在一层map之外,增加了一层map,我们看WareHouse这个类,里面有两个静态Map:

    // Cache route and metas
    static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
    static Map<String, RouteMeta> routes = new HashMap<>();

RouteMeta很简单,就是对跳转目标的封装,其内包含了目标组件的信息,比如Activity的Class。定义如下:

public class 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
    private String name;

    private Map<String, Autowired> injectConfig;  // Cache inject config.

    public RouteMeta() {
    }

IRouteGroup是一个接口,只有一个方法loadInto。

public interface IRouteGroup {
    /**
     * Fill the atlas with routes in group.
     */
    void loadInto(Map<String, RouteMeta> atlas);
}

至于实现了这个方法的类,就是之前提到的apt生成的以“Arouter$$Group$$分组”命名的类,他们创建了RouteMeta并填充到atlas中,形成Map映射文件,映射路径到具体的类。我们从上面拿一个下来看看:

public class ARouter$$Group$$app implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/app/main", RouteMeta.build(RouteType.ACTIVITY, MainActivity.class, "/app/main", "app", null, -1, -2147483648));
    atlas.put("/app/profile", RouteMeta.build(RouteType.ACTIVITY, ProfileActivity.class, "/app/profile", "app", new java.util.HashMap<String, Integer>(){{put("login", 8); }}, -1, -2147483648));
  }
}

一目了然,这个类加载的映射关系,都是在一个组内。可以总结出:ARouter通过apt技术,为每个组生成了一个以Arouter$$Group开头的类,这个类负责向atlas这个Hashmap中填充组内路由数据。

那IRouteGroup外面的这一层HashMap呢?也就是groupsIndex呢?经过查阅,这个Map对应的正是以"Arouter$$Root$$组件名"命名的类,也就是我们的ARouter$$Root$$app.java:

public class ARouter$$Root$$app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("app", ARouter$$Group$$app.class);
    routes.put("login", ARouter$$Group$$login.class);
  }
}

这个类实现了IRouteRoot,在loadInto方法中,他将组名和组对应的“组加载器”保存到了routes这个map中。也就是说,这个类将所有的“组加载器”给索引了下来,通过任意一个组名,可以找到对应的“组加载器”。

到此为止,Arouter只是完成了分组工作,而这个分组,也就是之前2.1.2讲到的分组加载的依仗。

下面用一张图来理清他们的关系:

Arouter分组按需加载演示图

左侧groupsIndex是“组映射”,右侧routes是“路由映射”。Arouter在初始化的时候,通过反射技术,将所有的“组加载器”索引到groupsIndex这个map中,而此时,右侧的routes还是空的。在用户调用navigation()进行跳转的时候,会根据路径提取组名,由组名根据groupsIndex获取到相应组的“组加载器”,由组加载器加载对应组内的路由信息,此时保存全局“路由目标映射的”routes这个map中就保存了刚才组的所有路由映射关系了。同样,当其他组请求时,其他组也会加载组对应的路由映射,这样就实现了整个App运行时,只有用到的组才会加到内存中,没有去过的组就不会加载到内存中,达到了节省内存的目的。

上一篇下一篇

猜你喜欢

热点阅读