在日常開發(fā)中,我們常常需要為每個功能頁面分配獨立的路由。傳統(tǒng)做法通常有兩種方案:
-
集中式管理路由:實現(xiàn)一個統(tǒng)一的路由類(如
URLHandler),在其中集中注冊所有路由。新功能上線時,需要手動向該類中添加路由邏輯。 -
模塊化管理路由:為每個功能實現(xiàn)一個對應(yīng)的路由類(如
FunNewURLHandler),再將該類名添加到plist或模塊靜態(tài)數(shù)組中,統(tǒng)一加載。
第一種方案隨著功能數(shù)量增長,URLHandler 會變得臃腫且耦合度高,維護(hù)難度大。第二種方式雖然更合理,但仍然存在一些問題:
- 管理中心化,需要開發(fā)者主動注冊;
- 會增加類的數(shù)量,影響啟動速度;
- 注冊步驟繁瑣,若遺漏注冊路徑(如
plist或模塊靜態(tài)數(shù)組),可能導(dǎo)致路由不生效。
那么是否有一種“去中心化”的方案,使得每個功能頁面只需定義自己的路由及處理邏輯,無需額外注冊操作,App 運行時可自動完成注冊?
這正是本文將介紹的實現(xiàn)方式。
利用 Mach-O 特性實現(xiàn)自動注冊
通過如下方式,我們可以在編譯階段將數(shù)據(jù)寫入 Mach-O 文件的指定段中:
__attribute__((used, section("__DATA,__urlhandle")))
-
used:防止 Release 環(huán)境下被鏈接器優(yōu)化掉; -
section:指定寫入位置,這里是__DATA段下的__urlhandle區(qū)域。
1. 路由結(jié)構(gòu)體定義
typedef void(*UrlHandleImp)(NSURL*, UINavigationController*, NSDictionary*);
struct URL_HANDLE {
const char *url;
UrlHandleImp handle;
};
url:存儲路由路徑;
handle:指向路由觸發(fā)時調(diào)用的函數(shù)指針。
通過 __attribute__ 聲明,將路由所對應(yīng)的 static struct URL_HANDLE寫入指定 section。App 運行時,使用 dyld 懶加載讀取,構(gòu)建路由表,并添加至 FBURLRouter中。
2. 路由注冊宏定義
為了方便注冊路由,定義了一套宏:
#define URL_EXPORT(...) \
URL_EXPORT_WITH_LINE_CODE(__LINE__, __VA_ARGS__)
#define URL_EXPORT_WITH_LINE_CODE(line, ...) \
static void URL_EXPORT_IMPL_NAME(line)(NSURL *, UINavigationController *, NSDictionary *); \
url_macro_concat(URL_EXPORT_, url_macro_argcount(__VA_ARGS__))(line, __VA_ARGS__) \
static void URL_EXPORT_IMPL_NAME(line)(NSURL *url, UINavigationController *navi, NSDictionary *parameters)
多參數(shù)展開示例(最多支持10個參數(shù)):
#define URL_EXPORT_10(line, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) \
URL_EXPORT_9(line, _1, _2, _3, _4, _5, _6, _7, _8, _9) \
URL_EXPORT_EACH(line, _10, 10)
每個url被展開為:
#define URL_EXPORT_EACH(line, router, suffix) \
__attribute__((used, section(FB_URL_EXPORT_SEGMENT "," URL_EXPORT_SECTION))) \
static const struct URL_HANDLE URL_EXPORT_VAR_NAME(line, suffix) = { \
(const char *)((char[sizeof(format_to_c_conststring(router))]){format_to_c_conststring(router)}), \
URL_EXPORT_IMPL_NAME(line), \
};
為了同時支持 NSString* 和 const char*:
#define format_to_c_conststring(a) \
(__builtin_choose_expr( \
__builtin_types_compatible_p(__typeof__(a), NSString*), \
#a, \
a \
))
format_to_c_conststring("c") //展開為 "c"
format_to_c_conststring(@"c") //展開為 "@/"c/""
// 對于NSString轉(zhuǎn)化的c字符串,讀取時需要進(jìn)行處理
NSString *url = [NSString stringWithUTF8String:entry.url];
if ([url hasPrefix:@"@\""] && [url hasSuffix:@"\""] && url.length >= 3) {
url = [url substringWithRange:NSMakeRange(2, url.length-3)];
}
URL_EXPORT 最多支持10個參數(shù),宏使用__LINE__自動生成唯一函數(shù)和變量名,所以在同一源文件中可多次調(diào)用 URL_EXPORT。
3. 路由加載流程
- 使用
URL_EXPORT在編譯期間將路由注入到__urlhandlesection中 - 在 App 啟動時將
AutoRegURLHandler注冊到FBURLRouter; - 為了避免影響啟動速度,
AutoRegURLHandler不立即構(gòu)建路由,延遲至調(diào)用+patterns時; - 構(gòu)建路由時,使用 dyld 讀取
__urlhandlesection中所有的URL_HANDLE結(jié)構(gòu)體,構(gòu)建對應(yīng)的_InnerURLHandler并加入_RegisterHandlers,通知添加路由至_RegisterURLs; - 我們忽略了系統(tǒng)的動態(tài)庫,僅處理 .app 下的動態(tài)庫,同時支持通過修改
EXCLUDE_IMAGE_FOR_URL_HANDLE忽略不必要模塊(目前默認(rèn)跳過 SDK 與 dylib)。
使用示例
單路由注冊
URL_EXPORT(@"/setting") {
SettingViewController *settingVC = [[SettingViewController alloc] init];
[navi pushViewController:settingVC animated:YES];
}
帶參數(shù)的路由注冊
URL_EXPORT("/{courseId}/paramPage") {
NSString *courseId = parameters[@"courseId"];
NSString *param1 = parameters[@"param1"];
NSString *param2 = parameters[@"param2"];
ParamViewController *vc = [[ParamViewController alloc] initWithCourseId:courseId param1:param1 param2:param2];
vc.hidesBottomBarWhenPushed = YES;
[navi pushViewController:vc animated:YES];
}
多路由注冊
#define FuncPageURL_1 @"/func1"
#define FuncPageURL_2 "/func2"
#define FuncPageURL_3 "/func3"
URL_EXPORT(FuncPageURL_1, FuncPageURL_2) {
if ([url.absoluteString containsString:FuncPageURL_1]) {
// 轉(zhuǎn)到 func 頁面 1
} else if ([url.absoluteString containsString:FuncPageURL_2]) {
// 轉(zhuǎn)到 func 頁面 2
}
}
URL_EXPORT(FuncPageURL_3) {
// 轉(zhuǎn)到 func 頁面 3
}
路由校驗機制
AutoRegURLHandler 提供校驗方法,用于檢查:
- 路由是否注冊成功;
- 是否存在重復(fù)路由;
Debug 環(huán)境下使用:
#ifdef DEBUG
NSString *errorString = nil;
BOOL urlValid = [AutoRegURLHandler checkValid:&errorString];
NSAssert(urlValid, errorString);
#endif
?? 項目地址
完整實現(xiàn)與 Demo 示例:
?? https://github.com/linjunyi/URLHandlerDemo