背景
為什么要重復造輪子呢?
- 我認為只有站在作者的角度才能更透徹的理解框架的設計思想
- 去踩大神們所踩過的坑。
- 才能深入的理解框架的所提供的功能
- 學習優秀的作品中從而提高自己
在開始之前我先提出關于ARouter的幾個問題
- 為什么要在module的build.gradle文件中增加下面配置? 它的作用是什么?它跟我們定義的url中的分組有什么關系?
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName: project.getName()]
}
}
- 有這么一種業務場景,新建一個業務組件user,user組件中有頁面UserActivity,配置url
/user/main
;有一個服務接口,其實現類在app中,配置url為/user/info
;代碼如下:
//module:user
@Route(path = "/user/main")
public class UserActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.user_activity);
}
}
public interface IUserService extends IProvider {
void test(String s);
}
//module:app
//user服務
@Route(path = "/user/info")
public class UserServiceImpl implements IUserService {
public void test(String test) {
Log.d("xxxx->",test);
}
}
好了開發完成,讓我們編譯一下項目看看,編譯結果如下圖(ps:這里編譯的是我自己的項目,但效果和ARouter是一樣的):
Why???
讓我們帶著這兩個問題開始RouterManager之旅。
第一步架構設計思路(處理頁面跳轉)
我們的目標是根據一個url來打開指定的頁面,該如何做呢?很簡單,我們把url和對應的頁面做一個對應關系,比如放到map中以url為key,對應的頁面activity為value即可;這樣當我們要打開這個activity時,根據傳給我們的url去map中找到對應的activity,然后調用startActivity就OK了。
你可能會問那我們這個map該如何維護呢?我們怎么把這個對應關系存到map中呢?總不能手動去put吧,你別說貌似還真行,我們在app啟動的時候先把我的映射關系手動初始化好,這樣在打開頁面是直接通過url來獲取就行了。那么問題來了,大哥你累不累???對于一個懶人來說首先會想到的是能不能自動生成這個映射關系表呢?答案是肯定的。
思路總結
我們可以利用編譯注解的特性,新增一個注解,給每個需要通過url打開的activity加上此注解。在注解處理器中獲取所有被注解的類,動態生成映射關系表,然后在app啟動時把所生成的映射關系load到內存(其實就是讀到一個map中)
第二部擼代碼
0x01
首先我們需要創建三個module,如下圖:
為什么要三個項目呢?原因如下:
我們需要用到的注解處理器AbstractProcessor是在javax包下,而android項目中是沒有這個包的,因此我們需要建一個java library,也就是router-compiler,它的作用是幫我們動態生成代碼,只存在于編譯期間
既然router-compiler只存在于編譯期間,那我們的注解是需要在項目中用到的,這個類應該放在那里呢?這就有了第二個java library,router-annotation,用來專門存放我們定義的注解和一些要被打進app中代碼。
由于上述兩個library都是java項目,而我們最終是要用到android工程中的,因此對外提供api時肯定會用到android工程中的類,如Context。所以就有了第三個module router-api用于處理生成產物。如把生成映射關系表load到內存,并提供統一的調用入口。
0x02
我們先定義我們自己的注解:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {
String path();
String group() default "";
String name() default "";
int extras() default Integer.MIN_VALUE;
int priority() default -1;
}
定義自己的route處理器RouterProcessor
@AutoService(Processor.class) //自動注冊注解處理器
@SupportedOptions({Consts.KEY_MODULE_NAME}) //參數
@SupportedSourceVersion(SourceVersion.RELEASE_7) //指定使用的Java版本
@SupportedAnnotationTypes({ANNOTATION_ROUTER_NAME}) //指定要處理的注解類型
public class RouterProcessor extends AbstractProcessor{
private Map<String,Set<RouteMeta>> groupMap = new HashMap<>(); //收集分組
private Map<String,String> rootMap = new TreeMap<>();
private Filer mFiler;
private Logger logger;
private Types types;
private TypeUtils typeUtils;
private Elements elements;
private String moduleName = "app"; //默認app
private TypeMirror iProvider = null; //IProvider類型
//......
其中SupportedAnnotationTypes指定的就是我們上面定義的注解Route
接下來就是收集所有被注解的類,生成映射關系,代碼如下:
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
if(CollectionUtils.isNotEmpty(set)) {
//獲取到所有被注解的類
Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(Route.class);
try {
logger.info(">>> Found routers,start... <<<");
parseRoutes(elementsAnnotatedWith);
} catch (IOException e) {
logger.error(e);
}
return true;
}
return false;
}
獲取完之后交給了parseRoutes方法:
private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
if(CollectionUtils.isNotEmpty(routeElements)) {
logger.info(">>> Found routes, size is " + routeElements.size() + " <<<");
rootMap.clear();
//.......
TypeMirror type_activity = elements.getTypeElement(ACTIVITY).asType();
for (Element element : routeElements) {
TypeMirror tm = element.asType();
Route route = element.getAnnotation(Route.class);
RouteMeta routeMeta;
if(types.isSubtype(tm,type_activity)) { //activity
logger.info(">>> Found activity route: "+ tm.toString() + " <<<");
routeMeta = new RouteMeta(route,element,RouteType.ACTIVITY,null);
} 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_fragment) || types.isSubtype(tm,type_v4_fragment)) { //Fragment
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);
}
//.......
這個方法比較長,我們先看看最主要的處理,遍歷routeElements,判斷當前被注解的類的類型,分別是activity,IProvider,Fragment這三中,也就是說注解Route可以用來注解activity ,IProvider,和Fragment(注意這里fragment包括原生包中的和v4包中的fragment)然后根據類型構造出routeMate對象,構造完之后傳給了categories方法:
private void categories(RouteMeta routeMete) {
if (routeVerify(routeMete)) {
logger.info(">>> Start categories, group = " + routeMete.getGroup() + ", path = " + routeMete.getPath() + " <<<");
//groupMap是一個全局變量,用來按分組存儲routeMeta
Set<RouteMeta> routeMetas = groupMap.get(routeMete.getGroup());
if (CollectionUtils.isEmpty(routeMetas)) {
Set<RouteMeta> routeMetaSet = new TreeSet<>(new Comparator<RouteMeta>() {
@Override
public int compare(RouteMeta r1, RouteMeta r2) {
try {
return r1.getPath().compareTo(r2.getPath());
} catch (NullPointerException npe) {
logger.error(npe.getMessage());
return 0;
}
}
});
routeMetaSet.add(routeMete);
groupMap.put(routeMete.getGroup(), routeMetaSet);
} else {
routeMetas.add(routeMete);
}
} else {
logger.warning(">>> Route meta verify error, group is " + routeMete.getGroup() + " <<<");
}
}
我們看到這個方法中首先根據當前url分組去groupMap中查找,也就是看是否有該分組,如果有取出對應的RouterMeta集合,把本次生成的routeMeta放進去;沒有就新存一個集合。
到這里我們已經把所有的注解類都獲取到并且已經按分組分類。接下來就是生成java類來存放這些信息:
這里暫且只看對activity映射關系處理的代碼:
// (1)
for (Map.Entry<String, Set<RouteMeta>> entry : groupMap.entrySet()) {
String groupName = entry.getKey();
// (2)
MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(groupParamSpec);
Set<RouteMeta> groupData = entry.getValue();
for (RouteMeta meta : groupData) {
ClassName className = ClassName.get((TypeElement) meta.getRawType());
//...... (3)
loadIntoMethodOfGroupBuilder.addStatement(
"atlas.put($S," +
"$T.build($T." + meta.getType() + ",$T.class,$S,$S," + (StringUtils.isEmpty(mapBody) ? null : ("new java.util.HashMap<String, Integer>(){{" + mapBodyBuilder.toString() + "}}")) + ", " + meta.getPriority() + "," + meta.getExtra() + "))",
meta.getPath(),
routeMetaCn,
routeTypeCn,
className,
meta.getPath().toLowerCase(),
meta.getGroup().toLowerCase());
}
//Generate groups (4)
String groupFileName = NAME_OF_GROUP + groupName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(groupFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(type_IRouteGroup))
.addModifiers(Modifier.PUBLIC)
.addMethod(loadIntoMethodOfGroupBuilder.build())
.build()
).build().writeTo(mFiler);
logger.info(">>> Generated group: " + groupName + "<<<");
rootMap.put(groupName, groupFileName);
}
// (5)
if(MapUtils.isNotEmpty(rootMap)) {
for (Map.Entry<String, String> entry : rootMap.entrySet()) {
loadIntoMethodOfRootBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(PACKAGE_OF_GENERATE_FILE, entry.getValue()));
}
}
// ......
// Write root meta into disk. (6)
String rootFileName = NAME_OF_ROOT + moduleName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(rootFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(elements.getTypeElement(IROUTE_ROOT)))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfRootBuilder.build())
.build()
).build().writeTo(mFiler);
logger.info(">>> Generated root, name is " + rootFileName + " <<<");
}
現將上述這段代碼解釋如下:
- 遍歷我們之前存儲的groupMap,取出對應的集合,如注釋(1)
- 生成一個方法體,并且把集合中的所有映射關系都put到參數map中。如 (2)(3)
- 生成java類,類名為RouterManager
+ moduleName,這里的moduleName就是在build.gradle文件中配置的,如不配置,活獲取為null 如(4)
- 把每個分組和所生成的類做個映射關系,作用就是為了實現按分組加載功能 如 (5)(6)
下面我們看下一生成的產物
/**
DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY ROUTERMANAGER. */
public class RouterManager$$Root$$app implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("service", RouterManager$$Group$$service.class);
}
}
存儲分組對應關系
/**
* DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY ROUTERMANAGER. */
public class RouterManager$$Group$$service implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/service/test/main",RouteMeta.build(RouteType.ACTIVITY,OtherActivity.class,"/service/test/main","service",null, -1,-2147483648));
}
}
就這樣映射關系自動生成好了,那么該如何使用呢?下面就讓我隆重介紹一下我們Api
0x03
由于我們的映射關系表是全局存在的,所以肯定需要在Application中做初始化操作,其目的就是把映射關系load到內存,下面讓我們看看具體實現代碼
首先我們得需要一個容器來存儲我們的映射關系,因此就有了Warehouse類
class Warehouse {
// Cache route and metas
static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
static Map<String, RouteMeta> routes = new HashMap<>();
//......
static void clear() {
providers.clear();
providersIndex.clear();
}
}
我們在此類中實例化兩個map用來存儲我們的分組信息和每個分組中的對應關系信息
groupIndex:用來存放分組信息,這個會優先load數據
routes:用來存儲對應關系數據
接下來我們在App初始化時會調用如下代碼來初始化:
RouterManager.init(this);
那么我們進去init方法中看看具體干了什么?
public static synchronized void init(Application application){
if(!hasInit) {
hasInit = true;
mContext = application;
mHandler = new Handler(Looper.getMainLooper());
logger = new DefaultLogger();
LogisticsCenter.init(mContext,logger);
}
}
可以看到這里最關鍵的一行代碼是 LogisticsCenter.init(mContext,logger)
那就讓我們繼續去LogisticsCenter.init(mContext,logger);方法中看看:
public synchronized static void init(Context context, ILogger log) {
logger = log;
Set<String> routeMap;
try {
if(RouterManager.debuggable() || PackageUtils.isNewVersion(context)) { //開發模式或版本升級時掃描本地件
logger.info(TAG,"當前環境為debug模式或者新版本,需要重新生成映射關系表");
//these class was generated by router-compiler
routeMap = ClassUtils.getFileNameByPackageName(context, Consts.ROUTE_ROOT_PAKCAGE);
if(!routeMap.isEmpty()) {
PackageUtils.put(context,Consts.ROUTER_SP_KEY_MAP,routeMap);
}
PackageUtils.updateVersion(context);
} else{ //讀取緩存
logger.info(TAG,"讀取緩存中的router映射表");
routeMap = PackageUtils.get(context,Consts.ROUTER_SP_KEY_MAP);
}
logger.info(TAG,"router map 掃描完成");
//將分組數據加載到內存
for (String className : routeMap) {
//Root
if(className.startsWith(Consts.ROUTE_ROOT_PAKCAGE + Consts.DOT + Consts.SDK_NAME + Consts.SEPARATOR + Consts.SUFFIX_ROOT)) {
((IRouteRoot)(Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
}
//......
}
logger.info(TAG,"將映射關系讀到緩存中");
if(Warehouse.groupsIndex.size() == 0) {
logger.error(TAG,"No mapping files,check your configuration please!");
}
if (RouterManager.debuggable()) {
logger.debug(TAG, String.format(Locale.getDefault(), "LogisticsCenter has already been loaded, GroupIndex[%d], ProviderIndex[%d]", Warehouse.groupsIndex.size(), Warehouse.providersIndex.size()));
}
} catch (Exception e) {
e.printStackTrace();
logger.error(TAG,"RouterManager init logistics center exception! [" + e.getMessage() + "]");
}
}
具體解釋如下:
1)、首先是根據包名去掃描所有生成的類文件,并放在routeMap中。當然這里會根據版本判斷然后緩存到本地,目的是為了避免重復掃描
2)、遍歷掃描到的數組,將所有分組信息緩存到Warehouse.groupIndex中
可以看到初始化時只干了這兩件事,掃描class文件,讀取分組信息;仔細想想你會發現這里并沒有去讀取我們的url和activity映射關系信息,這就是所謂的按需加載。
到這里我們所有的準備工作都已完成了,那么該怎么使用呢?
下面讓我們看看具體的用法
0x04
我們先來看一段代碼:
RouterManager.getInstance().build("/user/main").navigation(MainActivity.this);
上述代碼是我們打開UserActivty頁面所使用的方式,可以發現這里只傳了一個url。那就讓我們看看內部是如何實現的?
首先我們去build方法中看看具體的代碼:
public Postcard build(String path) {
if(TextUtils.isEmpty(path)) {
throw new HandlerException("Parameter is invalid!");
} else {
return build(path,extractGroup(path));
}
}
public Postcard build(String path,String group) {
if(TextUtils.isEmpty(path)) {
throw new HandlerException("Parameter is invalid!");
} else {
return new Postcard(path,group);
}
}
發現這里是一個重載方法,最后返回的是一個Postcard對象,然后調用Postcard的navigation方法??梢钥吹竭@里Postcard其實只是一個攜帶數據的實體。下面看看navigation方法:
public Object navigation(Context context) {
return RouterManager.getInstance().navigation(context,this,-1);
}
可以發現這里只是做了一個中轉,最終調用的是RouterManager的navigation方法:
Object navigation(final Context context,final Postcard postcard,final int requestCode) {
try {
LogisticsCenter.completion(postcard);
} catch (HandlerException e) {
e.printStackTrace();
return null;
}
final Context currentContext = context == null ? mContext : context;
switch (postcard.getType()) {
case ACTIVITY:
final Intent intent = new Intent(currentContext,postcard.getDestination());
intent.putExtras(postcard.getExtras());
int flags = postcard.getFlags();
if(flags != -1) {
intent.setFlags(flags);
} else if(!(currentContext instanceof Activity)) { //如果當前上下文不是activity,則啟動activity時需要new一個新的棧
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
runInMainThread(new Runnable() {
@Override
public void run() {
startActivity(requestCode,currentContext,intent,postcard);
}
});
break;
//......
}
return null;
}
由上述代碼可以看出首先調用的是LogisticsCenter.completion()方法把postcard對象傳進去,那讓我們先去這個方法中看個究竟:
/**
* 填充數據
* @param postcard
*/
public synchronized static void completion(Postcard postcard) {
RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
if(routeMeta != null) {
postcard.setDestination(routeMeta.getDestination());
postcard.setType(routeMeta.getType());
postcard.setPriority(routeMeta.getPriority());
postcard.setExtra(routeMeta.getExtra());
//......
} else {
Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());
if(groupMeta == null) {
throw new NoRouteFoundException("There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
} else {
try {
//按組加載數據,美其名曰-按需加載
IRouteGroup iRouteGroup = groupMeta.getConstructor().newInstance();
iRouteGroup.loadInto(Warehouse.routes);
Warehouse.groupsIndex.remove(postcard.getGroup());
} catch (Exception e) {
throw new HandlerException("Fatal exception when loading group meta. [" + e.getMessage() + "]");
}
}
completion(postcard); //分組加載完成后重新查找
}
}
這里首先去根據url去Warehouse.routes中查找對應的RouteMeta信息,如何是首次調用的話這里一定是沒有的,所以會執行else方法,else方法里先根據分組獲取對應的分組class,然后反射其實例對象并調用loadInfo()方法,把該分組中的所有映射關系讀取到Warehouse.routes中,然后繼續調用當前方法填充相關的信息。
信息填充完成之后繼續回到navigation方法中:
switch (postcard.getType()) {
case ACTIVITY:
final Intent intent = new Intent(currentContext,postcard.getDestination());
intent.putExtras(postcard.getExtras());
int flags = postcard.getFlags();
if(flags != -1) {
intent.setFlags(flags);
} else if(!(currentContext instanceof Activity)) { //如果當前上下文不是activity,則啟動activity時需要new一個新的棧
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
runInMainThread(new Runnable() {
@Override
public void run() {
startActivity(requestCode,currentContext,intent,postcard);
}
});
break;
//......
}
可以看到這里使用的是常規的啟動方式startActivity去啟動一個新activity。
Ok到此為止整個流程算是走完了,至于傳遞參數,獲取fragment,以及服務IProvider什么的套路都一樣,這里不再重復贅述。
總結
ARouter的思路很好簡單,就是通過編譯時注解生成url與頁面的映射關系表,然后在程序啟動時將該映射關系表load到內存中,使用時直接去內存中查找然后執行常規的頁面啟動方式。
下面我們來回答前面提出的兩個問題
第一:為什么要在每個build.gradle文件中配置一個moduleName呢?
這是因為編譯時注解是以module為單位去生成代碼的,也就是說我們需要給每個module項目都配置該注解生成器的依賴,為了保證生成java文件的名字不會重復需要加上module為后綴。此配置和分組沒有任何關系。只是為了避免生成的分組類重復。
第二:為什么會報多個類重名的問題?
我們知道Router的映射表有兩張表,第一張是用來存儲分組和分組對應的class的,第二張是用來存儲每個分組中具體url映射關系的。而在第一個問題中我們根據moduleName來避免存放分組的class重名的問題。那么每個分組class本身有沒有重名的可能呢?答案是一定有的。比如:我們在user組件中配置的url:/user/main分組為user,這個時候在編譯user組件時就會自動生成一個類名為 RouterManager$$Group$$user
的類,用來存放所有的以user為分組的頁面映射關系。那么當我們在app的中也配置分組名為user的分組后,編譯app時就會在app中生成類名為RouterManager$$Group$$user
的類。而我們app項目是依賴的user組件的,這就導致有兩個類名一樣的文件。編譯時自然就會報錯。
對RouterManager的幾點思考
- RouterManager能否用于夸進程調用:
我認為是可以的,RouterManager的關系映射表是存在一個全局靜態變量中的,當我們需要在其他進程訪問時只需要提供一個接口來得到映射關系即可。
- RouterManager能否在RePlugin中的使用:
答案也是可以的,由于RePlugin采用的是多個classloader機制,這就導致我們在主項目的classloader獲取的對象和在插件classloader中獲取的是兩個獨立的對象,如果想在插件中使用RouterManager去打開一個宿主的頁面,直接調用的話肯定是沒有對應的映射關系的,因為在插件里獲取的RouterManager對象并不是宿主的單例對象,而是創建了一個新的對象。那怎么辦呢?答案很簡單,我們在插件中使用反射獲取到宿主的RouterManager實例即可正常使用。
注:RouterManager框架的思路來源與ARouter,這里只實現了頁面跳轉,fragment獲取和服務Provider的獲取功能。至于其他的降級策略,依賴注入功能就不在一一實現了
項目源碼請移駕到本人的github倉庫查看:https://github.com/qiangzier/RouterManager