在阅读源码前,请先下载源码:ARouter

最近阅读ARouter源码,发现这真的是一个非常优秀的框架。激发出兴趣来读一下他的源码,实际上,这个框架的结构非常简单。这个框架可以分为主流程辅助流程来拆开分析。

主流程包含编译时运行时两个部分,其中编译时主要做的是路由路径表的构建,运行时主要做的是路由路径表的加载;

辅助流程主要就是做启动优化

主流程

1. 编译时

这部分主要涉及到的是路由路径表的构建,其实现原理是APT,即注解处理器

使用ARouter时候,需要在目标类上,通过@Route注解进行标记,注解处理器处理的就是这个注解。打开源码路径下的arouter-compiler这个module,找到RouteProcessor,这个类就是用来处理@Route注解的类。这里需要了解的知识,除了APT,还有java-poet,请自行了解这些。

Processor类的入口方法是process方法,这个方法返回true,则这个处理器已经完成了自己的任务,不会被重复调用。其他比较重要的方法有getSupportedSourceVersiongetSupportedAnnotationTypes等。

Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);

通过这个方法,获取所有被@Route标记的元素。

获取到routeElements后,在parseRoutes方法进行处理。我们以最常用的Activity为例,进行分析。

rootMap.clear();	//用来分类存储标记元素

// 用来检测元素是否为对应的类型,通过Types.isSubtype()方法来检测。
TypeMirror type_Activity = elementUtils.getTypeElement(ACTIVITY).asType();
TypeMirror type_Service = elementUtils.getTypeElement(SERVICE).asType();
TypeMirror fragmentTm = elementUtils.getTypeElement(FRAGMENT).asType();
TypeMirror fragmentTmV4 = elementUtils.getTypeElement(Consts.FRAGMENT_V4).asType();

最后将分类号的元素信息,存储在成员变量groupMap中去。

private Map<String, Set<RouteMeta>> groupMap = new HashMap<>();

然后再通过这个groupMap,借助java-poet,来生成真实的类。如下:

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>(), -1, -2147483648));
    atlas.put("/test/activity2", RouteMeta.build(RouteType.ACTIVITY, Test2Activity.class, "/test/activity2", "test", new java.util.HashMap<String, Integer>(), -1, -2147483648));
    atlas.put("/test/activity3", RouteMeta.build(RouteType.ACTIVITY, Test3Activity.class, "/test/activity3", "test", new java.util.HashMap<String, Integer>(), -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>(), -1, -2147483648));
    atlas.put("/test/webview", RouteMeta.build(RouteType.ACTIVITY, TestWebview.class, "/test/webview", "test", null, -1, -2147483648));
  }
}

2. 运行时

这部分主要做的是,在ARouter.init()时候,将上过程生成的路径表加载到内存中。

如果你以官方demo程序验证这一步,需要将app/build.gradle中的apply plugin: 'com.alibaba.arouter'这一行代码注释掉。

我们以ARouter.init方法为入口,实际上最终实现init流程的是LogisticsCenter类的init方法。

public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
  ...
  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.
    if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
      logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
      // These class was generated by arouter-compiler.
      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>()));
    }
  }
}

这里需要着重看的是这一句:

routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);

我们查看这个方法:

public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException {
  final Set<String> classNames = new HashSet<>();

  List<String> paths = getSourcePaths(context);
  final CountDownLatch parserCtl = new CountDownLatch(paths.size());

  for (final String path : paths) {
    Log.v(TAG, "getFileNameByPackageName path=" + path);
    DefaultPoolExecutor.getInstance().execute(new Runnable() {
      @Override
      public void run() {
        DexFile dexfile = null;

        try {
          if (path.endsWith(EXTRACTED_SUFFIX)) {
            //NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
            dexfile = DexFile.loadDex(path, path + ".tmp", 0);
          } else {
            dexfile = new DexFile(path);
          }

          Enumeration<String> dexEntries = dexfile.entries();
          while (dexEntries.hasMoreElements()) {
            String className = dexEntries.nextElement();
            if (className.startsWith(packageName)) {
              Log.v(TAG, "find CLASS NAME " + className);
              classNames.add(className);
            }
          }
        } catch (Throwable ignore) {
          Log.e("ARouter", "Scan map file in dex files made error.", ignore);
        } finally {
          if (null != dexfile) {
            try {
              dexfile.close();
            } catch (Throwable ignore) {
            }
          }

          parserCtl.countDown();
        }
      }
    });
  }

  parserCtl.await();

  Log.d(Consts.TAG, "Filter " + classNames.size() + " classes by packageName <" + packageName + ">");
  return classNames;
}

注意其中的getSourcePaths方法,这是从代码目录,来获取所有代码目录,然后在

getFileNameByPackageName找出以com.alibaba.android.arouter.routes为开头包名的类,这些就是我们在步骤1中生成的辅助类。

这个方法结束后,回到LogisticsCenter#init方法,接下来要做的就是,把加载到的辅助类,通过反射生成对象,再调用其loadTo方法,将路由路径表加载到Warehouse类中去,方便以后的查询。

辅助流程

在以上的流程中,有一个严重的问题,那就是执行ARouter#init方法的时间过长,以源码的demo为例,在InstantRun的情况下,OnePlus5T需要100多毫秒才能初始化完,这对于程序启动优化来说,是一个不可忽视的时间了。那么如何解决这个问题呢?

MainActivity: init cost 134

这就是在上一步中,要求你注释掉的代码起作用了,将apply plugin: 'com.alibaba.arouter'解除注释,让其发挥作用。

这里需要关注的module是arouter-gradle-plugin

先来看一下,使用了apply plugin: 'com.alibaba.arouter'的神奇效果。

