超强超屌的路由框架

image.png

LJRouter:高性能 iOS 路由框架深度解析

1. 概述

LJRouter 是一套结合了 编译期注入运行时动态调用 和 智能参数匹配 的工业级 iOS 路由框架。它旨在解决传统 URL Router 参数类型不安全、硬编码严重的问题,同时比 Protocol Router 具有更高的灵活性。

核心优势:

  • 去中心化注册:利用 Mach-O Section 注入,无需手动维护注册表。

  • 原生调用性能:基于 NSInvocation,损耗极小。

  • 类型安全:完备的编译期与运行期类型检查。

  • 统一管理:将“页面跳转”与“功能调用”统一分发。


2. 核心架构设计

LJRouter 内部通过单例维护核心逻辑,采用双中心级联查找机制来分离职责。

2.1 核心组件

  • LJRouter (核心单例): 对外暴露接口,管理内部组件。

  • LJInvocationCenter (调用中心): 存储路由规则 (LJInvocationCenterItem),执行匹配算法。

  • LJRouterConvertManager (类型转换): 负责 JSON/String 到 Native 类型(int, float, NSNumber 等)的自动转换。

  • LJRouterKeyNodeManager (节点监控): 负责路由全生命周期的埋点与监控。

2.2 双中心查找机制

为了区分页面与服务,LJRouter 维护了两个独立的调用中心:

  • pageInvocationCenter:

    • 职责: 专门负责页面路由。

    • 要求: 目标方法必须返回 UIViewController 实例。

    • 特性: 自动处理 VC 的 routerCallBack 和 pageFinishedBlock。

  • actionInvocationCenter:

    • 职责: 负责通用功能调用(如弹窗、埋点、数据处理)。

    • 要求: 可返回任意值或 void。

查找流程 (findInvocation):

  • Page 优先: 收到请求时,优先在 pageInvocationCenter 查找。

  • Action 兜底: 若未找到,再尝试在 actionInvocationCenter 查找。


3. 注册机制:Mach-O Section 注入黑科技

LJRouter 摒弃了在 load 方法中手动注册的方式,利用 Clang 属性将注册信息直接写入二进制文件。

3.1 宏定义注册

开发者在 .m 文件中使用宏即可完成注册,无需对外暴露头文件。

// 定义页面路由:参数为 userId (String)
LJRouterDefinePage("name:String,age:int", "user_detail_page", ...)

3.2 底层存储 (struct LJRouterRegister)

宏在编译预处理阶段展开为静态结构体,并被标记写入 __DATA, __LJRouter 段:

struct LJRouterRegister {
    char* objcFunctionName; // 注册所在的函数名
    char* filePath;         // 文件路径
    BOOL isAction;          // 是 Page 还是 Action
    NSString *key;          // 路由 Key
    NSString *returnTypeName; // 返回类型
    struct LJRouterRegisterParam *params; // 参数列表
    uint32_t paramscount;   // 参数个数
    NSString *selName;      // 对应的方法名
};

3.3 启动加载 (loadAllPageProperty)

在 App 启动或首次使用时:

  • 遍历 Image: 使用 _dyld_image_count 和 _dyld_get_image_header 遍历动态库和主程序。

  • 读取 Section: 使用 getsectiondata 直接读取 __LJRouter 段数据。

  • 内存映射: 将结构体转换为内存对象 LJInvocationCenterItem 并存入字典。

  • 优势: 相比执行成千上万次 registerURL 方法,直接读取二进制段效率极高。

宏观数据流向 (从编译到运行)

