Arouter核心思路和源碼詳解


前言

閱讀本文之前,建議讀者:

  • 對Arouter的使用有一定的了解。
  • 對Apt技術有所了解。

Arouter是一款Alibaba出品的優秀的路由框架,本文不對其進行全面的分析,只對其最重要的功能進行源碼以及思路分析,至於其攔截器,降級,ioc等功能感興趣的同學請自行閱讀源碼,強烈推薦閱讀雲棲社區的官方介紹

對於一個框架的學習和講解,我個人喜歡先將其最核心的思路用簡單一兩句話總結出來:ARouter通過Apt技術,生成保存路徑(路由path)被注解(@Router)的組件類的映射關系的類,利用這些保存了映射關系的類,Arouter根據用戶的請求postcard(明信片)尋找到要跳轉的目標地址(class),使用Intent跳轉。

原理很簡單,可以看出來,該框架的核心是利用apt生成的映射關系,這里要用到Apt技術,讀者可以自行搜索了解一下。

分析

我們先看最簡單的代碼的使用:

首先需要在需要跳轉的組件添加注解

@Route(path = "/main/homepage")
public class HomeActivity extends BaseActivity {
		onCreate()
		....
}

然后在需要跳轉的時候調用

Arouter.getInstance().build("main/hello").navigation;

這里的路徑“main/hello”是用戶唯一配置的東西,我們需要通過這個path找到對應的Activity。最簡單的思路就是通過APT技術,尋找到所有帶有注解@Router的組件,將其注解值path和對應的Activity保存到一個map里,比如像下面這樣:

class RouterMap {
	 public Map getAllRoutes {
	 		Map map = new HashMap<String,Class<?>>;
	 		map.put("/main/homepage",HomeActivity.class);
	 		map.put("/main/setting",SettingActivity.class);
	 		map.put("/login/register",LoginRegisterActivity.class);
	 		....
      
      return map;
	 }
}

然后在工程代碼中將這個map加載到內存中,需要的時候直接get(path)就可以了,這種方案似乎能解決我們的問題。

發現問題

上面的思路確實能夠實現路由功能,但是這么做會存在一個較大的問題:對於一個大型項目,組件數量會很多,可能會有一兩百或者更多,把這么多組件都放到這個Map里,顯然會對內存造成很大的壓力,因此,Arouter作為一款阿里出品的優秀框架,顯然是要解決這個問題的。

這里建議讀者自行思考一下,如何解決一次性加載所有映射關系帶來的內存損耗問題,我在思考這個問題的時候首先想到的是“懶加載”,但是僅僅懶加載是不夠的,因為懶加載后如果還是一次性把所有映射關系加載進來,內存損耗還是一樣大的。因此,再深入思考一下,可能還能想出解決一個思路:分段懶加載,思路有了,如何實現呢?這里還是建議大家在閱讀下面的內容之前思考一下,或許你能想到一套不同於Arouter的方案來哦。

Arouter采用的方法就是“分組+按需加載”,分組還帶來的另一個好處是便於管理,下面我們來看一下實現原理。

解決步驟一:分組

首先看如何分組的,Arouter在一層map之外,增加了一層map,我們看WareHouse這個類,里面有兩個靜態Map:

    static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
    static Map<String, RouteMeta> routes = new HashMap<>();
  • groupsIndex 保存了group名字到IRouteGroup類的映射,這一層映射就是Arouter新增的一層映射關系。

  • routes 保存了路徑path到RouteMeta的映射,其中,RouteMeta是目標地址的包裝。這一層映射關系跟我門自己方案里的map是一致的,我們路徑跳轉也是要用這一層來映射。

這里出現了兩個我們不認識的類,IRouteGroup和RouteMeta,后者很簡單,就是對跳轉目標的封裝,我們后續稱其為“目標”,其內包含了目標組件的信息,比如Activity的Class。那IRouteGroup是個什么東西?

public interface IRouteGroup {
    /**
     * Fill the atlas with routes in group.
     */
    void loadInto(Map<String, RouteMeta> atlas);
}

一個接口,只有一個方法loadInto,都有誰實現了這個接口呢?我拿我手上的一個項目為例,Arouter通過apt生成了下面幾個類:

