如何提高 ARouter 的初始化速度
- 在 app module 的 build.gradle 中 加入:
apply plugin: 'com.alibaba.arouter'
- 在項目的 build.gradle中加入:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "com.alibaba:arouter-register:1.0.2"
}
}
我的在 華爲 P40 上面能夠提高800毫秒,在 榮耀10上提高1.1秒
二、爲什麼使用插件能夠提高啓動速度呢?
來看看初始化的代碼:
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(application)
中,如下:
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(mContext, executor)
真正實現的:
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
mContext = context;
executor = tpe;
try {
long startInit = System.currentTimeMillis();
//billy.qi modified at 2017-12-06
//load by plugin first
// 是否使用了插件,如果使用了,那麼 registerByPlugin = true,後續的就不會執行了。
loadRouterMap();
if (registerByPlugin) {
logger.info(TAG, "Load router map by arouter-auto-register plugin.");
} else {
Set<String> routerMap;
// debug模式或者是最新版本的話每次都會重建路由表,否則從SP中讀取路由表
if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
// 獲取前綴爲com.alibaba.android.arouter.routes的class類,放到set集合裏面
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
if (!routerMap.isEmpty()) {
// 存到 sp 中
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 {
// 從 sp 中獲取路由表
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)) {
// 將com.alibaba.android.arouter.routes.ARouter$$Root前綴的class類放到Warehouse.groupsIndex中
((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
// 將com.alibaba.android.arouter.routes.ARouter$$Interceptors前綴的class類放到Warehouse.interceptorsIndex中
((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
// 將com.alibaba.android.arouter.routes.ARouter$$Providers前綴的class類放到Warehouse.providersIndex中
((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() + "]");
}
}
主要看看 ClassUtils.getFileNameByPackageName
如下:
public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException {
final Set<String> classNames = new HashSet<>();
// 獲取所有的 dex 路徑
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;
}
這裏面的邏輯很簡單:
-
就是通過
getSourcePaths(context)
方法獲取所有的 dex 路徑,==getSourcePaths(context)==稍後再講,先把 getFileNameByPackageName 方法流程講完。 -
通過線程池把 dex 路徑轉換成 DexFile,這裏有個區別:
● 如果路徑是以 .zip結尾的(即是虛擬機不支持MultiDex,爲什麼下面分析),那麼利用 DexFile.loadDex 生成 DexFile,DexFile.loadDex 過程中會把普通的dex文件轉化成成odex,這個過程是很慢的;
● 如果是支持 MultiDex,那麼根據 dex 的路徑直接 new 一個 DexFile -
過濾所有以 com.alibaba.android.arouter.routes開頭的類名,存入集合
-
使用 CountDownLatch 保證線程池的所有任務都執行完後,返回所有類名集合。
再看看 getSourcePaths 方法:
public static List<String> getSourcePaths(Context context) throws PackageManager.NameNotFoundException, IOException {
ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
File sourceApk = new File(applicationInfo.sourceDir);
List<String> sourcePaths = new ArrayList<>();
sourcePaths.add(applicationInfo.sourceDir); //默認生成apk的目錄路徑...base.apk
//the prefix of extracted file, ie: test.classes
String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
// 如果VM已經支持了MultiDex,就不要去Secondary Folder加載 Classesx.zip了,那裏已經麼有了
// 通過是否存在sp中的multidex.version是不準確的,因爲從低版本升級上來的用戶,是包含這個sp配置的
if (!isVMMultidexCapable()) {
//the total dex numbers
int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
//for each dex file, ie: test.classes2.zip, test.classes3.zip...
String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
File extractedFile = new File(dexDir, fileName);
if (extractedFile.isFile()) {
sourcePaths.add(extractedFile.getAbsolutePath());
//we ignore the verify zip part
} else {
throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'");
}
}
}
if (ARouter.debuggable()) {
// Search instant run support only debuggable
sourcePaths.addAll(tryLoadInstantRunDexFile(applicationInfo));
}
return sourcePaths;
}
這個方法可以看出:
如果不支持 Multidex,那麼去Secondary Folder加載 Classesx.zip了,返回的路徑是以==.zip==結尾的。
上一張流程圖:
以上分析的是沒有使用插件的初始化,那麼使用插件後呢?
自動加載路由表是通過arouter-register來實現的,主要通過在編譯期給LogisticsCenter的loadRouterMap方法插入register方法調用的代碼,反編譯後的代碼如下:
Arouter自動加載路由表的插件是使用的通過gradle插樁技術在編譯期插入代碼來達到自動加載路由表信息。那麼在 ARouter 初始化的時候就不會再去查找過濾相應的以 com.alibaba.android.arouter.routes
開頭的類名了,從而達到減少初始化時間的目的。