MainActivity: init cost 19

通过优化,让ARouter#init消耗时间直接降低了一个数量级,那么arouter-gradle-plugin是怎么做到的呢?

这需要你先了解一下ASM。简单来说,这是一种字节码编程技术,通过修改编译后的字节码的方式,来对原始逻辑增强。

我们再来看ARouter#init的最终实现类和方法LogisticsCenter#init

public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
  ....
  try {
    long startInit = System.currentTimeMillis();
    //billy.qi modified at 2017-12-06
    //load by plugin first
    loadRouterMap();
    if (registerByPlugin) {
      logger.info(TAG, "Load router map by arouter-auto-register plugin.");
    } else {
      ....
    }
    .....
  } catch (Exception e) {
    throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");
  }
}

我们可以看到,当registerByPlugin为true的时候,则只是打印了一句日志,我们再看loadRouterMap()这个方法:

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

这里的逻辑非常简单,到底在哪里去加载的路由路径表呢?我们看这个方法的注释,发现,这个方法是被arouter-auto-register自动生成的。

我们打开arouter-gradle-plugin/resources/META-INF/gradle-plugins这个目录,可以看到,有一个com.alibaba.arouter.properties文件,查看其内容:

implementation-class=com.alibaba.android.arouter.register.launch.PluginLaunch

这个PluginLaunch便是此gradle plugin的入口类。查看此类:

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

}

我们查看其代码,可以发现,这里一共做了三件事:

  1. 判断是否为app module,如果不是,则不做任何事,在app module下做2和3两步;
  2. 生成了一个RegisterTransform,并为其静态变量registerList赋值,注意此处赋值的registerList中包含的三个对象
  3. 注册此RegisterTransform。

接下来,就轮到RegisterTransform来执行了。

Transform类,简单来说,就是可以在编译时,扫描所有的jar和class,包括引用类库中的。在扫描过程中,就可以借助ASM技术对目标类进行更改。

我们看其入口方法transform

@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
  inputs.each { TransformInput input ->
    // scan all jars
    input.jarInputs.each { JarInput jarInput ->
      ...
      if (ScanUtil.shouldProcessPreDexJar(src.absolutePath)) {
        ScanUtil.scanJar(src, dest)
      }
      ...
    }
  }
  input.directoryInputs.each { DirectoryInput directoryInput ->
    ...
    directoryInput.file.eachFileRecurse { File file ->
      ...
      if(file.isFile() && ScanUtil.shouldProcessClass(path)){
        ScanUtil.scanClass(file)
      }
    }
    ...
  }
}

省去了一些细节,只保留了主线逻辑,我们可以看到,其扫描到的jar和class都经过了ScanUtils的方法来处理,我们继续跟踪下去,会发现,scanJar也是循环调用的scanClass,这样我们直接看scanClass方法:

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)
    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)) {
              ext.classList.add(name)
            }
          }
        }
      }
    }
  }
}

这里就又涉及到了ASM的知识,ScanClassVisitor是访问某个类的内部结构。

version:类的版本;

access:表示类的访问权限,public,private,protected等;

name:类的名字;

signature:有无泛型;

superName:其父类;

interfaces:其实现的接口;

ScanClassVisitor中,并没有对类做修改,只是从遍历过的类中,把我们关心的类挑出来。那么,我们关心哪些类呢?

PluginLaunch类中,我们注册了三个ScanSettings类,分别是IRouteRootIInterceptorGroupIProviderGroup,也就是说,我们把实现了这三个接口的类,挑出来,加入到各自对应的ScanSettings类中记录起来。这三个接口是不是很熟悉?就是通过APT生成的用来记录路由路径表的类。

等收集好了这些记录的路径表信息后,就可以对LogisticsCenter通过ASM进行修改了。我们接着看RegisterTransform#transform方法中剩下的逻辑。

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

注意此处的insertInitCodeTo方法,这就是ASM修改的入口了。这里不对修改过程进行详细解释了。我们直接对比看LogisticsCenter修改前后关键代码的对比。

app/build目录下,找到生成的apk文件,通过AndroidStudio来查看其中的class,找到关键LogisticsCenter关键方法loadRouterMap

具体过程如下:

app/build/outputs/apk/debug/app-debug.apk -> classes.dex(双击) -> 找到LogisticsCenter#loadRouterMap方法 -> 右键: show Bytecode。

// 不使用apply plugin: 'com.alibaba.arouter'
.method private static loadRouterMap()V
    .registers 1

    .line 64
    const/4 v0, 0x0

    sput-boolean v0, Lcom/alibaba/android/arouter/core/LogisticsCenter;->registerByPlugin:Z

    .line 69
    return-void
.end method
// 使用apply plugin: 'com.alibaba.arouter'
.method private static loadRouterMap()V
    .registers 1

    .line 64
    const/4 v0, 0x0

    sput-boolean v0, Lcom/alibaba/android/arouter/core/LogisticsCenter;->registerByPlugin:Z

    .line 69
    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Root$$app"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Interceptors$$app"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$modulejava"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$modulekotlin"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$arouterapi"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    const-string v0, "com.alibaba.android.arouter.routes.ARouter$$Providers$$app"

    invoke-static {v0}, Lcom/alibaba/android/arouter/core/LogisticsCenter;->register(Ljava/lang/String;)V

    return-void
.end method

对比发现,使用apply plugin: 'com.alibaba.arouter'后,这个方法增加了很多代码,基本上就是在加载路由路径表。使用这个gradle插件的基本思想就是,将查找路由路径表的过程,从运行时提前到了编译时,这算是一种AOT(Ahead of time)思想。

将最耗时的查找过程提前,也就解决了ARouter初始化时间过长的问题。