ARouter是阿里巴巴開源的組件化架構框架,能幫助組件化項目中實現不同模塊間的跳轉,以及AOP面向切面的編程,能對頁面跳轉的過程進行很好的干預。本文將從源碼角度入手,對該框架的原理進行分析。
項目集成時會集成兩個Library,也對應了ARouter的兩個階段。arouter-compiler是用於編譯期的,而arouter-api是面向運行期的。下面就從這兩個階段開始講起。
dependencies {
// Replace with the latest version
compile 'com.alibaba:arouter-api:?'
annotationProcessor 'com.alibaba:arouter-compiler:?'
...
}
編譯階段
ARouter是可以自動註冊頁面映射關係的,在每個目標頁面上使用註解來標註一些參數,比方Path標註其路徑。使用註解時會遇到的第一個問題就是需要找到處理註解的時機,如果在運行期處理註解則會大量地運用反射,而這在軟件開發中是非常不合適的,因爲反射本身就存在性能問題,如果大量地使用反射會嚴重影響APP的用戶體驗,而又因爲路由框架是非常基礎的框架,所以大量使用反射也會使得跳轉流程的用戶體驗非常差。所以ARouter最終使用的方式是在編譯期處理被註解的類,這樣就可以做到在運行中儘可能不使用反射,這就是註解處理器的作用。
頁面註冊的整個流程首先通過註解處理器掃出被標註的類文件,然後按照不同種類的源文件進行分類,分別生成固定格式的類文件,命名規則是工程名ARouter+$$ +xxx+$ $ +模塊名。可以看出這裏麪包含了Group、Interceptor、Providers以及Root。這部分完成之後就意味着編譯期的工作已經結束了,之後的初始化其實是發生在運行期的,在運行期只需要通過固定的包名來加載映射文件就可以了,因爲註解是由開發者自己完成的,所以瞭解其中的規則,就可以在使用的時候利用相應的規則反向地提取出來。這就是頁面自動註冊的整個流程。
自動生成的類:
編譯期流程圖:
運行階段
運行階段也分爲兩部分來分析,初始化和頁面跳轉。
初始化
// Initialize the SDK
ARouter.init(mApplication); // As early as possible, it is recommended to initialize in the Application
//代碼流程
...
ARouter.init(mApplication) -> _ARouter.init(application) -> LogisticsCenter.init(mContext, executor)
...
沿着代碼流程init會走到LogisticsCenter.init(mContext, executor),該函數通過ClassUtils.getFileNameByPackageName遍歷包名"com.alibaba.android.arouter.routes"下的所有class存入routerMap中(即把所有編譯期自動生成的class讀取出來),然後通過for循環把這些class按照不同類型(Root,Interceptors, Providers)進行處理。通過class的路徑,利用Class.forName的方式得到class實例,然後調用該class的loadInto方法把該class關聯到Warehouse(Warehouse是整個項目的倉庫,裏面存有所有class的映射關係)的相應的結構體中,這樣所有自動生成的映射關係就存儲在內存中了,以便於後續查找時使用。
/**
* 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 {
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() + "]");
}
}
細心的讀者可能發現了,這裏加載了Root分組,卻沒有加載Group分組,那麼爲什麼要這樣設計呢,這兩者又有什麼關係呢?這裏就涉及ARouter的一個加載理念:分組管理,按需加載。
加載理念
如果一個App有上百個頁面的時候,一次性將所有頁面都加載到內存中本身對於內存的損耗是非常可怕的,同時對於性能的損耗也是不可忽視的。所以ARouter中提出了分組的概念,ARouter允許某一個模塊下有多個分組,所有的分組最終會被一個root節點管理。如下圖中所示,假設有4個模塊,每個模塊下面都有一個root結點,每個root結點都會管理整個模塊中的group節點,每個group結點則包含了該分組下的所有頁面,也就是說可以按照一定的業務規則或者命名規範把一部分頁面聚合成一個分組,每個分組其實就相當於路徑中的第一段,而每個模塊中都會有一個攔截器節點就是Interceptor結點,除此之外每個模塊還會有控制攔截反轉的provider結點。
ARouter在初始化的時候只會一次性地加載所有的Root結點,而不會加載任何一個Group結點,這樣就會極大地降低初始化時加載結點的數量。那麼什麼時候加載分組結點呢?其實就是當某一個分組下的某一個頁面第一次被訪問的時候,整個分組的全部頁面都會被加載進去,這就是ARouter的按需加載。其實在整個APP運行的週期中,並不是所有的頁面都需要被訪問到,可能只有20%的頁面能夠被訪問到,所以這時候使用按需加載的策略就顯得非常重要了,這樣就會減輕很大的內存壓力。
至此初始化階段就分析結束了,下面看跳轉階段。
跳轉
我們也從跳轉代碼說起,通常的跳轉代碼如下。
ARouter.getInstance().build("/moudlea/activitya").navigation();
...
//代碼流程
build("path") -> _ARouter.getInstance().build(path) -> build(String path, String group) -> new Postcard(path, group) ->
Postcard.navigation() -> LogisticsCenter.completion(postcard) -> _navigation(context, postcard, requestCode, callback);
...
_ARouter.getInstance().build(path)源碼,構建並返回了一個Postcard結構,該結構的定義是A container that contains the roadmap(包含roadmap的一個容器)。
函數首先去查找PathReplaceService.class接口的實現類,該實現類的作用就是實現 “運行期動態修改路由”,如果找到則利用forString方法修改path。如沒有此類則返回build(path, extractGroup(path)),其中extractGroup()是從路徑中獲取默認的分組信息。最後根據提供的path和group創建一個Postcard對象返回。
/**
* Build postcard by path and default group
*/
protected Postcard build(String path) {
if (TextUtils.isEmpty(path)) {
throw new HandlerException(Consts.TAG + "Parameter is invalid!");
} else {
PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
if (null != pService) {
path = pService.forString(path);
}
return build(path, extractGroup(path));
}
}
接下來會調用Postcard.navigation()方法。該函數首先拼裝了postcard結構體,並判斷是否需要攔截器功能,如果不需要則直接調用_navigation()進行跳轉了。
/**
* Use router navigation.
*
* @param context Activity or null.
* @param postcard Route metas
* @param requestCode RequestCode
* @param callback cb
*/
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
try {
LogisticsCenter.completion(postcard);
} catch (NoRouteFoundException ex) {
logger.warning(Consts.TAG, ex.getMessage());
if (debuggable()) {
// Show friendly tips for user.
runInMainThread(new Runnable() {
@Override
public void run() {
Toast.makeText(mContext, "There's no route matched!\n" +
" Path = [" + postcard.getPath() + "]\n" +
" Group = [" + postcard.getGroup() + "]", Toast.LENGTH_LONG).show();
}
});
}
if (null != callback) {
callback.onLost(postcard);
} else { // No callback for this invoke, then we use the global degrade service.
DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class);
if (null != degradeService) {
degradeService.onLost(context, postcard);
}
}
return null;
}
if (null != callback) {
callback.onFound(postcard);
}
//綠色通道校驗 需要攔截處理
if (!postcard.isGreenChannel()) { // It must be run in async thread, maybe interceptor cost too mush time made ANR.
//調用攔截器截面控制器,遍歷內存倉庫的自定義攔截器,並在異步線程中執行攔截函數
interceptorService.doInterceptions(postcard, new InterceptorCallback() {
/**
* Continue process
*
* @param postcard route meta
*/
@Override
public void onContinue(Postcard postcard) {
_navigation(context, postcard, requestCode, callback);
}
/**
* Interrupt process, pipeline will be destory when this method called.
*
* @param exception Reson of interrupt.
*/
@Override
public void onInterrupt(Throwable exception) {
if (null != callback) {
callback.onInterrupt(postcard);
}
logger.info(Consts.TAG, "Navigation failed, termination by interceptor : " + exception.getMessage());
}
});
} else {
return _navigation(context, postcard, requestCode, callback);
}
return null;
}
其中兩個主要的方法是LogisticsCenter.completion()和_navigation()。
completion()裏面填充postcard裏面剩下的信息,在這裏可以看到,如果該class沒有被加載(即上面說的延時加載),在這裏會把該模塊下的所有類加載起來。最終得到完整的postcard結構體。
/**
* Completion the postcard by route metas
*
* @param postcard Incomplete postcard, should complete by this method.
*/
public synchronized static void completion(Postcard postcard) {
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 {
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
}
} else {
postcard.setDestination(routeMeta.getDestination());
postcard.setType(routeMeta.getType());
postcard.setPriority(routeMeta.getPriority());
postcard.setExtra(routeMeta.getExtra());
Uri rawUri = postcard.getUri();
if (null != rawUri) { // Try to set params into bundle.
Map<String, String> resultMap = TextUtils.splitQueryParameters(rawUri);
Map<String, Integer> paramsType = routeMeta.getParamsType();
if (MapUtils.isNotEmpty(paramsType)) {
// Set value by its type, just for params which annotation by @Param
for (Map.Entry<String, Integer> params : paramsType.entrySet()) {
setValue(postcard,
params.getValue(),
params.getKey(),
resultMap.get(params.getKey()));
}
// Save params name which need auto inject.
postcard.getExtras().putStringArray(ARouter.AUTO_INJECT, paramsType.keySet().toArray(new String[]{}));
}
// Save raw uri
postcard.withString(ARouter.RAW_URI, rawUri.toString());
}
}
}
_navigation是真正實現跳轉的地方,通過postcard.getDestination()獲取目標class的實例(在初始化時已經加載到內存),用大家熟悉的startactivity方法進行了跳轉。
private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
final Context currentContext = null == context ? mContext : context;
switch (postcard.getType()) {
case ACTIVITY:
// Build intent
final Intent intent = new Intent(currentContext, postcard.getDestination());
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.
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;
case PROVIDER:
return postcard.getProvider();
case BOARDCAST:
case CONTENT_PROVIDER:
case FRAGMENT:
Class fragmentMeta = postcard.getDestination();
try {
Object instance = fragmentMeta.getConstructor().newInstance();
if (instance instanceof Fragment) {
((Fragment) instance).setArguments(postcard.getExtras());
} else if (instance instanceof android.support.v4.app.Fragment) {
((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());
}
return instance;
} catch (Exception ex) {
logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace()));
}
case METHOD:
case SERVICE:
default:
return null;
}
return null;
}
至此整個源碼分析結束。