apt下目錄結構

這幾個類都以Arouter$$Group開頭,我們隨便拿一個看看:

public class ARouter$$Group$$main implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/main/fa/leakscan", RouteMeta.build(RouteType.ACTIVITY, MainFaLeakScanActivity.class, "/main/fa/leakscan", "main", }}, -1, 1));
    atlas.put("/main/login", RouteMeta.build(RouteType.ACTIVITY, LoginActivity.class, "/main/login", "main", null, -1, -2147483648));
    atlas.put("/main/register", RouteMeta.build(RouteType.ACTIVITY, RegPhoneActivity.class, "/main/register", "main", null, -1, -2147483648));
  }
}

我們看到,他實現了loadInto方法,在這個方法中,它往這個HashMap中填充了好多數據,填充的是什么呢?填充的是路徑path和它對應的目標RouteMeta,也就是我們最終需要的那層映射關系。而且,我們還能觀察到:這個類下面所有的路由path都有一個共同點,即全是“/main”開頭的,也就是說,這個類加載的映射關系,都是在一個組內的。因此我們總結出:

Arouter通過apt技術,為每個組生成了一個以Arouter$$Group開頭的類,這個類負責向atlas這個Hashmap中填充組內路由數據。

IRouteGroup正如其名字,它就是一個能裝載該組路由映射數據的類,其實有點像個工具類,為了方便后續講解,我們姑且稱上面這樣一個實現了IRouteGroup的類叫做“組加載器”,本質是一個類。上圖中的類是一個組加載器,其他所有以Arouter$$Group開頭的類都是一個“組加載器”。回到之前的主線,Warehoust中的兩個Hashmap,其中groupsIndex這個map中保存的是什么呢?我們通過它的調用找到這一行代碼(已簡化):

for (String className : routerMap) {
     if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR +SUFFIX_ROOT)) {
        ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
			}
}

其中 ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR +SUFFIX_ROOT 這行代碼是幾個靜態字符串拼起來的,它等於 com.alibaba.android.arouter.routes.Arouter$$Root 。另外routerMap是什么呢?它是一個HashSet<String>:

routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);

這一行代碼對它進行了初始化,目的是找到 com.alibaba.android.arouter.routes 這個包名下所有的類,將其類名保存到routerMap中。因此,上面的代碼意思就是將com.alibaba.android.arouter.routes 包下所有名字以 com.alibaba.android.arouter.routes.Arouter$$Root 開頭的類找出來,通過反射實例化並強轉成IRouteRoot,然后調用loadInto方法。這里又出來一個新的接口:IRouteRoot,我們看代碼:

public interface IRouteRoot {

    /**
     * Load routes to input
     * @param routes input
     */
    void loadInto(Map<String, Class<? extends IRouteGroup>> routes);
}

跟IRouteGroup長得還挺像,也是loadInto,我們看它的實現。還是以我的項目為例,在apt生成的文件夾下查找:
apt生成文件目錄.png

最底下一行,有個Arouter$$Root$$app,它符合前面名字規則,我們進去看看:

public class ARouter$$Root$$app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("YDY", ARouter$$Group$$YDY.class);
    routes.put("app", ARouter$$Group$$app.class);
    routes.put("main", ARouter$$Group$$main.class);
    routes.put("payment", ARouter$$Group$$payment.class);
    routes.put("wallet", ARouter$$Group$$wallet.class);
  }
}

這個類實現了IRouteRoot,在loadInto方法中,他將組名和組對應的“組加載器”保存到了routes這個map中。也就是說,這個類將所有的“組加載器”給索引了下來,通過任意一個組名,可以找到對應的“組加載器”,我們再回到前面講的初始化Arouter時候的方法中:

