手寫路由框架,瞭解ARouter框架核心原理

前言

路由框架是幹什麼的:
首先看百度百科,路由_百度百科,“路由(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 進行組件化改造的框架 —— 支持模塊間的路由、通信、解耦。
準備知識:
要想理解本篇所涉及的知識,需要事先做一定的功課,如果都瞭解可以忽略。

  1. 註解知識,路由框架通過註解進行依賴注入。有需要可以參考:Android註解-看這篇文章就夠了
  2. APT即註解處理器或者反射知識。由於本篇的註解類型相比與簡單框架難度有所提高,有需要的可以參考:手寫簡化EventBus,理解框架核心原理,此篇爲反射實現。 和手寫簡化EventBus之註解處理器方式,理解框架核心原理。 由於eventbus框架較簡單,對此框架熟悉的可以先看此兩篇文章加深過程印象。 對AutoService和javapoet有一定了解。
  3. 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$$moduleNameZRouter$$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編程的思想。註解處理器通了,幾乎就可以很方便的瞭解大部分框架的實現原理。希望文章能夠幫助到需要的人,如有任何問題歡迎留言溝通。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章