动手撸一个ARouter (ARouter源码分析)

背景

为何要重复造轮子呢?java

  • 我认为只有站在做者的角度才能更透彻的理解框架的设计思想
  • 去踩大神们所踩过的坑。
  • 才能深刻的理解框架的所提供的功能
  • 学习优秀的做品中从而提升本身

在开始以前我先提出关于ARouter的几个问题

  • 为何要在module的build.gradle文件中增长下面配置? 它的做用是什么?它跟咱们定义的url中的分组有什么关系?
javaCompileOptions {
    annotationProcessorOptions {
        arguments = [moduleName: project.getName()]
    }
}
复制代码
  • 有这么一种业务场景,新建一个业务组件user,user组件中有页面UserActivity,配置url /user/main;有一个服务接口,其实现类在app中,配置url为/user/info;代码以下:
//module:user
@Route(path = "/user/main")
public class UserActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.user_activity);
    }
}

public interface IUserService extends IProvider {
    void test(String s);
}

//module:app
//user服务
@Route(path = "/user/info")
public class UserServiceImpl implements IUserService {

    public void test(String test) {
        Log.d("xxxx->",test);
    }
}
复制代码

好了开发完成,让咱们编译一下项目看看,编译结果以下图(ps:这里编译的是我本身的项目,但效果和ARouter是同样的):android

Why???

让咱们带着这两个问题开始RouterManager之旅。git

第一步架构设计思路(处理页面跳转)

咱们的目标是根据一个url来打开指定的页面,该如何作呢?很简单,咱们把url和对应的页面作一个对应关系,好比放到map中以url为key,对应的页面activity为value便可;这样当咱们要打开这个activity时,根据传给咱们的url去map中找到对应的activity,而后调用startActivity就OK了。github

你可能会问那咱们这个map该如何维护呢?咱们怎么把这个对应关系存到map中呢?总不能手动去put吧,你别说貌似还真行,咱们在app启动的时候先把个人映射关系手动初始化好,这样在打开页面时直接经过url来获取就好了。那么问题来了,大哥你累不累啊?对于一个懒人来讲首先会想到的是能不能自动生成这个映射关系表呢?答案是确定的。api

思路总结

咱们能够利用编译注解的特性,新增一个注解,给每一个须要经过url打开的activity加上此注解。在注解处理器中获取全部被注解的类,动态生成映射关系表,而后在app启动时把所生成的映射关系load到内存便可。数组

第二部撸代码

0x01

首先咱们须要建立三个module,以下图:缓存

为何要三个项目呢?缘由以下:bash

  • 咱们须要用到的注解处理器AbstractProcessor是在javax包下,而android项目中是没有这个包的,所以咱们须要建一个java library,也就是router-compiler,它的做用是帮咱们动态生成代码,只存在于编译期间架构

  • 既然router-compiler只存在于编译期间,那咱们的注解是须要在项目中用到的,这个类应该放在那里呢?这就有了第二个java library,router-annotation,用来专门存放咱们定义的注解和一些要被打进app中代码。app

  • 因为上述两个library都是java项目,而咱们最终是要用到android工程中的,所以对外提供api时确定会用到android工程中的类,如Context。因此就有了第三个module router-api用于处理生成产物。如把生成映射关系表load到内存,并提供统一的调用入口。

0x02

咱们先定义咱们本身的注解:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {

    String path();

    String group() default "";

    String name() default "";

    int extras() default Integer.MIN_VALUE;

    int priority() default -1;
}
复制代码

定义本身的route处理器RouterProcessor

@AutoService(Processor.class)       //自动注册注解处理器
@SupportedOptions({Consts.KEY_MODULE_NAME})     //参数
@SupportedSourceVersion(SourceVersion.RELEASE_7)        //指定使用的Java版本
@SupportedAnnotationTypes({ANNOTATION_ROUTER_NAME}) //指定要处理的注解类型
public class RouterProcessor extends AbstractProcessor{

    private Map<String,Set<RouteMeta>> groupMap = new HashMap<>();  //收集分组
    private Map<String,String> rootMap = new TreeMap<>();
    private Filer mFiler;
    private Logger logger;
    private Types types;
    private TypeUtils typeUtils;
    private Elements elements;
    private String moduleName = "app"; //默认app
    private TypeMirror iProvider = null; //IProvider类型
    
    //......
复制代码

其中SupportedAnnotationTypes指定的就是咱们上面定义的注解Route

接下来就是收集全部被注解的类,生成映射关系,代码以下:

public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if(CollectionUtils.isNotEmpty(set)) {
            //获取到全部被注解的类
            Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(Route.class);
            try {
                logger.info(">>> Found routers,start... <<<");
                parseRoutes(elementsAnnotatedWith);
            } catch (IOException e) {
                logger.error(e);
            }
            return true;
        }
        return false;
    }
复制代码

获取完以后交给了parseRoutes方法:

private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
        if(CollectionUtils.isNotEmpty(routeElements)) {

            logger.info(">>> Found routes, size is " + routeElements.size() + " <<<");
            rootMap.clear();
            //.......
            TypeMirror type_activity = elements.getTypeElement(ACTIVITY).asType();

            for (Element element : routeElements) {
                TypeMirror tm = element.asType();
                Route route = element.getAnnotation(Route.class);
                RouteMeta routeMeta;

                if(types.isSubtype(tm,type_activity)) { //activity
                    logger.info(">>> Found activity route: "+ tm.toString() + " <<<");
                    routeMeta = new RouteMeta(route,element,RouteType.ACTIVITY,null);
                } else if(types.isSubtype(tm,iProvider)) { //IProvider
                    logger.info(">>> Found provider route: " + tm.toString() + " <<<");
                    routeMeta = new RouteMeta(route,element,RouteType.PROVIDER,null);
                } else if(types.isSubtype(tm,type_fragment) || types.isSubtype(tm,type_v4_fragment)) { //Fragment
                    logger.info(">>> Found fragment route: " + tm.toString() + " <<< ");
                    routeMeta = new RouteMeta(route,element,RouteType.parse(FRAGMENT),null);
                } else {
                    throw new RuntimeException("ARouter::Compiler >>> Found unsupported class type, type = [" + types.toString() + "].");
                }

                categories(routeMeta);
            }
            
            //.......
复制代码

这个方法比较长,咱们先看看最主要的处理,遍历routeElements,判断当前被注解的类的类型,分别是activity,IProvider,Fragment这三中,也就是说注解Route能够用来注解activity ,IProvider,和Fragment(注意这里fragment包括原生包中的和v4包中的fragment)而后根据类型构造出routeMeta对象,构造完以后传给了categories方法:

private void categories(RouteMeta routeMete) {
        if (routeVerify(routeMete)) {
            logger.info(">>> Start categories, group = " + routeMete.getGroup() + ", path = " + routeMete.getPath() + " <<<");
            //groupMap是一个全局变量,用来按分组存储routeMeta
            Set<RouteMeta> routeMetas = groupMap.get(routeMete.getGroup());
            if (CollectionUtils.isEmpty(routeMetas)) {
                Set<RouteMeta> routeMetaSet = new TreeSet<>(new Comparator<RouteMeta>() {
                    @Override
                    public int compare(RouteMeta r1, RouteMeta r2) {
                        try {
                            return r1.getPath().compareTo(r2.getPath());
                        } catch (NullPointerException npe) {
                            logger.error(npe.getMessage());
                            return 0;
                        }
                    }
                });
                routeMetaSet.add(routeMete);
                groupMap.put(routeMete.getGroup(), routeMetaSet);
            } else {
                routeMetas.add(routeMete);
            }
        } else {
            logger.warning(">>> Route meta verify error, group is " + routeMete.getGroup() + " <<<");
        }
    }
复制代码

咱们看到这个方法中首先根据当前url分组去groupMap中查找,也就是看是否有该分组,若是有取出对应的RouterMeta集合,把本次生成的routeMeta放进去;没有就新存一个集合。

到这里咱们已经把全部的注解类都获取到而且已经按分组分类。接下来就是生成java类来存放这些信息:

这里暂且只看对activity映射关系处理的代码:

// (1)
for (Map.Entry<String, Set<RouteMeta>> entry : groupMap.entrySet()) {
                String groupName = entry.getKey();

                // (2)
                MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
                        .addAnnotation(Override.class)
                        .addModifiers(Modifier.PUBLIC)
                        .addParameter(groupParamSpec);

                Set<RouteMeta> groupData = entry.getValue();

                for (RouteMeta meta : groupData) {
                    ClassName className = ClassName.get((TypeElement) meta.getRawType());
                    
                   //......   (3)

                    loadIntoMethodOfGroupBuilder.addStatement(
                            "atlas.put($S," +
                                    "$T.build($T." + meta.getType() + ",$T.class,$S,$S," + (StringUtils.isEmpty(mapBody) ? null : ("new java.util.HashMap<String, Integer>(){{" + mapBodyBuilder.toString() + "}}")) + ", " + meta.getPriority() + "," + meta.getExtra() + "))",
                            meta.getPath(),
                            routeMetaCn,
                            routeTypeCn,
                            className,
                            meta.getPath().toLowerCase(),
                            meta.getGroup().toLowerCase());
                }

                //Generate groups   (4)
                String groupFileName = NAME_OF_GROUP + groupName;
                JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                        TypeSpec.classBuilder(groupFileName)
                                .addJavadoc(WARNING_TIPS)
                                .addSuperinterface(ClassName.get(type_IRouteGroup))
                                .addModifiers(Modifier.PUBLIC)
                                .addMethod(loadIntoMethodOfGroupBuilder.build())
                                .build()
                ).build().writeTo(mFiler);

                logger.info(">>> Generated group: " + groupName + "<<<");
                rootMap.put(groupName, groupFileName);
            }

            // (5)
            if(MapUtils.isNotEmpty(rootMap)) {
                for (Map.Entry<String, String> entry : rootMap.entrySet()) {
                    loadIntoMethodOfRootBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(PACKAGE_OF_GENERATE_FILE, entry.getValue()));
                }
            }

            // ......

            // Write root meta into disk.   (6)
            String rootFileName = NAME_OF_ROOT + moduleName;
            JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                    TypeSpec.classBuilder(rootFileName)
                            .addJavadoc(WARNING_TIPS)
                            .addSuperinterface(ClassName.get(elements.getTypeElement(IROUTE_ROOT)))
                            .addModifiers(PUBLIC)
                            .addMethod(loadIntoMethodOfRootBuilder.build())
                            .build()
            ).build().writeTo(mFiler);

            logger.info(">>> Generated root, name is " + rootFileName + " <<<");
        }