((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);

理解了吧,這個方法的意義就在於將所有的組路由加載類索引到了groupsIndex這個map中。因此我們就明白了:

WareHouse中的groupsIndex保存的是組名到“組加載器”的映射關系

說句題外話:回過頭想想前面用到的兩個接口:IRouteGroup和IRouteRoot,它們其實是apt生成的類和我們項目中代碼之間溝通的橋梁,熟悉AIDL的同學可能會覺得很熟悉,二者其實是異曲同工的,兩個系統進行交互的時候都是通過接口來溝通的。當然,在使用apt生成的類時,我們需要用到反射技術。

總結一下Arouter的分組設計:Arouter在原來path到目標的map外,加了一個新的map,該map保存了組名到“組加載器”的映射關系。其中“組加載器”是一個類,可以加載其組內的path到目標的映射關系。

到此為止,Arouter只是完成了分組工作,但這么做的目的是什么呢?別着急,前面的都只是鋪墊,接下來才是這個分組設計發揮作用的地方,我們進入“按需加載”的代碼分析:

解決步驟二:按需加載

之前說過,Arouter使用的是分組按需加載,分組是為了按需做准備的。我們看Arouter是怎么按需加載的,我們還是從代碼的使用入手:

Arouter.getInstance().build("main/hello").navigation;

在navigation這個方法中,最終會跳轉到這里:

protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        try {
          //請關注這一行
            LogisticsCenter.completion(postcard);
        } catch (NoRouteFoundException ex) {
            logger.warning(Consts.TAG, ex.getMessage());
				....//簡化代碼
        }
  			//調用Intent跳轉
        return _navigation(context, postcard, requestCode, callback)

最后一行的return語句很簡單,就是去調用Intent喚起組件了,我們看前面try中的第一行 LogisticsCenter.completion(postcard),我們進到這個函數里:

//從緩存里取路由信息
RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
//如果為空,需要加載該組的路由
if (null == routeMeta) {
  Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());
  IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
  iGroupInstance.loadInto(Warehouse.routes);
  Warehouse.groupsIndex.remove(postcard.getGroup());
} 
//如果不為空,走后續流程
else {
     postcard.setDestination(routeMeta.getDestination());
  	 ...
}

這段代碼就是“按需加載”的核心邏輯所在了,我對其進行了簡化,分析其邏輯是這樣的:

  • 首先從Warehouse.routes(前面說了,這里存放的是path到目標的映射)里拿到目標信息,如果找不到,說明這個信息還沒加載,需要加載,實際上,剛開始這個routes里面什么都沒有。
  • 加載流程:首先從Warehouse.groupsIndex里獲取“組加載器”,組加載器是一個類,需要通過反射將其實例化,實例化為iGroupInstance,接着調用組加載器的加載方法loadInto,將該組的路由映射關系加載到Warehouse.routes中,加載完成后,routes中就緩存下來當前組的所有路由映射了,因此這個組加載器其實就沒用了,為了節省內存,將其從Warehouse.groupsIndex移除。
  • 如果之前加載過,則在Warehouse.routes里面是可以找到路有映射關系的,因此直接將目標信息routeMeta傳遞給postcard,保存在postcard中,這樣postcard就知道了最終要去哪個組件了。

到此為止分組按需加載的邏輯就都分析完了,通過這兩個步驟,解決了路由映射一次性加載到內存占用內存過大的缺點,這是Arouter這個框架優秀的重要原因之一。當然Arouter還有一些優秀的功能,比如攔截器,依賴注入等,總之,功能全,性能好,使用方便,這些都是Arouter受歡迎的原因,這點值得我們所有開發者去學習。

總結

最后結合一張圖總結一下Arouter的分組按需加載的邏輯:

Arouter分組按需加載演示圖

圖中左側groupsIndex是“組映射”,右側routes是“路由映射”。Arouter在初始化的時候,通過反射技術,將所有的“組加載器”索引到groupsIndex這個map中,而此時,右側的routes還是空的。在用戶調用navigation()進行跳轉的時候,會根據路徑提取組名,由組名根據groupsIndex獲取到相應組的“組加載器”,由組加載器加載對應組內的路由信息,此時保存全局“路由目標映射的”routes這個map中就保存了剛才組的所有路由映射關系了。同樣,當其他組請求時,其他組也會加載組對應的路由映射,這樣就實現了整個App運行時,只有用到的組才會加到內存中,沒有去過的組就不會加載到內存中,達到了節省內存的目的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM