ARouter启动优化引发的探索

前言

笔者在做启动优化时,发现第一次启动应用时,ARouter初始化耗时占用了接近2s的时间。查询优化方案时,发现只需要通过一个插件就可以解决了。感觉解决方法挺新奇的,但由于对ARouter底层实现不是非常了解,所以本文就诞生了,从一个小白的角度分析下这个插件是如何做到的,实现思路对我们又有什么启发。

ARouter的基本使用

ARouter的基本使用比较简单,官方README写的也比较清楚 ,也可以参考ARouter之基本使用博客食用。这里就不多介绍。

ARouter启动优化

通过 gradle插件进行自动注册,可以缩短初始化时间,解决应用加固导致无法直接访问dex文件

apply plugin: 'com.alibaba.arouter'

buildscript {
    repositories {
        mavenCentral()
    }

    dependencies {
        classpath "com.alibaba:arouter-register:?"
    }
}

源码设计分析

下面从启动优化的角度分析ARouter是如何设计的,为什么启动这么耗时,又是如何通过一个插件就优化掉这种耗时的。

初始化入口

寻找入口,必然是在初始化时机

public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
  // 判断是不是通过`arouter-register`插件自动加载路由表
  loadRouterMap();
  if (registerByPlugin) {
    // 1.插件逻辑
  }else{
    // 2.非插件逻辑
    Set routerMap;
    // 获取到apk中前缀为com.alibaba.android.arouter.routes的类
    routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
    //  加载前缀为com.alibaba.android.arouter.routes的class,放到set集合里面
    if (!routerMap.isEmpty()) {
          context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).
            edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
    }
    // 将不同前缀的class类放到不同路径下下
    for (String className : routerMap) {
       if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
          // This one of root elements, load root.
          ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
       } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
       // Load interceptorMeta
          ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
       } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
       // Load providerIndex
          ((IProviderGroup)(Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
       }
     }
  }
}

通过loadRouterMap方法判断是不是通过arouter-register自动加载路由表,如果是通过自动加载的则registerByPlugin=true,这里先不关心通过arouter-register自动加载的方式。

非插件方式

先分析不通过插件的方式,看看初始化的耗时到底在哪里?

public static Set getFileNameByPackageName(Context context, final String packageName){
  final Set classNames = new HashSet<>();
  // 获取所有的dex文件路径
  List paths = getSourcePaths(context);
  final CountDownLatch parserCtl = new CountDownLatch(paths.size());
  // ⭐️开启线程池扫描dex文件
  for (final String path : paths) {
    DefaultPoolExecutor.getInstance().execute(new Runnable() {
      @Override
      public void run() {
        DexFile dexfile = null;
        //  EXTRACTED_SUFFIX = ".zip";
        if (path.endsWith(EXTRACTED_SUFFIX)) {
          dexfile = DexFile.loadDex(path, path + ".tmp", 0);
        } else {
          dexfile = new DexFile(path);
        }
        Enumeration dexEntries = dexfile.entries();
        while (dexEntries.hasMoreElements()) {
          String className = dexEntries.nextElement();
          if (className.startsWith(packageName)) {
              classNames.add(className);
          }
        }
      }
    });
  }
  parserCtl.await();
}

开启一个线程池取扫描dex文件,线程池的配置:

private static final int INIT_THREAD_COUNT = CPU_COUNT + 1;
private static final int MAX_THREAD_COUNT = INIT_THREAD_COUNT;
private static final long SURPLUS_THREAD_LIFE = 30L;
public class DefaultPoolExecutor extends ThreadPoolExecutor {
    public static DefaultPoolExecutor getInstance() {
        if (null == instance) {
            synchronized (DefaultPoolExecutor.class) {
                if (null == instance) {
                    // 核心线程数是cpu个数+1;最大线程=核心线程;工作队列最多是64个任务的阻塞队列
                    instance = new DefaultPoolExecutor(
                            INIT_THREAD_COUNT,
                            MAX_THREAD_COUNT,
                            SURPLUS_THREAD_LIFE,
                            TimeUnit.SECONDS,
                            new ArrayBlockingQueue(64),
                            new DefaultThreadFactory());
                }
            }
        }
        return instance;
    }
}

