Android开源框架系列-组件化之ARouter Activity页面无参数跳转源码分析

前言

ARouter是阿里技术团队开源的组件化框架,用于组件化项目中实现跨模块调用。我会从两个方面对ARouter框架源码进行分析:运行期源码和编译期源码。编译期间,ARouter内部的注解处理器会根据相关注解进行辅助代码生成,这些生成的代码是ARrouter之所以能够提供跨模块调用的关键;运行期间,再借助于这些编译器产物来实现跨模块调用。本篇先进行运行时源码的分析,引出问题,读者先可以思考一下,编译器产物的实现逻辑,我会在下一篇中和大家一起进行分析。

demo项目结构

我们以ARouter提供的demo为例来进行ARouter源码的分析,在分析源码之前,我们先了解一下demo的架构组成,有助于我们对ARouter进行更全面的了解。

graph TD app --> module-java app --> module-java-export app --> module-kotlin module-java --> arouter-api module-java-export --> arouter-api module-kotlin --> arouter-api module-java --> arouter-compiler module-java-export --> arouter-compiler module-kotlin --> arouter-compiler module-java --> arouter-annotation module-java-export --> arouter-annotation module-kotlin --> arouter-annotation

编译期产物

今天的重点是无参数传递时的Activity页面跳转,我们先了解一下编译期间产生的辅助代码。

module-java模块

module-java模块中被注解的类以及其参数配置很多,我们以Test2Activity作为分析的入口。

scala 复制代码
@Route(path = "/test/activity2")
public class Test2Activity extends AppCompatActivity {

group

首先我们要知道一点,ARouter会按照module名为每个mudule生成一个名为ARouter$$Root$$modulejava格式的类,其中modulejava表示当前模块名。如module-java模块生成的类为:

ruby 复制代码
public class ARouter$$Root$$modulejava implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("m2", ARouter$$Group$$m2.class);
    routes.put("module", ARouter$$Group$$module.class);
    routes.put("test", ARouter$$Group$$test.class);
    routes.put("yourservicegroupname", ARouter$$Group$$yourservicegroupname.class);
  }
}

也就是说,有几个模块(配置了ARouter)就会生成几个ARouter$$Root$$xxx类,从上边的示例可以看出,这个类是用来管理本模块中定义的所有group的,module-java模块中定义了四个group,分别是m2、module、test、yourservicegroupname。

借助这个辅助类,就可以通过group名找到该group的管理类ARouter$$Group$$xxxARouter$$Group$$xxx内部管理了该分组下的所有path信息,接下来我们看看这些path信息。

path

继续分析编译期的第二个产物,也就是ARouter$$Root$$modulejavamap映射中存储的value-Class<? extends IRouteGroup>,如上边test分组对应的ARouter$$Group$$test.class,它内部存储的是本分组下所有path的映射关系。

