当前位置:   article > 正文

我为什么要手撸一个简易版ARouter

我 要撸

作者:星星y

链接:

https://www.jianshu.com/p/2840cf1a71e1

本文由作者授权发布。

阅读源码一直是一个非常好的进阶的方式,大厂开源一个方案,自己通过阅读源码可以了解其原理,然后遇到特殊的场景,就可以自己进行扩展,例如本文作者因为遇到了插件化的场景。

如果有能力扩展后,后续甚至有能力根据场景优化,即作者后续通过 Transform API 做了一些事情,非常值得我们学习。

最近用Small实现原有项目的插件化,效果还不错,只要工程是组件化的结构就很好重构。但在使用ARouter时,由于初始化时,查询的apk路径只有base.apk,所以不能找到由Route注解自动生成的ARouter

Group
xxx文件。

Small插件化实战总结

https://www.jianshu.com/p/4263420d98a8

为了适配插件化版本,所以需要自己手动打造简易版的ARouter框架。

一、APT

通过APT处理用注解标记的Activity类,生成对应的映射文件。这里创建两个类型为java library的module。一个library(ARouter处理逻辑),一个compiler(处理注解,生成源码

gradle引入相关依赖

library的build.gradle

 
 

compilerOnly里的是Android的相关类库

compiler的build.gradle

  1. apply plugin'java'
  2. dependencies {
  3.     compile 'com.squareup:javapoet:1.9.0'
  4.     compile 'com.google.auto.service:auto-service:1.0-rc3'
  5.     compile project(':library')
  6. }
  7. targetCompatibility = '1.7'
  8. sourceCompatibility = '1.7'

auto-service会自动在META-INF文件夹下生成Processor配置信息文件,使得编译时能找到annotation对应的处理类。javapoet则是由square公司出的开源库,能有优雅的生成java源文件。

创建注解@Route

接着,我们在library中创建一个注解类,Target表明修饰的类型(类或接口、方法、属性,TYPE表示类或接口),Retention表明可见级别(编译时,运行时期等,CLASS表示在编译时可见)

 
 

然后在app的gradle引入依赖

 
 

注意:gradle2.2以下需要将annotationProcessor改为apt,同时在工程根目录引入

classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

在MainActivity中添加注解

  1. ...
  2. import io.github.iamyours.aarouter.annotation.Route;
  3. @Route(path = "/app/main")
  4. public class MainActivity extends AppCompatActivity {
  5. ...
  6. }
创建注解处理类RouteProcessor

  1. package io.github.iamyours.compiler;
  2. import com.google.auto.service.AutoService;
  3. import java.util.LinkedHashSet;
  4. import java.util.Set;
  5. import javax.annotation.processing.AbstractProcessor;
  6. import javax.annotation.processing.Processor;
  7. import javax.annotation.processing.RoundEnvironment;
  8. import javax.lang.model.SourceVersion;
  9. import javax.lang.model.element.TypeElement;
  10. import io.github.iamyours.aarouter.annotation.Route;
  11. /**
  12.  * Created by yanxx on 2017/7/28.
  13.  */
  14. @AutoService(Processor.class)
  15. public class RouteProcessor extends AbstractProcessor {
  16.     @Override
  17.     public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
  18.         System.out.println("============="+roundEnvironment);
  19.         return true;
  20.     }
  21.     @Override
  22.     public Set<String> getSupportedAnnotationTypes() {
  23.         Set<String> annotations = new LinkedHashSet<>();
  24.         annotations.add(Route.class.getCanonicalName());
  25.         return annotations;
  26.     }
  27.     @Override
  28.     public SourceVersion getSupportedSourceVersion() {
  29.         return SourceVersion.latestSupported();
  30.     }
  31. }

