ARouter
ARouter 是阿里云出品的Android中間件,負責處理盅跳轉頁面時的邏輯,簡化和優化之前跳轉頁面的方法。同時他也是組件化的基礎之一,實現了模塊間的解耦。
ARouter使用
項目的主頁有提供ARouter的使用方法,主要就是
- 注解可以跳轉的類(Activity,Service,ContentProvider,Fragment等)
- 要跳轉頁面的時候使用Arouter,類似于
ARouter.getInstance().build("/kotlin/test").withString("name", "老王").withInt("age", 23).navigation();
用法是不能再簡單了,猜想是把一個String
與一個Activity
對應起來就可以了,然而實際代碼應該比猜想復雜N多倍。下面一起分析一下這個中間,挖掘他的所有信息。
ARouter源碼解析
下面分析源碼分為幾部分
- 跳轉頁面流程分析
- 在類上和成員上的注解都做了什么
- 解釋在ARouter文檔上的所有功能特點和典型應用是如何實現的,把這些特點抄到下面來,方便查看,我們會挨個把所有的特點分析一遍
一、功能介紹
- 支持直接解析標準URL進行跳轉,并自動注入參數到目標頁面中
- 支持多模塊工程使用
- 支持添加多個攔截器,自定義攔截順序
- 支持依賴注入,可單獨作為依賴注入框架使用
- 支持InstantRun
- 支持MultiDex(Google方案)
- 映射關系按組分類、多級管理,按需初始化
- 支持用戶指定全局降級與局部降級策略
- 頁面、攔截器、服務等組件均自動注冊到框架
- 支持多種方式配置轉場動畫
- 支持獲取Fragment
- 完全支持Kotlin以及混編(配置見文末 其他#5)
- 支持第三方 App 加固(使用 arouter-register 實現自動注冊)
二、典型應用
- 從外部URL映射到內部頁面,以及參數傳遞與解析
- 跨模塊頁面跳轉,模塊間解耦
- 攔截跳轉過程,處理登陸、埋點等邏輯
- 跨模塊API調用,通過控制反轉來做組件解耦
跳轉頁面流程分析
使用的方法是
ARouter.getInstance()
.build("/kotlin/test")
.withString("name", "老王")
.withInt("age", 23)
.navigation();
ARouter使用了單例,內部存儲了頁面的映射表,初始化狀態,debug信息等,等一下都會用到的信息(其實在保存在_ARouter單例中),同時也方便在使用的時候直接使用ARouter的靜態方法。
實際上除了ARouter之外還有一個_ARouter類,ARouter中幾乎是把所有的方法都直接給了_ARouter處理,這一層的轉換把_ARouter中復雜的方法轉化為ARouter中簡單的方法向外暴露,算是一種門面吧,值得學習一下。
build方法不例外地轉給了_ARouter
public Postcard build(String path) {
return _ARouter.getInstance().build(path);
}
在_ARouter中構造了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 {
//這個Service里可以將傳入的path處理,換在另外一個,也相當于一個攔截器
PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
if (null != pService) {
path = pService.forString(path);
}
return build(path, extractGroup(path));
}
}
其中PathReplaceService相關處理邏輯是根據當前的path換成另外的path繼續走下面的流程,所以主流程還是build方法,其中有有個extractGroup(path)方法調用,將path中第一部分抽取出來作為group,繼續看主線
/**
* Build postcard by path and group
*/
protected Postcard build(String path, String group) {
if (TextUtils.isEmpty(path) || TextUtils.isEmpty(group)) {
throw new HandlerException(Consts.TAG + "Parameter is invalid!");
} else {
PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
if (null != pService) {
path = pService.forString(path);
}
return new Postcard(path, group);
}
}
這里又找了一次PathReplaceService,出于什么目的,很簡單,因為這個方法有可能不是上面那個路徑調過來的,_ARouter還有兩個參數的builde方法,直接調用了這個方法,但是不做任何區分直接再找一次也有點可以優化的空間??
返回的結果就是最后構造出來的一個PostCard,這個類上的解釋是A container that contains the roadmap.
,包含這一次路由過程中所要的所有信息。PostCard的構造方法沒有什么邏輯,就是另外構造了一個bundle放在了PostCard里面備下面使用。
到這里就返回到了最初調用build的地方,再往下是兩個withXXX方法,就是向PostCard中放入幾個跳轉頁面要帶過去的信息,都是直接放到了bundle里面,當然這不是主線。
繼續看下面的navigation方法。
/**
* Navigation to the route with path in postcard.
* No param, will be use application context.
*/
public Object navigation() {
//后面會用application的Context
return navigation(null);
}
給出的例子都是使用沒有參數所navigation方法,后面拿application
的Context
,要設置intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
感覺有點誤導的感覺,這里正正經經地傳個activity過來才應該是最廣泛的使用方法。
/**
* Navigation to the route with path in postcard.
*
* @param context Activity and so on.
*/
public Object navigation(Context context) {
//這里沒有caback,這個callback有onFound,onLost,onArrival,和onInterrupt方法,都是跳轉時的各種回調,單獨的跳轉降級就是通過這個callback完成的
return navigation(context, null);
}
/**
* Navigation to the route with path in postcard.
*
* @param context Activity and so on.
*/
public Object navigation(Context context, NavigationCallback callback) {
//這里又增加了一個-1的參數,不求不需求forResult
return ARouter.getInstance().navigation(context, this, -1, callback);
}
到這里方法調用就出了PostCard,到了ARouter中,當然又會委托給_ARouter進行真正的業務。
看_ARouter的方法。
/**
* Use router navigation.
*/
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.
Toast...
}
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);
}
//主流程,根據是否綠色通道走攔截的邏輯
//這里還有個友情提示: It must be run in async thread, maybe interceptor cost too mush time made ANR.
if (!postcard.isGreenChannel()) {
interceptorService.doInterceptions(postcard, new InterceptorCallback() {
@Override
public void onContinue(Postcard postcard) {
//走完攔截器并通過的,繼續走跳轉的邏輯
_navigation(context, postcard, requestCode, callback);
}
@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;
}
主流程沒有幾名話,先補全路由的信息,走攔截邏輯,然后真正的跳轉,其中補全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.
//沒有找到RouteMeta,檢查他所有的組是否還沒有加載,如果已經加載,則異常,沒有加載去加載
Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup()); // Load route meta.
if (null == groupMeta) {
throw new NoRouteFoundException(...);
} else {
// Load route and cache it into memory, then delete from metas.
try {
IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
//加載所在的組,如果加載完成就把這個group從未加載列表中刪除
iGroupInstance.loadInto(Warehouse.routes);
//將些組從未加載中移除,防止重復加載,
Warehouse.groupsIndex.remove(postcard.getGroup());
} catch (Exception e) {
throw new HandlerException(...);
}
// 加載了組信息,重新去重新去走補全的邏輯
completion(postcard); // Reload
} else {
將RouteMeta的信息放到PostCard中,
如果是通過uri跳轉的話再將路徑中的信息解析出來放到postCard的bound中
下面又對兩種類型的跳轉做的特殊處理
1. PROVIDER
從倉庫中找到provider的實例,疳賦值給postCard
設置綠色通道,防止攔截
2. FRAGMENT
設置綠色通道,防止攔截
}
}
此時post的信息已經完全了,我們路過攔截的邏輯,直接看下面真正的跳轉方法,感覺是沒有必要把代碼再拿出來,就是根據類型區分了一下,activity就直接new Intent進行跳轉,如果是Privider,Fragment就返回實例。
到這里基本完成了一次跳轉頁面所走的全部路徑。并沒有高深難懂的邏輯,一個比較好玩的就是PathReplaceService,DegradeService,SerializationService等都是通過注冊一個Service完成的,這就大大增加了這個框架的靈活性,而且框架向外提供的這個功能,自己內部已經先用起來了,這個也是挺有意思的。
ARouter中的注解有什么用,是怎么起作用的
@Route
作用:注解一個類,這個類就可以通過ARouter找到使用
Route主要有兩個屬性,path和group,在RouteProcessor中處理這個注解,在注解處理的方法中會根據注解的類型創建上面使用過的RouteMeta
for (Element element : routeElements) {
TypeMirror tm = element.asType();
Route route = element.getAnnotation(Route.class);
RouteMeta routeMeta = null;
if (types.isSubtype(tm, type_Activity)) { // Activity
logger.info(">>> Found activity route: " + tm.toString() + " <<<");
// Get all fields annotation by @Autowired
Map<String, Integer> paramsType = new HashMap<>();
for (Element field : element.getEnclosedElements()) {
if (field.getKind().isField() && field.getAnnotation(Autowired.class) != null && !types.isSubtype(field.asType(), iProvider)) {
// It must be field, then it has annotation, but it not be provider.
Autowired paramConfig = field.getAnnotation(Autowired.class);
paramsType.put(StringUtils.isEmpty(paramConfig.name()) ? field.getSimpleName().toString() : paramConfig.name(), typeUtils.typeExchange(field));
}
}
routeMeta = new RouteMeta(route, element, RouteType.ACTIVITY, paramsType);
} else if (types.isSubtype(tm, iProvider)) { // IProvider
logger.info(">>> Found provider route: " + tm.toString() + " <<<");
routeMeta = new RouteMeta(route, element, RouteType.PROVIDER, null);
} else if (types.isSubtype(tm, type_Service)) { // Service
logger.info(">>> Found service route: " + tm.toString() + " <<<");
routeMeta = new RouteMeta(route, element, RouteType.parse(SERVICE), null);
} else if (types.isSubtype(tm, fragmentTm) || types.isSubtype(tm, fragmentTmV4)) {
logger.info(">>> Found fragment route: " + tm.toString() + " <<<");
routeMeta = new RouteMeta(route, element, RouteType.parse(FRAGMENT), null);
} else {
throw new RuntimeException("ARouter::Compiler >>> Found unsupported class type, type = [" + types.toString() + "].");
}
categories(routeMeta);
// if (StringUtils.isEmpty(moduleName)) { // Hasn't generate the module name.
// moduleName = ModuleUtils.generateModuleName(element, logger);
// }
}
分別構建出來RouteMeta,還構建出來一個分組的信息,下面將這些信息構建兩個java文件。類似于這樣
public class ARouter$$Root$$app implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("service", ARouter$$Group$$service.class);
routes.put("test", ARouter$$Group$$test.class);
}
}
rootInfo,存放所有的組的信息,就是上面找不到RouteMeta的時候會從這里找到對應的組,再找到組信息對應的類,然后加載
還有一個組詳細信息的類,類似開這樣
public class ARouter$$Group$$test implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/test/activity2", RouteMeta.build(RouteType.ACTIVITY, Test2Activity.class, "/test/activity2", "test", new java.util.HashMap<String, Integer>(){{put("key1", 8); }}, -1, -2147483648));
atlas.put("/test/activity4", RouteMeta.build(RouteType.ACTIVITY, Test4Activity.class, "/test/activity4", "test", null, -1, -2147483648));
atlas.put("/test/fragment", RouteMeta.build(RouteType.FRAGMENT, BlankFragment.class, "/test/fragment", "test", null, -1, -2147483648));
}
}
加載的時候把這些信息加載到map中,以后跳轉使用
@Interceptor
作用:設置全局跳轉的攔截器,可以設置優先級
處理注解基本和和@Route一樣,得到類,得到屬性,javapoet寫出一個類似于這樣的類:
public class ARouter$$Interceptors$$app implements IInterceptorGroup {
@Override
public void loadInto(Map<Integer, Class<? extends IInterceptor>> interceptors) {
interceptors.put(7, Test1Interceptor.class);
}
}
這個map是個特別的map,根據key的值自動排序,如果key重復會異常,也也是這個攔截器可以按優先級排序的原因
@Autowired
作用:自動裝配,注解成員后,可以自動從Intent中解出數據并賦值給變量
實現也很相似,找到被注解的成員,生成一個helper,在需要將intent的數據解出來的時候使用helper的inject方法,ARouter又使用了一個AutowiredService專門做這個事,只要將要注入的類傳過來就可以了
@Override
public void autowire(Object instance) {
String className = instance.getClass().getName();
try {
if (!blackList.contains(className)) {
// 只有一個inject方法
ISyringe autowiredHelper = classCache.get(className);
if (null == autowiredHelper) { // No cache.
autowiredHelper = (ISyringe) Class.forName(instance.getClass().getName() + SUFFIX_AUTOWIRED).getConstructor().newInstance();
}
// autowiredHelper就是根據注解生成的特定helper
autowiredHelper.inject(instance);
classCache.put(className, autowiredHelper);
}
} catch (Exception ex) {
blackList.add(className); // This instance need not autowired.
}
}
javaPoet實在是有點煩瑣,真的不愿把他的代碼拿來。有意的同學可以直接去arouter查看
解釋所有官方列舉的特點
- 支持直接解析標準URL進行跳轉,并自動注入參數到目標頁面中
在Manifast頁面中注冊了兩個filter
<intent-filter>
<data
android:host="m.aliyun.com"
android:scheme="arouter"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
</intent-filter>
<!-- App Links -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:host="m.aliyun.com"
android:scheme="http"/>
<data
android:host="m.aliyun.com"
android:scheme="https"/>
</intent-filter>
第一個是處理特定的協議,如果協議中有arouter,則會用這個Acitity處理
還有一個是處理applink的,在網頁上點擊特定連接,也是在這個Activity中處理
在這個Activity主要代碼就兩行:
Uri uri = getIntent().getData();
ARouter.getInstance().build(uri).navigation(this, new NavCallback() {
@Override
public void onArrival(Postcard postcard) {
finish();
}
});
這里使用的是一個URI,在構造PostCard的時候會將URI后面掛的參數直接轉化到bound中去,也就解釋了第一個特征。
- 支持多模塊工程使用
各模塊只是依賴一個String,編譯時會掃描整個所有的工程,所以直接就支持多模塊的工程。但是有個問題就是別的頁面要跳轉的時候都要將字符串寫死進去,如果定義常量的話會出現多個模塊依賴一個常量類的情況。
- 支持添加多個攔截器,自定義攔截順序
攔截器注解定義
如果設置了這個優先級別,生成的java代碼中會將這個優先級做為key,放到傳過來的一個容器中,而這個窗口的定義在com.alibaba.android.arouter.core.Warehouse
中,是一個UniqueKeyTreeMap,保證key是唯一的,并且按key進行排序
這里也就解釋了自定義攔截順序的特點
- 支持依賴注入,可單獨作為依賴注入框架使用
不知道講的是什么。。
navigation方法返回的是一個Object
- 支持InstantRun
- 支持MultiDex(Google方案)
這里看到源碼中找了一個所有的的dex文件,再從這些所有的dex中查找要找的router的類,應該就是處理這個問題。等于沒有說。。就簡單看一下調用鏈吧
Arouter.init() -> LogisticsCenter.init(mContext, executor)
-> ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);//這里就是是指定的ali的包名,就是生成的那些類包名
-> ClassUtils.getSourcePaths(context);//從多dex下找類
-> ClassUtils.tryLoadInstantRunDexFile(applicationInfo)//instantRun
-> 找到所有生成的注冊router的類
- 映射關系按組分類、多級管理,按需初始化
路由的注解中可以注冊一個group信息,如果不定義這個group信息,arouter會拿路徑中的第一段做為group。處理注解的時候會生成兩組信息,第一是組信息,其中有所有group的信息,每一組都會指向一個描述這個組中所有路徑的類。
初始化時僅僅加載了組的信息,并沒有加載每一組內的所有路由,使用路由時會先查找有沒有這個路由信息,如果沒有的話就去加載這一組所有的路由。做到了按需初始化。
這個過程上面路由過程已經用代碼分析過了。
- 支持用戶指定全局降級與局部降級策略
每一次使用路由時可以傳入一個callback,作為單次路由失敗的降級策略,其實也不僅僅是降級策略,callback提供了多個回調方法使用:
public interface NavigationCallback {
//找到路由
void onFound(Postcard postcard);
//沒有找到,降級吧
void onLost(Postcard postcard);
//向android發出了startActivity的請求
void onArrival(Postcard postcard);
//使用攔截器時
void onInterrupt(Postcard postcard);
}
也可以注冊一個IProvider,用來處理所有的降級策略。
- 頁面、攔截器、服務等組件均自動注冊到框架
使用注解,編譯期處理,運行時直接無反射運行(多dex什么的還是要反射)
- 支持多種方式配置轉場動畫
支持,無特殊
- 支持獲取Fragment
navigate的時候支持返回一個fragment,只要注冊了路由的fragment,都可以通過路由來得到實例。
- 完全支持Kotlin以及混編(配置見文末 其他#5)
- 支持第三方 App 加固(使用 arouter-register 實現自動注冊)