组件化解耦 | 浅析ARouter路由发现原理与简单实践

Posted 疯狂的皮卡

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了组件化解耦 | 浅析ARouter路由发现原理与简单实践相关的知识,希望对你有一定的参考价值。

前言

2022新年好,回顾过去两年虽然零零散散多少也学了点东西,但是缺少了总结输出,一方面是因为工作比之前忙了很多,一方面也是因为自己懈怠了,正好前段时间公司有重构需求需要了解下路由框架,借这个机会重新捡起一些丢掉的东西吧!

ARouter在众多路由框架中也算是经典了,但是对于SDK开发来说,ARouter不免有点过重了,里面有大量对Activity和Fragment的业务,对于SDK解耦基本用不上,但是我们还是可以参考ARouter的路由发现思路,完成一个自己的小路由框架,直接进入主题吧

项目地址/资源

项目概览

对于最新版本(1.5.2)比起两年前发现多了一个gradle插件(可能是之前没看到),是可选项,可以加快首次启动加载速度,项目模块和对应功能:

模块功能
arouter-api核心API,包括路由发现、初始化跳转等
arouter-compiler注解处理器,根据注解和模块生成对应的类
arouter-annotation注解相关信息
arouter-gradle-plugingradle 插件工程,利用ASM插桩加快加载速度

路由动态注册与生成

核心思路与技术要点

  • 利用注解生成 路径 - 类 映射表,以及拓展功能(降级、拦截器、绿色通道等);再通过核心API封装进行跳转查找
  • APT 注解处理生成各个模块的路由类
  • 【可选】ASM 插桩代码,加快找到路由注册表(原有方式是扫描APK下所有dex文件固定包名下的类)

APT处理注解

arouter-compiler编译时扫描符合的注解,ARouter共有三个Processor,以主要的RouteProcessor为例,会生成三类文件

  • ARouter$$Root$$模块名: 添加模块内 《组名 - Gronp类》的映射表
  • ARouter$$Group**$$**组名: 各个组内《路径 - 路由信息》的映射表(不同模块同个组名会导致覆盖!!)
  • ARouter$$Providers$$模块名: 一个模块一个,存放所有Provider类型的映射

图为demo工程生成的类:

还是以基础的路由发现为例,在demo的moudlejava模块中,会生成

  • ARouter$$Root$$modulejava.java
  • ARouter$$Group$$yourservicegroupname.java
  • ARouter$$Group$$test.java
  • ARouter$$Group$$m2.java
  • ARouter$$Group$$module.java

其中ARouter$$Root$$modulejava 内容如下

public interface IRouteRoot 
    void loadInto(Map<String, Class<? extends IRouteGroup>> routes);


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$$Group$$test为例,其内容如下


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


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));
  

可以看到存放的是完整路径对应的类、优先级、类型等关系,看到这里应该就大概明白,通过这些生成的类,维护着一个映射关系表,从而实现路由发现和查找

那么接下来就是如何将这一个个文件中的路由关系收集起来,进而实现各种业务逻辑

路由初始化

上面说到基础原理是通过维护一个映射关系表,从而实现路由查找功能的,那么ARouter是如何找到这些类并且加载的呢?这里看到 arouter-api 模块中初始化是如何调用的,直接上图

跟踪代码可以发现最后逻辑在LogisticsCenter#init 方法,逻辑参照注释

    /**
     * LogisticsCenter init, load all metas in memory. Demand initialization
     */
    public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException 
        mContext = context;
        executor = tpe;

        try 
            
            //load by plugin first  
            loadRouterMap(); // 注意:这个方法是个空实现,如果使用了gradle插件,会在这个方法进行插桩
            // registerByPlugin 默认为false 只有使用了gradle插件才会置为true
            if (registerByPlugin) 
                logger.info(TAG, "Load router map by arouter-auto-register plugin.");
             else 
                
                Set<String> routerMap;

                // It will rebuild router map every times when debuggable.
                // 如果首次启动或者debug模式,就会扫描特定包名下所有的类
                if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) 
                    
                    // These class was generated by arouter-compiler.
                    // 工具类 扫描特定包名下类,有兴趣的可以翻源码查看,主要是根据各种不同的dex情况进行查找
                    routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
                    if (!routerMap.isEmpty()) 
                        context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
                    

                    PackageUtils.updateVersion(context);    // Save new version name when router map update finishes.
                 else 
                    logger.info(TAG, "Load router map from cache.");
                    routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
                

         ...

         catch (Exception e) 
            throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");
        
    

结合逻辑图和源码,大概可以知道整个框架的路由发现这块的逻辑就是查找固定包名下所有的类,这里不纠结细节,继续往下看使用Gradle插件时的逻辑以及他是如何加快启动速度的

Gradle插件实现

gradle插件是一个可选的模块,通过初始化的源码可以看到如果没有应用此插件,在应用启动的时候会对包下所有的dex进行扫描并找到特定包名下所有的类,这样有个问题就是首次启动会慢一点,于是便有了这个插件进行ASM进行插桩代码,直接在打包时就把路由信息打入,直接看下应用插件后的效果

  • 应用插件前
 //  LogisticsCenter
   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());
    
  • 应用插件后
