主頁 > 移動端開發 > 組件化解耦 | 淺析ARouter路由發現原理與簡單實踐

組件化解耦 | 淺析ARouter路由發現原理與簡單實踐

2022-01-05 21:45:06 移動端開發

組件化解耦 | 淺析ARouter路由查找原理與簡單實踐

      • 前言
      • 專案地址/資源
      • 專案概覽
      • 路由動態注冊與生成
        • 核心思路與技術要點
        • APT處理注解
        • 路由初始化
        • Gradle插件實作
          • 目標類查找
          • 插入代碼
      • 實作一個簡單的路由發現框架
        • 模塊組成
        • ZRouter-annotation
        • ZRouter-compiler
        • ZRouter-api
      • 結尾

前言

2022新年好,回顧過去兩年雖然零零散散多少也學了點東西,但是缺少了總結輸出,一方面是因為作業比之前忙了很多,一方面也是因為自己懈怠了,正好前段時間公司有重構需求需要了解下路由框架,借這個機會重新撿起一些丟掉的東西吧!

ARouter在眾多路由框架中也算是經典了,但是對于SDK開發來說,ARouter不免有點過重了,里面有大量對Activity和Fragment的業務,對于SDK解耦基本用不上,但是我們還是可以參考ARouter的路由發現思路,完成一個自己的小路由框架,直接進入主題吧

專案地址/資源

  • 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, ClassReader.EXPAND_FRAMES)
        inputStream.close()
    }

    static class ScanClassVisitor extends ClassVisitor {

        ScanClassVisitor(int api, ClassVisitor cv) {
            super(api, cv)
        }

        void visit(int version, int access, String name, String signature,
                   String superName, String[] interfaces) {
            super.visit(version, access, name, signature, superName, interfaces)
            // 這里的ext是一個ScanSetting類,里面有掃描的資訊包括IRouteRoot、IProviderGroup、IInterceptorGroup
            RegisterTransform.registerList.each { ext ->
                // 這里判斷是不是符合的類
                if (ext.interfaceName && interfaces != null) {
                    interfaces.each { itName ->
                        if (itName == ext.interfaceName) {
                            //fix repeated inject init code when Multi-channel packaging
                            if (!ext.classList.contains(name)) {
                               // 存放在ScanSetting.classList 中
                                ext.classList.add(name)
                            }
                        }
                    }
                }
            }
        }
    }
插入代碼

插入代碼這里也是遍歷ScanSetting串列了,然后通過RegisterCodeGenerator.(ScanSetting registerSetting)方法插入代碼

// RegisterCodeGenerator#insertInitCodeTo
    static void insertInitCodeTo(ScanSetting registerSetting) {
        if (registerSetting != null && !registerSetting.classList.isEmpty()) {
            RegisterCodeGenerator processor = new RegisterCodeGenerator(registerSetting)
            // 這里的file是指LogisticsCenter.class 所在的jar,掃描時標注賦值的,畢竟要在一個類插入代碼,首先要知道這個類在哪里
            File file = RegisterTransform.fileContainsInitClass
            if (file.getName().endsWith('.jar'))
                processor.insertInitCodeIntoJarFile(file)
        }
// RegisterCodeGenerator#insertInitCodeIntoJarFile
private File insertInitCodeIntoJarFile(File jarFile) {
        if (jarFile) {
            ...

            while (enumeration.hasMoreElements()) {
                ...
                if (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == entryName) {
                    // 找到目標檔案開始插入
                    def bytes = referHackWhenInit(inputStream)
                    jarOutputStream.write(bytes)
                } else {
                    jarOutputStream.write(IOUtils.toByteArray(inputStream))
                }
                inputStream.close()
                jarOutputStream.closeEntry()
            }
            ...
        }
        return jarFile
    }   

// RegisterCodeGenerator#referHackWhenInit
    private byte[] referHackWhenInit(InputStream inputStream) {
        ClassReader cr = new ClassReader(inputStream)
        ClassWriter cw = new ClassWriter(cr, 0)
        ClassVisitor cv = new MyClassVisitor(Opcodes.ASM5, cw)
        cr.accept(cv, ClassReader.EXPAND_FRAMES)
        return cw.toByteArray()
    }     

前面掃描標注了LogisticsCenter.class所在的jar,這里也是找到這個jar,然后遍歷這個jar找到LogisticsCenter.class,然后呼叫了MyClassVisitor,MyClassVisitor里面再呼叫一個RouteMethodVisitor找到目標方法然后插入代碼,這一塊應該很好理解,直接看RouteMethodVisitor的實作

class RouteMethodVisitor extends MethodVisitor {

        RouteMethodVisitor(int api, MethodVisitor mv) {
            super(api, mv)
        }

        @Override
        void visitInsn(int opcode) {
            //generate code before return
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
                // 遍歷前一步存放的類名串列
                extension.classList.each { name ->
                    name = name.replaceAll("/", ".")
                    mv.visitLdcInsn(name)//類名
                    // generate invoke register method into LogisticsCenter.loadRouterMap()
                    mv.visitMethodInsn(Opcodes.INVOKESTATIC
                            , ScanSetting.GENERATE_TO_CLASS_NAME
                            , ScanSetting.REGISTER_METHOD_NAME
                            , "(Ljava/lang/String;)V"
                            , false)
                }
            }
            super.visitInsn(opcode)
        }
        @Override
        void visitMaxs(int maxStack, int maxLocals) {
            super.visitMaxs(maxStack + 4, maxLocals)
        }
    }