该线程池配置的线程个数和cpu个数相关联,cpu个数多,可以启动的线程数就多,那么扫描dex文件的速度就快,整个处理时间相对就短。

在高端多核心机器这么做速度还算快,但在低端机上就放大了该问题,尤其是对一些大的项目,它的dex文件多,再加上cpu性能差,整个耗时就更长了,对于初次启动的应用非常不友好。

使用插件优化

摘自官方文档的一句话:

通过 ARouter提供的注册插件进行路由表的自动加载,通过 gradle插件进行自动注册可以缩短初始化时间解决应用加固导致无法直接访问 dex文件。初始化失败的问题,需要注意的是,该插件必须搭配 api 1.3.0 以上版本使用!

现在再回看一下LogisticsCenter.init方法中的插件逻辑代码:

public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
  loadRouterMap();
  if (registerByPlugin) {
    // 1.插件逻辑
  }
}

先调用了loadRouterMap(),从代码注释中,会根据arouter-auto-register插件自动生成类似registerRouteRoot(new ARouter..Root..modulejava())的代码。由判断逻辑也可以推断出registerByPlugin肯定会被改为true

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-gradle-plugin是如何做的:

1.插件入口
public class PluginLaunch implements Plugin {
  @Override
  public void apply(Project project) {
    def isApp = project.plugins.hasPlugin(AppPlugin)
    if (isApp) {
      def android = project.extensions.getByType(AppExtension)
      // RegisterTransform
      def transformImpl = new RegisterTransform(project)
      // ⭐️初始化注册列表,后面会用到
      ArrayList list = new ArrayList<>(3)
      list.add(new ScanSetting('IRouteRoot'))
      list.add(new ScanSetting('IInterceptorGroup'))
      list.add(new ScanSetting('IProviderGroup'))
      RegisterTransform.registerList = list
      // 注册transform
      android.registerTransform(transformImpl)
    }
  }

关键逻辑都在RegisterTransform中了,看它的transform做了什么:

class RegisterTransform extends Transform {
  Project project
  static ArrayList registerList
  static File fileContainsInitClass;
  
  @Override
  void transform(Context context, Collection inputs , Collection referencedInputs, TransformOutputProvider outputProvider , boolean isIncremental) {
      inputs.each { TransformInput input ->
         // 1.扫描所有jar文件
        input.jarInputs.each { JarInput jarInput ->
          String destName = jarInput.name
          def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath)
          // 去掉.jar的后缀
          if (destName.endsWith(".jar")) {
            destName = destName.substring(0, destName.length() - 4)
          }
          File src = jarInput.file
          File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
          if (ScanUtil.shouldProcessPreDexJar(src.absolutePath)) {
            // 扫描jar
            ScanUtil.scanJar(src, dest)
          }
          FileUtils.copyFile(src, dest)
        }
      }
    }
  
  // 2.扫描所有class文件 (和jar类型一样,这里省略)
  ...
  // 3.放到后面解释
  ...
}

2.扫描文件

(1)遍历所有的jar文件

static void scanJar(File jarFile, File destFile) {
   if (jarFile) {
       def file = new JarFile(jarFile)
       Enumeration enumeration = file.entries()
       while (enumeration.hasMoreElements()) {
         JarEntry jarEntry = (JarEntry) enumeration.nextElement()
         String entryName = jarEntry.getName()
         // ROUTER_CLASS_PACKAGE_NAME = 'com/alibaba/android/arouter/routes/'
         if (entryName.startsWith(ScanSetting.ROUTER_CLASS_PACKAGE_NAME)) {
           InputStream inputStream = file.getInputStream(jarEntry)
           // ⭐️扫描ROUTER_CLASS_PACKAGE_NAME包下的类
           scanClass(inputStream)
           inputStream.close()
           // GENERATE_TO_CLASS_FILE_NAME = 'com/alibaba/android/arouter/core/LogisticsCenter.class'
         } else if (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == entryName) {
           // 扫描到LogisticsCenter.class,把所有的jar文件记录下来
           RegisterTransform.fileContainsInitClass = destFile
         }
       }
       file.close()
   }
}

