Android 项目中用到了阿里开源路由框架 https://github.com/alibaba/ARouter
但是低端机首次安装启动发现耗时比较长,那么我们分析一下原因,read thd fuck source code
初始化逻辑:ARouter.init方法
/**
* Init, it must be call before used router.
*/
public static void init(Application application) {
if (!hasInit) {
logger = _ARouter.logger;
_ARouter.logger.info(Consts.TAG, "ARouter init start.");
hasInit = _ARouter.init(application);
if (hasInit) {
_ARouter.afterInit();
}
_ARouter.logger.info(Consts.TAG, "ARouter init over.");
}
}
调用到 _ARouter.init
protected static synchronized boolean init(Application application) {
mContext = application;
LogisticsCenter.init(mContext, executor);
logger.info(Consts.TAG, "ARouter init success!");
hasInit = true;
mHandler = new Handler(Looper.getMainLooper());
return true;
}
继续跟踪到LogisticsCenter.init,就发现耗时所在地方了
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
/**
* LogisticsCenter init, load all metas in memory. Demand initialization
*/
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
mContext = context;
executor = tpe;
try {
long startInit = System.currentTimeMillis();
//load by plugin first
loadRouterMap();
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>()));
}
logger.info(TAG, "Find router map finished, map size = " + routerMap.size() + ", cost " + (System.currentTimeMillis() - startInit) + " ms.");
startInit = System.currentTimeMillis();
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);
}
}
}
logger.info(TAG, "Load root element finished, cost " + (System.currentTimeMillis() - startInit) + " ms.");
if (Warehouse.groupsIndex.size() == 0) {
logger.error(TAG, "No mapping files were found, check your configuration please!");
}
if (ARouter.debuggable()) {
logger.debug(TAG, String.format(Locale.getDefault(), "LogisticsCenter has already been loaded, GroupIndex[%d], InterceptorIndex[%d], ProviderIndex[%d]", Warehouse.groupsIndex.size(), Warehouse.interceptorsIndex.size(), Warehouse.providersIndex.size()));
}
} catch (Exception e) {
throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");
}
}
这个方法在首次安装,没有缓存时,开启多个任务提交到线程池,加载APK的多个dex文件,扫描搜索在编译期生成的含有Arouter框架指定前缀的类名信息,用一个CountDownLatch并发计数器进行多线程任务总控制,在主线程 parserCtl.await();
/**
* 通过指定包名,扫描包下面包含的所有的ClassName
*
* @param context U know
* @param packageName 包名
* @return 所有class的集合
*/
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) {
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)) {
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;
}
我们看一下这个扫描dex文件线程池的配置,线程池配置的的核心线程数是cpu+1个数,最大核心数是也一样,工作队列是最大64个任务的有界队列。
public class DefaultPoolExecutor extends ThreadPoolExecutor {
// Thread args
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
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;
private static volatile DefaultPoolExecutor instance;
public static DefaultPoolExecutor getInstance() {
if (null == instance) {
synchronized (DefaultPoolExecutor.class) {
if (null == instance) {
instance = new DefaultPoolExecutor(
INIT_THREAD_COUNT,
MAX_THREAD_COUNT,
SURPLUS_THREAD_LIFE,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(64),
new DefaultThreadFactory());
}
}
}
return instance;
}
。。。。。。。。。。。。。。。。。
}
在高端多核心机器这么做并发是很快的,CPU核心多,可以启动的线程多,线程池池子大,parserCtl.await()等待的时间不会很长。另外,只会在首次启动时扫描缓存保存在SP中,后续的冷启动直接拿到缓存就不需要扫描这种耗时操作了。但是仍然不完美,首次启动仍然耗时。在低端机非常明显,原因就是现在的项目APK动辄5到6个dex文件甚至更多,低端机CPU核数小,性能也差,比如双核的老机器,按这个线程池配置只能开核心线程3个跑任务,如果有6个dex则有对应到6个任务同时提交,任务也没满,则会多的任务处于一个等待执行的情况,这个多个线程扫描过程会是成3个一组并行执行,执行后才到后一组3个并行,主线程被CountDownLatch.await住了等待的时间会变长,耗时大大增长,低端机会出现卡白屏几秒的情况。
优化方案:框架官方维护者其实早发现了这个问题,也给了对应的解决方案,使用他们的字节码自动注册插件,在编译期完成对生成类的收集,并注入初始化注册代码,再也不用扫描dex这种耗时操作,如下面配置就可以,来自官方文档。不过也有缺点,AS编译耗时肯定会增长,但是与用户首次启动耗时体验相比,这个非常值得的。
apply plugin: 'com.alibaba.arouter'
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Replace with the latest version
classpath "com.alibaba:arouter-register:?"
}
}
这个gradle插件的原理是,利用 gradle的Transform在编译期进行字节码插入操作,访问到路由生成类的时候,往LogisticsCenter里的loadRouterMap方法插入register(”编译期生成类名“)代码,完成自动注册,类似下面的 register(”xxx") 代码。
/**
* arouter-auto-register plugin will generate code inside this method
* call this method to register all Routers, Interceptors and Providers
*/
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());
register("xxx");
register("xxx");
register("xxx");
register("xxx");
}
/**
* register by class name
* Sacrificing a bit of efficiency to solve
* the problem that the main dex file size is too large
*/
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);
}
}
}
这样在LogisticsCenter.init初始化时,会执行这个逻辑 loadRouterMap(); 执行插件在编译期插入的register代码并标记 registerByPlugin = true; 不再走扫描dex文件逻辑,这样首次安装性能也达到了最优,只有反射创建类实例的耗时。
/**
* LogisticsCenter init, load all metas in memory. Demand initialization
*/
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
mContext = context;
executor = tpe;
try {
long startInit = System.currentTimeMillis();
//load by plugin first
loadRouterMap();
if (registerByPlugin) { //gradle插件注册,不再走扫描dex文件逻辑
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() + "]");
}
}