至此,Gradle插件就完成了所有的作業

實作一個簡單的路由發現框架

前面說到這次重新看ARouter的原因主要是公司專案重構,而且主要是想用一下這個路由發現業務,在分析完ARouter路由注冊和發現這塊的邏輯,可以看到還是比較好實作的,下面示例一個簡單版本的實作

模塊組成

  • zrouter-annotation 定義了注解還有包含注解的資訊
  • zrouter-api 核心API,包括初始化等
  • zrouter-compiler 注解處理器,用來根據注解動態生成模塊代碼

ZRouter-annotation

這個模塊主要定義了用到的注解和處理注解資訊生成的bean類工具類等,示例也非常簡單直接參考ARouter的


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

    String path();

    int priority() default -1;

    int extras() default Integer.MIN_VALUE;

}

// RouteMeta.java

public class RouteMeta {

    private Class<?> destination;   
    private String path;           
    private String group;          
    private int priority = -1;     
    private int extra;
    
    ...
    // 省略 get set方法和建構式
}

ZRouter-compiler

直接上代碼,核心部分是process,其他一些api工具也貼上方便以后參考,三方庫主要使用了javapoet生成代碼以及auto-service動態注冊


// ZRouterCompiler.java
@AutoService(Processor.class)
public class ZRouterCompiler extends AbstractProcessor {

    /**
     * 節點工具類(類、函式、屬性都是節點)
     */
    private Elements mElementUtils;

    /**
     * 類資訊工具類
     */
    private Types mTypeUtils;

    /**
     * 檔案生成器
     */
    private Filer mFiler;

    /**
     * 日志資訊列印器
     */
    private Messager mMessager;

    // Module name, maybe its 'app' or others
    String moduleName = null;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mElementUtils = processingEnv.getElementUtils();
        mTypeUtils = processingEnv.getTypeUtils();
        mFiler = processingEnv.getFiler();
        mMessager = processingEnv.getMessager();


        //通過 key 獲取 build.gradle 中對應的 value
//        processingEnv.getOptions().get("KeyValue");

        // Attempt to get user configuration [moduleName]
        Map<String, String> options = processingEnv.getOptions();
        if (MapUtils.isNotEmpty(options)) {
            moduleName = options.get(KEY_MODULE_NAME);
        }