graph TD subgraph 编译期 [阶段一 编译期 写入] SourceCode["代码 (.m 文件)
LJRouterRegistAction(...)"] -->|Clang 预处理| MacroExpand["展开为 C Struct
(struct LJRouterRegister)"] -->|编译器写入| MachO["二进制文件 (Mach-O)
Segment: __DATA
Section: __LJRouter"] end subgraph 运行期 [阶段二:运行期 读取] MachO -->|dyld 加载| Memory["内存中的 Raw Data
(二进制结构体数组)"] Memory -->|1 getsectiondata| Pointer["获取内存指针"] Pointer -->|2 内存映射 & 转换| OC_Objects["OC 对象
(LJInvocationCenterItem)"] OC_Objects -->|3 存储| Map["全局路由表 (NSDictionary)
allItem"] end style MachO fill:#f9f,stroke:#333,stroke-width:2px style Map fill:#bbf,stroke:#333,stroke-width:2px

运行时详细执行流程 (loadAllPageProperty)

flowchart TD Start(开始: loadAllPageProperty) --> InitLoop[初始化: 遍历 Image 计数器 i = 0] subgraph 遍历所有镜像 [Image 遍历循环] InitLoop --> CheckCount{i < _dyld_image_count ?} CheckCount -- No --> End CheckCount -- Yes --> GetHeader["获取 Header
header = _dyld_get_image_header(i)"] GetHeader --> GetSection["读取自定义段
getsectiondata(header, '__LJRouter', ...)"] GetSection --> HasSection{该镜像有
__LJRouter 段吗?} HasSection -- No (系统库等) --> NextImage[i++] HasSection -- Yes (业务库) --> GetPointer[获取段的内存首地址 & 大小] subgraph 解析结构体 [结构体转换循环] GetPointer --> LoopStructs[遍历内存块中的 Struct] LoopStructs --> ReadCStruct["读取 C 结构体
(struct LJRouterRegister)"] ReadCStruct --> Convert["关键转换 (内存映射)
1. C String -> NSString
2. 拼接方法签名
3. 构造 LJInvocationCenterItemRule"] Convert --> AddToDict["存入字典
[allItem setObject:item forKey:key]"] AddToDict --> NextStruct{还有下一个 Struct?} NextStruct -- Yes --> LoopStructs end NextStruct -- No --> NextImage end NextImage --> CheckCount style GetSection fill:#ff9,stroke:#333 style Convert fill:#9f9,stroke:#333 style AddToDict fill:#9ff,stroke:#333

图解说明

  • 左侧/上侧(编译期)

    • 你写的宏(LJRouterRegistAction)就像是“刻模具”。

    • 编译器把这些模具刻进了二进制文件的一个特定抽屉(__DATA, __LJRouter)里。这一步在 App 还没运行的时候就做完了。

  • 右侧/下侧(运行期 - 遍历循环)

    • _dyld_image_count():就像知道房间总数。

    • _dyld_get_image_header(i):就像拿到每个房间的钥匙。

    • getsectiondata(…):拿着钥匙进房间,直奔那个叫 __LJRouter 的抽屉。

      • 注意:系统自带的库(UIKit, Foundation)没有这个抽屉,代码会直接跳过(No 分支),效率很高。
    • Convert (转换):把抽屉里压扁的“纸片”(C Struct)拿出来,吹气膨胀成“立体模型”(Objective-C 对象)。

    • AddToDict (存表):把模型摆到架子上(Dictionary),以后有人要找,直接指架子就行。

这两张图结合起来,就是 LJRouter “编译期埋雷,运行期扫雷” 的完整逻辑。


4. 智能路由匹配与调用

LJRouter 抛弃了简单的 Key-Value 匹配,实现了类似 C++ 的方法重载 (Overload) 支持。

4.1 评分匹配算法 (nearleastInvocationWithKey)

同一个 Key 可以注册多个不同参数列表的方法。调用时,路由内部会进行评分:

  • 遍历规则: 获取该 Key 下所有注册的 Rule。

  • 必选校验: 检查传入参数是否包含所有 isRequire 的字段。

  • 最大匹配原则: 选择匹配参数数量最多的 Rule。

  • 次序兜底: 若匹配数相同,选择定义顺序靠前的(sortIndex)。

4.2 参数传递与自动转换

  • 原生参数: 基于 NSInvocation,支持传递复杂对象(Model, UIImage)。

  • 自动类型转换: LJRouterConvertManager 内置了基础类型转换逻辑。若 URL 传入字符串 “123”,目标参数是 int 或 NSNumber,会自动转换。

  • 剩余参数 (LJRouterAdditionalParams): 未被方法参数消耗的字段,会自动聚合到 LJRouterAdditionalParams 类型的参数中,用于向前兼容。

这段关于 NSInvocation 和 参数传递 的逻辑确实比较底层,涉及到了 Objective-C 的 runtime 内存模型。为了让你彻底理解,我们把那些复杂的术语先抛开,用**“万能填空题”**的比喻来拆解这个过程。


4.2.1. 核心道具:NSInvocation 是什么?

想象你平时写代码调用方法 [userWrapper setAge:18],就像是直接说话,说完就执行了。

而 NSInvocation 就像是一张**“待执行的任务单”**(或者说是“填空题”)。它不会马上执行,而是允许你先把所有条件都填好,最后喊一声“Action”才执行。

这张任务单长这样:

  • 目标 (Target): userWrapper (那个对象)

  • 动作 (Selector): setAge: (那个方法)

  • 参数 1 (Argument at index 2): ____ (待填空的内存槽)

    • (注:index 0 是 self,index 1 是 _cmd,所以参数从 index 2 开始)

NSInvocation 的超能力:它不在乎你填进来的是什么,它只关心内存地址。只要你能把数据的内存地址给它,它就能把数据塞进那个方法的参数槽里。


4.2.2. LJRouter 是如何“填空”的?

当你在 LJRouter 里调用 routerKey:@”…” params:@{…} 时,LJRouter 就在做一个**“匹配 -> 转换 -> 填空”**的过程。

我们分三种情况来看看它是怎么填空的:

情况 A:传递复杂对象(比如 UIImage, User, UIView)

场景

  • 目标方法:- (void)openImage:(UIImage *)image;

  • 传入参数:params = @{ @“image”: myImageObject }

原理
在 Objective-C 中,所有的对象(Object)其实都是指针
UIImage *image 这个变量,本质上存的是一块内存地址(比如 0x10086)。

填空过程

  • LJRouter 看一眼方法签名:发现参数类型是对象 (@)。

  • 看一眼传入参数:发现 myImageObject 也是个对象。

  • 直接填入:LJRouter 直接把 myImageObject 的地址(0x10086)塞进 NSInvocation 的槽里。

通俗解释
就像方法需要一张“身份证”,你手里正好有一张“身份证”,直接递过去就行了。不需要任何转换,这就是为什么支持传递 Model、View 的原因——本质上只是传了个地址而已。


情况 B:传递基础类型(比如 int, BOOL)与自动转换

场景

  • 目标方法:- (void)setAge:(int)age;

  • 传入参数:params = @{ @“age”: @“18” } (注意这里传的是字符串或者 NSNumber)

难题
目标方法要的是一个 Raw Int (比如占 4 个字节的整数),但你给的是一个 OC 对象 (NSString 或 NSNumber)。你不能把一个对象塞进 int 的坑里,会爆炸。

填空过程(LJRouterConvertManager 出场)

  • LJRouter 看一眼方法签名:发现参数类型是 int (i)。

  • 看一眼传入参数:发现给的是对象 @“18”。类型不匹配!

  • 自动转换

    • 调用 LJRouterConvertManager。

    • Manager 内部执行逻辑:int value = [@“18” intValue];

    • Manager 申请一小块临时内存(buffer),把 18 这个整数写进去。

  • 填入地址:把这块临时内存的地址塞给 NSInvocation。

  • 释放:用完后,清理这块临时内存。

通俗解释
方法需要“现金”(int),但你给的是“支票”(NSNumber)。LJRouter 会先去银行把支票兑换成现金,然后再把现金塞进信封里给目标方法。


情况 C:剩余参数 (LJRouterAdditionalParams)

场景

  • 目标方法:- (void)updateUser:(NSString *)name extra:(NSDictionary *)extra;

  • 传入参数:params = @{ @“name”: @“Jack”, @“age”: 18, @“sex”: @“M” }

填空过程

  • 匹配 name:找到 params 里的 name,填入第一个空。

  • 发现剩余:age 和 sex 在目标方法里没有对应的独立参数。

  • 打包:LJRouter 发现目标方法里有一个特殊的参数类型叫 LJRouterAdditionalParams(其实就是 NSDictionary)。

  • 注入:它自动把剩下的 age 和 sex 打包成一个新的字典,填入 extra 这个空里。

通俗解释
这就是“吃不了兜着走”。方法只吃了“name”,剩下的菜(age, sex),LJRouter 帮你打包进一个袋子(Dictionary)里,一并交给方法。


4.2.3. 为什么要用 NSInvocation?

你可能会问,为什么不直接用 performSelector?

  • performSelector 最多只能传 2 个参数,而且只能传对象。

  • NSInvocation 可以传无限个参数,而且不限类型(int, float, struct, block 都可以)。

4.2.4. 总结图解

graph LR Input[传入参数 Params] Method[目标方法签名 Signature] Invocation[NSInvocation 任务单] Input -->|1 分析参数| Router[LJRouter 核心逻辑] Method -->|2 分析需求| Router Router -- 对象直接传 --> Direct[直接把对象指针塞入槽位] Router -- 类型不符 --> Convert[LJRouterConvertManager
拆箱: NSNumber -> int] Convert --> Buffer[临时内存 Buffer] Buffer -->|把 Buffer 地址塞入槽位| Invocation Direct --> Invocation Invocation -->|invoke| RealCall[执行真正的代码!]
flowchart LR subgraph BuildTime[编译期] A[LJRouterInit
LJRouterRegistAction
等宏展开] --> B[生成 struct LJRouterRegister
+ 参数数组] B --> C[写入 Mach-O 段
__DATA,__LJRouter] A -.-> D[LJRouterUse* 宏
写入 __DATA,__LJRouterUseINF] end subgraph Launch[运行期首次使用] E[_dyld_image_count/_get_image_header
遍历所有镜像] --> F[getsectiondata 读取
__LJRouter 数据] F --> G[构造 LJInvocationCenterItemRule
+ LJInvocationCenterItem] G --> H[注册到 pageInvocationCenter / actionInvocationCenter
allItem & oAllItem 字典] D --> I[checkAllMethodTypeName:
遍历 __LJRouterUseINF
校验签名一致性] end subgraph Routing[调用阶段] J[routerKey/routerUrlString] --> K[pageInvocationCenter 查找
NSInvocation] K -->|未命中| L[actionInvocationCenter 查找] K -->|命中| M[prepareInvocation:
注入 senderContext、callback、additionalParams] L -->|命中| M M --> N[NSInvocation setArgument:
原生对象 / ConvertManager 转换] N --> O[invoke -> 获取 UIViewController/结果] O --> P[openControllerBlock or resultBlock
处理返回/回调] O --> Q[LJRouterKeyNodeManager
记录关键节点] end

NSInvocation 构建细节

flowchart LR Data[原始 params JSON / 对象] --> Match[匹配到 LJInvocationCenterItemRule] Match --> Loop{遍历 paramas} Loop -->|对象或 block| SetObj[setArgument:&obj] Loop -->|标量/结构| Conv[LJRouterConvertManager
转成 C buffer] Conv --> SetC[setArgument:buffer] Loop -->|sender| SenderCtx[LJRouterSenderContext
注入上下文] Loop -->|callback| WrapCB[包装 block
桥接回调用方] Loop -->|Additional| AddParams[注入剩余字段] SetObj --> INV[NSInvocation ready] SetC --> INV SenderCtx --> INV WrapCB --> INV AddParams --> INV INV --> Invoke[invocation invoke
得到结果/VC]

5. 上下文注入与依赖解耦

解决了“在模块内部如何获取外部环境(如当前 VC)”的痛点。

5.1 自动注入 SenderContext

如果在注册方法中声明了 LJRouterSenderContext * 参数,Router 会自动构建并注入 Context 对象。

  • 原理: 利用 Responder Chain,从调用方传入的 sender(通常是 View)向上查找,获取 contextViewController、rootWindow、appDelegate 等。

5.2 回调注入

  • LJRouterCallbackBlock: 自动将调用方传入的 Block 桥接到目标方法。

  • VC 属性注入: 若目标 VC 实现了 routerCallBack 属性,Router 创建 VC 后会自动赋值。

5.3 页面打开策略

openViewController 方法支持多种策略:

  • 优先尝试 openControllerWithContextBlock(带上下文)。

  • 检查 VC 是否实现 LJNavigationOpenedByViewController 协议。

  • 最后由容器层决定是 Push 还是 Present。


6. 安全性与监控

6.1 类型安全检查 (Type Safety)

动态路由最大的风险是参数错乱导致 Crash。LJRouter 提供了极致的防御:

  • 编译/启动自检:

    • 开发者使用 LJRouterUsePage 宏时,调用签名被写入 __DATA, __LJRouterUseINF。

    • 启动时,checkAllMethodTypeName 对比“注册信息”与“使用信息”。

  • CheckPolicy: 发现类型不匹配时,可配置策略:

    • Assert: 开发环境直接崩溃提醒。

    • Console: 控制台报警。

    • Alert: 弹窗提示。

6.2 拦截器机制 (Interceptor)

模块作用关键代码
LJRouter+Interceptor 分类提供拦截器增删、执行逻辑;维护优先级队列和实例缓存Pods/LJBaseRouter/LJBaseRouter/Classes/LJRouter/LJRouter+Interceptor.m:21
LJRouterInterceptor 协议规定拦截器三要素:判定方法、真正执行、可选优先级Pods/LJBaseRouter/LJBaseRouter/Classes/LJRouter/LJRouter+Interceptor.h:33
extraParams运行期传入的上下文字典,含 VC、sender、pageBlock、callback 等Pods/LJBaseRouter/LJBaseRouter/Classes/LJRouter/LJRouter+Interceptor.h:23
LJRouterKeyNodeManager在拦截发生时记录关键节点 BeforeInterrupt / AfterInterruptPods/LJBaseRouter/LJBaseRouter/Classes/LJRouter/LJRouter+Interceptor.m:148
routerUrlString: / routerKey:负责在不同时间点调用 _handleRouterInterceptorPods/LJBaseRouter/LJBaseRouter/Classes/LJRouter/LJUrlRouter.m:229、Pods/LJBaseRouter/LJBaseRouter/Classes/LJRouter/LJRouter.m:578

支持 AOP 切面编程,处理路由跳转前后的逻辑(如登录检查)。

classDiagram class LJRouter { -NSMutableArray *interceptorClassArray -NSMutableArray *highInterceptorClassArray -NSMutableArray *lowInterceptorClassArray -NSMutableDictionary *interceptorInstanceDic +addRouterInterceptor(Class) +removeRouterInterceptor(Class) -_handleRouterInterceptor(...) } class Interceptor { <> +interceptorRouter:schemeParams:opportunity: +startInterceptor:schemeParams:extraParams:complete: +priority (optional) } LJRouter --> "1" interceptorClassArray LJRouter --> "1" highInterceptorClassArray LJRouter --> "1" lowInterceptorClassArray LJRouter --> "1" interceptorInstanceDic LJRouter --> Interceptor : manages

 三组数组:highInterceptorClassArray、interceptorClassArray(默认)、lowInterceptorClassArray 存放类对象(Pods/LJBaseRouter/LJBaseRouter/Classes/LJRouter/LJRouter+Interceptor.m:22、35、44)。

  - 实例字典:interceptorInstanceDic 用 URL/路由 key 作为 key,保证同一个 scheme 在未完成前不会重复实例化(Pods/LJBaseRouter/LJBaseRouter/Classes/LJRouter/LJRouter+Interceptor.m:55)。  

  • 生命周期: BeforeInterrupt (前置拦截) -> 路由执行 -> AfterInterrupt (后置处理)。
sequenceDiagram participant Router as LJRouter participant List as InterceptorClassArray participant Cls as InterceptorClass participant Inst as InterceptorInstance participant KeyNode as LJRouterKeyNodeMgr Router->>List: 组装高/默认/低优先级数组 loop 每个拦截器 Router->>Cls: +interceptorRouter:schemeParams:opportunity: alt 返回 ProcessedAndBreak / Continue note left of Cls: 命中 Router->>KeyNode: 记录 Before/After Interrupt Router->>Inst: 若未在实例字典中,init + 存入 Inst->>Inst: startInterceptor(..., complete) Inst-->>Router: complete block 清理字典 note right of Router: 结束循环 (Break) else NotProcessed note left of Cls: 未命中,继续 end end

判定阶段:调用 +interceptorRouter:schemeParams:opportunity:(类方法,无需实例化),返回三个枚举之一:

      - NotProcessed:未拦截,继续下一个类。

      - ProcessedAndBreak:拦截并阻断后续流程。

      - ProcessedAndContinue:拦截但允许路由继续(Pods/LJBaseRouter/LJBaseRouter/Classes/LJRouter/LJRouter+Interceptor.h:12)。

  - 实例化阶段:若某个类返回 ProcessedAnd* 并且 interceptorInstanceDic 中不存在该 scheme,立刻 [[cls alloc] init],保存到字典防止同一 scheme 重入(Pods/LJBaseRouter/LJBaseRouter/Classes/LJRouter/

    LJRouter+Interceptor.m:155)。

  - 执行阶段:调用 -startInterceptor:schemeParams:extraParams:complete:,并在 complete 中移除缓存;complete 由拦截器负责调用,允许异步操作(Pods/LJBaseRouter/LJBaseRouter/Classes/LJRouter/

    LJRouter+Interceptor.m:165)。

  - 关键节点:一旦 result = ProcessedAndBreak,立刻通过 LJRouterKeyNodeManager 记录节点,注明拦截器类名与 URL/参数(Pods/LJBaseRouter/LJBaseRouter/Classes/LJRouter/LJRouter+Interceptor.m:148)。

 4. 触发点与时序

  - Before 机会:

      - routerUrlString: 在解析完 URL(_converturl)后、查找 NSInvocation 之前就调用 handleRouerInterceptor:LJRouterInterceptorOpportunityBefore,传入 result=nil(Pods/LJBaseRouter/LJBaseRouter/Classes/LJRouter/

        LJUrlRouter.m:229)。

      - 旧接口 routerUrlString:pageBlock: 同样在 LJRouter routerKey:… 前调用 _handleRouterInterceptor(Pods/LJBaseRouter/LJBaseRouter/Classes/LJRouter/LJUrlRouter.m:184)。

  - After 机会:

      - routerKey:url:data: 在成功拿到 VC/结果后,组装 extraParams(包含 kRouterTargetVC、kRouterSender 等)再调用 _handleRouterInterceptor:url … opportunity:After(Pods/LJBaseRouter/LJBaseRouter/Classes/

        LJRouter/LJRouter.m:578)。

      - URL 新接口 routerUrlString:sender:callback:resultBlock: 在结果回调后调用 handleRouerInterceptor:After(Pods/LJBaseRouter/LJBaseRouter/Classes/LJRouter/LJUrlRouter.m:265)。

  extraParams 内容

  - kRouterTargetVC:页面路由的 VC。

  - kRouterSender:原始 sender。

  - kRouterPageBlock/kRouterCallbackBlock:调用方的 result/callback block。

  - kRouterCanNotRouterBlock:旧接口的失败回调(Pods/LJBaseRouter/LJBaseRouter/Classes/LJRouter/LJRouter+Interceptor.h:23)。

  • 控制权: 拦截器可返回 ProcessedAndBreak 中断路由。

示例流程:

flowchart TD URL[url string 输入] --> Parse[解析 key/params] Parse --> BeforeIntercept %% 修正拼写 Rouer -> Router,并添加引号包裹含()的文本 BeforeIntercept["handleRouterInterceptor(Before)"] -->|ProcessedAndBreak| Stop[终止路由] BeforeIntercept -->|NotProcessed / Continue| Lookup[page/action Invocation 匹配] Lookup --> Invoke[NSInvocation invoke] Invoke --> AfterIntercept["handleRouterInterceptor(After)"] AfterIntercept --> Complete[返回 result、执行 resultBlock] Stop --> ReturnFalse[routerUrlString 返回 NO]

  判定方法要轻量:+interceptorRouter:schemeParams:opportunity: 是在主线程同步执行的,适合快速判断;任何耗时逻辑应放在实例 startInterceptor 中。

  complete 调用必不可少:忘记调用会导致 interceptorInstanceDic 中残留对象,同一 scheme 再次触发前置拦截时不会创建新实例。

  extraParams 的生命周期:After 阶段可能拿到 nil 的 kRouterTargetVC(Action 路由),拦截器要做好判空。

  关键节点整合:拦截器阻断后会触发 BeforeInterrupt 或 AfterInterrupt 节点,可以结合 LJRouterExportKeyNodeObserver 快速定位是谁拦截了某条路由。

  ———

  综上,LJRouter 的拦截器是针对路由事件的“分层钩子”机制:通过优先级队列 + 类方法判定 + 实例化执行 + 上下文透传,构建出一个高内聚的前/后拦截链路,而无需对任意方法做 swizzling 或切面编程。


6.3 节点监控 (KeyNode)

LJRouterKeyNodeManager 定义了路由生命周期的关键节点(KeyNode):

  • FindInvocation: 查找开始。

  • BeforeInterrupt: 进入拦截器。

  • RouterFinished: 路由完成。

  • 监控导出: 配合 LJRouterExportKeyNodeObserver,业务方可轻松接入 APM 系统,监控路由性能和死链。


7. 总结

LJRouter 的设计精髓在于平衡了灵活性安全性

  • 底层技术: 利用 Mach-O Section 实现了真正的分布式注册,主工程零感知。

  • 调用体验NSInvocation 带来原生级体验,支持方法重载参数自动补全

  • 开发效率: 宏定义简单快捷,配套的静态分析工具类型检查杜绝了运行时错误。

  • 扩展能力: 完善的拦截器和节点监控,使其成为一个可治理、可观测的路由系统。