美文网首页Android开发经验谈Android开发Android进阶之路
Android组件化架构 —— 基础(三) - ARouter

Android组件化架构 —— 基础(三) - ARouter

作者: 雷小歪 | 来源:发表于2021-09-18 17:58 被阅读0次
    xwzz.jpg

    前篇回顾

    上篇文章,我们了解到“路由”是如何进行工作的,并尝试手写了实现代码;

    本篇,以ARouter为例,我们将剖析它是如何完成工作的,并通过源码了解其原理。

    还是以Activity跳转Fragment获取跨模块功能调用三个方向为出发点,看看ARouter将如何实现这些功能。

    集成ARouter

    咱们先快速将ARouter集成到项目中,当然你也可以参照官方文档来操作。

    第一篇文章中,我们将业务模块的依赖关系集中到了config.gradle文件中,现修改如下:

    • 1、config.gradle中管理依赖版本
    ext { 
        // ARouter 版本号
        arouter_api_version = "1.5.2"
        arouter_compiler_version = "1.5.2"
    
        //各模块引入的第三方公共库 
        dependenciesImport = [
                ...
                ...
                arouter_api     : "com.alibaba:arouter-api:$arouter_api_version"
        ]
    
        //注解处理器 
        dependenciesKapt = [
                ...
                ...
                arouter: "com.alibaba:arouter-compiler:$arouter_compiler_version"
        ]
    }
    
    • 2、子模块的build.gradle中,传递APT所需参数,并引入依赖
    def dependenciesImport = rootProject.ext.dependenciesImport
    def dependenciesKapt = rootProject.ext.dependenciesKapt
    
    android {
        defaultConfig {
            ...
            ...
            // ARouter APT 传参
            kapt {
                arguments {
                    arg("AROUTER_MODULE_NAME", project.getName())
                }
            }   
        }
    }
    
    dependencies {
    
        implementation fileTree(dir: 'libs', include: ['*.jar'])
    
        implementation project(":lib_comm")
    
        // 导入依赖库
        dependenciesImport.each { k, v -> implementation(v) }
        // 导入依赖的注解处理器
        dependenciesKapt.each { k, v -> kapt(v) }
    }
    
    • 3、初始化ARouter
    class App : Application() {
    
        override fun onCreate() {
            super.onCreate()
            initRouter()
        }
    
        private fun initRouter() {
            if (BuildConfig.DEBUG) {
                ARouter.openLog()   // 打印日志
                ARouter.openDebug() // 开启调试模式
            }
            ARouter.init(this) // 初始化
        }
    }
    
    • 4、运行查看Log记录:
    2021-09-13 17:47:07.911 10088-10088/com.ljb.component I/ARouter::: ARouter init success![ ] 
    

    ARouter - Activity跳转

    根据官方文档,只需2步便可完成功能:

    • 1、注解标记需要跳转的Activity
    @Route(path = "/user/UserMainActivity" , group =  "user")
    class UserMainActivity : AppCompatActivity() {
          ...
    }
    
    • 2、发起跳转
    ARouter.getInstance().build("/user/UserMainActivity").navigation()
    

    很神奇!与前篇我们实现的路由相比,ARouter的代码要简洁很多,完全不需要手动注册路由就可完成跳转,它是怎么做到的呢?

    通过跟进navigation()函数调用过程,我们把目光聚焦到两个容器中:

    // ARouter源码
    class Warehouse {
        // Cache route and metas
        static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
        static Map<String, RouteMeta> routes = new HashMap<>();
        
        ...
    }
    
    • Warehouse.groupsIndex: 用于存储所有的路由组
    public interface IRouteGroup {
        /**
         * Fill the atlas with routes in group.
         * atlas用于存储当前组里的所有路由,实际传入的就是Warehouse.routes
         */
        void loadInto(Map<String, RouteMeta> atlas);
    }
    
    • Warehouse.routes:用于存储已注册的所有路由
     // 路由包装类,路由目标的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;
        
        ...
    }
    

    ARouter对路由提出了分组概念,上面 UserMainActivity就属于user组下,当路由path存在2级及以上时,group字段也可以省略,ARouter默认会使用第一个反斜杠后面的path作为组名。

    // group可省略不写
    @Route(path = "/user/UserMainActivity")
    class UserMainActivity : AppCompatActivity() {
          ...
    }
    

    一般情况下,我们会将同一模块的路由划分在同一个组下,例如App模块下的所有路由都在“app”这个分组下 , user模块的路由都在“user”分组下;当然,同一模块拥有多个分组也是完全可行的,只要保证与其它模块中的路由分组不重名即可。

    通过翻阅源码,分析这两个容器的作用,大致如下:

    • 1、当传入path进行跳转时,优先从Warehouse.routes中直接获取路由对象;
    • 2、路由对象不存在,就需要通过Warehouse.groupsIndex路由组来完成注册功能;
    • 3、注册成功后,当前path所在组的所有路由都将存储到Warehouse.routes中;
    • 4、回到第1步,获取路由对象;
    • 5、读取路由对象信息;
    • 6、完成跳转。

    下方是ARouter源码中的实现代码,我把不重要的部分进行了删减:

    //ARouter源码 
    public class LogisticsCenter {
        ...
    
        public synchronized static void completion(Postcard postcard) {
            
            //1、当传入path进行跳转时,优先从Warehouse.routes中直接获取路由包装对象;
            RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
    
            if (null == routeMeta) {
                
                //2、包装对象不存在,就需要通过Warehouse.groupsIndex路由组来完成注册功能;
                addRouteGroupDynamic(postcard.getGroup(), null);
                
                //4、回到第1步,获取包装对象;
                completion(postcard);   
            } else {
                // 5、读取包装对象信息;
                postcard.setDestination(routeMeta.getDestination());
                postcard.setType(routeMeta.getType());
                
                ...
            }
        
        
        public synchronized static void addRouteGroupDynamic(String groupName, IRouteGroup group) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            
            if (Warehouse.groupsIndex.containsKey(groupName)){
                //3、注册成功后,当前path所在组的所有路由都将存储到Warehouse.routes中;
                Warehouse.groupsIndex.get(groupName).getConstructor().newInstance().loadInto(Warehouse.routes);
                Warehouse.groupsIndex.remove(groupName);
            }
    
    
            if (null != group) {
                // 注册本组路由
                group.loadInto(Warehouse.routes);
            }
        }
    
        ...
    }
    
    final class _ARouter {
    
        protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
                ...
    
                LogisticsCenter.completion(postcard);
                
                ...         
                // 6、完成跳转。
                switch (postcard.getType()) {
                    case ACTIVITY:
                        // Build intent
                        final Intent intent = new Intent(currentContext, postcard.getDestination());
                        intent.putExtras(postcard.getExtras());
                        
                        //...
                        // Navigation in main looper.
                        runInMainThread(new Runnable() {
                            @Override
                            public void run() {
                                startActivity(requestCode, currentContext, intent, postcard, callback);
                            }
                        });
                        break;
                    
                    ...
                }
            ...
        
        }
    }
    

    整个跳转的源代码过程不难理解,但关键点在于:

    • Warehouse.groupsIndex容器是何时将路由组加载至内存的?
    • group.loadInto(Warehouse.routes)又是如何完成注册的?

    回答这两个问题,需回过头来看ARouter初始化都做了什么。

     ARouter.init(this)   // ARouter初始化
    

    随着代码的跟进,我们会发现下面这几行代码:

    //ARouter源码 
    public final class Consts {
    
      public static final String SDK_NAME = "ARouter";
      public static final String SEPARATOR = "$$";
      public static final String SUFFIX_ROOT = "Root";
      public static final String DOT = ".";
      public static final String ROUTE_ROOT_PAKCAGE = "com.alibaba.android.arouter.routes";
    }
      
    public class LogisticsCenter {
    
        public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
        
                // 获取com.alibaba.android.arouter.routes下的所有class文件
                routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
                
        
                for (String className : routerMap) {
                    if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
    
                        // 将名为ARouter$$Root开头的Class对象创建出来,并调用loadInto(),将分组信息加载进Warehouse.groupsIndex中
                        ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
                    }
                    ...
                }
        }
    
    }
    

    源码中会从com.alibaba.android.arouter.routes包下读取所有的Class文件,并将文件名以ARouter$$Root为开头的Class文件挑选出来,最后通过反射创建对象,并调用其loadInto()函数,注意这里传入参数正是Warehouse.groupsIndex,我们的路由组容器,从而将分组信息载入Warehouse.groupsIndex中。

    果不其然,我们在user模块的build文件夹下找到了这些文件:

    ARouter生成的文件.png
    • 关于ARouter$$Root 前缀文件,可以看到其后缀是当前模块名module_user,而其loadInto()函数主要是将user模块下涉及到的所有路由组存储到Warehouse.groupsIndex中;
    public class ARouter$$Root$$module_user implements IRouteRoot {
    
      @Override
      public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
        routes.put("user", ARouter$$Group$$user.class);
      }
    }
    
    • 关于ARouter$$Group前缀文件,其后缀是当前路由所属的的分组名user,前面在梳理Activity跳转的过程中,我们知道当路由未注册时,会通过分组名获取到组,并创建组对象调用其loadInto()函数完成对Warehouse.routes的注册,此文件中编写的既是具体的注册过程。
    //3、注册成功后,当前path所在组的所有路由都将存储到Warehouse.routes中;
    Warehouse.groupsIndex.get(groupName).getConstructor().newInstance().loadInto(Warehouse.routes);
    
    
    public class ARouter$$Group$$user implements IRouteGroup {
      @Override
      public void loadInto(Map<String, RouteMeta> atlas) {
        // loadInto被调用时,完成对Warehouse.routes的注册
        atlas.put("/user/AppMainActivity", RouteMeta.build(RouteType.ACTIVITY, AppMainActivity.class, "/user/appmainactivity", "user", null, -1, -2147483648));
      }
    }
    

    ARouter - APT技术

    通过上述分析,我们知道路由的注册离不开ARouter生成的这两个文件,那这两文件又是怎么来的呢?

    这里不得不提到APT技术( Annotation Processing Tool),译:注解处理器。如果对该技术有所耳闻,你应该知道在Android中我们常用的EventBusButterknifeDagger2等框架都有它的身影。如果你是第一次听说也没关系,暂时只需知道通过这门技术可以做到“在编译期时,可使编译器为我们生成具有一定规则的模板性代码”即可。

    显然,这两个文件就是ARouter通过APT生成的模板代码,至于具体的实现过程,本篇中不做过多阐述,后续处理自定义路由时,我们会尝试编写一个自己的注解处理器,到时再细聊APT技术的相关细节。
    不过我把ARouter注解处理器中生成这两个文件的核心代码贴在下方,看懂它的思路即可:

    //ARouter 注解处理器源码 
    @AutoService(Processor.class)
    @SupportedAnnotationTypes({ANNOTATION_TYPE_ROUTE, ANNOTATION_TYPE_AUTOWIRED})
    public class RouteProcessor extends BaseProcessor {
    
       @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    
            // 获取所有被 @Route 注解标记的类元素
            Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
            
            //解析这些类元素
            this.parseRoutes(routeElements);
            
        }
        
        
        private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
        
            // 解析元素
            for (Element element : routeElements) {
            
                if (types.isSubtype(tm, type_Activity)) {
                    // Activity              
                    routeMeta = new RouteMeta(route, element, RouteType.ACTIVITY, paramsType);  
                            
                    Set<RouteMeta> routeMetas = groupMap.get(routeMete.getGroup()); 
                    routeMetaSet.add(routeMete);
                    groupMap.put(routeMete.getGroup(), routeMetaSet);               
                }
            }
            
            // 根据解析元素生成Java文件
            for (Map.Entry<String, Set<RouteMeta>> entry : groupMap.entrySet()) {
              // 生成ARouter$$Group$$前缀文件
              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);
            }
            
            // 生成ARouter$$Root$$前缀文件
            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);
        }
    }
    

    至此,整个Activity跳转涉及到的源码大致就是这些,总结下ARouter都做了什么:

    • 1、在编译期,ARouter注解处理器将所有@Route注解标记的类查找出来,并根据这些类的相关信息生成ARouter$$相关文件;
    • 2、ARouter.init()初始化时,将ARouter$$Group前缀文件涉及到的路由组存储到Warehouse.groupsIndex中;
    • 3、调用ARouter...navigation()跳转页面时,通过Warehouse.groupsIndex查询路由所在组,将路由组中的所有路由注册到Warehouse.routes中,从Warehouse.routes中取出路由信息(包含Activity.class对象),完成跳转。

    Fragment获取

    依旧根据文档,获取Fragment歩奏也只需2步:

    • 1、注解标记需要获取的Fragemnt
    @Route(path = "/user/UserFragment")
    class UserFragment : Fragment() {
        ...
    }
    
    • 2、获取Fragment
    val f  = ARouter.getInstance().build("/user/UserFragment").navigation() as Fragment
    

    有了对Activity跳转源码分析的铺垫,Fragment的获取原理基本是一样的,只是在最后一步发生跳转的代码,变为返回Fragment对象:

    final class _ARouter {
    
        protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
            ...         
            // 6、完成跳转。
            switch (postcard.getType()) {
                
                // Activity跳转
                case ACTIVITY:
                    // Build intent
                    final Intent intent = new Intent(currentContext, postcard.getDestination());
                    intent.putExtras(postcard.getExtras());
                    
                    //...
                    // Navigation in main looper.
                    runInMainThread(new Runnable() {
                        @Override
                        public void run() {
                            startActivity(requestCode, currentContext, intent, postcard, callback);
                        }
                    });
                    break;
                
                // Fragment获取
                case FRAGMENT:
                    Class<?> fragmentMeta = postcard.getDestination();
                    Object instance = fragmentMeta.getConstructor().newInstance();
                    if (instance instanceof Fragment) {
                        ((Fragment) instance).setArguments(postcard.getExtras());
                    } else if (instance instanceof android.support.v4.app.Fragment) {
                        ((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());
                    }
                    return instance;
                ...
            }
            ...
        }
    }
    

    跨模块功能调用

    ARouter的方案和我们之前的实现的方案原理是一样的,都需将对外的服务暴露到lib_comm中,只是如Activity和Fragment一样注册的功能通过APT技术来实现。

    • 1、lib_comm中定义暴露的接口
    interface IUserService2 : IProvider {
    
        /**
         * 是否登录
         */
        fun isLogin(): Boolean
    }
    
    • 2、业务模块实现接口,并通过@Route注解标记
    @Route(path = "/user/UserService")
    class IUserServiceImpl2 : IUserService2 {
    
        override fun init(context: Context?) {
    
        }
    
        override fun isLogin(): Boolean {
            return UserUtils.isLogin()
        }
    
    }
    
    • 3、获取服务
    ARouter.getInstance().build("/user/UserService").navigation() as IUserService2
    

    和Fragment类似,只不过在获取时,多了一步缓存操作,缓存相关的核心代码我贴在了下方:

    class Warehouse {
        // Cache provider
        static Map<Class, IProvider> providers = new HashMap<>();
    }
    
    // 创建服务,并缓存
     switch (routeMeta.getType()) {
        case PROVIDER:  // if the route is provider, should find its instance
            // Its provider, so it must implement IProvider
            Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();
            IProvider instance = Warehouse.providers.get(providerMeta);
            if (null == instance) { // There's no instance of this provider
                IProvider provider;
                try {
                    provider = providerMeta.getConstructor().newInstance();
                    provider.init(mContext);
                    // 进行缓存
                    Warehouse.providers.put(providerMeta, provider);
                    instance = provider;
                } catch (Exception e) {
                    logger.error(TAG, "Init provider failed!", e);
                    throw new HandlerException("Init provider failed!");
                }
            }
            postcard.setProvider(instance);
            postcard.greenChannel();    // Provider should skip all of interceptors
            break;
    }
    

    获取时,只需从postcard.getProvider()中取即可:

    final class _ARouter {
    
        protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
            ...         
            // 6、完成跳转。
            switch (postcard.getType()) {
                
                // Activity跳转
                case ACTIVITY:
                    // Build intent
                    final Intent intent = new Intent(currentContext, postcard.getDestination());
                    intent.putExtras(postcard.getExtras());
                    
                    //...
                    // Navigation in main looper.
                    runInMainThread(new Runnable() {
                        @Override
                        public void run() {
                            startActivity(requestCode, currentContext, intent, postcard, callback);
                        }
                    });
                    break;
                
                // Fragment获取
                case FRAGMENT:
                    Class<?> fragmentMeta = postcard.getDestination();
                    Object instance = fragmentMeta.getConstructor().newInstance();
                    if (instance instanceof Fragment) {
                        ((Fragment) instance).setArguments(postcard.getExtras());
                    } else if (instance instanceof android.support.v4.app.Fragment) {
                        ((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());
                    }
                    return instance;
    
                // 获取Service
                case PROVIDER:
                    return postcard.getProvider();
                ...
            }
            ...
        }
    }
    

    OK,ARouter路由核心三剑客功能已摸了个底朝天,与我们自己实现的路由框架相比,其核心思想是一样的,但在细节上ARouter的处理明显优于我们,例如:

    • 通过APT来实现路由注册,省掉手动注册的繁琐,且无需在App启动时注册所有路由(与我们startup方案相比),仅在使用到某组路由时才进行注册,避免不必要的内存开销。
    • 对外提供功能的Service对象,在使用时进行缓存,避免重复创建。

    小结

    本篇对ARouter的Activity跳转、Fragment获取、扩模块功能调用花费了大幅篇章来讲解源码,是希望读者能了解其基本原理,后续我在处理自定义路由时,会参考ARouter实现来造轮子,也方便读者对后续篇章的理解。

    ARouter还支持很多功能,例如:拦截器、标准Scheme URL跳转、全局降级策略、动态路由注册等,这些功能后续篇章中一部分会讲到,一部分也可能使用不到,但并不代表这些功能没用,每个项目或团队对组件化架构的理解都会有所不同,最后落地的项目结构、路由交互方案也就各具特色。无论是怎样的项目,个人认为:

    • 组件化,它是由实际业务开发过程中,开发人员通过经验积累,设计出的一套适用于多人协作开发的架构模式;
    • 具备可复用性、可替代性、热插拔、可独立调试特性的模块,就是组件化模块(组件);
    • 由多个组件化模块组成的项目架构,就是组件化架构。

    下篇,聊聊组件通讯之Scheme URL,我们下篇再见!

    Android组件化架构 —— 基础(四) URL Scheme

    相关文章

      网友评论

        本文标题:Android组件化架构 —— 基础(三) - ARouter

        本文链接:https://www.haomeiwen.com/subject/tqkrwltx.html