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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容