复制代码

现将上述这段代码解释以下:

  • 遍历咱们以前存储的groupMap,取出对应的集合,如注释(1)
  • 生成一个方法体,而且把集合中的全部映射关系都put到参数map中。如 (2)(3)
  • 生成java类,类名为RouterManager$$Group$$ + moduleName,这里的moduleName就是在build.gradle文件中配置的,如不配置,活获取为null 如(4)
  • 把每一个分组和所生成的类作个映射关系,做用就是为了实现按分组加载功能 如 (5)(6)

下面咱们看下一辈子成的产物

/**
 DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY ROUTERMANAGER. */
public class RouterManager$$Root$$app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("service", RouterManager$$Group$$service.class);
  }
}

复制代码

存储分组对应关系

/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY ROUTERMANAGER. */
public class RouterManager$$Group$$service implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/service/test/main",RouteMeta.build(RouteType.ACTIVITY,OtherActivity.class,"/service/test/main","service",null, -1,-2147483648));
  }
}

复制代码

就这样映射关系自动生成好了,那么该如何使用呢?下面就让我隆重介绍一下咱们Api

0x03

因为咱们的映射关系表是全局存在的,因此确定须要在Application中作初始化操做,其目的就是把映射关系load到内存,下面让咱们看看具体实现代码

首先咱们得须要一个容器来存储咱们的映射关系,所以就有了Warehouse类

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

    static void clear() {
        providers.clear();
        providersIndex.clear();
    }
}

复制代码

咱们在此类中实例化两个map用来存储咱们的分组信息和每一个分组中的对应关系信息

groupIndex:用来存放分组信息,这个会优先load数据 routes:用来存储对应关系数据

接下来咱们在App初始化时会调用以下代码来初始化:

RouterManager.init(this);
复制代码

那么咱们进去init方法中看看具体干了什么?

public static synchronized void init(Application application){
        if(!hasInit) {
            hasInit = true;
            mContext = application;
            mHandler = new Handler(Looper.getMainLooper());
            logger = new DefaultLogger();
            LogisticsCenter.init(mContext,logger);
        }
    }
复制代码

能够看到这里最关键的一行代码是 LogisticsCenter.init(mContext,logger)

那就让咱们继续去LogisticsCenter.init(mContext,logger);方法中看看:

public synchronized static void init(Context context, ILogger log) {
        logger = log;
        Set<String> routeMap;
        try {
            if(RouterManager.debuggable() || PackageUtils.isNewVersion(context)) { //开发模式或版本升级时扫描本地件
                logger.info(TAG,"当前环境为debug模式或者新版本,须要从新生成映射关系表");
                //these class was generated by router-compiler
                routeMap = ClassUtils.getFileNameByPackageName(context, Consts.ROUTE_ROOT_PAKCAGE);
                if(!routeMap.isEmpty()) {
                    PackageUtils.put(context,Consts.ROUTER_SP_KEY_MAP,routeMap);
                }
                PackageUtils.updateVersion(context);
            } else{ //读取缓存
                logger.info(TAG,"读取缓存中的router映射表");
                routeMap = PackageUtils.get(context,Consts.ROUTER_SP_KEY_MAP);
            }

            logger.info(TAG,"router map 扫描完成");
            //将分组数据加载到内存
            for (String className : routeMap) {
                //Root
                if(className.startsWith(Consts.ROUTE_ROOT_PAKCAGE + Consts.DOT + Consts.SDK_NAME + Consts.SEPARATOR + Consts.SUFFIX_ROOT)) {
                    ((IRouteRoot)(Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
                } 
                //......
            }

            logger.info(TAG,"将映射关系读到缓存中");

            if(Warehouse.groupsIndex.size() == 0) {
                logger.error(TAG,"No mapping files,check your configuration please!");
            }

            if (RouterManager.debuggable()) {
                logger.debug(TAG, String.format(Locale.getDefault(), "LogisticsCenter has already been loaded, GroupIndex[%d], ProviderIndex[%d]", Warehouse.groupsIndex.size(), Warehouse.providersIndex.size()));
            }

        } catch (Exception e) {
            e.printStackTrace();
            logger.error(TAG,"RouterManager init logistics center exception! [" + e.getMessage() + "]");
        }
    }
复制代码

具体解释以下:

1)、首先是根据包名去扫描全部生成的类文件,并放在routeMap中。固然这里会根据版本判断而后缓存到本地,目的是为了不重复扫描 2)、遍历扫描到的数组,将全部分组信息缓存到Warehouse.groupIndex中

能够看到初始化时只干了这两件事,扫描class文件,读取分组信息;仔细想一想你会发现这里并无去读取咱们的url和activity映射关系信息,这就是所谓的按需加载。

到这里咱们全部的准备工做都已完成了,那么该怎么使用呢?

下面让咱们看看具体的用法

0x04

咱们先来看一段代码:

RouterManager.getInstance().build("/user/main").navigation(MainActivity.this);
复制代码

上述代码是咱们打开UserActivty页面所使用的方式,能够发现这里只传了一个url。那就让咱们看看内部是如何实现的?

首先咱们去build方法中看看具体的代码:

public Postcard build(String path) {
        if(TextUtils.isEmpty(path)) {
            throw new HandlerException("Parameter is invalid!");
        } else {
            return build(path,extractGroup(path));
        }
    }

    public Postcard build(String path,String group) {
        if(TextUtils.isEmpty(path)) {
            throw new HandlerException("Parameter is invalid!");
        } else {
            return new Postcard(path,group);
        }
    }
复制代码

发现这里是一个重载方法,最后返回的是一个Postcard对象,而后调用Postcard的navigation方法。能够看到这里Postcard其实只是一个携带数据的实体。下面看看navigation方法:

public Object navigation(Context context) {
        return RouterManager.getInstance().navigation(context,this,-1);
    }
复制代码

能够发现这里只是作了一个中转,最终调用的是RouterManager的navigation方法:

Object navigation(final Context context,final Postcard postcard,final int requestCode) {
        try {
            LogisticsCenter.completion(postcard);
        } catch (HandlerException e) {
            e.printStackTrace();
            return null;
        }

        final Context currentContext = context == null ? mContext : context;
        switch (postcard.getType()) {
            case ACTIVITY:
                final Intent intent = new Intent(currentContext,postcard.getDestination());
                intent.putExtras(postcard.getExtras());

                int flags = postcard.getFlags();
                if(flags != -1) {
                    intent.setFlags(flags);
                } else if(!(currentContext instanceof Activity)) { //若是当前上下文不是activity,则启动activity时须要new一个新的栈
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }

                runInMainThread(new Runnable() {
                    @Override
                    public void run() {
                        startActivity(requestCode,currentContext,intent,postcard);
                    }
                });
                break;
            //......
        }
        return null;
    }
复制代码

由上述代码能够看出首先调用的是LogisticsCenter.completion()方法把postcard对象传进去,那让咱们先去这个方法中看个究竟:

/**
     * 填充数据
     * @param postcard
     */
    public synchronized static void completion(Postcard postcard) {
        RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
        if(routeMeta != null) {
            postcard.setDestination(routeMeta.getDestination());
            postcard.setType(routeMeta.getType());
            postcard.setPriority(routeMeta.getPriority());
            postcard.setExtra(routeMeta.getExtra());

            //......
        } else {
            Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());
            if(groupMeta == null) {
                throw new NoRouteFoundException("There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
            } else {
                try {
                    //按组加载数据,美其名曰-按需加载
                    IRouteGroup iRouteGroup = groupMeta.getConstructor().newInstance();
                    iRouteGroup.loadInto(Warehouse.routes);
                    Warehouse.groupsIndex.remove(postcard.getGroup());
                } catch (Exception e) {
                    throw new HandlerException("Fatal exception when loading group meta. [" + e.getMessage() + "]");
                }
            }

            completion(postcard); //分组加载完成后从新查找
        }
    }

复制代码

这里首先去根据url去Warehouse.routes中查找对应的RouteMeta信息,如何是首次调用的话这里必定是没有的,因此会执行else方法,else方法里先根据分组获取对应的分组class,而后反射其实例对象并调用loadInfo()方法,把该分组中的全部映射关系读取到Warehouse.routes中,而后继续调用当前方法填充相关的信息。

信息填充完成以后继续回到navigation方法中:

switch (postcard.getType()) {
            case ACTIVITY:
                final Intent intent = new Intent(currentContext,postcard.getDestination());
                intent.putExtras(postcard.getExtras());

                int flags = postcard.getFlags();
                if(flags != -1) {
                    intent.setFlags(flags);
                } else if(!(currentContext instanceof Activity)) { //若是当前上下文不是activity,则启动activity时须要new一个新的栈
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }

                runInMainThread(new Runnable() {
                    @Override
                    public void run() {
                        startActivity(requestCode,currentContext,intent,postcard);
                    }
                });
                break;
            //......
        }
复制代码

能够看到这里使用的是常规的启动方式startActivity去启动一个新activity。

Ok到此为止整个流程算是走完了,至于传递参数,获取fragment,以及服务IProvider什么的套路都同样,这里再也不重复赘述。

总结

ARouter的思路很好简单,就是经过编译时注解生成url与页面的映射关系表,而后在程序启动时将该映射关系表load到内存中,使用时直接去内存中查找而后执行常规的页面启动方式。

下面咱们来回答前面提出的两个问题

第一:为何要在每一个build.gradle文件中配置一个moduleName呢?

这是由于编译时注解是以module为单位去生成代码的,也就是说咱们须要给每一个module项目都配置该注解生成器的依赖,为了保证生成java文件的名字不会重复须要加上module为后缀。此配置和分组没有任何关系。只是为了不生成的分组类重复。

第二:为何会报多个类重名的问题?

咱们知道Router的映射表有两张表,第一张是用来存储分组和分组对应的class的,第二张是用来存储每一个分组中具体url映射关系的。而在第一个问题中咱们根据moduleName来避免存放分组的class重名的问题。那么每一个分组class自己有没有重名的可能呢?答案是必定有的。好比:咱们在user组件中配置的url:/user/main分组为user,这个时候在编译user组件时就会自动生成一个类名为 RouterManager$$Group$$user的类,用来存放全部的以user为分组的页面映射关系。那么当咱们在app的中也配置分组名为user的分组后,编译app时就会在app中生成类名为RouterManager$$Group$$user的类。而咱们app项目是依赖的user组件的,这就致使有两个类名同样的文件。编译时天然就会报错。

对RouterManager的几点思考

  • RouterManager可否用于夸进程调用:

我认为是能够的,RouterManager的关系映射表是存在一个全局静态变量中的,当咱们须要在其余进程访问时只须要提供一个接口来获得映射关系便可。

  • RouterManager可否在RePlugin中的使用:

答案也是能够的,因为RePlugin采用的是多个classloader机制,这就致使咱们在主项目的classloader获取的对象和在插件classloader中获取的是两个独立的对象,若是想在插件中使用RouterManager去打开一个宿主的页面,直接调用的话确定是没有对应的映射关系的,由于在插件里获取的RouterManager对象并非宿主的单例对象,而是建立了一个新的对象。那怎么办呢?答案很简单,咱们在插件中使用反射获取到宿主的RouterManager实例便可正常使用。

注:RouterManager框架的思路来源与ARouter,这里只实现了页面跳转,fragment获取和服务Provider的获取功能。至于其余的降级策略,依赖注入功能就不在一一实现了

项目源码请移驾到本人的github仓库查看:github.com/qiangzier/R…

相关文章
相关标签/搜索