前言
笔者在做启动优化时,发现第一次启动应用时,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:IRouteRoot
、IInterceptorGroup
、IProviderGroup
然后把扫描到包含interfaceName
相同的类名存储到ScanSetting
中classList
集合中(后面重点用到)
(2)扫描class
文件,和扫描jar
文件类似
(3)对上面两步扫描得到的结果classList
操作,遍历classList
,使用RegisterCodeGenerator
的insertInitCodeTo
方法,对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修改字节码技术,整个流程大致是这样:
用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
}
}
使用RouteMethodVisitor
对loadRouterMap
方法进行操作:
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原理与缺陷解析