比较关键的是scanClass方法,用到了ASM操作字节码:

static void scanClass(InputStream inputStream) {
  ClassReader cr = new ClassReader(inputStream)
  ClassWriter cw = new ClassWriter(cr, 0)
  // 通过ScanClassVisitor处理
  ScanClassVisitor cv = new ScanClassVisitor(Opcodes.ASM5, cw)
  // EXPAND_FRAMES:解压缩这些帧
  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)
    // 遍历注册列表(插件入口处注册的)interfaceName:(IRouteRoot、IInterceptorGroup、IProviderGroup)
    RegisterTransform.registerList.each { ext ->  //ext: ScanSetting
        if (ext.interfaceName && interfaces != null) {
            interfaces.each { itName ->
                if (itName == ext.interfaceName) {
                    if (!ext.classList.contains(name)) {
                      // ⭐️将符合条件的name加入到ScanSetting类的集合数组中
                      ext.classList.add(name)
                    }
                }
            }
        }
    }
}

遍历注册列表(插件入口处注册的)interfaceName:IRouteRootIInterceptorGroupIProviderGroup

然后把扫描到包含interfaceName相同的类名存储到ScanSettingclassList集合中(后面重点用到)

(2)扫描class文件,和扫描jar文件类似

(3)对上面两步扫描得到的结果classList操作,遍历classList,使用RegisterCodeGeneratorinsertInitCodeTo方法,对jar文件插入代码。

File fileContainsInitClass
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 {
          // classList不为空
          ext.classList.each {
              Logger.i(it)
          }
          RegisterCodeGenerator.insertInitCodeTo(ext)
        }
    }
}

# RegisterCodeGenerator.groovy
static void insertInitCodeTo(ScanSetting registerSetting) {
    if (registerSetting != null && !registerSetting.classList.isEmpty()) {
      RegisterCodeGenerator processor = new RegisterCodeGenerator(registerSetting)
      File file = RegisterTransform.fileContainsInitClass
      if (file.getName().endsWith('.jar'))
        // ⭐️对jar文件插入代码
        processor.insertInitCodeIntoJarFile(file)
  }
}

3.插入代码

比较关键的一步来了,看看插入了什么代码进去:

private File insertInitCodeIntoJarFile(File jarFile) {
    if (jarFile) {
        def optJar = new File(jarFile.getParent(), jarFile.name + ".opt")
        if (optJar.exists())
            optJar.delete()
        def file = new JarFile(jarFile)
        Enumeration enumeration = file.entries()
        JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar))

        while (enumeration.hasMoreElements()) {
          JarEntry jarEntry = (JarEntry) enumeration.nextElement()
          String entryName = jarEntry.getName()
          ZipEntry zipEntry = new ZipEntry(entryName)
          InputStream inputStream = file.getInputStream(jarEntry)
          jarOutputStream.putNextEntry(zipEntry)
          // GENERATE_TO_CLASS_FILE_NAME -> LogisticsCenter.class
          if (ScanSetting.GENERATE_TO_CLASS_FILE_NAME == entryName) {
              Logger.i('Insert init code to class >> ' + entryName)
              // ⭐️初始化时倾入
              def bytes = referHackWhenInit(inputStream)
              jarOutputStream.write(bytes)
          } else {
              jarOutputStream.write(IOUtils.toByteArray(inputStream))
          }
          inputStream.close()
          jarOutputStream.closeEntry()
        }
        jarOutputStream.close()
        file.close()

        if (jarFile.exists()) {
            jarFile.delete()
        }
        optJar.renameTo(jarFile)
    }
    return jarFile
}

