上一篇文章我們記錄了在編譯期ARoute將目標注解生成文件保存的過程,這一節我們來探究下在用戶使用時ARoute做了什么
兩條路徑保證
在官方README中我們能看見關于AutoRegister
的介紹,借由這個gradle插件能實現路由表的自動加載。這節我們先不講這個,關心下普通狀態下的加載過程。
前期準備工作
初始化
在使用ARoute之前,我們需要進行初始化ARouter.init
,初始化期間會將儲存信息讀取到靜態變量中,使我們能順序完成跳轉,因此先來看下初始化的內容:
ARoute的初始化代碼:
public static void init(Application application) {
// 忽略不重要的過程
hasInit = _ARouter.init(application);
}
// _ARouter.init
protected static synchronized boolean init(Application application) {
// 主要是進行數據倉庫初始化
LogisticsCenter.init(mContext, executor);
}
初始化過程都是基操,維護一個主線程Handler用于線程切換、保存Context 等
接下來我們主要看下LogisticsCenter.init
,這里會進行一次判斷
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>()));
}
}
這里看到先做了一次預加載,如果是debug模式或者版本號升級的話預先做一次加載。但是使用版本號來判斷是否需要重新讀取組件,在未升級版本號的情況下可能會出現問題。
getFileNameByPackageName
這個函數的目的是讀取安裝包中現有的class,這些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);
、、、暫時忽略
public static List<String> getSourcePaths(Context context) throws PackageManager.NameNotFoundException, IOException {
ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
// app 根目錄
File sourceApk = new File(applicationInfo.sourceDir);
List<String> sourcePaths = new ArrayList<>();
// 應用存放數據目錄
sourcePaths.add(applicationInfo.sourceDir); //add the default apk path
//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;
}
private static boolean isVMMultidexCapable() {
boolean isMultidexCapable = false;
String vmName = null;
try {
if (isYunOS()) { // YunOS需要特殊判斷(我懷疑你在打廣告)
vmName = "'YunOS'";
isMultidexCapable = Integer.valueOf(System.getProperty("ro.build.version.sdk")) >= 21;
} else { // 非YunOS原生Android
vmName = "'Android'";
String versionString = System.getProperty("java.vm.version");
if (versionString != null) {
Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
if (matcher.matches()) {
try {
int major = Integer.parseInt(matcher.group(1));
int minor = Integer.parseInt(matcher.group(2));
isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
|| ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
&& (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
} catch (NumberFormatException ignore) {
// let isMultidexCapable be false
}
}
}
}
} catch (Exception ignore) {
}
Log.i(Consts.TAG, "VM with name " + vmName + (isMultidexCapable ? " has multidex support" : " does not have multidex support"));
return isMultidexCapable;
}
代碼中會到安裝目錄下讀取apk包( /data/app/com.alibaba.android.arouter.demo-S8YOQ-yxwK8cUikcaWvAVA==/base.apk),這里還有分包的可能
這里的是否支持分包isVMMultidexCapable
是通過判斷版本號來決定的,version>2.1,這是一個可以學習的點。
拿到apk后,開啟CountDownLatch,多線程查詢所有文件
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 "
使用DexFile讀取文件,遍歷文件中的class,拿到所有包名開頭的;
接下來就是根據讀到的信息,獲取三類我們想要的文件:IRouteRoot、IInterceptorGroup、IProviderGroup 讀取進內存;這里我們比較關心的是IRouteRoot,他就是我們上節提到的ARouter$$Root$$app
,這里包含了所有我們的組信息:
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);
}
}
初始化結束,我們的組信息儲存在Warehouse.groupsIndex
中
路由定位
最簡單的形式:
ARouter.getInstance().build("/test/activity2").navigation();
build會生成一個PostCard對象
new Postcard(path, group)
,其中的組信息group也是根據簡單的第一個/分割而來
接下來的navigation自然也是交給Postcard來處理了,事實上他只是充當了數據model的作用,保存了組信息,調用轉發給了ARoute
ARouter.getInstance().navigation(context, this, -1, callback);
接下來交給數據倉庫嘗試路由:
try {
LogisticsCenter.completion(postcard);
}
首先會在已經解析的Route信息中查找,如果找不到則到groupsIndex 中找對應的組信息,然后實例化組信息記錄類,加載數據:
RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
if (null == routeMeta) { // Maybe its does't exist, or didn't load.
Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup()); // Load route meta.
if (null == groupMeta) {
throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
} else {
// Load route and cache it into memory, then delete from metas.
try {
if (ARouter.debuggable()) {
logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] starts loading, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
}
IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
iGroupInstance.loadInto(Warehouse.routes);
Warehouse.groupsIndex.remove(postcard.getGroup());
if (ARouter.debuggable()) {
logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] has already been loaded, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
}
} catch (Exception e) {
throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
}
completion(postcard); // Reload:這里是重新調了一次自身
}
}
在找到的情況下,填充自身數據:
// 比較重要的是這個,記錄目標class
postcard.setDestination(routeMeta.getDestination());
postcard.setType(routeMeta.getType());
postcard.setPriority(routeMeta.getPriority());
postcard.setExtra(routeMeta.getExtra());
接下來回到_ARoute中,找到記錄就告知上層:
if (null != callback) {
callback.onFound(postcard);
}
然后是實際的跳轉:
return _navigation(context, postcard, requestCode, callback);
我們看下Activity的處理情況:
case ACTIVITY:
// Build intent
final Intent intent = new Intent(currentContext, postcard.getDestination());
// 我們在外層調用withString等方法傳遞參數的時候,就會保存到PostCard的mBundle中
intent.putExtras(postcard.getExtras());
// Set flags.
int flags = postcard.getFlags();
if (-1 != flags) {
intent.setFlags(flags);
} else if (!(currentContext instanceof Activity)) { // Non activity, need less one flag.
// 應該有很多朋友遇到這種奔潰,非Activity源啟動需要加上New_Task標志
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
// Set Actions
String action = postcard.getAction();
if (!TextUtils.isEmpty(action)) {
intent.setAction(action);
}
// Navigation in main looper.
runInMainThread(new Runnable() {
@Override
public void run() {
startActivity(requestCode, currentContext, intent, postcard, callback);
}
});
break;
這段代碼不難看明白,基本是我們平時跳轉的基操。
課后題:通過插件掃描dex是怎么實現的
在LogisticsCenter
有這么一個方法loadRouterMap
,我們在調初始化的時候會調用它,所以可否在這里入手呢?
com.alibaba.arouter
注冊的插件PluginLaunch
會執行 RegisterTransform
的轉換操作,經過層層調用,最終是使用asm來修改現有代碼的,RouteMethodVisitor
會修改LogisticsCenter. loadRouterMap
在其中注入 LogisticsCenter. register
方法,把掃描到的目標類IRoute作為參數注入。