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,
- 方式二、 使用auto-register的方式去优化方式一的耗时(实测在一个大小为60M左右的base.apk查找指定类大概需要1s左右,这也是ARouter使用耗时的根源所在),默认在loadRouteMap( )就一个标志位置为false
ps: WareHouse类实现:
but 原理不重要,重要的是实现用到技术啊,难不成你还能在其他地方用到它的原理?
三、ARouter实现页面跳转涉及到的技能点
-
- 控制反转+依赖注入(概念很高大上);
- 控制反转+依赖注入(概念很高大上);
-
- 外观设计模式、单例设计模式、访问者设计模式;
- 外观设计模式、单例设计模式、访问者设计模式;
-
- 自定义注解的处理(apt方式);
- 自定义注解的处理(apt方式);
-
- 自定义插件优化启动耗时;
- 自定义插件优化启动耗时;
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的作者)的自定义插件,写的很简单,但是也有难点存在。
要实现:
-
-
问题一:找指定的类
-
要在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代码在源码基础上等效改写。