        if (StringUtils.isNotEmpty(moduleName)) {
            moduleName = moduleName.replaceAll("[^0-9a-zA-Z_]+", "");

            mMessager.printMessage(Diagnostic.Kind.NOTE,"The user has configuration the module name, it was [" + moduleName + "]");
        } else {
            mMessager.printMessage(Diagnostic.Kind.ERROR,"NO_MODULE_NAME");
            throw new RuntimeException("ZRouter::Compiler >>> No module name, for more information, look at gradle log.");
        }

    }


    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        if (CollectionUtils.isEmpty(annotations)) {
            return false;
        }

        List<Element> elementList = new ArrayList<>(roundEnv.getElementsAnnotatedWith(Route.class));
        Map<Element,RouteMeta> metaMap = new HashMap<>();


       try {
           // 獲取注解資訊,構建bean類
           for (Element element : elementList){
               System.out.println("ele:" + element.getSimpleName());
               RouteMeta routeMeta = new RouteMeta();
               Route annotation = element.getAnnotation(Route.class);
               routeMeta.setPath(annotation.path());
               routeMeta.setExtra(annotation.extras());
               routeMeta.setPriority(annotation.priority());
               metaMap.put(element,routeMeta);
           }

           // javapoet API,構建一個方法       
           MethodSpec.Builder loadPluginMethodBuilder = Utils.getLoadPluginMethodBuilder();

           // 遍歷注解資訊,生成代碼
           for (Map.Entry<Element,RouteMeta> entry : metaMap.entrySet()){
               RouteMeta routeMeta = entry.getValue();
               Element element = entry.getKey();

               loadPluginMethodBuilder.addStatement("data.add(new $T($T.class,$S,$S," +
                       routeMeta.getPriority()+ "," + routeMeta.getExtra()+ "))",
                       CLS_ROUTE_META,
                       ClassName.get((TypeElement) element),
                       routeMeta.getPath(),
                       routeMeta.getGroup()
               );

           }

           // 生成檔案
           String fileName = PREFIX_CLASS_NAME + moduleName;
           System.out.println("ready to generate file:" + fileName);
           JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                   TypeSpec.classBuilder(fileName)
                           .addJavadoc(FILE_TIP)
                           .addSuperinterface(CLS_ROUTER_ACQUIRER)
                           .addModifiers(PUBLIC)
                           .addMethod(loadPluginMethodBuilder.build())
                           .build()
           ).addFileComment(FILE_TIP).build().writeTo(mFiler);




       }catch (Exception e){
           e.printStackTrace();
           System.out.println("=====build route error =====");
       }

        return true;
    }

    /**
     * 接收外來傳入的引數,最常用的形式就是在 build.gradle 腳本檔案里的 javaCompileOptions 的配置
     *
     * @return 屬性的 Key 集合
     */
    @Override
    public Set<String> getSupportedOptions() {
//        Set<String> hashSet = new LinkedHashSet<>();
//        hashSet.add("MODULE_NAME");
        return super.getSupportedOptions();
    }

    /**
     * 當前注解處理器支持的注解集合,如果支持,就會呼叫 process 方法
     *
     * @return 支持的注解集合
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> hashSet = new LinkedHashSet<>();
        hashSet.add(Route.class.getCanonicalName());
//        hashSet.add(BindClick.class.getCanonicalName());
        return hashSet;
    }

    /**
     * 編譯當前注解處理器的 JDK 版本
     *
     * @return JDK 版本
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.RELEASE_8;
    }

工具類和常量

// Utils.java
public class Utils {

    static final ClassName ROUTER_ACQUIRER = ClassName.get("com.hjl.zrouter_api","IRouteAcquirer");

    static ParameterizedTypeName inputMapType = ParameterizedTypeName.get(
            ClassName.get(List.class),
            ClassName.get(RouteMeta.class)
    );

    public static MethodSpec.Builder getLoadPluginMethodBuilder(){
        return MethodSpec.methodBuilder(METHOD_LOAD_PLUGIN)
                .addAnnotation(Override.class)
                .addModifiers(PUBLIC)
                .addParameter(inputMapType,"data");
    }

}

// Constant.java
interface Constant {
    String KEY_MODULE_NAME = "ZROUTER_MODULE_NAME";
    String PREFIX_CLASS_NAME = "ZRouter$$";
    String METHOD_LOAD_PLUGIN = "loadPlugin";
    String PACKAGE_OF_GENERATE_FILE = "com.hjl.zrouter.routes";
    String FILE_TIP = "This file generate by ZRouter, Do not modify";

    ClassName CLS_ROUTER_ACQUIRER = ClassName.get("com.hjl.zrouter_api","IRouteAcquirer");
    ClassName CLS_ROUTE_META = ClassName.get(RouteMeta.class);
}

build.gradle

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])


    implementation project(':zrouter:zrouter-annotation')

    implementation 'com.squareup:javapoet:1.13.0'
    implementation 'org.apache.commons:commons-lang3:3.5'
    implementation 'org.apache.commons:commons-collections4:4.1'

    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
    compileOnly 'com.google.auto.service:auto-service-annotations:1.0-rc7'
}

核心代碼邏輯還是很簡單的,主要是思路,生成的代碼也很簡單,接下來的就是遍歷包名找到這些類了

// TestJavaChannelAdapter.java
@Route(path = "/test/java")
public class TestJavaChannelAdapter implements IChannelAdapter {
   ... 
   // 省略業務邏輯
}

// 下面是對應生成的類

/**
 * This file generate by ZRouter, Do not modify
 */
