前言
路由框架是幹什麼的:
首先看百度百科,路由_百度百科,“路由(routing)是指分組從源到目的地時,決定端到端路徑的網絡範圍的進程。” 在Android程序裏,相當於有一個可以幫用戶轉發兩個客戶的通信信息。比如頁面路由轉發,即Activity跳轉,但這裏的框架不限於此。
我需要麼?
一般android開發中,進行頁面跳轉時,一般寫法如下:
Intent intent = new Intent(mContext, XXActivity.class);
intent.putExtra(“key”,“value”);
startActivity(intent);
這樣的寫法通常導致依賴性增加,各種跳轉添加的intent-filter不好維護,不利多人開發。項目做到一定程度,代碼量和功能集都非常大,導致耦合嚴重,不利於應對功能變化。所以我們要組件化開發,分成多個module由不同的人開發,不同module間的通信和頁面跳轉就需要路由框架支持。
ARouter框架
ARouter:一個用於幫助 Android App 進行組件化改造的框架 —— 支持模塊間的路由、通信、解耦。
準備知識:
要想理解本篇所涉及的知識,需要事先做一定的功課,如果都瞭解可以忽略。
- 註解知識,路由框架通過註解進行依賴注入。有需要可以參考:Android註解-看這篇文章就夠了
- APT即註解處理器或者反射知識。由於本篇的註解類型相比與簡單框架難度有所提高,有需要的可以參考:手寫簡化EventBus,理解框架核心原理,此篇爲反射實現。 和手寫簡化EventBus之註解處理器方式,理解框架核心原理。 由於eventbus框架較簡單,對此框架熟悉的可以先看此兩篇文章加深過程印象。 對AutoService和javapoet有一定了解。
- javapoet, 模板文件生成代碼的框架。可以自動按我們的設定去生成代碼。
ARouter框架使用
想要學習一個框架,首先需要了解框架的基本使用,然後才能對框架中代碼的作用有一定的瞭解,當然所有的源碼都是爲了使用設定的。由於框架目前的功能較多,這裏只提綱挈領的介紹,最終我們自己動手擼框架也是實現其核心原理,否則,要實現一樣功能集的框架那時間就太久了。如果想看詳細說明,可以參見ARouter框架的git地址:https://github.com/alibaba/ARouter/blob/master/README_CN.md
添加依賴和配置
android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
//這裏是在gradle配置中將module的名字作爲參數傳入,可以在註解處理器中的init方法中收到,用來生成不同的類文件
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
}
dependencies {
// 替換成最新版本, 需要注意的是api
// 要與compiler匹配使用,均使用最新版可以保證兼容
compile 'com.alibaba:arouter-api:x.x.x'
//每個使用了註解的module都需要添加,用來開始註解處理
annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'
...
}
添加註解
// 在支持路由的頁面上添加註解(必選)
// 這裏的路徑需要注意的是至少需要有兩級,/xx/xx, 註解處理器生成模板代碼時會根據第一級名字生成類名。
@Route(path = "/test/activity")
public class YourActivity extend Activity {
...
}
初始化SDK
ARouter.init(mApplication); // 儘可能早,推薦在Application中初始化
發起路由操作
// 1. 應用內簡單的跳轉(通過URL跳轉在'進階用法'中)
ARouter.getInstance().build("/test/activity").navigation();
// 2. 跳轉並攜帶參數
ARouter.getInstance().build("/test/1")
.withLong("key1", 666L)
.withString("key3", "888")
.withObject("key4", new Test("Jack", "Rose"))
.navigation();
通過依賴注入解耦:服務管理(一) 暴露服務
由於我們不只會使用頁面跳轉,還會調用其他module提供的接口方法,也可以通過路由框架進行解耦,依賴注入。
// 聲明接口,其他組件通過接口來調用服務,這裏對外提供了一個方法接口,而此接口需要繼承IProvider,用於路由框架知道此接口需要處理,即依賴注入。
public interface HelloService extends IProvider {
String sayHello(String name);
}
// 實現接口
@Route(path = "/yourservicegroupname/hello", name = "測試服務")
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String name) {
return "hello, " + name;
}
....
}
通過依賴注入解耦:服務管理(二) 發現服務
public class Test {
@Autowired
HelloService helloService;
@Autowired(name = "/yourservicegroupname/hello")
HelloService helloService2;
HelloService helloService3;
HelloService helloService4;
public Test() {
ARouter.getInstance().inject(this);
}
public void testService() {
// 1. (推薦)使用依賴注入的方式發現服務,通過註解標註字段,即可使用,無需主動獲取
// Autowired註解中標註name之後,將會使用byName的方式注入對應的字段,不設置name屬性,會默認使用byType的方式發現服務(當同一接口有多個實現的時候,必須使用byName的方式發現服務)
helloService.sayHello("Vergil");
helloService2.sayHello("Vergil");
// 2. 使用依賴查找的方式發現服務,主動去發現服務並使用,下面兩種方式分別是byName和byType
helloService3 = ARouter.getInstance().navigation(HelloService.class);
helloService4 = (HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation();
helloService3.sayHello("Vergil");
helloService4.sayHello("Vergil");
}
}
框架原理
如下爲Arouter路由框架的核心部分原理圖。其實也和事件總線框架有相似的地方,就是代碼中不需要直接依賴調用方的類,而是由中間層進行統管調度轉發。
上節中框架使用例子中的path路徑,即是兩方聯絡的暗號,這樣,如果是兩個模塊或者兩個組件即不需要直接聯繫,而是由這個框架去完成通信。 如果有多個模塊或組件呢,那就像是下圖所示黑色部分的互相聯繫調用,各模塊互相依賴類方法,改動一處可能影響多個組件的功能,而綠色箭頭則使用了路由框架,對需要依賴的組件都由路由進行聯繫,自己維護的代碼對其他組件沒有依賴。
手寫路由
接下來我們開始根據理解手寫路由框架,當然大部分設計是參考ARouter框架的源碼,我們只把核心部分代碼和部分數據集合參考使用到手寫框架中。ARouter目前的功能集很強大,這裏只抓住主幹瞭解核心,這樣,其他部分的實現也是類似的,我們便能夠更快的理解。爲了與ARouter進行區分,這裏我們改名爲ZRouter。通過本篇內容,希望可以幫助需要的人,從簡單的框架理解核心設計原理,然後再去學習ARouter的源碼,會收到事半功倍的效果。
相關源碼見github地址:https://github.com/qingdaofu1/ZRouter
1.自定義註解
根據ARouter的使用經驗,我們需要創建自定義註解,這是最基礎的工作。這裏採用和ARouter同樣的註解名字,以方便我們聯想到ARouter的使用經驗,加深印象。
爲了管理註解、方便路由框架使用、應用使用和註解處理器解析,這裏將註解單獨module管理,不過這裏是java-library module。如下爲自定義註解Route和AutoWired,功能和ARouter的註解功能一致,前者是頁面跳轉或接口路由,用path指代目標,後者是變量的註解,用以傳遞值和調用接口。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Route {
String path();
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface AutoWired {
String name() default "";
}
另外,爲了管理各種使用註解的類,還需要自定義RouteType用以區分不同類的使用,如Activity、Fragment、Provider等。
2.創建路由Api
第二步即創建路由SDK接口,用於向用戶提供功能接口,用戶按照接口使用規範去實現組件化的解耦能力。
基本接口如下,這裏借鑑ARouter的同名方法,並用自己的源碼實現功能,當然思路有借鑑☻。具體方法的含義請參閱ARouter基本使用。
public static void init(Application application)
public RouteManager build(String path)
public <T> T navigation(Class<? extends T> serviceClass)
public void inject(Object object)
根據接口設計類文件名如下所示:
設計的多個Interface接口都是用於註解生成類文件需要繼承實現的接口,這樣,路由框架就能夠獲取生成類文件生成的頁面或接口信息。
重點是ClassUtils中的兩個方法,getFileNameByPackageName用來獲得所有對應包名的類文件名,當然,下一節我們會知道,生成的類都是在固定的包路徑,可以方便我們全部獲取。
/**
* 通過指定包名,掃描包下面包含的所有的ClassName
*
* @param context U know
* @param packageName 包名
* @return 所有class的集合
*/
public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException {
和getSourcePaths獲取所有dex文件路徑
/**
* get all the dex path
*
* @param context the application context
* @return all the dex path
* @throws PackageManager.NameNotFoundException
* @throws IOException
*/
public static List<String> getSourcePaths(Context context) throws PackageManager.NameNotFoundException, IOException {
3.生成模板類
模板類的生成,一個可以根據自己設定的規則生成,爲了普適性,需要考慮不會和用戶的類文件有衝突。 這裏呢,就不浪費腦細胞了 ,依葫蘆畫瓢。ARouter什麼規則,我們也按其規則設定,不過文件的Interface接口文件都已經在步驟2中確定了的。這裏的改動是將ARouter改爲ZRouter。
這裏呢肯定是經過一番測試,最終確定了ARouter生成的文件規則。
ZRouter$$Root$$moduleName:
此文件記錄所有Activity頁面信息和所有提供服務的繼承了IProvider的生成類信息,一個module只會生成一個此類,且在固定的包名下,ARouter是生成在com.alibaba.android.arouter.routes包路徑下。
public class ZRouter$$Root$$WeatherModule implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("weathermodule", ZRouter$$Group$$weathermodule.class);
routes.put("wetherservice", ZRouter$$Group$$weatherservice.class);
}
}
ZRouter$$Providers$$moduleName:
此類記錄所有提供IProvider服務的類的信息,一個module只會生成一個類。且在固定的包名下,ARouter是生成在com.alibaba.android.arouter.routes包路徑下。
public class ZRouter$$Providers$$weathermodule implements IProviderGroup {
@Override
public void loadInto(Map<String, RouteModel> providers) {
providers.put("com.example.weathermodule.IWeatherService", new RouteModel(RouteType.PROVIDER,
"/wetherservice/getinfo", WeatherServiceImpl.class));
}
}
ZRouter$$Group$$groupName:
此類記錄每個組名的所有頁面信息,每個分組生成一個類文件。即如果@Route(path = “/test/activity”),則test是組名。且在固定的包名下,ARouter是生成在com.alibaba.android.arouter.routes包路徑下。
public class ZRouter$$Group$$weathermodule implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteModel> atlas) {
atlas.put("/weather/weatheractivity", new RouteModel(RouteType.ACTIVITY,
"/weather/weatheractivity", WeatherMainActivity.class));
}
}
groupName$$ZRouter$$AutoWired:
此文件爲類中註解了AutoWired的變量進行依賴注入賦值,即調用inject時,通過參數this,根據路由映射表對變量進行賦值。此類生成在對應文件的相同包名路徑下。
public class WeatherMainActivity$$ZRouter$$AutoWired implements IAutoWiredInject {
@Override
public void inject(Object object) {
WeatherMainActivity substitute = (WeatherMainActivity) object;
substitute.msg = substitute.getIntent().getStringExtra("map");
}
}
到這裏我們把模板文件生成ok了,接下來需要測試模板代碼的正確性,因爲這就是要註解處理器自動生成的代碼部分,需要先確保模板的正確性。
由於我們是在源碼目錄中生成的,如果待會兒註解處理器生成了相同的類文件就會出現問題,爲了規避,下一步,我們可以將分隔符$$
改爲$$$
,用以區分,當然對應的ZRouter SDK中的方法中也要找對應的分隔符。
4.註解處理器生成代碼
註解處理器是處理自定義註解使用的,本項目設定了兩個自定義註解Route和AutoWired,所以我們需要兩個註解處理器RouteCompiler和AutoWiredCompiler,名字可以自定義,但是類內部需要通過註解或實現抽象方法標明要處理的註解路徑。
如RouteCompiler類:
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("com.example.annotations.Route")
public class RouteCompiler extends BaseProcessor {
其中BaseProcessor 是抽離了兩個註解處理器相同的工具類和公用變量部分,實際的抽象接口是要實現AbstractProcessor的process方法。
其gradle配置中需要加入如下配置。
dependencies {
implementation group: 'com.google.auto.service', name: 'auto-service', version: '1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
implementation group: 'com.squareup', name: 'javapoet', version: '1.12.1'
implementation project(path: ':annotations')
}
其中的各庫需要詳細瞭解的可以去搜索瞭解,這裏就不再介紹了,再之前的手寫Eventbus框架的文章裏也有簡單的介紹。 主要是通過javapoet框架實現上小節中的模板代碼。哦,對了,這個註解處理器所在的module也是java-library類型。
在RouteCompiler類中,需要創建的類有:ZRouter$$Root$$moduleName
,ZRouter$$Providers$$moduleName
和按組數量創建的ZRouter$$Group$$groupName
文件。這裏就不貼代碼了,最後會貼出github地址,有需要可以瞭解下,最主要的是練習javapoet的接口使用,只看用法說明不行,實際上手還是需要手動操作。
這裏只附上主方法中的代碼,相關文件具體實現見github地址:https://github.com/qingdaofu1/ZRouter。
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (alreadyHandledModule.contains(moduleName)) {
return false;
}
alreadyHandledModule.add(moduleName);
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Route.class);
Map<String, CompilerRouteModel> routeMap = new HashMap<>();
for (Element element : elements) {
TypeElement typeElement = (TypeElement) element;
Route annotation = typeElement.getAnnotation(Route.class);
String path = annotation.path();
messager.printMessage(Diagnostic.Kind.NOTE, "path is " + path);
//path = "/weather/weatheractivity" 獲取GroupName 此例爲weather
String[] split = path.split("/");
if (split.length < 3) {
messager.printMessage(Diagnostic.Kind.NOTE, "the path is incorrect, need two \\");
return false;
}
String groupName = split[1];
CompilerRouteModel compilerRouteModel = routeMap.get(groupName);
if (compilerRouteModel == null) {
compilerRouteModel = new CompilerRouteModel();
routeMap.put(groupName, compilerRouteModel);
}
//同一group的model的集合
compilerRouteModel.putElement(path, typeElement);
}
createGroupFiles(routeMap);
createProviderFile(providerMap);
createRootFile(groupFileMap);
return false;
}
測試驗證
爲了驗證接口的正確性,一開始其實就設計了demo程序用於持續的驗證。這裏以兩個module作爲例子。其中app module可調用頁面weather並傳遞String類型數據,和調用接口IWeatherService 、IMediaService 接口。 weathermodule的頁面調用頁面app,並傳遞String數據。
Module:app
這個頁面的path設爲"/main/activity", 即groupName爲main,在生成的中間類文件中會有體現。且IWeatherService 有多種調用方式,本例示例了三種方式。
@Route(path = "/main/activity")
public class MainActivity extends AppCompatActivity {
@AutoWired(name = "/wetherservice/getinfo")
IWeatherService weatherService;
@AutoWired(name="ok")
public String extra;
findViewById(R.id.btn_jump_app2).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ZRouter.getInstance()
.build("/weather/weatheractivity")
.withString("map", "hello kitty")
.navigation();
finish();
}
});
Toast.makeText(this, "get extra = " + extra, Toast.LENGTH_SHORT).show();
findViewById(R.id.btn_getweather).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int type = 1;
//方式1
// String weatherInfo = weatherService.getWeatherInfo("上海");
// Toast.makeText(MainActivity.this, weatherInfo, Toast.LENGTH_SHORT).show();
//方式2
// IWeatherService weatherService1 = (IWeatherService) ZRouter.getInstance()
// .build("/wetherservice/getinfo")
// .navigation();
// String weatherInfo1 = weatherService1.getWeatherInfo("北京");
// Toast.makeText(MainActivity.this, weatherInfo1, Toast.LENGTH_SHORT).show();
//方式3
IWeatherService weatherService2 = ZRouter.getInstance()
.navigation(IWeatherService.class);
String weatherInfo2 = weatherService2.getWeatherInfo("杭州");
Toast.makeText(MainActivity.this, weatherInfo2, Toast.LENGTH_SHORT).show();
}
});
findViewById(R.id.btn_getsinger).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
IMediaService mediaService = (IMediaService) ZRouter.getInstance()
.build("/wetherservice_group2/getsinger")
.navigation();
Toast.makeText(MainActivity.this, " singer is "+mediaService.getArtister(), Toast.LENGTH_SHORT).show();
}
});
Module:weathermodule
activity界面的代碼
@Route(path = "/weather/weatheractivity")
public class WeatherMainActivity extends AppCompatActivity {
@AutoWired(name = "map")
public String msg;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_weather_main);
ZRouter.getInstance().inject(this);
findViewById(R.id.btn_jump_to1).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ZRouter.getInstance()
.build("/main/activity")
.withString("ok", "dddddddddddddd")
.navigation();
finish();
}
});
Toast.makeText(this, "get string extra is= " + msg, Toast.LENGTH_SHORT).show();
}
}
然後是提供天氣信息接口的實現類WeatherServiceImpl ,繼承接口IWeatherService ,設了path爲"/wetherservice/getinfo";
@Route(path = "/wetherservice/getinfo")
public class WeatherServiceImpl implements IWeatherService {
@Override
public String getWeatherInfo(String city) {
return city + "今天天氣挺好的";
}
@Override
public void init(Context context) {
}
}
最後是MediaImpl ,繼承接口IMediaService
@Route(path = "/wetherservice_group2/getsinger")
public class MediaImpl implements IMediaService {
@Override
public String getArtister() {
return "周杰倫";
}
@Override
public void init(Context context) {
}
}
驗證結果:
後記
目前很多框架都使用了APT的框架,以實現其切面AOP編程的思想。註解處理器通了,幾乎就可以很方便的瞭解大部分框架的實現原理。希望文章能夠幫助到需要的人,如有任何問題歡迎留言溝通。