最近可能入了魔怔,也多是閒的蛋疼,本身私下學習了ARouter的原理以及一些APT的知識,爲了加深對技術的理解,同時也本着熱愛開源的精神爲你們提供分享,因此就帶着你們強行擼碼,分析下ARouter路由原理和Android中APT的使用吧
本篇文章我會帶着你們一步步手動實現路由框架來理解相似ARouter的路由框架原理,擼碼的demo我會附在文末。本路由框架就叫EaseRouter。(注:demo裏搭建了組件化開發,組件化和路由自己並無什麼聯繫,可是兩個單向依賴的組件之間須要互相啓動對方的Activity,由於沒有相互引用,startActivity()是實現不了的,必須須要一個協定的通訊的方式,此時相似ARouter和ActivityRouter的框架就派上用場了)。java
本文的重點是對路由框架的實現進行介紹,因此對於組件化的基本知識在文中不會過多闡述,若有同窗對組件化有不理解,能夠參考網上衆多的博客等介紹,而後再閱讀demo中的組件化配置進行熟悉,這裏附上github demo地址:ARouter原理剖析及手動實現,點我訪問源碼,歡迎star。
git
如上圖,在組件化中,爲了業務邏輯的完全解耦,同時也爲了每一個module均可以方便的單獨運行和調試,上層的各個module不會進行相互依賴(只有在正式聯調的時候纔會讓app殼module去依賴上層的其餘組件module),而是共同依賴於base module,base module中會依賴一些公共的第三方庫和其餘配置。那麼在上層的各個module中,如何進行通訊呢?
咱們知道,傳統的Activity之間通訊,經過startActivity(intent),而在組件化的項目中,上層的module沒有依賴關係(即使兩個module有依賴關係,也只能是單向的依賴),那麼假如login module中的一個Activity須要啓動pay_module中的一個Activity便不能經過startActivity來進行跳轉。那麼你們想一下還有什麼其餘辦法呢? 可能有同窗會想到隱式跳轉,這固然也是一種解決方法,可是一個項目中不可能全部的跳轉都是隱式的,這樣Manifest文件會有不少過濾配置,並且很是不利於後期維護。固然你用反射也能夠實現跳轉,可是第一:大量的使用反射跳轉對性能會有影響,第二:你須要拿到Activity的類文件,在組件開發的時候,想拿到其餘module的類文件是很麻煩的(由於組件開發的時候組件module之間是沒有相互引用的,你只能經過找到類的路徑去拿到這個class,顯然很是麻煩),那麼有沒有一種更好的解決辦法呢?辦法固然是有的。下面看圖:
在組件化中,咱們一般都會在base_module上層再依賴一個router_module,而這個router_module就是負責各個模塊之間服務暴露和頁面跳轉的。
用過ARouter路由框架的同窗應該都知道,在每一個須要對其餘module提供調用的Activity中,都會聲明相似下面@Route註解,咱們稱之爲路由地址github
@Route(path = "/main/main")
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
@Route(path = "/module1/module1main")
public class Module1MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_module1_main);
}
}
複製代碼
那麼這個註解有什麼用呢,路由框架會在項目的編譯器掃描全部添加@Route註解的Activity類,而後將route註解中的path地址和Activity.class文件一一對應保存,如直接保存在map中。爲了讓你們理解,我這裏來使用近乎僞代碼給你們簡單演示一下。api
//項目編譯後經過apt生成以下方法
public HashMap<String, ClassBean> routeInfo() {
HashMap<String, ClassBean> route = new HashMap<String, ClassBean>();
route.put("/main/main", MainActivity.class);
route.put("/module1/module1main", Module1MainActivity.class);
route.put("/login/login", LoginActivity.class);
}
複製代碼
這樣咱們想在app模塊的MainActivity跳轉到login模塊的LoginActivity,那麼便只需調用以下:bash
//不一樣模塊之間啓動Activity
public void login(String name, String password) {
HashMap<String, ClassBean> route = routeInfo();
LoginActivity.class classBean = route.get("/login/login");
Intent intent = new Intent(this, classBean);
intent.putExtra("name", name);
intent.putExtra("password", password);
startActivity(intent);
}
複製代碼
用過ARouter的同窗應該知道,用ARouter啓動Activity應該是下面這個寫法app
// 2. Jump with parameters
ARouter.getInstance().build("/test/login")
.withString("password", 666666)
.withString("name", "小三")
.navigation();
複製代碼
那麼ARouter背後的原理是怎麼樣的呢?實際上它的核心思想跟上面講解的是同樣的,咱們在代碼里加入的@Route註解,會在編譯時期經過apt生成一些存儲path和activityClass映射關係的類文件,而後app進程啓動的時候會拿到這些類文件,把保存這些映射關係的數據讀到內存裏(保存在map裏),而後在進行路由跳轉的時候,經過build()方法傳入要到達頁面的路由地址,ARouter會經過它本身存儲的路由表找到路由地址對應的Activity.class(activity.class = map.get(path)),而後new Intent(),當調用ARouter的withString()方法它的內部會調用intent.putExtra(String name, String value),調用navigation()方法,它的內部會調用startActivity(intent)進行跳轉,這樣即可以實現兩個相互沒有依賴的module順利的啓動對方的Activity了。框架
簡單講,要經過apt生成咱們的路由表,首先第一步須要定義註解ide
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Route {
/**
* 路由的路徑
* @return
*/
String path();
/**
* 將路由節點進行分組,能夠實現動態加載
* @return
*/
String group() default "";
}
複製代碼
這裏看到Route註解裏有path和group,這即是仿照ARouter對路由進行分組。由於當項目變得愈來愈大龐大的時候,爲了便於管理和減少首次加載路由表過於耗時的問題,咱們對全部的路由進行分組。在ARouter中會要求路由地址至少須要兩級,如"/xx/xx",一個模塊下能夠有多個分組。這裏咱們就將路由地址定爲必須大於等於兩級,其中第一級是group。如app module下的路由註解:函數
@Route(path = "/main/main")
public class MainActivity extends AppCompatActivity {}
@Route(path = "/main/main2")
public class Main2Activity extends AppCompatActivity {}
@Route(path = "/show/info")
public class ShowActivity extends AppCompatActivity {}
複製代碼
在項目編譯的時候,咱們將會經過apt生成EaseRouter_Root_app文件和EaseRouter_Group_main、EEaseRouter_Group_show等文件,EaseRouter_Root_app文件對應於app module,裏面記錄着本module下全部的分組信息,EaseRouter_Group_main、EaseRouter_Group_show文件分別記載着當前分組的全部路由地址和ActivityClass映射信息。
本demo在編譯的時候會生成類以下所示,先不要管這些類是怎麼生成的,仔細看類的內容工具
public class EaseRouter_Root_app implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("main", EaseRouter_Group_main.class);
routes.put("show", EaseRouter_Group_show.class);
}
}
public class EaseRouter_Group_main implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/main/main",RouteMeta.build(RouteMeta.Type.ACTIVITY,Main2\Activity.class,"/main/main","main"));
atlas.put("/main/main2",RouteMeta.build(RouteMeta.Type.ACTIVITY,Main2\Activity.class,"/main/main2","main"));
}
}
public class EaseRouter_Group_show implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/show/info",RouteMeta.build(RouteMeta.Type.ACTIVITY,ShowActivity.class,"/show/info","show"));
}
}
複製代碼
你們會看到生成的類分別實現了IRouteRoot和IRouteGroup接口,而且實現了loadInto()方法,而loadInto方法經過傳入一個特定類型的map就能把分組信息放入map裏。這兩個接口是幹嗎的咱們先擱置,繼續往下看
若是咱們在login_module中想啓動app_module中的MainActivity類,首先,咱們已知MainActivity類的路由地址是"/main/main",第一個"/main"表明分組名,那麼咱們豈不是能夠像下面這樣調用去獲得MainActivity類文件,而後startActivity。這裏的RouteMeta只是存有Activity class文件的封裝類,先不用理會。
public void test() {
EaseRouter_Root_app rootApp = new EaseRouter_Root_app();
HashMap<String, Class<? extends IRouteGroup>> rootMap = new HashMap<>();
rootApp.loadInto(rootMap);
//獲得/main分組
Class<? extends IRouteGroup> aClass = rootMap.get("main");
try {
HashMap<String, RouteMeta> groupMap = new HashMap<>();
aClass.newInstance().loadInto(groupMap);
//獲得MainActivity
RouteMeta main = groupMap.get("/main/main");
Class<?> mainActivityClass = main.getDestination();
Intent intent = new Intent(this, mainActivityClass);
startActivity(intent);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
複製代碼
能夠看到,只要有了這些實現了IRouteRoot和IRouteGroup的類文件,咱們便能輕易的啓動其餘module的Activity了。這些類文件,咱們能夠約定好以後,在代碼的編寫過程當中本身動手實現,也能夠經過apt生成。做爲一個框架,固然是自動解析Route註解而後生成這些類文件更好了。那麼就看下節,如何去生成這些文件。
經過上節咱們知道在Activity類上加上@Route註解以後,即可經過apt來生成對應的路由表,那麼這節咱們就來說述一下如何經過apt來生成路由表。這節我會拿着demo裏面的代碼來跟你們詳細介紹,咱們先來了解一下apt吧!
APT是Annotation Processing Tool的簡稱,即註解處理工具。它是在編譯期對代碼中指定的註解進行解析,而後作一些其餘處理(如經過javapoet生成新的Java文件)。咱們經常使用的ButterKnife,其原理就是經過註解處理器在編譯期掃描代碼中加入的@BindView、@OnClick等註解進行掃描處理,而後生成XXX_ViewBinding類,實現了view的綁定。
第一步:定義註解處理器,用來在編譯期掃描加入@Route註解的類,而後作處理。
這也是apt最核心的一步,新建RouterProcessor 繼承自 AbstractProcessor,而後實現process方法。在項目編譯期會執行RouterProcessor的process()方法,咱們即可以在這個方法裏處理Route註解了。此時咱們須要爲RouterProcessor指明它須要處理什麼註解,這裏引入一個google開源的自動註冊工具AutoService,以下依賴(也能夠手動進行註冊,不過略微麻煩):
implementation 'com.google.auto.service:auto-service:1.0-rc2'
複製代碼
這個工具能夠經過添加註解來爲RouterProcessor指定它須要的配置(固然也能夠本身手動去配置,不過會有點麻煩),以下所示
@AutoService(Processor.class)
public class RouterProcessor extends AbstractProcessor {
//...
}
複製代碼
完整的RouterProcessor註解處理器配置以下:
@AutoService(Processor.class)
/**
處理器接收的參數 替代 {@link AbstractProcessor#getSupportedOptions()} 函數
*/
@SupportedOptions(Constant.ARGUMENTS_NAME)
/**
* 指定使用的Java版本 替代 {@link AbstractProcessor#getSupportedSourceVersion()} 函數
*/
@SupportedSourceVersion(SourceVersion.RELEASE_7)
/**
* 註冊給哪些註解的 替代 {@link AbstractProcessor#getSupportedAnnotationTypes()} 函數
*/
@SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE)
public class RouterProcessor extends AbstractProcessor {
/**
* key:組名 value:類名
*/
private Map<String, String> rootMap = new TreeMap<>();
/**
* 分組 key:組名 value:對應組的路由信息
*/
private Map<String, List<RouteMeta>> groupMap = new HashMap<>();
/**
* 節點工具類 (類、函數、屬性都是節點)
*/
private Elements elementUtils;
/**
* type(類信息)工具類
*/
private Types typeUtils;
/**
* 文件生成器 類/資源
*/
private Filer filerUtils;
private String moduleName;
private Log log;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
//得到apt的日誌輸出
log = Log.newLog(processingEnvironment.getMessager());
elementUtils = processingEnvironment.getElementUtils();
typeUtils = processingEnvironment.getTypeUtils();
filerUtils = processingEnvironment.getFiler();
//參數是模塊名 爲了防止多模塊/組件化開發的時候 生成相同的 xx$$ROOT$$文件
Map<String, String> options = processingEnvironment.getOptions();
if (!Utils.isEmpty(options)) {
moduleName = options.get(Constant.ARGUMENTS_NAME);
}
if (Utils.isEmpty(moduleName)) {
throw new RuntimeException("Not set processor moudleName option !");
}
log.i("init RouterProcessor " + moduleName + " success !");
}
/**
*
* @param set 使用了支持處理註解的節點集合
* @param roundEnvironment 表示當前或是以前的運行環境,能夠經過該對象查找找到的註解。
* @return true 表示後續處理器不會再處理(已經處理)
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
if (!Utils.isEmpty(set)) {
//被Route註解的節點集合
Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(Route.class);
if (!Utils.isEmpty(rootElements)) {
processorRoute(rootElements);
}
return true;
}
return false;
}
//...
}
複製代碼
咱們經過@SupportedOptions(Constant.ARGUMENTS_NAME)拿到每一個module的名字,用來生成對應module下存放路由信息的類文件名。在這以前,咱們須要在module的gradle下配置以下
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName: project.getName()]
}
}
複製代碼
Constant.ARGUMENTS_NAME即是每一個module的名字。
@SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE)指定了須要處理的註解的路徑地址,在此就是Route.class的路徑地址。
RouterProcessor中咱們實現了init方法,拿到log apt日誌輸出工具用以輸出apt日誌信息,並經過如下代碼獲得上面提到的每一個module配置的moduleName
//參數是模塊名 爲了防止多模塊/組件化開發的時候 生成相同的 xx$$ROOT$$文件
Map<String, String> options = processingEnvironment.getOptions();
if (!Utils.isEmpty(options)) {
moduleName = options.get(Constant.ARGUMENTS_NAME);
}
if (Utils.isEmpty(moduleName)) {
throw new RuntimeException("Not set processor moudleName option !");
}
複製代碼
第二步,在process()方法裏開始生成EaseRouter_Route_moduleName類文件和EaseRouter_Group_moduleName文件。這裏在process()裏生成文件用javapoet,這是squareup公司開源的一個庫,經過調用它的api,能夠很方便的生成java文件,在含有註解處理器(demo中apt相關的代碼實現都在easy-compiler module中)的module中引入依賴以下:
implementation 'com.squareup:javapoet:1.7.0'
複製代碼
好了,咱們終於能夠生成文件了,在process()方法裏有以下代碼,
if (!Utils.isEmpty(set)) {
//被Route註解的節點集合
Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(Route.class);
if (!Utils.isEmpty(rootElements)) {
processorRoute(rootElements);
}
return true;
}
return false;
複製代碼
set就是掃描獲得的支持處理註解的節點集合,而後獲得rootElements,即被@Route註解的節點集合,此時就能夠調用 processorRoute(rootElements)方法去生成文件了。processorRoute(rootElements)方法實現以下:
private void processorRoute(Set<? extends Element> rootElements) {
//得到Activity這個類的節點信息
TypeElement activity = elementUtils.getTypeElement(Constant.ACTIVITY);
TypeElement service = elementUtils.getTypeElement(Constant.ISERVICE);
for (Element element : rootElements) {
RouteMeta routeMeta;
//類信息
TypeMirror typeMirror = element.asType();
log.i("Route class:" + typeMirror.toString());
Route route = element.getAnnotation(Route.class);
if (typeUtils.isSubtype(typeMirror, activity.asType())) {
routeMeta = new RouteMeta(RouteMeta.Type.ACTIVITY, route, element);
} else if (typeUtils.isSubtype(typeMirror, service.asType())) {
routeMeta = new RouteMeta(RouteMeta.Type.ISERVICE, route, element);
} else {
throw new RuntimeException("Just support Activity or IService Route: " + element);
}
categories(routeMeta);
}
TypeElement iRouteGroup = elementUtils.getTypeElement(Constant.IROUTE_GROUP);
TypeElement iRouteRoot = elementUtils.getTypeElement(Constant.IROUTE_ROOT);
//生成Group記錄分組表
generatedGroup(iRouteGroup);
//生成Root類 做用:記錄<分組,對應的Group類>
generatedRoot(iRouteRoot, iRouteGroup);
}
複製代碼
上節中提到過生成的root文件和group文件分別實現了IRouteRoot和IRouteGroup接口,就是經過下面這兩行文件代碼拿到IRootGroup和IRootRoot的字節碼信息,而後傳入generatedGroup(iRouteGroup)和generatedRoot(iRouteRoot, iRouteGroup)方法,這兩個方法內部會經過javapoet api生成java文件,並實現這兩個接口。
TypeElement iRouteGroup = elementUtils.getTypeElement(Constant.IROUTE_GROUP);
TypeElement iRouteRoot = elementUtils.getTypeElement(Constant.IROUTE_ROOT);
複製代碼
generatedGroup(iRouteGroup)和generatedRoot(iRouteRoot, iRouteGroup)就是生成上面提到的EaseRouter_Root_app和EaseRouter_Group_main等文件的具體實現,代碼太多,我粘出一個實現供你們參考,其實生成java文件的思路都是同樣的,咱們只須要熟悉javapoet的api如何使用便可。你們能夠後續在demo裏詳細分析,這裏我只是講解核心的實現。
/**
* 生成Root類 做用:記錄<分組,對應的Group類>
* @param iRouteRoot
* @param iRouteGroup
*/
private void generatedRoot(TypeElement iRouteRoot, TypeElement iRouteGroup) {
//建立參數類型 Map<String,Class<? extends IRouteGroup>> routes>
//Wildcard 通配符
ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(
ClassName.get(Map.class),
ClassName.get(String.class),
ParameterizedTypeName.get(
ClassName.get(Class.class),
WildcardTypeName.subtypeOf(ClassName.get(iRouteGroup))
));
//參數 Map<String,Class<? extends IRouteGroup>> routes> routes
ParameterSpec parameter = ParameterSpec.builder(parameterizedTypeName, "routes").build();
//函數 public void loadInfo(Map<String,Class<? extends IRouteGroup>> routes> routes)
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constant.METHOD_LOAD_INTO)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(parameter);
//函數體
for (Map.Entry<String, String> entry : rootMap.entrySet()) {
methodBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(Constant.PACKAGE_OF_GENERATE_FILE, entry.getValue()));
}
//生成$Root$類
String className = Constant.NAME_OF_ROOT + moduleName;
TypeSpec typeSpec = TypeSpec.classBuilder(className)
.addSuperinterface(ClassName.get(iRouteRoot))
.addModifiers(Modifier.PUBLIC)
.addMethod(methodBuilder.build())
.build();
try {
JavaFile.builder(Constant.PACKAGE_OF_GENERATE_FILE, typeSpec).build().writeTo(filerUtils);
log.i("Generated RouteRoot:" + Constant.PACKAGE_OF_GENERATE_FILE + "." + className);
} catch (IOException e) {
e.printStackTrace();
}
}
複製代碼
能夠看到,ParameterizedTypeName是建立參數類型的api,ParameterSpec是建立參數的實現,MethodSpec是函數的生成實現等等。最後,當參數、方法、類信息都準備好了以後,調用JavaFileapi生成類文件。JavaFile的builder ()方法傳入了PACKAGE_OF_GENERATE_FILE變量,這個就是指定生成的類文件的目錄,方便咱們在app進程啓動的時候去遍歷拿到這些類文件。
經過前幾節的講解,咱們知道了看似很複雜的路由框架,其實原理很簡單,咱們能夠理解爲一個map(實際上是兩個map,一個保存group列表,一個保存group下的路由地址和activityClass關係)保存了路由地址和ActivityClass的映射關係,而後經過map.get("router address") 拿到AncivityClass,經過startActivity()調用就行了。但一個框架的設計要考慮的事情遠遠沒有這麼簡單。下面咱們就來分析一下:
要實現這麼一個路由框架,首先咱們須要在用戶使用路由跳轉以前把這些路由映射關係拿到手,拿到這些路由關係最好的時機就是應用程序初始化的時候,前面的講解中我貼過幾行代碼,是經過apt生成的路由映射關係文件,爲了方便你們理解,我把這些文件從新粘貼到下面代碼中(這幾個類都是單獨的文件,在項目編譯後會在各個模塊的/build/generated/source/apt文件夾下面生成,爲了演示方便我只貼出來了app模塊下生成的類,其餘模塊如module一、module2下面的類跟app下面的沒有什麼區別),在程序啓動的時候掃描這些生成的類文件,而後獲取到映射關係信息,保存起來。
public class EaseRouter_Root_app implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("main", EaseRouter_Group_main.class);
routes.put("show", EaseRouter_Group_show.class);
}
}
public class EaseRouter_Group_main implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/main/main",RouteMeta.build(RouteMeta.Type.ACTIVITY,Main2\Activity.class,"/main/main","main"));
atlas.put("/main/main2",RouteMeta.build(RouteMeta.Type.ACTIVITY,Main2\Activity.class,"/main/main2","main"));
}
}
public class EaseRouter_Group_show implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/show/info",RouteMeta.build(RouteMeta.Type.ACTIVITY,ShowActivity.class,"/show/info","show"));
}
}
複製代碼
能夠看到,這些文件中,實現了IRouteRoot接口的類都是保存了group分組映射信息,實現了IRouteGroup接口的類都保存了單個分組下的路由映射信息。只要咱們獲得實現IRouteRoot接口的全部類文件,便能經過循環調用它的loadInfo()方法獲得全部實現IRouteGroup接口的類,而全部實現IRouteGroup接口的類裏面保存了項目的全部路由信息。IRouteGroup的loadInfo()方法,經過傳入一個map,便會將這個分組裏的映射信息存入map裏。能夠看到map裏的value是「RouteMeta.build(RouteMeta.Type.ACTIVITY,ShowActivity.class,"/show/info","show")」,RouteMeta.build()會返回RouteMeta,RouteMeta裏面便保存着ActivityClass的全部信息。那麼咱們這個框架,就有了第一個功能需求,即是在app進程啓動的時候進行框架的初始化(或者在你開始用路由跳轉以前進行初始化均可以),在初始化中拿到映射關係信息,保存在map裏,以便程序運行中能夠快速找到路由映射信息實現跳轉。下面看具體的初始化代碼。
注:這裏咱們只講解大致的思路,不會細緻到講解每個方法每一行代碼的具體做用,跟着個人思路你會明白框架設計的具體細節,每一步要實現的功能是什麼,可是精確到方法和每一行代碼的具體含義你還須要仔細研讀demo。
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
EasyRouter.init(this);
}
}
public class EasyRouter {
private static final String TAG = "EasyRouter";
private static final String ROUTE_ROOT_PAKCAGE = "com.xsm.easyrouter.routes";
private static final String SDK_NAME = "EaseRouter";
private static final String SEPARATOR = "_";
private static final String SUFFIX_ROOT = "Root";
private static EasyRouter sInstance;
private static Application mContext;
private Handler mHandler;
private EasyRouter() {
mHandler = new Handler(Looper.getMainLooper());
}
public static EasyRouter getsInstance() {
synchronized (EasyRouter.class) {
if (sInstance == null) {
sInstance = new EasyRouter();
}
}
return sInstance;
}
public static void init(Application application) {
mContext = application;
try {
loadInfo();
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "初始化失敗!", e);
}
}
//...
}
複製代碼
能夠看到,init()方法中調用了loadInfo()方法,而這個loadInfo()即是咱們初始化的核心。
private static void loadInfo() throws PackageManager.NameNotFoundException, InterruptedException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//得到全部 apt生成的路由類的全類名 (路由表)
Set<String> routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
for (String className : routerMap) {
if (className.startsWith(ROUTE_ROOT_PAKCAGE + "." + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
//root中註冊的是分組信息 將分組信息加入倉庫中
((IRouteRoot) Class.forName(className).getConstructor().newInstance()).loadInto(Warehouse.groupsIndex);
}
}
for (Map.Entry<String, Class<? extends IRouteGroup>> stringClassEntry : Warehouse.groupsIndex.entrySet()) {
Log.d(TAG, "Root映射表[ " + stringClassEntry.getKey() + " : " + stringClassEntry.getValue() + "]");
}
}
複製代碼
咱們首先經過ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE)獲得apt生成的全部實現IRouteRoot接口的類文件集合,經過上面的講解咱們知道,拿到這些類文件即可以獲得全部的routerAddress---activityClass映射關係。
這個ClassUtils.getFileNameByPackageName()方法就是具體的實現了,下面咱們看具體的代碼:
/**
* 獲得路由表的類名
* @param context
* @param packageName
* @return
* @throws PackageManager.NameNotFoundException
* @throws InterruptedException
*/
public static Set<String> getFileNameByPackageName(Application context, final String packageName)
throws PackageManager.NameNotFoundException, InterruptedException {
final Set<String> classNames = new HashSet<>();
List<String> paths = getSourcePaths(context);
//使用同步計數器判斷均處理完成
final CountDownLatch countDownLatch = new CountDownLatch(paths.size());
ThreadPoolExecutor threadPoolExecutor = DefaultPoolExecutor.newDefaultPoolExecutor(paths.size());
for (final String path : paths) {
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
DexFile dexFile = null;
try {
//加載 apk中的dex 並遍歷 得到全部包名爲 {packageName} 的類
dexFile = new DexFile(path);
Enumeration<String> dexEntries = dexFile.entries();
while (dexEntries.hasMoreElements()) {
String className = dexEntries.nextElement();
if (!TextUtils.isEmpty(className) && className.startsWith(packageName)) {
classNames.add(className);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != dexFile) {
try {
dexFile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//釋放一個
countDownLatch.countDown();
}
}
});
}
//等待執行完成
countDownLatch.await();
return classNames;
}
複製代碼
這個方法會經過開啓子線程,去掃描apk中全部的dex,遍歷找到全部包名爲packageName的類名,而後將類名再保存到classNames集合裏。
List paths = getSourcePaths(context)這句代碼會得到全部的apk文件(instant run會產生不少split apk),這個方法的具體實現你們kandemo便可,再也不闡述。這裏用到了CountDownLatch類,會分path一個文件一個文件的檢索,等到全部的類文件都找到後便會返回這個Set集合。因此咱們能夠知道,初始化時找到這些類文件會有必定的耗時,因此ARouter這裏會有一些優化,只會遍歷找一次類文件,找到以後就會保存起來,下次app進程啓動會檢索是否有保存這些文件,若是有就會直接調用保存後的數據去初始化。
經過上節的介紹,咱們知道在初始化的時候已經拿到了全部的路由信息,那麼實現跳轉便好作了。
@Route(path = "/main/main")
public class MainActivity extends AppCompatActivity {
public void startModule1MainActivity(View view) {
EasyRouter.getsInstance().build("/module1/module1main").navigation();
}
}
複製代碼
在build的時候,傳入要跳轉的路由地址,build()方法會返回一個Postcard對象,咱們稱之爲跳卡。而後調用Postcard的navigation()方法完成跳轉。用過ARouter的對這個跳卡都應該很熟悉吧!Postcard裏面保存着跳轉的信息。下面我把Postcard類的代碼實現粘下來:
public class Postcard extends RouteMeta {
private Bundle mBundle;
private int flags = -1;
//新版風格
private Bundle optionsCompat;
//老版
private int enterAnim;
private int exitAnim;
//服務
private IService service;
public Postcard(String path, String group) {
this(path, group, null);
}
public Postcard(String path, String group, Bundle bundle) {
setPath(path);
setGroup(group);
this.mBundle = (null == bundle ? new Bundle() : bundle);
}
public Bundle getExtras() {return mBundle;}
public int getEnterAnim() {return enterAnim;}
public int getExitAnim() {return exitAnim;}
public IService getService() {
return service;
}
public void setService(IService service) {
this.service = service;
}
/**
* Intent.FLAG_ACTIVITY**
* @param flag
* @return
*/
public Postcard withFlags(int flag) {
this.flags = flag;
return this;
}
public int getFlags() {
return flags;
}
/**
* 跳轉動畫
*
* @param enterAnim
* @param exitAnim
* @return
*/
public Postcard withTransition(int enterAnim, int exitAnim) {
this.enterAnim = enterAnim;
this.exitAnim = exitAnim;
return this;
}
/**
* 轉場動畫
*
* @param compat
* @return
*/
public Postcard withOptionsCompat(ActivityOptionsCompat compat) {
if (null != compat) {
this.optionsCompat = compat.toBundle();
}
return this;
}
public Postcard withString(@Nullable String key, @Nullable String value) {
mBundle.putString(key, value);
return this;
}
public Postcard withBoolean(@Nullable String key, boolean value) {
mBundle.putBoolean(key, value);
return this;
}
public Postcard withShort(@Nullable String key, short value) {
mBundle.putShort(key, value);
return this;
}
public Postcard withInt(@Nullable String key, int value) {
mBundle.putInt(key, value);
return this;
}
public Postcard withLong(@Nullable String key, long value) {
mBundle.putLong(key, value);
return this;
}
public Postcard withDouble(@Nullable String key, double value) {
mBundle.putDouble(key, value);
return this;
}
public Postcard withByte(@Nullable String key, byte value) {
mBundle.putByte(key, value);
return this;
}
public Postcard withChar(@Nullable String key, char value) {
mBundle.putChar(key, value);
return this;
}
public Postcard withFloat(@Nullable String key, float value) {
mBundle.putFloat(key, value);
return this;
}
public Postcard withParcelable(@Nullable String key, @Nullable Parcelable value) {
mBundle.putParcelable(key, value);
return this;
}
public Postcard withStringArray(@Nullable String key, @Nullable String[] value) {
mBundle.putStringArray(key, value);
return this;
}
public Postcard withBooleanArray(@Nullable String key, boolean[] value) {
mBundle.putBooleanArray(key, value);
return this;
}
public Postcard withShortArray(@Nullable String key, short[] value) {
mBundle.putShortArray(key, value);
return this;
}
public Postcard withIntArray(@Nullable String key, int[] value) {
mBundle.putIntArray(key, value);
return this;
}
public Postcard withLongArray(@Nullable String key, long[] value) {
mBundle.putLongArray(key, value);
return this;
}
public Postcard withDoubleArray(@Nullable String key, double[] value) {
mBundle.putDoubleArray(key, value);
return this;
}
public Postcard withByteArray(@Nullable String key, byte[] value) {
mBundle.putByteArray(key, value);
return this;
}
public Postcard withCharArray(@Nullable String key, char[] value) {
mBundle.putCharArray(key, value);
return this;
}
public Postcard withFloatArray(@Nullable String key, float[] value) {
mBundle.putFloatArray(key, value);
return this;
}
public Postcard withParcelableArray(@Nullable String key, @Nullable Parcelable[] value) {
mBundle.putParcelableArray(key, value);
return this;
}
public Postcard withParcelableArrayList(@Nullable String key, @Nullable ArrayList<? extends
Parcelable> value) {
mBundle.putParcelableArrayList(key, value);
return this;
}
public Postcard withIntegerArrayList(@Nullable String key, @Nullable ArrayList<Integer> value) {
mBundle.putIntegerArrayList(key, value);
return this;
}
public Postcard withStringArrayList(@Nullable String key, @Nullable ArrayList<String> value) {
mBundle.putStringArrayList(key, value);
return this;
}
public Bundle getOptionsBundle() {
return optionsCompat;
}
public Object navigation() {
return EasyRouter.getsInstance().navigation(null, this, -1, null);
}
public Object navigation(Context context) {
return EasyRouter.getsInstance().navigation(context, this, -1, null);
}
public Object navigation(Context context, NavigationCallback callback) {
return EasyRouter.getsInstance().navigation(context, this, -1, callback);
}
public Object navigation(Context context, int requestCode) {
return EasyRouter.getsInstance().navigation(context, this, requestCode, null);
}
public Object navigation(Context context, int requestCode, NavigationCallback callback) {
return EasyRouter.getsInstance().navigation(context, this, requestCode, callback);
}
}
複製代碼
若是你是一個Android開發,Postcard類裏面的東西就不用我再給你介紹了吧!(哈哈)我相信你一看就明白了。咱們只介紹一個方法navigation(),他有好幾個重載方法,方法裏面會調用EasyRouter類的navigation()方法。EaseRouter的navigation()方法,就是跳轉的核心了。下面請看:
protected Object navigation(Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
try {
prepareCard(postcard);
}catch (NoRouteFoundException e) {
e.printStackTrace();
//沒找到
if (null != callback) {
callback.onLost(postcard);
}
return null;
}
if (null != callback) {
callback.onFound(postcard);
}
switch (postcard.getType()) {
case ACTIVITY:
final Context currentContext = null == context ? mContext : context;
final Intent intent = new Intent(currentContext, postcard.getDestination());
intent.putExtras(postcard.getExtras());
int flags = postcard.getFlags();
if (-1 != flags) {
intent.setFlags(flags);
} else if (!(currentContext instanceof Activity)) {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
mHandler.post(new Runnable() {
@Override
public void run() {
//可能須要返回碼
if (requestCode > 0) {
ActivityCompat.startActivityForResult((Activity) currentContext, intent,
requestCode, postcard.getOptionsBundle());
} else {
ActivityCompat.startActivity(currentContext, intent, postcard
.getOptionsBundle());
}
if ((0 != postcard.getEnterAnim() || 0 != postcard.getExitAnim()) &&
currentContext instanceof Activity) {
//老版本
((Activity) currentContext).overridePendingTransition(postcard
.getEnterAnim()
, postcard.getExitAnim());
}
//跳轉完成
if (null != callback) {
callback.onArrival(postcard);
}
}
});
break;
case ISERVICE:
return postcard.getService();
default:
break;
}
return null;
}
複製代碼
這個方法裏先去調用了prepareCard(postcard)方法,prepareCard(postcard)代碼我貼出來,
private void prepareCard(Postcard card) {
RouteMeta routeMeta = Warehouse.routes.get(card.getPath());
if (null == routeMeta) {
Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(card.getGroup());
if (null == groupMeta) {
throw new NoRouteFoundException("沒找到對應路由:分組=" + card.getGroup() + " 路徑=" + card.getPath());
}
IRouteGroup iGroupInstance;
try {
iGroupInstance = groupMeta.getConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("路由分組映射表記錄失敗.", e);
}
iGroupInstance.loadInto(Warehouse.routes);
//已經準備過了就能夠移除了 (不會一直存在內存中)
Warehouse.groupsIndex.remove(card.getGroup());
//再次進入 else
prepareCard(card);
} else {
//類 要跳轉的activity 或IService實現類
card.setDestination(routeMeta.getDestination());
card.setType(routeMeta.getType());
switch (routeMeta.getType()) {
case ISERVICE:
Class<?> destination = routeMeta.getDestination();
IService service = Warehouse.services.get(destination);
if (null == service) {
try {
service = (IService) destination.getConstructor().newInstance();
Warehouse.services.put(destination, service);
} catch (Exception e) {
e.printStackTrace();
}
}
card.setService(service);
break;
default:
break;
}
}
}
複製代碼
注意,Warehouse就是專門用來存放路由映射關係的類,這在ARouter裏面也是。這段代碼Warehouse.routes.get(card.getPath())經過path拿到對應的RouteMeta,這個RouteMeta裏面保存了activityClass等信息。繼續往下看,若是判斷拿到的RouteMeta是空,說明這個路由地址尚未加載到map裏面(爲了效率,這裏是用了懶加載),只有在第一次用到當前路由地址的時候,會去Warehouse.routes裏面拿routeMeta,若是拿到的是空,會根據當前路由地址的group拿到對應的分組,經過反射建立實例,而後調用實例的loadInfo方法,把它裏面保存的映射信息添加到Warehouse.routes裏面,而且再次調用prepareCard(card),這時再經過Warehouse.routes.get(card.getPath())就能夠順利拿到RouteMeta了。進入else{}裏面,調用了card.setDestination(routeMeta.getDestination()),這個setDestination就是將RouteMeta裏面保存的activityClass放入Postcard裏面,下面switch代碼塊能夠先不用看,這是實現ARouter中經過依賴注入實現Provider 服務的邏輯,有心研究的同窗能夠去讀一下demo。
好了,prepareCard()方法調用完成後,咱們的postcard裏面就保存了activityClass,而後switch (postcard.getType()){}會判斷postcard的type爲ACTIVITY,而後經過ActivityCompat.startActivity啓動Activity。到這裏,路由跳轉的實現已經講解完畢了。
EaseRouter自己只是參照ARouter手動實現的路由框架,而且剔除掉了不少東西,如過濾器等,若是想要用在項目裏,建議仍是用ARouter更好(畢竟這只是個練手項目,功能也不夠全面,固然有同窗想對demo擴展後使用那固然更好,遇到什麼問題及時聯繫我)。個人目的是經過本身手動實現來加深對知識的理解,這裏面涉及到的知識點如apt、javapoet和組件化思路、編寫框架的思路等。看到這裏,若是感受乾貨不少,歡迎點個star或分享給更多人。
仿ARouter一步步實現一個路由框架,點我訪問源碼,歡迎star