less 复制代码
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("ser", 9); put("ch", 5); put("fl", 6); put("dou", 7); put("boy", 0); put("url", 8); put("pac", 10); put("obj", 11); put("name", 8); put("objList", 11); put("map", 11); put("age", 3); put("height", 3); }}, -1, -2147483648));
    atlas.put("/test/activity2", RouteMeta.build(RouteType.ACTIVITY, Test2Activity.class, "/test/activity2", "test", new java.util.HashMap<String, Integer>(){{put("key1", 8); }}, -1, -2147483648));
    atlas.put("/test/activity3", RouteMeta.build(RouteType.ACTIVITY, Test3Activity.class, "/test/activity3", "test", new java.util.HashMap<String, Integer>(){{put("name", 8); 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/fragment", RouteMeta.build(RouteType.FRAGMENT, BlankFragment.class, "/test/fragment", "test", new java.util.HashMap<String, Integer>(){{put("ser", 9); put("pac", 10); put("ch", 5); put("obj", 11); put("fl", 6); put("name", 8); put("dou", 7); put("boy", 0); put("objList", 11); put("map", 11); put("age", 3); put("height", 3); }}, -1, -2147483648));
    atlas.put("/test/webview", RouteMeta.build(RouteType.ACTIVITY, TestWebview.class, "/test/webview", "test", null, -1, -2147483648));
  }
}

可以看到,方法体中执行时,是以path为key,以被注解类的相关信息封装出来的RouteMeta为value存入map中,有了这个类,就可以根据定义的path来找到被注解的类的相关信息。

group + path

了解了group和path的辅助类生成的形式之后,你是不是也能猜到ARouter跨模块调用的大概实现了?如A、B两个没有依赖关系的模块需要跨模块调用,A想要打开B中的某Activity页面,只需要在该Activity上添加@Route注解,指定该Activity的group和path,如@Route(path = "/login/loginApi") 然后编译过程中,注解处理器会根据B模块名生成该模块下的group管理类ARouter$$Root$$B,有了ARouter$$Root$$B再传入group名(login),就可以拿到本分组下的所有path信息管理类,ARouter$$Group$$loginARouter$$Group$$login内存储的又是所有path的合集,再通过指定path就可以找到该path对应的被注解的类的信息。所以,即使没有依赖关系的两个模块,在ARouter的帮助下,也能实现调用到B模块中类的效果。

源码分析

了解了编译器产物后,接下来我们开始从下面两个方法入手,进行Activity页面跳转(无参数)源码分析。

  • ARouter.init(getApplication())
  • ARouter.getInstance().build("/test/activity2").navigation()

init

init是ARouter的初始化方法,跟进这个方法,会进入到LogisticsCenter的init方法中。

java 复制代码
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
    //通过插件加载
    loadRouterMap();
    if (registerByPlugin) {
        //如果是通过插件注册,则不需要做任何处理,registerByPlugin默认为false,那么它是
        //在哪被赋值的?loadRouterMap
    } else {
        //非插件加载
    }
}

ARouter的初始化有两种方式,一种是通过插件在编译期生成代码,另一种是运行期遍历所有dex文件解析出注解类。我们先看看loadRouterMap方法做了什么。

插件注册

csharp 复制代码
/**
 * arouter-auto-register plugin will generate code inside this method
 * call this method to register all Routers, Interceptors and Providers
 */
private static void loadRouterMap() {
    registerByPlugin = false;
    // auto generate register code by gradle plugin: arouter-auto-register
    // looks like below:
    // registerRouteRoot(new ARouter..Root..modulejava());
    // registerRouteRoot(new ARouter..Root..modulekotlin());
}

可以看到这里除了将registerByPlugin赋值为false之外,再无任何代码,但是从方法注释上可以看出,ARouter插件会通过自动生成代码插入到这个方法中来注册所有的Routers, Interceptors and Providers,生成代码的形式如方法体中注释那样,通过插入代码调用registerRouteRoot、registerProvider、registerInterceptor方法进行注册(registerProvider、registerInterceptor注释上没体现出来,但实际会有),而传入的参数就是上边提到过的编译期产物如ARouter$$Root$$modulejava

scss 复制代码
private static void registerRouteRoot(IRouteRoot routeRoot) {
    markRegisteredByPlugin();
    if (routeRoot != null) {
        routeRoot.loadInto(Warehouse.groupsIndex);
    }
}

registerRouteRoot被调用后会先调用markRegisteredByPlugin将registerByPlugin变量赋值为true,终于找到registerByPlugin赋值的地方了。紧接着将Warehouse.groupsIndex这个静态map作为参数,调用IRouteRoot实现类的loadInto方法,我们以ARouter$$Root$$modulejava辅助类的实现为例,看下这个loadInto方法的实现,它会将modulejava模块下所有分组信息存入Warehouse.groupsIndex这个map集合中。

ruby 复制代码
public class ARouter$$Root$$modulejava implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("m2", ARouter$$Group$$m2.class);
    routes.put("module", ARouter$$Group$$module.class);
    routes.put("test", ARouter$$Group$$test.class);
    routes.put("yourservicegroupname", ARouter$$Group$$yourservicegroupname.class);
  }
}

也就是说,所有的Routers, Interceptors and Providers分组信息都会被一次性在loadRouterMap方法中被存入到Warehouse.groupsIndex、Warehouse.interceptorsIndex、Warehouse.providersIndex等map集合中,这就是初始化时做的准备工作。

代码注册

接下来我们再看下非插件类型的注册。前边在说插件类型初始化时,是通过调用registerRouteRoot方法直接创建各模块下的形如ARouter$$Root$$modulejava的对象,将其new出来存入map。插件当然是可以直接在编译期获取到各模块下生成的这个对象,但是若不使用插件生成的方式,又该怎么拿到这些类并创建对象呢?ARouter的做法就是遍历所有dex文件。