public class ZRouter$$testmodulejava implements IRouteAcquirer {
  @Override
  public void loadPlugin(List<RouteMeta> data) {
    data.add(new RouteMeta(TestJavaChannelAdapter.class,"/test/java",null,-1,-2147483648));
  }
}

ZRouter-api

這里主要是獲取到所有的注解類以及一些跳轉邏輯的處理了,個人認為跳轉邏輯根據業務需要可以直接實作,掃描包內或者Gradle插件可以直接參考ARouter,這里只是非常簡單的示范了下


public interface IRouteAcquirer {
    void loadPlugin(List<RouteMeta> data);
}

public class ZRouter {
    private List<RouteMeta> routeMetaList = new ArrayList<>();
    
    public void init(Context context){

        mContext = context;
        Log.i(TAG, "start init");
        routeMetaList.clear();
        loadRouterMap();
        if (isLoadByPlugin){
            Log.i(TAG, "load route meta from plugin");
        }else {
            try {
                // 耗時操作 可用Gradle插件解決 ,可以直接拿ARouter的改一改,這里的工具類直接使用ARouter的
                Set<String> clsNameSet = ClassUtils.getFileNameByPackageName(context, "com.hjl.zrouter.routes");
                parseCls(clsNameSet);
            } catch (Exception e) {
                e.printStackTrace();
                Log.e(TAG,"load route meta error :",e);
            }
        }
    }
    
    private static void loadRouterMap(){
        // TODO : ASM 代碼插入
    }
    
    // 決議類并添加到串列
    private void parseCls(Set<String> data) throws Exception {
        for (String cls : data){
            ((IRouteAcquirer)Class.forName(cls).getConstructor().newInstance()).loadPlugin(routeMetaList);
        }
    }

     // 獲取決議好的注解類資訊
    public List<RouteMeta> getRouteMetaList() {
        return routeMetaList;
    }
}            

獲取到決議好的注解類資訊后,就可以根據具體業務去實作相關邏輯了,demo示范一下獲取單例集合渠道串列

        val supportChannelList = mutableListOf<IChannelAdapter>()
        ZRouter.getInstance().init(context)

        ZRouter.getInstance().routeMetaList.forEach {

            Log.i("ward", "get route meta:$it ")
            val newInstance = it.destination.getConstructor().newInstance() as IChannelAdapter
            supportChannelList.add(newInstance)
        }

至此,我們完成了可以用于SDK解耦的一個小型的路由注解發現框架

結尾

其實可以發現大部分好用的開源框架都是運用了注解AOP和Gradle插件ASM插入代碼分別在不同的時機插入代碼,減少了大量的作業,因此對于閱讀第三方框架的原始碼和了解的他們思想來說,熟悉這兩項技術的流程和運用還是很有必要的,同時也可以在自己的專案運用來解決一些需求場景

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/403996.html

標籤:其他

上一篇:不會編程如何制作Android輔助功能之三(簡書)

下一篇:畢業半個月,我是如何同時做好位元組程式媛、模特與寫作博主的?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more