然后我们make project以下,得到如下日志信息,则表明apt配置成功。

  1. :app:javaPreCompileDebug
  2. :compiler:compileJava UP-TO-DATE
  3. :compiler:processResources NO-SOURCE
  4. :compiler:classes UP-TO-DATE
  5. :compiler:jar UP-TO-DATE
  6. :app:compileDebugJavaWithJavac
  7. =============[errorRaised=false, rootElements=[io.github.iamyours.aarouter.MainActivity, ...]
  8. =============[errorRaised=false, rootElements=[], processingOver=true]
  9. :app:compileDebugNdk NO-SOURCE
  10. :app:compileDebugSources

二、使用javapoet生成源文件

javapoet的用法可以看这里

https://github.com/square/javapoet

为了保存由Route注解标记的class类名,这里用一个映射类通过方法调用的形式保存。

具体生成的类如下

 
 

为了之后能够从Android apk中的DexFile中找到映射类,我们要把这些映射java类放到同一个package下,具体实现逻辑如下:

 
 

在compiler中

  1. @AutoService(Processor.class)
  2. public class RouteProcessor extends AbstractProcessor {
  3.     private Filer filer;
  4.     private Map<StringString> routes = new HashMap<>();
  5.     private String moduleName;
  6.     @Override
  7.     public synchronized void init(ProcessingEnvironment processingEnvironment) {
  8.         super.init(processingEnvironment);
  9.         filer = processingEnvironment.getFiler();
  10.     }
  11.     @Override
  12.     public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
  13.         for (Element e : roundEnvironment.getElementsAnnotatedWith(Route.class)) {
  14.             addRoute(e);
  15.         }
  16.         createRouteFile();
  17.         return true;
  18.     }
  19.     private void createRouteFile() {
  20.         TypeSpec.Builder builder = TypeSpec.classBuilder("AARouterMap_" + moduleName).addModifiers(Modifier.PUBLIC);
  21.         TypeName superInterface = ClassName.bestGuess("io.github.iamyours.aarouter.IRoute");
  22.         builder.addSuperinterface(superInterface);
  23.         TypeName stringType = ClassName.get(String.class);
  24.         TypeName mapType = ParameterizedTypeName.get(ClassName.get(Map.class), stringType, stringType);
  25.         MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("loadInto")
  26.                 .addAnnotation(Override.class)
  27.                 .returns(void.class)
  28.                 .addModifiers(Modifier.PUBLIC)
  29.                 .addParameter(mapType, "routes");
  30.         for (String key : routes.keySet()) {
  31.             methodBuilder.addStatement("routes.put($S,$S)", key, routes.get(key));
  32.         }
  33.         builder.addMethod(methodBuilder.build());
  34.         JavaFile javaFile = JavaFile.builder(ARouter.ROUTES_PACKAGE_NAME, builder.build()).build();//将源码输出到ARouter.ROUTES_PACKAGE_NAME,
  35.         try {
  36.             javaFile.writeTo(filer);
  37.         } catch (IOException e) {
  38. //            e.printStackTrace();
  39.         }
  40.     }
  41.     /*
  42.      这里有一个注意的点事moduleName,由于每个library或application模块的环境不同,
  43. 也只能取到当前模块下的注解,因此需要生成不同的映射文件保存到每个模块下,
  44. 阿里的获取的方法是在每个模块的build文件通过annotationProcessorOptions传入,
  45. 这边简化直接从path获取(如“/app/login”取app,"/news/newsinfo"取news)
  46.     */
  47.     private void addRoute(Element e) {
  48.         Route route = e.getAnnotation(Route.class);
  49.         String path = route.path();
  50.         String name = e.toString();
  51.         moduleName = path.substring(1,path.lastIndexOf("/"));
  52.         routes.put(path, name);
  53.     }
  54.     @Override
  55.     public Set<String> getSupportedAnnotationTypes() {
  56.         Set<String> annotations = new LinkedHashSet<>();
  57.         annotations.add(Route.class.getCanonicalName());
  58.         return annotations;
  59.     }
  60.     @Override
  61.     public SourceVersion getSupportedSourceVersion() {
  62.         return SourceVersion.latestSupported();
  63.     }
  64. }

三、

ARouter初始化