scss 复制代码
Set<String> routerMap;
if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
    //debug状态下或者新版本时,需要重新扫描dex并将所有包名以"com.alibaba.android.arouter.routes"
    //开头的类获取到,getFileNameByPackageName方法实现就不再深入了
    routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
    if (!routerMap.isEmpty()) {
        //拿到一次后,以json字符串的形式保存到SharedPreferences,下次同一版本不再进行扫描,因为扫描本身是很耗时的
        context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
    }
    PackageUtils.updateVersion(context);  
} else {
    //不需要重新扫描的情况,直接从sp中读取
    routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
}
//逻辑走到这里,routerMap中存储的全部是IRouteRoot、IInterceptorGroup、IProviderGroup类型的类,
//分别通过反射的方式将对象创建出来,并调用loadInto填充三个map集合
for (String className : routerMap) {
    if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
        ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
    } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
        ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
    } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
        ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
    }
}

方法的最后,拿到各个IRouteRoot、IInterceptorGroup、IProviderGroup实现类后,通过反射创建对象,并调用各自的loadInto方法,这样一来,初始化完成后,三个静态map:Warehouse.groupsIndex、Warehouse.interceptorsIndex、Warehouse.providersIndex中就有信息了。

至此,我们就把ARouter初始化的逻辑分析完了。接下来开始看看,它是怎么借助这些准备好的信息实现跨模块调用的,以下面一行代码调用为例。

scss 复制代码
ARouter.getInstance().build("/test/activity2").navigation();

getInstance

创建出ARouter单例。

csharp 复制代码
public static ARouter getInstance() {
    if (!hasInit) {
        throw new InitException("ARouter::Init::Invoke init(context) first!");
    } else {
        if (instance == null) {
            synchronized (ARouter.class) {
                if (instance == null) {
                    instance = new ARouter();
                }
            }
        }
        return instance;
    }
}

build

typescript 复制代码
public Postcard build(String path) {
    return _ARouter.getInstance().build(path);
}

ARouter的build方法会进入_ARouter的build方法。

scss 复制代码
protected Postcard build(String path) {
    //这里有一个获取PathReplaceService的逻辑,与本次分析无关,我们先不关注,默认为null
    PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
    if (null != pService) {
        path = pService.forString(path);
    }
    //extractGroup从path上解析出group,如"/test/activity2"的group就是test
    return build(path, extractGroup(path), true);
}

同样,这里的PathReplaceService跳过,默认为null。可以看到,最终build方法就是构建了一个Postcard,并将group和path两个值保存为其成员变量。

typescript 复制代码
protected Postcard build(String path, String group, Boolean afterReplace) {
    ......
    return new Postcard(path, group);
}

因为build方法的返回值是Postcard对象,那么navigation自然也是Postcard的方法了。不断跟进此方法,你会发现最终又来到了_ARouter中,_ARouter的navigation方法中,将postcard对象传了进去,所以_ARouter中也就包含了postcard的信息。

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

_ARouter的navigation方法,这里的内容较多,我们只关注和本次分析相关的,去掉一些非主线代码。

java 复制代码
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
    try {
        //填充postcard信息
        LogisticsCenter.completion(postcard);
    } catch (NoRouteFoundException ex) {
        return null;
    }

    //这里是一个拦截器机制,跟本次分析无关,看完LogisticsCenter.completion直接进入它的onContinue方法中
    interceptorService.doInterceptions(postcard, new InterceptorCallback() {
        @Override
        public void onContinue(Postcard postcard) {
            _navigation(postcard, requestCode, callback);
        }
    });

    return null;
}

LogisticsCenter.completion,看看这个方法做了什么。

java 复制代码
public synchronized static void completion(Postcard postcard) {
    if (null == postcard) {
        throw new NoRouteFoundException(TAG + "No postcard!");
    }
    //首先根据传入的路径获取到RouteMeta对象。RouteMeta对象是什么?
    //还记得我们前面分析path生成的辅助类时提到的下面这一行代码,
    //RouteMeta就是封装了被@Route注解的类的信息。
    //如RouteMeta.build(RouteType.ACTIVITY, TestModule2Activity.class, "/module/2", "m2", null, -1, -2147483648)
    RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
    //拿到这个信息后,又分两种情况处理,为空和不为空,为空的情况不在我们今天分析的范围内,所以就不考虑了,我们来看非空的逻辑。
    if (null == routeMeta) {
        ......
    } else {
        //给postcard填充信息,将routeMeta保存的目标信息设置进去
        postcard.setDestination(routeMeta.getDestination());
        postcard.setType(routeMeta.getType());
        postcard.setPriority(routeMeta.getPriority());
        postcard.setExtra(routeMeta.getExtra());
        //本次分析不涉及uri,所以直接跳过
        Uri rawUri = postcard.getUri();
        if (null != rawUri) {   // Try to set params into bundle.
            ......
        }
        //这里也都不满足,因为我们的routeMeta.getType()是RouteType.ACTIVITY,跳过
        switch (routeMeta.getType()) {
            case PROVIDER:  
                break;
            case FRAGMENT:
            default:
                break;
        }
    }
}

