ARouter实现页面跳转功能剖析

1,465 阅读10分钟

ARouter是一个很经典的开源项目,本次我们剖析它的主体功能之一:实现页面跳转的功能。

一、ARouter的跳转功能使用:

1.配置moduleName
     javaCompileOptions {
                annotationProcessorOptions {
                    arguments=["AROUTER_MODULE_NAME": project.getName()]
                }
     }
     
 2.引入arouter库
     dependencies {
        api 'com.alibaba:arouter-api:?'
        annotationProcessor 'com.alibaba:arouter-compiler:?'
    }
    
 3.给要跳转的页面添加Route注解
    @Route(path = "/test/second")
    public class SecondActivity  extends AppCompatActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_second);
        }
    }
    
4.发起跳转
   ARouter.getInstance().build("/test/second").navgation();

二、从上面的使用方法,让你来实现这个跳转,你会怎么做?

先说下ARouter实现大致流程:

实现页面跳转,最终还是使用startActivity方式,现在的问题是怎么通过path("/test/second")获取到SecondActivity.class,拿到Class类才可以跳转。也就是实现页面跳转就变成了怎么去获取path对应的Class类,ARouter引入了IOC,通过依赖注入获取Class类。

提供了两种方式:

  • 方式一、在初始化ARouter时遍历base.apk(5.0以及5.0以上,低版本需要遍历多个文件,当然如果要支持instantRun 或者 VM不支持Multidex等情形需要额外处理)中的类,

找到IRouterRoot的子类 如下:

找到它之后反射创建对象,并调用loadInto方法保存数据到WareHouse,当页面跳转时,反射创建ARoute?Group?Test类对象,然后调用loadInto方法并保存数据到WareHouse,

当我们跳转基于path去匹配的时候,就可以拿到这个SecondActivity.class,然后页面跳转完成。

  • 方式二、 使用auto-register的方式去优化方式一的耗时(实测在一个大小为60M左右的base.apk查找指定类大概需要1s左右,这也是ARouter使用耗时的根源所在),默认在loadRouteMap( )就一个标志位置为false

需要在编译期完成代码插入操作,遍历所有的class,找到LogisticsCenter类,然后在其loadRouteMap( )中去插入如下代码:

在初始化时先调用loadRouteMap( ),反射创建IRouterRoot的子类对象,然后保存到WareHouse,后续逻辑同方式一。

ps:  WareHouse类实现:

but  原理不重要,重要的是实现用到技术啊,难不成你还能在其他地方用到它的原理?

三、ARouter实现页面跳转涉及到的技能点

    1. 控制反转+依赖注入(概念很高大上);
    1. 外观设计模式、单例设计模式、访问者设计模式;
    1. 自定义注解的处理(apt方式);
    1. 自定义插件优化启动耗时;

1. 控制反转+依赖注入

引用别人的大白话: IOC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合,更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IOC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的使程序的整个体系结构变得非常灵活。在运行期,在外部容器动态的将依赖对象注入组件,当外部容器启动后,外部容器就会初始化。创建并管理bean对象,以及销毁他,这种应用本身不负责依赖对象的创建和维护,依赖对象的创建和维护是由外部容器负责的称为控制反转。

IOC容器就是 WareHouse类的几个Map

依赖注入 :build("/test/second")

这种解耦方式很实用,可以轻松实现不具备依赖的模块间获取数据,当然他们一定会有一个公用的模块,且这个模块会下沉到基础模块去。这种思路也体现在一些组件化框架中。

2. 外观设计模式

这种设计模式,可能我们经常用,但是我们不知道这其实是一种设计模式。这里:ARouter类就是_ARouter类的代言人,ARouter类给使用者只暴露仅有的几个方法,但是要实现这个几个方法需要_ARouter类去统筹好多对象的方法,这种设计模式其实我们做SDK时候也用到了,比如我们要提供一个SDK给第三方用,我们对外暴露一般就只暴露两个方法,一个是初始化方法,一个是拉起sdk的方法,那实现这两个方法就,需要一个真正的干活的类去统筹多个对象的方法。

3. 单例+访问者设计模式

单例设计模式:如果使用双重检查加锁实现的话,记得要在成员变量上加volatile,保证有序性和可见性。
访问者设计模式:ASM修改字节码时用到了,由ASM源码实现。

   static byte[] actuallyInsertCode(InputStream inputStream) {
        ClassReader cr = new ClassReader(inputStream);
        ClassWriter cw = new ClassWriter(cr, 0);
        InsertClassVisitor cv = new InsertClassVisitor(Opcodes.ASM5, cw);
        cr.accept(cv, ClassReader.EXPAND_FRAMES);
        return cw.toByteArray()
    }
    static class InsertClassVisitor extends ClassVisitor {
        InsertClassVisitor(int api) {
            super(api)
        }
        InsertClassVisitor(int api, ClassVisitor classVisitor) {
            super(api, classVisitor)
        }
        @Override
        MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
            if (name == LOAD_ROUTE_MAP) {
                return new InsertMethodVisitor(Opcodes.ASM5, mv);
            }
            return mv;
        }
    }

ClassReader是被访问者,ClassWriter(public class ClassWriter extends ClassVisitor)是访问者,现在的目的:在原有的操作上新增新的操作,且不改变原来的结构,这里就新增加一个新的类InsertClassVisitor,重写了visitMethod方法,cr.accept(cv, ClassReader.EXPAND_FRAMES);这个方法调用时,就可以在不更改ClassReader的代码结构的情况下,去定义新的操作。

4. 自定义注解的处理

4.1.  在build.gradle中定义annotationProcessorOptions 有什么用?

这个如同EventBus中定义index一样,给AbstractProcessor实现类传递参数,这个参数是基于key_value的。

在build.gradle的defaultConfig领域下添加:
 javaCompileOptions {
            annotationProcessorOptions {
                arguments=["AROUTER_MODULE_NAME": project.getName()]
            }
 }
 ---------------------------------------------  
 @SupportedOptions("AROUTER_MODULE_NAME")
 public class ArouterProcessor extends AbstractProcessor {
 @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mOptions = processingEnv.getOptions();
    }
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        mModuleName = mOptions.get("AROUTER_MODULE_NAME");
        ......
 }

定义这个的用途就是:在生成IRouterRoot的子类类名上用到了。

4.2.  ARouter在生成新类的时候用了JavaPoet来生成,但EventBus生成新类却用的BufferedWriter配合Filer生成,那我们要生成新类时,怎么选择?

当我们生成多个新类的时候,如果类的导包是固定的,且方法很多代码固定,少量动态变化的,首选BufferedWriter配合Filer实现。 当导包涉及到很多类,且不固定,那首选javaPoet吧,因其完全不用关心导包。

4.3. Javapoet使用:

a. MethodSpec.Builder的addStatement的方法,跟一般的String.format不一样,只能使用大写的S和T 大S表示字符串。T表示类名,请使用JavaPoet自带的api ClassName去获取。

    loadIntoPath.addStatement("warehouse.put($S,new RouteMeta($S,$S,$T.class))",
          routeMeta.getPath(), routeMeta.getPath(),routeMeta.getGroup()
          ,ClassName.get(routeMeta.getElement()));

b. 参数类型表示:Map<String, Class<? extends IRouteGroup>>
编译期的时候,我们只知道这个IRouteGroup的全路径,要拿到这个Class类,可以借助Elements工具类获取到类TypeElement(编译期类的表示)。

TypeElement irouteGroup = mElements.getTypeElement("com.docwei.arouter_api.template.IRouterGroup");
ParameterizedTypeName map_string_irouteGroup = ParameterizedTypeName
        .get(Map.class, String.class,
                ParameterizedTypeName.get(ClassName.get(Class.class)
                        , WildcardTypeName.subtypeOf(irouteGroup.getClass())).getClass());

c. 参数名表示:Map<String, Class<? extends IRouteGroup>> warehouse

ParameterSpec warehouse_group = ParameterSpec.builder(map_string_irouteGroup,
        "warehouse").build();

d. 方法表示:

MethodSpec.Builder loadIntoPath = MethodSpec.methodBuilder("loadInto")
        .addAnnotation(Override.class)
        .addParameter(warehouse_path)
        .addModifiers(Modifier.PUBLIC);
        
 方法体:
 loadIntoPath.addStatement("warehouse.put($S,new RouteMeta($S,$S,$T.class))",
        routeMeta.getPath(), routeMeta.getPath(),routeMeta.getGroup(),
        ClassName.get(routeMeta.getElement()));

e. 创建类文件:

String className = NAME_OF_GROUP + SEPARATOR + groupName;
try {
    JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
            TypeSpec.classBuilder(className).addSuperinterface(ClassName.get(irouteGroup))
                    .addMethod(loadIntoPath.build()).build())
            .build().writeTo(mFiler);
} catch (IOException e) {
    e.printStackTrace();
}

4.4. ARouter的初始化耗时分析: 已做等效改写

    final Set<String> fileNames = new HashSet<>();
    ApplicationInfo applicationInfo = context.getApplicationInfo();
    //获取app的apk的路径
    final String path = applicationInfo.sourceDir;
    //因为5.0以上直接就是一个base.apk的路径,所以不考虑多线程执行
    //耗时大概1s左右,这个是Arouter耗时的关键
    DexFile dexFile = null;
    try {
        dexFile = new DexFile(path);
        Enumeration<String> entries = dexFile.entries();
        while (entries.hasMoreElements()) {
            String element = entries.nextElement();
            //去找含有这个com.docwei.arouter.routes路径的文件名
            if (element.contains(Consts.PACKAGE_OF_GENERATE_FILE)) {
                fileNames.add(element);
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    for (String fileName : fileNames) {
        if (fileName.startsWith(Consts.PACKAGE_OF_GENERATE_FILE + "." + NAME_OF_ROOT)) {
            //反射去创建这个类对象,然后保存到仓库
            try {
                ((IRouterRoot) (Class.forName(fileName).getConstructor().newInstance()))
                .loadInto(WareHouse.sGroups);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

源码这里使用CountDownLatch和线程池来处理的,但是实测5.0以及5.0以上只会扫描一个base.apk文件, 源码让子线程去查找,主线程await,后续的代码执行也得等子线程执行完才行,所以跟直接放到主线程执行差不多,上面的改写直接放到主线程了。

5. 自定义插件优化启动耗时

来自billy.qi 大佬(组件化框架——CC的作者)的自定义插件,写的很简单,但是也有难点存在。

要实现:

在LogisticsCenter类,插入指定代码,那它要解决的问题有两个。

    • 问题一:找指定的类

要在jarInputs(很多jar包)里面去找到LogisticsCenter.class所在的jar包。
对的,是jar,而不是LogisticsCenter.class,如果使用ARouter在线依赖或者module引入,打包时,LogisticsCenter.class只可能在JarInputs里面,不会在directoryInputs里面(directoryInput只会有App module下的类,其余Module下的,以及远程依赖的包,或者本地依赖的jar包统统都会进入jarInputs里面)。
也要在这些jarInputs中找到IRouterRoot接口的实现类的全路径,作为register方法的参数。

class ScanUtil {
    static String IROUTE_ROOT = "IRouterRoot";
    static String IROUTE_ROOT_PACKAGE = "com/docwei/arouter_api/template/";
    static String LOGISTICS_CENTER_PACKAGE="com/docwei/arouter_api/LogisticsCenter"
    static String LOGISTICS_CENTER =  LOGISTICS_CENTER_PACKAGE+".class";
    //这个包名是固定的  com.docwei.arouter.routes
    static String IROUTE_ROOT_CHILD_PACKAGE = "com/docwei/arouter/routes/ARouter\$\$Root\$\$";
    static String IROUTE_ROOT_CHILD_NAME_PREFIX = "ARouter\$\$Root\$\$"
    static void scanJar(JarInput jarInput,File destFile) {
        File file = jarInput.getFile();
        try {
            JarFile jarFile = new JarFile(file);
            Enumeration<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry jarEntry = entries.nextElement();
                if (shouldLockUpClasses(jarEntry.getName())&& jarEntry.getName().startsWith(IROUTE_ROOT_CHILD_PACKAGE)) {
                    //针对IROUTE_ROOT的子类,需要使用ASM去判断
                    //todo Q:可以通过JarFile获取指定文件(jarEntry)的流
                    scanClass(jarFile.getInputStream(jarEntry));
                }
                if (jarEntry.getName().equals(LOGISTICS_CENTER)) {
                    //找到logisticsCenter.class所在的jar 文件
                    AutoRegisterTransform.logisticsCenterFile = destFile;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    static void scanClass(InputStream inputStream) {
        //todo 导包很容易搞错
        ClassReader cr = new ClassReader(inputStream);
        ClassWriter cw = new ClassWriter(cr, 0);
        //使用ASM5的api
        ScanClassVistor classVistor = new ScanClassVistor(Opcodes.ASM5, cw);
        cr.accept(classVistor, ClassReader.EXPAND_FRAMES)
        inputStream.close()

    }

    static boolean shouldLockUpClasses(String name) {
        return name.endsWith(".class") && !(name.startsWith("androidx") || name.contains("R\$") || name.endsWith("R.class"));
    }
    //去查找类头的情况 找出IRouterRoot的子类
   static class ScanClassVistor extends ClassVisitor {
       ScanClassVistor(int api) {
            super(api)
        }
       ScanClassVistor(int api, ClassVisitor classVisitor) {
            super(api, classVisitor)
        }
        @Override
        void visit(int version, int access, String name, String signature,
                   String superName, String[] interfaces) {
            super.visit(version, access, name, signature, superName, interfaces)
            //public class ARouter$$Root$$app implements IRouterRoot {
            //很明显我们找的就是IRouterRoot的实现类
            interfaces.each { String interfaceName ->
                if ((IROUTE_ROOT_PACKAGE+IROUTE_ROOT).equals(interfaceName)&&!AutoRegisterTransform.childrenForIRoutRoot.contains(name)) {
                    AutoRegisterTransform.childrenForIRoutRoot.add(name);
                }

            }
        }
    }
}

使用ASM去帮忙找IRouterRoot接口的实现类,

  • 问题二:插入指定代码

上面说了LogisticsCenter.class是在jar包里面,不是裸露出来的字节码啊(单纯的字节码,io流配合ASM改写很简单的),所以我们要做的就是,创建一个空jar,读原始的jar包,每读一个JarEntry,写入一个JarEntry到空jar,读到LogisticsCenter.class后就改写它的loadRouteMap方法,然后写入,最后删除原始jar,将空jar重命名为原始jar名。

class InsertCodeUtil {
    static String LOAD_ROUTE_MAP = "loadRouteMap";

    static void insert(File rawFile) {
        //未被修改的文件
        //新建一个空的jar.opt文件
        File optFile = new File(rawFile.getParent(), rawFile.getName() + ".opt")
        if (optFile.exists()) {
            optFile.delete()
        }
        //从原来的jar里面获取数据用,需要封装成JarFile
        JarFile rawJarFile = new JarFile(rawFile);
        Enumeration<JarEntry> entries = rawJarFile.entries();
        // 创建新的jar.opt文件流 用于写入到文件
        JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optFile))
        while (entries.hasMoreElements()) {
            JarEntry jarEntry = entries.nextElement();
            String entryName = jarEntry.getName();
            ZipEntry zipEntry = new ZipEntry(entryName);
            //从指定的JarFile获取对应的流
            InputStream inputStream = rawJarFile.getInputStream(jarEntry)
            //放入一个空的zipEntry
            jarOutputStream.putNextEntry(zipEntry)
            if (ScanUtil.LOGISTICS_CENTER == entryName) {
                byte[] bytes = actuallyInsertCode(inputStream)
                jarOutputStream.write(bytes);
            } else {
                jarOutputStream.write(IOUtils.toByteArray(inputStream))
            }
            inputStream.close()
            jarOutputStream.closeEntry()
        }
        jarOutputStream.close()
        rawJarFile.close()
        if(rawFile.exists()){
            rawFile.delete()
        }
        //最后改成之前一样的文件名
        optFile.renameTo(rawFile)
    }
    static byte[] actuallyInsertCode(InputStream inputStream) {
        ClassReader cr = new ClassReader(inputStream);
        ClassWriter cw = new ClassWriter(cr, 0);
        InsertClassVisitor cv = new InsertClassVisitor(Opcodes.ASM5, cw);
        cr.accept(cv, ClassReader.EXPAND_FRAMES);
        return cw.toByteArray()
    }
    static class InsertClassVisitor extends ClassVisitor {
        InsertClassVisitor(int api) {
            super(api)
        }
        InsertClassVisitor(int api, ClassVisitor classVisitor) {
            super(api, classVisitor)
        }
        @Override
        MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
            if (name == LOAD_ROUTE_MAP) {
                return new InsertMethodVisitor(Opcodes.ASM5, mv);
            }
            return mv;
        }
    }
    static class InsertMethodVisitor extends MethodVisitor {
        InsertMethodVisitor(int api) {
            super(api)
        }

        InsertMethodVisitor(int api, MethodVisitor methodVisitor) {
            super(api, methodVisitor)
        }

        // loadRouteMap() 是一个空参方法,visitInsn(int opcode)是空参方法必走的方法
        @Override
        void visitInsn(int opcode) {
            if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
                AutoRegisterTransform.childrenForIRoutRoot.each {
                    String name=it.replaceAll("/",".")
                    mv.visitLdcInsn(name);
                    mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/docwei/arouter_api/LogisticsCenter",
                            "register", "(Ljava/lang/String;)V", false);
               }
            }
            super.visitInsn(opcode)
        }
    }
}

先定位到LogisticsCenter,再定位到loadRouteMap方法,再定位到空参方法必走的方法,最后在方法体尾部插入,就需要 if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) 判断。

ps: 以上两个groovy代码在源码基础上等效改写。

四、ARouter实现页面跳转的精简demo奉上

ARouterDemo