为了得到所有有@Route注解标记的路由,需要从DexFile中找到ARouter.ROUTES_PACKAGE_NAME目录下的AARouterMap_xxx的class文件,通过反射初始化调用loadInto加载路由。

  1. public class ARouter {
  2.     private Map<StringString> routes = new HashMap<>();
  3.     private static final ARouter instance = new ARouter();
  4.     public static final String ROUTES_PACKAGE_NAME = "io.github.iamyours.aarouter.routes";
  5.     public void init(Context context){
  6.         try {//找到ROUTES_PACKAGE_NAME目录下的映射class文件
  7.             Set<String> names = ClassUtils.getFileNameByPackageName(context,ROUTES_PACKAGE_NAME);
  8.             initRoutes(names);
  9.         } catch (Exception e) {
  10.             e.printStackTrace();
  11.         }
  12.     }
  13.     //通过反射初始化路由
  14.     private void initRoutes(Set<String> names) throws ClassNotFoundExceptionIllegalAccessExceptionInstantiationException {
  15.         for(String name:names){
  16.             Class clazz = Class.forName(name);
  17.             Object obj = clazz.newInstance();
  18.             if(obj instanceof IRoute){
  19.                 IRoute route = (IRoute) obj;
  20.                 route.loadInto(routes);
  21.             }
  22.         }
  23.     }
  24.     private ARouter() {
  25.     }
  26.     public static ARouter getInstance() {
  27.         return instance;
  28.     }
  29.     public Postcard build(String path) {
  30.         String component = routes.get(path);
  31.         if (component == nullthrow new RuntimeException("could not find route with " + path);
  32.         return new Postcard(component);
  33.     }
  34. }

获取路由映射class文件

之前我们通过RouterProcessor将映射class放到了ROUTES_PACKAGE_NAME下,我们只需要在dex文件中遍历寻找到它们即可。而alibaba的ARouter取的是当前app应用目录的base.apk寻找的dex文件,然后通过DexClassLoader加载取得DexFile。

但如果项目插件化构成的,dexFile就不只是base.apk下了,因此需要通过其他方式获取了。

  1. public class ClassUtils {
  2.     //通过BaseDexClassLoader反射获取app所有的DexFile
  3.     private static List<DexFile> getDexFiles(Context context) throws IOException {
  4.         List<DexFile> dexFiles = new ArrayList<>();
  5.         BaseDexClassLoader loader = (BaseDexClassLoader) context.getClassLoader();
  6.         try {
  7.             Field pathListField = field("dalvik.system.BaseDexClassLoader","pathList");
  8.             Object list = pathListField.get(loader);
  9.             Field dexElementsField = field("dalvik.system.DexPathList","dexElements");
  10.             Object[] dexElements = (Object[]) dexElementsField.get(list);
  11.             Field dexFilefield = field("dalvik.system.DexPathList$Element","dexFile");
  12.             for(Object dex:dexElements){
  13.                 DexFile dexFile = (DexFile) dexFilefield.get(dex);
  14.                 dexFiles.add(dexFile);
  15.             }
  16.         } catch (ClassNotFoundException e) {
  17.             e.printStackTrace();
  18.         } catch (NoSuchFieldException e) {
  19.             e.printStackTrace();
  20.         } catch (IllegalAccessException e) {
  21.             e.printStackTrace();
  22.         }
  23.         return dexFiles;
  24.     }
  25.     private static Field field(String clazz,String fieldName) throws ClassNotFoundExceptionNoSuchFieldException {
  26.         Class cls = Class.forName(clazz);
  27.         Field field = cls.getDeclaredField(fieldName);
  28.         field.setAccessible(true);
  29.         return field;
  30.     }
  31.     /**
  32.      * 通过指定包名,扫描包下面包含的所有的ClassName
  33.      *
  34.      * @param context     U know
  35.      * @param packageName 包名
  36.      * @return 所有class的集合
  37.      */
  38.     public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws IOException {
  39.         final Set<String> classNames = new HashSet<>();
  40.         List<DexFile> dexFiles = getDexFiles(context);
  41.         for (final DexFile dexfile : dexFiles) {
  42.             Enumeration<String> dexEntries = dexfile.entries();
  43.             while (dexEntries.hasMoreElements()) {
  44.                 String className = dexEntries.nextElement();
  45.                 if (className.startsWith(packageName)) {
  46.                     classNames.add(className);
  47.                 }
  48.             }
  49.         }
  50.         return classNames;
  51.     }
  52. }

有了上面的实现,我们就可以在初始化时,通过传入context的classloader,获取到映射路由文件,然后反射初始化他们,调用loadInto,即可得到所有的路由。

而接下来的路由跳转就很简单了,只需包装成ComponentName就行

  1. public class ARouter {
  2.     ...
  3.     public Postcard build(String path) {
  4.         String component = routes.get(path);
  5.         if (component == nullthrow new RuntimeException("could not find route with " + path);
  6.         return new Postcard(component);
  7.     }
  8. }
 
 
项目地址

https://github.com/iamyours/AARouter

补充说明

现在这个版本虽然也适配Small,但是通过反射私有api找到映射class终究还是有些隐患。

后来想到另外一种方案:

每个模块build传入模块的包名,生成的文件统一命名为AARouterMap,初始化时small可以通过Small.getBundleVersions().keys获取每个插件的包名

ARouter.getInstance().init(Small.getBundleVersions().keys)

来获取每个插件的包名

  1. public void init(Set<String> appIds) {
  2.         try {
  3.             initRoutes(appIds);
  4.         } catch (IllegalAccessException e) {
  5.             e.printStackTrace();
  6.         } catch (InstantiationException e) {
  7.             e.printStackTrace();
  8.         }
  9.     }
  10.     private void initRoutes(Set<String> appIds) throws IllegalAccessExceptionInstantiationException {
  11.         for (String appId : appIds) {
  12.             Class clazz = null;
  13.             try {
  14.                 clazz = Class.forName(appId + ".AARouterMap");
  15.             } catch (ClassNotFoundException e) {
  16.                 e.printStackTrace();
  17.             }
  18.             if(clazz==null)continue;
  19.             Object obj = clazz.newInstance();
  20.             if (obj instanceof IRoute) {
  21.                 IRoute route = (IRoute) obj;
  22.                 route.loadInto(routes);
  23.             }
  24.         }
  25.     }

这样就不用遍历dex获取映射,性能和安全性也会好一点。非插件化的项目也可以通过手动传包名列表适配了。

             

后续

之前通过APT实现了一个简易版ARouter框架,碰到的问题是APT在每个module的上下文是不同的,导致需要通过不同的文件来保存映射关系表。

因为类文件的不确定,就需要初始化时在dex文件中扫描到指定目录下的class,然后通过反射初始化加载路由关系映射。

阿里的做法是直接开启一个异步线程,创建DexFile对象加载dex。这多少会带来一些性能损耗,为了避免这些,我们通过Transform api实现另一种更加高效的路由框架。

基于Transform实现更高效的组件化路由框架

https://www.jianshu.com/p/66cf509000d3

                        喜欢 就关注吧,欢迎投稿!

640?wx_fmt=jpeg

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/296620
推荐阅读
相关标签
  

闽ICP备14008679号