所以这个方法对于本次分析来说,也只是填充Postcard信息,前边Postcard先存储了group和path信息,现在又将目标类的信息存入进去,可见此类的命名为"明信片"还是很有意思的,明信片自然要包含目的地的信息,我们看一下代码执行到这里,Postcard已经包含了哪些信息,先有一个直观的了解。

继续回到navigation方法中,我们继续来看onContinue方法的执行,来到这里,才算是真正要开始进行页面跳转了,方法体中我们只保留了ACTIVITY类型,其他类型不在本次分析之内。

java 复制代码
private Object _navigation(final Postcard postcard, final int requestCode, final NavigationCallback callback) {
    final Context currentContext = postcard.getContext();
    switch (postcard.getType()) {
        case ACTIVITY:
            // 首先构建intent对象,从上边的截图中我们可以看到,getDestination得到的
            //是目标路径Test2Activity的class对象,正好用来构建intent
            final Intent intent = new Intent(currentContext, postcard.getDestination());
            //传递的intent参数,本次我们分析的是不带参数的跳转,所以这个暂不关注
            intent.putExtras(postcard.getExtras());
            // 设置flag类型
            int flags = postcard.getFlags();
            if (0 != flags) {
                intent.setFlags(flags);
            }
            // 如果currentContext不是Activity的上下文context对象,就设置FLAG_ACTIVITY_NEW_TASK
            if (!(currentContext instanceof Activity)) {
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            }
            // 暂不关注
            String action = postcard.getAction();
            if (!TextUtils.isEmpty(action)) {
                intent.setAction(action);
            }
            // 切换到主线程执行startActivity,看到这里是不是觉得很奇妙,原来ARouter
            //最终还是使用了最常规的activity打开方式进行跳转的
            runInMainThread(new Runnable() {
                @Override
                public void run() {
                    startActivity(requestCode, currentContext, intent, postcard, callback);
                }
            });

            break;
    }
    return null;
}

总结

至此我们就已经将ARouter跨模块实现页面跳转的逻辑分析完了,我们回顾一下ARouter到底是怎么做到的?我们在非组件化开发中,页面跳转只要拿到目标页面Activity的class对象,就可以直接调用startActivity进行跳转,我们也发现ARouter最终的实现也是如此,所以这个框架所做的逻辑就很清晰了,一句话形容,整个框架所做的核心就是在协助我们去拿到目标Activity页面的class对象。知道这一点之后,我们还要知道它是怎么拿到的?ARouter通过编译器生成辅助代码的方式来实现,辅助代码中保存好开发者定义的path和目标页面的映射关系,这样一来,开发者就可以通过path拿到目标页面的信息,实现跳转。

相关推荐
深海呐2 小时前
Android AlertDialog圆角背景不生效的问题
android
IT毕设梦工厂2 小时前
计算机毕业设计选题推荐-在线拍卖系统-Java/Python项目实战
java·spring boot·python·django·毕业设计·源码·课程设计
ljl_jiaLiang2 小时前
android10 系统定制:增加应用使用数据埋点,应用使用时长统计
android·系统定制
花花鱼2 小时前
android 删除系统原有的debug.keystore,系统运行的时候,重新生成新的debug.keystore,来完成App的运行。
android
落落落sss3 小时前
sharding-jdbc分库分表
android·java·开发语言·数据库·servlet·oracle
工业互联网专业4 小时前
毕业设计选题:基于springboot+vue+uniapp的驾校报名小程序
vue.js·spring boot·小程序·uni-app·毕业设计·源码·课程设计
消失的旧时光-19435 小时前
kotlin的密封类
android·开发语言·kotlin
服装学院的IT男7 小时前
【Android 13源码分析】WindowContainer窗口层级-4-Layer树
android
CCTV果冻爽8 小时前
Android 源码集成可卸载 APP
android
码农明明8 小时前
Android源码分析:从源头分析View事件的传递
android·操作系统·源码阅读