介紹
在組件化開發的時候,組件之間是相互獨立的沒有依賴關系,我們不能在使用顯示調用來跳轉頁面了,因為我們組件化的目的之一就是解決模塊間的強依賴問題,假如現在要從A業務組件跳轉到業務B組件,并且要攜帶參數跳轉,這時候怎么辦呢?
上學的時候在書上看到了一句很有意義的話:任何軟件工程遇到的問題都可以通過增加一個中間層來解決!
我們從這句話出發去思考:組件之間是平行結構的,它們之間相互沒有交集,要實現通信只有添加一個中間層將它們連接到一起,這就是“路由”的概念,由第一篇文章開始的組件化模型下的業務關系圖可知路由就是起到一個轉發的作用。路由就像一個橋梁一樣讓平行的河流(組件)之間可以通信和傳遞數據,這樣看起來好像他們之間又是強耦合的關系,維護起來還是代價還有點大,那么還有沒有好點的辦法呢?那就是路由層使用接口和其他module之間建立弱耦合的關系。
在組件化開發中實現跨組件跳轉的庫比較有名氣的是阿里的ARouter,ARouter的功能很豐富,這一節我們根據ARouter的源碼理解自己實現一個簡單版的ARouter,ARouter的功能太多了如過濾器,攔截器等,這里只是手寫實現一部分核心功能-路由跳轉。
準備
實現
定義注解
在router_annotation的module中定義注解Route.java
/**
* Target用于指定被此元注解標注的注解可以標出的程序元素
*
*/
@Target(ElementType.TYPE)
/**
* RetentionPolicy.SOURCE 源碼階段 注解信息只會保留在源碼中,編譯器在編譯源碼的時候會將其直接丟棄
* RetentionPolicy.CLASS 編譯階段 javapoet使用 注解信息保留在class文件中,VM不會持有其信息
* RetentionPolicy.RUNTIME 運行階段,注解信息保留在class文件中,而且VM也會持有此注解信息, 反射獲得注解信息
*/
@Retention(RetentionPolicy.CLASS)
public @interface Route {
/**
* 路由的路徑,標識一個路由節點
*/
String path();
/**
* 將路由節點進行分組,可以實現按組動態加載
*/
String group() default "";
}
Route注解里有path和group,這是仿照ARouter對路由進行分組。因為當項目變得越來越龐大的時候,為了便于管理和減小首次加載路由表過于耗時的問題,我們對所有的路由進行分組。
定義注解處理器生成路由映射文件
定義RouteProcessor實現AbstractProcessor類, 這里使用google的 AutoService注冊處理器,使用JavaPoet實現源文件的編寫。在router_compiler的build.gradle引入這兩個庫
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
implementation 'com.squareup:javapoet:1.11.1'
implementation project(':router_annotation')
}
// java控制臺輸出中文亂碼
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
sourceCompatibility = "7"
targetCompatibility = "7"
Processor 一般會重寫父類的4個方法:
init:初始化工作,我們可以得到一些有用的工具,例如 Filer,ElementUtils,TypeUtils.
process:最重要的方法,所有的注解處理都是在此完成
getSupportedAnnotationTypes:返回我們所要處理的注解的一個集合
getSupportedSourceVersion:要支持的java版本
@AutoService(Processor.class)
//處理器接受到的參數 代替 {@link AbstractProcessor#getSupportedOptions()} 函數
@SupportedOptions(Constants.ARGUMENTS_NAME)
/**
* 指定使用的Java版本 替代 {@link AbstractProcessor#getSupportedSourceVersion()} 函數
* 聲明我們注解支持的JDK的版本
*/
@SupportedSourceVersion(SourceVersion.RELEASE_7)
/**
* 注冊給哪些注解的 替代 {@link AbstractProcessor#getSupportedAnnotationTypes()} 函數
* 聲明我們要處理哪一些注解 該方法返回字符串的集合表示該處理器用于處理哪些注解
*/
@SupportedAnnotationTypes(Constants.ANN_TYPE_ROUTE)
public class RouteProcessor extends AbstractProcessor {
....
/**
* key是組名,value是注解標記的element的元素數據集合
* 使用Route的注解按照組名分表存儲
*/
private Map> groupMap = new HashMap<>();
/**
* key:組名 value:實現IRouteGroup接口的className
*/
private Map rootMap = new TreeMap<>();
...
}
init方法中根據ProcessingEnvironment得到一些工具和參數
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
Messager ms = processingEnvironment.getMessager();
Log.init(ms);
elementUtils = processingEnvironment.getElementUtils();
filerUtils = processingEnvironment.getFiler();
typeUtils = processingEnvironment.getTypeUtils();
Map options = processingEnvironment.getOptions();
if (!Utils.isEmpty(options)) {
moduleName = options.get(Constants.ARGUMENTS_NAME);
}
if (Utils.isBlank(moduleName)) {
throw new RuntimeException("Not set Processor Parmaters.");
}
}
掃描所有Route修飾的注解文件,根據組名進行分組
/**
* 處理注解
*
* @param set 使用了支持注解的節點集合
* @param roundEnvironment 上下文
* @return true 表示后續處理器不會再處理
*/
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
if (!Utils.isEmpty(set)) {
Set extends Element> elementsAnnotateds = roundEnvironment.getElementsAnnotatedWith(Route.class);
if (!Utils.isEmpty(elementsAnnotateds)) {
try {
parseRoute(elementsAnnotateds);
} catch (Exception e) {
Log.i("創建實現類異常---》"+e);
}
}
}
return true;
}
private void parseRoute(Set extends Element> routeElements) throws Exception {
// 通過elementUtils獲得節點
TypeElement activityTypeElement = elementUtils.getTypeElement(Constants.ACTIVITY);
//mirror 節點自描述
TypeMirror activityTypeMirror = activityTypeElement.asType();
TypeElement IServiceTypeElement = elementUtils.getTypeElement(Constants.ISERVICE);
TypeMirror IServiceTypeMirror = IServiceTypeElement.asType();
Log.i("activityTypeMirror=" + activityTypeMirror + ", IServiceTypeMirror=" + IServiceTypeMirror);
RouteMeta routeMeta;
for (Element routeElement : routeElements) {
Log.i("routeElement="+routeElement);
Route route = routeElement.getAnnotation(Route.class);
TypeMirror typeMirror = routeElement.asType();
if (typeUtils.isSubtype(typeMirror, activityTypeMirror)) {//activity節點
routeMeta = new RouteMeta(RouteMeta.Type.ACTIVITY, route, routeElement);
}/* else if (typeUtils.isSubtype(typeMirror, IServiceTypeMirror)) {//IService節點
routeMeta = new RouteMeta(RouteMeta.Type.ISERVICE, route, routeElement);
} **/else {
throw new RuntimeException("[Just Support Activity/IService Route] :" + routeElement);
}
//分組記錄信息, groupMap
categories(routeMeta);
}
//生成類 需要實現的接口
generatedGroup();
generatedRoot();
}
每次檢索到一個注解元素routeElement都要去groupMap 分類。
private void categories(RouteMeta routeMeta) throws ClassNotFoundException {
if (routeVerify(routeMeta)) {//1
Log.i("group name =" + routeMeta.getGroup() + ", path=" + routeMeta.getPath());
List routeMetas = groupMap.get(routeMeta.getGroup());
if (Utils.isEmpty(routeMetas)) {
routeMetas = new ArrayList<>();
groupMap.put(routeMeta.getGroup(), routeMetas);
}
//2
/* Class> destination = routeMeta.getDestination();
if (destination == null) {
String className = routeMeta.getElement().asType().toString();
Log.i("name="+className);
destination = Class.forName(className);
routeMeta.setDestination(destination);
}*/
routeMetas.add(routeMeta);
} else Log.e("group info error:" + routeMeta.getPath());
}
routeMeta合法后開始根據groupName進行分組歸類。
上邊的注釋2處,我的本意是通過反射給routeMeta.setDestination設置值,在后邊生成文件的時候直接通過routeMeta.getsetDestination使用。但是這樣做的時候會報錯 ClassNotFoundException。一時半會也不知道原因,晚上在看書的時候突然想起來,這是在編譯期完成的,反射是在運行期使用,所以會報ClassNotFoundException。
注釋1處的是驗證routeMeta是否包含group,包含group條件是,注解的時候聲明group,或者path的路徑必須是// 大于等于兩個“/”
private boolean routeVerify(RouteMeta meta) {
String path = meta.getPath();
String group = meta.getGroup();
//路由地址必須是/ 開頭
if (Utils.isBlank(path) || !path.startsWith("/")) {
return false;
}
String[] split = path.split("/");
if (split.length < 3) {
return false;
}
//如果沒有分組就以第一個/后的節點為分組
if (Utils.isBlank(group)) {
String defaultGroup = split[1];
if (Utils.isBlank(defaultGroup)) {
return false;
}
meta.setGroup(defaultGroup);
}
return true;
}
對RouteMeta進行分組整理后,在parseRoute方法中根據組名生成IRouteGroup的實現類,實現類中接受map參數,將掃描的路由文件存儲到map中。IRouteGroup接口的定義如下:
public interface IRouteGroup {
/**
* 掃描注解 搜集路由信息
* @param atlas key:路由路徑 path,value:Route注解修飾的路由信息數據
*/
void loadInto(Map atlas);
}
該文件是在route_core的module中聲明的。
下邊是通過Apt實現該接口的方法。
/**
* 生成IRouteGroup的實現類
* @throws Exception
*/
private void generatedGroup() throws Exception {
//參數類型 Map
ParameterizedTypeName atlas = ParameterizedTypeName.get(
ClassName.get(Map.class),
ClassName.get(String.class),
ClassName.get(RouteMeta.class)
);
//創建參數 Map atlas
ParameterSpec groupParamSpec = ParameterSpec.builder(atlas, "atlas").build();
//遍歷分組,每一個分組創建一個類
for (Map.Entry> entry : groupMap.entrySet()) {
//聲明方法
// @Override
// public void loadTo(Map atlas)
MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(Constants.METHOD_LOAD_INTO)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(void.class)
.addParameter(groupParamSpec);
//方法體 實現接口的抽象方法,將分組后的路由信息插入atlas中
//key 是path,value---RouteMeta
String groupName = entry.getKey();
List routeMetas = entry.getValue();
for (RouteMeta routeMeta : routeMetas) {
//atlas.put(path, RouteMeta.build(class, path, group)
// RouteMeta build(Type type, Class> destination, String path, String group)
loadIntoMethodOfGroupBuilder.addStatement("atlas.put($S, $T.build($T.$L,$T.class, $S, $S))",
routeMeta.getPath(),
ClassName.get(RouteMeta.class),
ClassName.get(RouteMeta.Type.class),
routeMeta.getType(),
ClassName.get((TypeElement) routeMeta.getElement()),
routeMeta.getPath().toLowerCase(),
groupName.toLowerCase()
);
}
TypeElement iRouteGroupTypeElement = elementUtils.getTypeElement(Constants.ROUTE_GROUP);
//創建Java文件
String groupClassName = Constants.NAME_OF_GROUP + groupName;
Log.i("groupClassName="+groupClassName);
JavaFile.builder(Constants.PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(groupClassName)
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ClassName.get(iRouteGroupTypeElement))
.addMethod(loadIntoMethodOfGroupBuilder.build()).build()
).build().writeTo(filerUtils);
Log.i("Generated RouteGroup: " + Constants.PACKAGE_OF_GENERATE_FILE + "." +
groupClassName);
rootMap.put(groupName, groupClassName);
}
}
在編寫的時候這里需要格外小心,出錯了很難排查,因為這是編譯期不能進行debug,所以只能借助Log在關鍵的地方加上日志。另外在編寫的時候,可以自己先在module2中創建一個實現該接口的類Router$$Group$$test,然后對比著進行編碼,最后記得把Router$$Group$$test注釋掉,
public class Router$$Group$$test implements IRouteGroup {
private Map> groupMap;
@Override
public void loadInto(Map atlas) {
for (String groupName : groupMap.keySet()) {
List routeMetas = groupMap.get(groupName);
for (RouteMeta routeMeta : routeMetas) {
//RouteMeta build(Type type, Class> destination, String path, String group)
Class> destination = routeMeta.getDestination();
if (destination == null) {
destination = ClassName.get((Type) routeMeta.getElement());
}//
atlas.put(routeMeta.getPath(),
RouteMeta.build(routeMeta.getType(),
routeMeta.getDestination(),
routeMeta.getPath(),
routeMeta.getGroup()));
}
}
}
}
回到parseRoute中下一步是generatedRoot()
private void generatedRoot() throws Exception {
TypeElement iRouteRootTypeElement = elementUtils.getTypeElement(Constants.ROUTE_ROOT);
TypeElement iRouteGroupTypeElement = elementUtils.getTypeElement(Constants.ROUTE_GROUP);
//Map> routes
ParameterizedTypeName routes = ParameterizedTypeName.get(
ClassName.get(Map.class),
ClassName.get(String.class),
ParameterizedTypeName.get(
ClassName.get(Class.class),
WildcardTypeName.subtypeOf(ClassName.get(iRouteGroupTypeElement))
)
);
//參數 Map routes
ParameterSpec rootParameterSpec = ParameterSpec.builder(routes, "routes").build();
//函數 public void loadInto(Map> routes> routes)
MethodSpec.Builder loadIntoMethodBuilder = MethodSpec.methodBuilder(Constants.METHOD_LOAD_INTO)
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(rootParameterSpec);
//函數體
for (String key : rootMap.keySet()) {
loadIntoMethodBuilder.addStatement("routes.put($S, $T.class)",
key,
ClassName.get(Constants.PACKAGE_OF_GENERATE_FILE, rootMap.get(key)));
}
String rootClassName = Constants.NAME_OF_ROOT + moduleName;
JavaFile.builder(Constants.PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(rootClassName)
.addSuperinterface(ClassName.get(iRouteRootTypeElement))
.addModifiers(Modifier.PUBLIC)
.addMethod(loadIntoMethodBuilder.build()).build())
.build().writeTo(filerUtils);
Log.i("Generated RouteRoot: " + Constants.PACKAGE_OF_GENERATE_FILE + "." + rootClassName);
}
上邊的moduleName是在init方法中初始化的
Map options = processingEnvironment.getOptions();
if (!Utils.isEmpty(options)) {
//Constants.ARGUMENTS_NAME = "moduleName"
moduleName = options.get(Constants.ARGUMENTS_NAME);
}
processingEnvironment能夠接收的參數需要在getSupportedOptions方法中定義,也可以用注解的方式定義,這里在定義RouteProcessor的時候使用@SupportedOptions(Constants.ARGUMENTS_NAME)
我們需要在依賴route_core庫的 組件的build.gradle的添加這個參數:
javaCompileOptions{
annotationProcessorOptions {
arguments = [moduleName : project.getName()]
}
}
generatedRoot方法根據rootMap來創建一個IRouteRoot接口的實現類,這個接口主要是記錄每個組名對應Group類。同樣在編碼前也是先寫一個實現類Router$$Root$$impl,然后對比著進行編碼。
public class Router$$Root$$impl implements IRouteRoot {
private Map> map;
@Override
public void loadInto(Map> routes) {
for (String groupName : map.keySet()) {
routes.put(groupName, map.get(groupName));
}
}
}
撰寫完成后記得把這個類注釋掉。注解和注解處理器我們都寫完了,我們寫一個demo看看效果,這里舉一個setting的demo,目錄結構如下:
setting_module.png
編譯后生成的文件如下
setting_build.png
public class Router$$Group$$business implements IRouteGroup {
@Override
public void loadInto(Map atlas) {
atlas.put("/business/news", RouteMeta.build(RouteMeta.Type.ACTIVITY,BusinessNewsActivity.class, "/business/news", "business"));
atlas.put("/business/other", RouteMeta.build(RouteMeta.Type.ACTIVITY,BusinessOtherActivity.class, "/business/other", "business"));
}
}
public class Router$$Group$$personal implements IRouteGroup {
@Override
public void loadInto(Map atlas) {
atlas.put("/personal/wallet", RouteMeta.build(RouteMeta.Type.ACTIVITY,MyWalletActivity.class, "/personal/wallet", "personal"));
atlas.put("/personal/userInfo", RouteMeta.build(RouteMeta.Type.ACTIVITY,UserInfoActivity.class, "/personal/userinfo", "personal"));
}
}
public class Router$$Root$$setting implements IRouteRoot {
@Override
public void loadInto(Map> routes) {
routes.put("business", Router$$Group$$business.class);
routes.put("personal", Router$$Group$$personal.class);
}
}
這里看到生成的類分別實現了IRouteRoot和IRouteGroup接口,并且實現了loadInto()方法,而loadInto方法通過傳入一個特定類型的map就能把分組信息放入map里,只要分組信息存入到特定的map里后,我們就可以隨意的從map里取路由地址對應的Activity.class做跳轉使用。
路由框架的初始化
我們要實現一個路由框架,就要考慮在合適的時機拿到這些映射文件中的信息,以供上層業務做跳轉使用。那么在什么時機去拿到這些映射文件中的信息呢?首先我們需要在上層業務做路由跳轉之前把這些路由映射關系拿到手,但我們不能事先預知上層業務會在什么時候做跳轉,那么拿到這些路由關系最好的時機就是應用程序初始化的時候。另外如何去拿這些路由信息呢?在上面已經介紹過IRouteRoot接口的所有實現文件里保存著各個module的分組文件(分組文件就是實現了IRouteGroup接口的類文件),那么只要拿到所有實現IRouteGroup接口的類的集合,就可以根據path實現頁面跳轉了。
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Router.init(this);
}
}
route_core module中的Router.java
public static void init(Application application){
context = application;
try {
loadInfo();
} catch (Exception e) {
Log.e(TAG, "初始化失敗!", e);
e.printStackTrace();
}
}
private static void loadInfo() throws PackageManager.NameNotFoundException, InterruptedException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//1.掃描apk中的所有dex文件找出使用注解生成的類
Set routeMap = ClassUtils.getFileNameByPackageName(context, Router.ROUTE_ROOT_PACKAGE);
if (routeMap == null) {
return;
}
for (String className : routeMap) {
if (className.startsWith(ROUTE_ROOT_PACKAGE+"."+SDK_NAME+SEPARATOR+SUFFIX_ROOT)) {
// root中注冊的分組信息,將分組信息加入倉庫中
((IRouteRoot)(Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
}
}
}
我們首先通過ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE)得到apt生成的所有實現IRouteRoot接口的類文件集合,通過上面的講解我們知道,拿到這些類文件便可以得到所有的路由地址和Activity映射關系。
路由跳轉實現
經過前面的介紹,我們已經能夠在app啟動的時候獲得所有的路由信息,接下來就可以實現跳轉了。
在app module的Mainctiviy中如下使用:
public void personalInfoJump(View view) {
Router.getInstance().build("/personal/userInfo").navigation();
}
public void businessNewsJump(View view) {
Router.getInstance().build("/business/news").navigation();
}
build的時候根據path路徑得到一個postCard對象,然后調用Postcard的navigation()方法完成跳轉。Postcard的內容如下:
public class Postcard extends RouteMeta {
private Bundle extras;
private int flag = -1;
private Bundle optionCompat;
private int enterAnim, exitAnim;
...
public Object navigation() {
return navigation(null, null);
}
public Object navigation(Context context) {
return navigation(context, null);
}
public Object navigation(Context context, NavigationCallback callback) {
return Router.getInstance().navigation(context, this, -1, callback);
}
...
}
下面看一下Router 的navigation核心功能:
public Object navigation(Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback){
prepareCard(postcard);
if (callback != null) {
callback.onFound(postcard);
}
switch (postcard.getType()) {
case ACTIVITY:
final Context currentContext = context==null?Router.context:context;
final Intent intent = new Intent(currentContext, postcard.getDestination());
intent.putExtras(postcard.getExtras());
if (postcard.getFlag()!=-1) {
intent.setFlags(postcard.getFlag());
}else if (!(currentContext instanceof Activity)){
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
if (requestCode>0) {
ActivityCompat.startActivityForResult((Activity) currentContext, intent,
requestCode, postcard.getOptionCompat());
}else {
ActivityCompat.startActivity(currentContext, intent, postcard.getOptionCompat());
}
if ((postcard.getEnterAnim()!=0||postcard.getExitAnim()!=0) && currentContext instanceof Activity) {
((Activity)currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
}
if (callback != null) {
callback.onArrival(postcard);
}
}
});
break;
}
return null;
}
prepareCard的實現如下:
private void prepareCard(Postcard card){
RouteMeta routeMeta = Warehouse.routes.get(card.getPath());
if (routeMeta == null) {
Class extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(card.getGroup());
if (groupMeta == null) {
throw new NoRouteFoundException("沒找到對應路由: " + card.getGroup() + " " +
card.getPath());
}
IRouteGroup iRouteGroup;
try {
iRouteGroup = groupMeta.getConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("路由分組映射表記錄失敗.", e);
}
iRouteGroup.loadInto(Warehouse.routes);
Warehouse.groupsIndex.remove(card.getGroup());
prepareCard(card);
}else {
card.setDestination(routeMeta.getDestination());
card.setType(routeMeta.getType());
}
}
這段代碼Warehouse.routes.get(card.getPath())通過path拿到對應的RouteMeta,這個RouteMeta里面保存了activityClass等信息。繼續往下看,如果判斷拿到的RouteMeta是空,說明這個路由地址還沒有加載到map里面(初始化時為了節省性能,只會加載所有的分組信息,而每個分組下的路由映射關系,會使用懶加載,在首次用到的時候去加載),只有在第一次用到當前路由地址的時候,會去Warehouse.routes里面拿routeMeta,如果拿到的是空,會根據當前路由地址的group拿到對應的分組,通過反射創建實例,然后調用實例的loadInto方法,把它里面保存的映射信息添加到Warehouse.routes里面,并且再次調用prepareCard(card),這時再通過Warehouse.routes.get(card.getPath())就可以順利拿到RouteMeta了。進入else{}里面,調用了card.setDestination(routeMeta.getDestination()),這個setDestination就是將RouteMeta里面保存的activityClass放入Postcard里面。
prepareCard()方法調用完成后,我們的postcard里面就保存了activityClass,然后switch (postcard.getType()){}會判斷postcard的type為ACTIVITY,然后通過ActivityCompat.startActivity啟動Activity。到這里,路由跳轉的實現已經講解完畢了。
結束語
關于組件之間傳遞數據,可以參考ARoute添加Extra 注解,ExtraProcessor處理器以及IExtra接口。
通過手寫ARoute我們會學到注解,注解處理器,JavaPoet和組件化思路,編寫框架的思路等等。