关键的一步还是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()
}

侵入过程是用ASM修改字节码技术,整个流程大致是这样:

ASM修改字节码流程图

MyClassVisitor处理字节码:

class MyClassVisitor extends ClassVisitor {
   MyClassVisitor(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)
    }
  
    @Override
    MethodVisitor visitMethod(int access, String name, String desc,
                              String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)
        // ⭐️ 对 GENERATE_TO_METHOD_NAME = loadRouterMap 方法操作
        if (name == ScanSetting.GENERATE_TO_METHOD_NAME) {
            mv = new RouteMethodVisitor(Opcodes.ASM5, mv)
        }
        return mv
    }
}

使用RouteMethodVisitorloadRouterMap方法进行操作:

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("/", ".")
              // name推到操作数栈
              mv.visitLdcInsn(name)
              // ⭐️访问 LogisticsCenter.register() 方法,最后一个参数false表示不是接口
              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)
    }
}

关键一步已浮出水面,通过ASM会去调用LogisticsCenter.register()方法,等同于在loadRouterMap方法中插入了register代码。以官方给的Demo为例,插桩的代码如下:

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

register方法会根据生成的类名类型(IRouteRoot、IProviderGroup、IInterceptorGroup),去调用不同的注册方法

private static void register(String className) {
    if (!TextUtils.isEmpty(className)) {
        try {
            Class clazz = Class.forName(className);
            Object obj = clazz.getConstructor().newInstance();
            if (obj instanceof IRouteRoot) {
                registerRouteRoot((IRouteRoot) obj);
            } else if (obj instanceof IProviderGroup) {
                registerProvider((IProviderGroup) obj);
            } else if (obj instanceof IInterceptorGroup) {
                registerInterceptor((IInterceptorGroup) obj);
            } else {
                logger.info(TAG, "register failed, class name: " + className
                        + " should implements one of IRouteRoot/IProviderGroup/IInterceptorGroup.");
            }
        } catch (Exception e) {
            logger.error(TAG,"register class error:" + className, e);
        }
    }
}

IRouteRoot类型为例,调用registerRouteRoot

private static void registerRouteRoot(IRouteRoot routeRoot) {
    markRegisteredByPlugin();
    if (routeRoot != null) {
        routeRoot.loadInto(Warehouse.groupsIndex);
    }
}

private static void markRegisteredByPlugin() {
    if (!registerByPlugin) {
        registerByPlugin = true;
    }
}

/**
 * 注解处理器自动生成的类
 */
public class ARouter$$Root$$modulejava implements IRouteRoot {
  @Override
  public void loadInto(Map> 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);
  }
}

registerRouteRoot方法会将是否使用插件标记置为true,然后会把开发者通过注解写的路由表信息加入到Warehouse的集合中记录下来。

总结

ARouter最初通过扫描dex找到符合条件的类,完成注册表信息的存储。带来的弊端就是第一次启动非常耗时,对于低端机型影响更大。另外对一些加固应用来说,扫描dex文件也可能会失败。

优化方式是巧妙的通过插件这种侵入式很低的方案解决了。判断如果使用了插件,在编译器就自动注册,完成注册表信息的存储。避免了扫描dex文件这种耗时的操作。

笔者由此获得了一个启发,在解决问题时,插件这种低侵入式的方案可以作为一种考虑。

另外,访问者模式作为一种不常用且难以理解的设计模式,在ASM中的应用也是恰到好处。

参考博客

阿里ARouter全面全面全面解析(使用介绍+源码分析+设计思路)

访问者模式一篇就够了

ARouter原理与缺陷解析

你可能感兴趣的:(ARouter启动优化引发的探索)