// LogisticsCenter
    private static void loadRouterMap() 
        registerByPlugin = false;
        register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
        register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
        register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
        register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
        register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulejava");
        register("com.alibaba.android.arouter.routes.ARouter$$Providers$$modulekotlin");
        register("com.alibaba.android.arouter.routes.ARouter$$Providers$$arouterapi");
    
    // 跟踪其他方法 调用国产
    private static void register(String className) 
        String str = "ARouter::";
        if (!TextUtils.isEmpty(className)) 
            try 
                Object obj = Class.forName(className).getConstructor(new Class[0]).newInstance(new Object[0]);
                if (obj instanceof IRouteRoot) 
                    registerRouteRoot((IRouteRoot) obj);
                 else if (obj instanceof IProviderGroup) 
                    registerProvider((IProviderGroup) obj);
                 else if (obj instanceof IInterceptorGroup) 
                    registerInterceptor((IInterceptorGroup) obj);
                 else 
                    ARouter.logger.info(str, "register failed, class name: " + className + " should implements one of IRouteRoot/IProviderGroup/IInterceptorGroup.");
                
             catch (Exception e) 
                ARouter.logger.error(str, "register class error:" + className, e);
            
        
    

    private static void registerRouteRoot(IRouteRoot routeRoot) 
        markRegisteredByPlugin();
        if (routeRoot != null) 
            routeRoot.loadInto(Warehouse.groupsIndex);
        
    
    
    private static void markRegisteredByPlugin() 
        if (!registerByPlugin) 
            registerByPlugin = true;
        
        
    

现在大概知道ARouter的gradle插件做了些什么东西了,反推实现在RegisterTransform 这个类中扫描了jar、源码里面相关的类,并记录下来,最后写入方法中,下面直接看插件和RegisterTransform实现源码
gradle插件这里是初始化了目标接口信息,将相关的接口封装成一个ScanSetting的列表(ScanSetting主要存放接口名称和查找到的类名列表),然后注册了RegisterTransform

public class PluginLaunch implements Plugin<Project> 

    @Override
    public void apply(Project project) 
        def isApp = project.plugins.hasPlugin(AppPlugin)
        //only application module needs this plugin to generate register code
        if (isApp) 
            Logger.make(project)

            Logger.i('Project enable arouter-register plugin')

            def android = project.extensions.getByType(AppExtension)
            def transformImpl = new RegisterTransform(project)

            //init arouter-auto-register settings
            ArrayList<ScanSetting> list = new ArrayList<>(3)
            list.add(new ScanSetting('IRouteRoot'))
            list.add(new ScanSetting('IInterceptorGroup'))
            list.add(new ScanSetting('IProviderGroup'))
            RegisterTransform.registerList = list
            //register this plugin
            android.registerTransform(transformImpl)
        
    


RegisterTransform实现

    // RegisterTransform#transform
    
    void transform(Context context, Collection<TransformInput> inputs
                   , Collection<TransformInput> referencedInputs
                   , TransformOutputProvider outputProvider
                   , boolean isIncremental) throws IOException, TransformException, InterruptedException 

        ...

        // 省略遍历所有code、jar的代码,最后都会走到ScanUtil的几个方法中,见下文解析
  

         // registerList 类型是 ArrayList<ScanSetting> 存放目标接口信息
        if (fileContainsInitClass) 
            registerList.each  ext ->
                Logger.i('Insert register code to file ' + fileContainsInitClass.absolutePath)

                if (ext.classList.isEmpty()) 
                    Logger.e("No class implements found for interface:" + ext.interfaceName)
                 else 
                    ext.classList.each 
                        Logger.i(it)
                    
                    // 这里插入代码
                    RegisterCodeGenerator.insertInitCodeTo(ext)
                
            
        

        Logger.i("Generate code finish, current cost time: " + (System.currentTimeMillis() - startTime) + "ms")
    
目标类查找

上面省略的代码在拿到特定包名所有的类后(jar、源码)走到了
ScanUtil.scanJar(src, dest)ScanUtil.scanClass(file) 这两个方法,跟踪调用后都走到ScanUtil#scanClass(InputStream inputStream),然后通过ASM遍历是否实现了相关接口,如果存在就存放在对应的ScanSetting里面的类名列表等待下一步插入代码

 static void scanClass(InputStream inputStream) 
        ClassReader cr = new ClassReader(inputStream)
        ClassWriter cw = new ClassWriter(cr, 0)
        ScanClassVisitor cv = new ScanClassVisitor(Opcodes.ASM5, cw)
        cr.accept(cv, 以上是关于组件化解耦 | 浅析ARouter路由发现原理与简单实践的主要内容,如果未能解决你的问题,请参考以下文章

ARouter的原理

Arouter之注解处理器

框架手写系列---apt方式实现ARouter框架

ARouter使用&源码小结[版本1.5.2]

ARouter框架使用总结及思考

Android-ARouter原理解析