美文网首页
OC 版的 JDRouter

OC 版的 JDRouter

作者: 大成小栈 | 来源:发表于2025-07-18 17:12 被阅读0次

JDRouter 完整实现及子模块调用流程

1. JDRouter 核心实现

  • JDRouter.h
#import <Foundation/Foundation.h>

// 模块接口声明宏
#define JDROUTER_EXTERN_METHOD(m, i, p, c) \
+ (id) routerHandle_##m##_##i:(NSDictionary*)arg callback:(void(^)(id result))callback

typedef void (^JDRouterHandler)(NSDictionary *params, void(^completion)(id result));
typedef BOOL (^JDInterceptorBlock)(NSString *url, NSDictionary *params);

@interface JDRouter : NSObject

// 单例
+ (instancetype)sharedRouter;

// 基础路由注册
- (void)registerURL:(NSString *)URL handler:(JDRouterHandler)handler;

// URL 跳转
- (BOOL)openURL:(NSString *)URL;
- (BOOL)openURL:(NSString *)URL params:(NSDictionary *)params;
- (BOOL)openURL:(NSString *)URL params:(NSDictionary *)params completion:(void(^)(id result))completion;

// 拦截器管理
- (void)addInterceptor:(JDInterceptorBlock)interceptor;
- (void)removeInterceptor:(JDInterceptorBlock)interceptor;

// 自动注册接口
+ (void)enableAutoRegister;

@end
  • JDRouter.m
#import "JDRouter.h"
#import <objc/runtime.h>

@interface JDRouter ()
@property (nonatomic, strong) NSMutableDictionary<NSString *, JDRouterHandler> *routeMap;
@property (nonatomic, strong) NSMutableArray<JDInterceptorBlock> *interceptors;
@property (nonatomic, strong) dispatch_semaphore_t lock;
@property (nonatomic, assign) BOOL autoRegisterEnabled;
@end

@implementation JDRouter

+ (instancetype)sharedRouter {
    static JDRouter *instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[JDRouter alloc] init];
    });
    return instance;
}

- (instancetype)init {
    if (self = [super init]) {
        _routeMap = [NSMutableDictionary dictionary];
        _interceptors = [NSMutableArray array];
        _lock = dispatch_semaphore_create(1);
    }
    return self;
}

#pragma mark - 路由注册
- (void)registerURL:(NSString *)URL handler:(JDRouterHandler)handler {
    if (!URL || !handler) return;
    
    dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
    NSString *normalizedURL = [[URL stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] lowercaseString];
    self.routeMap[normalizedURL] = [handler copy];
    dispatch_semaphore_signal(_lock);
}

#pragma mark - URL 跳转
- (BOOL)openURL:(NSString *)URL {
    return [self openURL:URL params:nil completion:nil];
}

- (BOOL)openURL:(NSString *)URL params:(NSDictionary *)params {
    return [self openURL:URL params:params completion:nil];
}

- (BOOL)openURL:(NSString *)URL params:(NSDictionary *)params completion:(void(^)(id))completion {
    // 1. 标准化URL
    NSString *normalizedURL = [[URL stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] lowercaseString];
    
    // 2. 执行拦截器链
    for (JDInterceptorBlock interceptor in self.interceptors) {
        if (!interceptor(normalizedURL, params)) {
            NSLog(@"[JDRouter] 跳转被拦截: %@", URL);
            return NO;
        }
    }
    
    // 3. 匹配Handler
    JDRouterHandler handler = nil;
    dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
    handler = self.routeMap[normalizedURL];
    dispatch_semaphore_signal(_lock);
    
    if (!handler) {
        // 降级处理
        [self degradeToWebView:normalizedURL params:params];
        return NO;
    }
    
    // 4. 参数合并与执行
    NSDictionary *queryParams = [self parseQueryParameters:normalizedURL];
    NSMutableDictionary *finalParams = [NSMutableDictionary dictionaryWithDictionary:params];
    [finalParams addEntriesFromDictionary:queryParams];
    
    // 5. 执行路由处理器
    handler(finalParams, completion);
    
    return YES;
}

#pragma mark - 拦截器管理
- (void)addInterceptor:(JDInterceptorBlock)interceptor {
    if (interceptor) {
        dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
        [self.interceptors addObject:[interceptor copy]];
        dispatch_semaphore_signal(_lock);
    }
}

- (void)removeInterceptor:(JDInterceptorBlock)interceptor {
    dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
    [self.interceptors removeObject:interceptor];
    dispatch_semaphore_signal(_lock);
}

#pragma mark - 自动注册
+ (void)enableAutoRegister {
    [JDRouter sharedRouter].autoRegisterEnabled = YES;
    [self performAutoRegistration];
}

+ (void)performAutoRegistration {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self scanAndRegisterAllModules];
    });
}

+ (void)scanAndRegisterAllModules {
    int classCount = objc_getClassList(NULL, 0);
    Class *classes = (Class *)malloc(sizeof(Class) * classCount);
    classCount = objc_getClassList(classes, classCount);
    
    for (int i = 0; i < classCount; i++) {
        Class cls = classes[i];
        
        // 跳过系统类
        if ([NSStringFromClass(cls) hasPrefix:@"NS"] || 
            [NSStringFromClass(cls) hasPrefix:@"UI"]) {
            continue;
        }
        
        unsigned int methodCount = 0;
        Method *methods = class_copyMethodList(object_getClass(cls), &methodCount);
        
        for (unsigned int j = 0; j < methodCount; j++) {
            SEL selector = method_getName(methods[j]);
            NSString *methodName = NSStringFromSelector(selector);
            
            if ([methodName hasPrefix:@"routerHandle_"]) {
                NSArray *components = [methodName componentsSeparatedByString:@"_"];
                if (components.count >= 3) {
                    NSString *module = components[1];
                    NSString *interface = components[2];
                    NSString *URL = [NSString stringWithFormat:@"jd://%@/%@", module, interface];
                    
                    [self registerDynamicRouteForClass:cls selector:selector URL:URL];
                }
            }
        }
        free(methods);
    }
    free(classes);
}

+ (void)registerDynamicRouteForClass:(Class)cls selector:(SEL)selector URL:(NSString *)URL {
    [[JDRouter sharedRouter] registerURL:URL handler:^(NSDictionary *params, void (^completion)(id)) {
        NSMethodSignature *signature = [cls methodSignatureForSelector:selector];
        if (signature.numberOfArguments != 4) {
            NSLog(@"[JDRouter] 方法参数数量不匹配: %@", NSStringFromSelector(selector));
            return;
        }
        
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
        invocation.target = cls;
        invocation.selector = selector;
        
        // 设置参数
        [invocation setArgument:&params atIndex:2];
        [invocation setArgument:&completion atIndex:3];
        
        [invocation invoke];
        
        if (signature.methodReturnLength > 0) {
            __unsafe_unretained id returnValue;
            [invocation getReturnValue:&returnValue];
            if (completion) completion(returnValue);
        }
    }];
}

#pragma mark - 辅助方法
- (NSDictionary *)parseQueryParameters:(NSString *)URLString {
    NSURL *url = [NSURL URLWithString:URLString];
    if (!url) return @{};
    
    NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
    NSMutableDictionary *params = [NSMutableDictionary dictionary];
    
    for (NSURLQueryItem *item in components.queryItems) {
        if (item.name && item.value) {
            params[item.name] = item.value;
        }
    }
    return [params copy];
}

- (void)degradeToWebView:(NSString *)URL params:(NSDictionary *)params {
    NSLog(@"[JDRouter] 降级到WebView: %@", URL);
    // 实际项目中会打开H5页面
}

@end

2. 子模块实现(商品模块)

  • JDProductModule.h
#import <Foundation/Foundation.h>

@interface JDProductModule : NSObject

// .h文件是空的

@end
  • JDProductModule.m
#import "JDProductModule.h"
#import "JDProductDetailViewController.h"
#import "JDRouter.h"

@implementation JDProductModule

+ (void)load {
    // 确保自动注册已启用
    [JDRouter enableAutoRegister];
}

// 商品详情页实现
JDROUTER_EXTERN_METHOD(JDProductModule, detail, arg, callback) {
    // 1. 参数解析
    NSString *productId = arg[@"id"];
    if (!productId) {
        if (callback) {
            callback(@{@"status": @"error", @"message": @"缺少商品ID"});
        }
        return nil;
    }
    
    // 2. 创建视图控制器
    JDProductDetailViewController *vc = [[JDProductDetailViewController alloc] init];
    vc.productId = productId;
    vc.sourceFrom = arg[@"source"] ?: @"unknown";
    
    // 3. 页面跳转
    UIViewController *topVC = [self topViewController];
    if ([topVC isKindOfClass:[UINavigationController class]]) {
        [(UINavigationController *)topVC pushViewController:vc animated:YES];
    } else {
        [topVC presentViewController:vc animated:YES completion:nil];
    }
    
    // 4. 返回结果
    NSDictionary *result = @{
        @"status": @"success",
        @"viewController": vc,
        @"timestamp": @(NSDate.date.timeIntervalSince1970)
    };
    
    if (callback) callback(result);
    return result;
}

// 获取顶层控制器
+ (UIViewController *)topViewController {
    UIViewController *rootVC = UIApplication.sharedApplication.keyWindow.rootViewController;
    return [self findTopViewController:rootVC];
}

+ (UIViewController *)findTopViewController:(UIViewController *)vc {
    if ([vc isKindOfClass:[UINavigationController class]]) {
        return [self findTopViewController:[(UINavigationController *)vc topViewController]];
    } else if ([vc isKindOfClass:[UITabBarController class]]) {
        return [self findTopViewController:[(UITabBarController *)vc selectedViewController]];
    } else if (vc.presentedViewController) {
        return [self findTopViewController:vc.presentedViewController];
    }
    return vc;
}

@end

3. 拦截器实现(登录检查)

// AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    // 添加登录拦截器
    [[JDRouter sharedRouter] addInterceptor:^BOOL(NSString *url, NSDictionary *params) {
        // 需要登录的页面路径
        if ([url containsString:@"jd://product/detail"] ||
            [url containsString:@"jd://order/create"]) {
            
            if (![UserManager isLogin]) {
                NSLog(@"[JDRouter] 需要登录,跳转到登录页");
                
                // 保存原始请求
                NSDictionary *redirectInfo = @{
                    @"url": url,
                    @"params": params ?: @{}
                };
                [[NSUserDefaults standardUserDefaults] setObject:redirectInfo forKey:@"redirectAfterLogin"];
                
                // 跳转到登录页
                [[JDRouter sharedRouter] openURL:@"jd://user/login"];
                return NO; // 中断原始跳转
            }
        }
        return YES;
    }];
    
    return YES;
}

4. 调用商品模块的完整流程

  • 其他页面调起商品详情
- (void)didSelectProduct:(NSString *)productId {
    NSString *url = [NSString stringWithFormat:@"jd://product/detail?id=%@&source=home", productId];
    
    [[JDRouter sharedRouter] openURL:url params:@{
        @"promotion": @"summer_sale",
        @"position": @(self.selectedIndex)
    } completion:^(id result) {
        if ([result[@"status"] isEqualToString:@"success"]) {
            NSLog(@"✅ 成功打开商品页: %@", productId);
        } else {
            NSLog(@"❌ 打开商品页失败: %@", result[@"message"]);
        }
    }];
}

5. 完整调用时序图

完整调用时序图

6. 关键步骤解析

  1. 自动注册触发

    • App 启动时调用 [JDRouter enableAutoRegister]
    • +load 中扫描所有类方法
    • 发现 routerHandle_Product_detail: 方法
    • 注册路由 jd://product/detail
  2. 调用过程

    调用过程
  1. 参数传递

    • URL 参数: id=123&source=home
    • 代码参数: @{@"promotion": @"summer_sale"}
    • 合并后参数:
      {
        "id": "123",
        "source": "home",
        "promotion": "summer_sale",
        "position": 2
      }
      
  2. 跨模块通信

    • 通过 completion block 返回结果:
      @{
        @"status": @"success",
        @"viewController": vc,
        @"timestamp": 1689291000
      }
      

7. 高级功能扩展

  1. 路由调试工具
// JDRouter+Debug.m
@implementation JDRouter (Debug)

- (void)printAllRoutes {
    dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
    NSLog(@"====== 注册的路由 ======");
    [self.routeMap enumerateKeysAndObjectsUsingBlock:^(NSString *url, JDRouterHandler handler, BOOL *stop) {
        NSLog(@"URL: %@", url);
    }];
    NSLog(@"======================");
    dispatch_semaphore_signal(_lock);
}

- (BOOL)canOpenURL:(NSString *)URL {
    NSString *normalizedURL = [[URL stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] lowercaseString];
    return self.routeMap[normalizedURL] != nil;
}

@end
  1. 动态路由更新
// 服务端下发路由配置
- (void)updateRoutesFromServer {
    [NetworkManager fetchRouteConfig:^(NSDictionary *config) {
        for (NSString *url in config[@"routes"]) {
            [self registerURL:url handler:^(NSDictionary *params, void (^completion)(id)) {
                // 跳转到H5页面
                [self openWebURL:url params:params];
            }];
        }
    }];
}
  1. AOP 路由监控
// 路由性能监控
[JDRouter sharedRouter].globalCompletion = ^(NSString *URL, NSTimeInterval duration) {
    [Analytics logEvent:@"router_performance" params:@{
        @"url": URL,
        @"duration": @(duration)
    }];
};

// 在openURL方法中添加
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
handler(finalParams, ^(id result){
    if (completion) completion(result);
    CFAbsoluteTime endTime = CFAbsoluteTimeGetCurrent();
    if (self.globalCompletion) {
        self.globalCompletion(URL, endTime - startTime);
    }
});
  1. 实际应用场景
  • 商品详情页跳转

    // Swift调用示例
    JDRouter.shared().openURL("jd://product/detail?id=10086", 
                             params: ["source": "recommend"],
                             completion: { result in
         if let resultDict = result as? [String: Any],
            resultDict["status"] as? String == "success" {
             print("成功打开商品页")
         }
    })
    
  • 服务调用

    // 调用加入购物车服务
    [[JDRouter sharedRouter] openURL:@"jd://cart/add" params:@{
         @"product_id": @"P1001",
         @"quantity": @2,
         @"sku": @"red_large"
    }];
    
  • 跨模块回调

    // 支付结果回调
    [[JDRouter sharedRouter] openURL:@"jd://order/pay" params:@{@"order_id": @"O2023001"} completion:^(NSDictionary *result) {
         if ([result[@"status"] isEqual:@"success"]) {
             [self showSuccessMessage:@"支付成功"];
         } else {
             [self showErrorMessage:result[@"reason"]];
         }
    }];
    

总结

JDRouter 大型 App 实现模块化架构的核心基础模块,主要技术点为:

  1. 路由注册

    • 自动扫描 routerHandle_ 前缀的类方法
    • 动态注册到路由表
    • 模块无需手动注册
  2. 路由调用

    • 标准化 URL 处理
    • 拦截器链机制
    • 参数自动合并
    • 支持 completion 回调
  3. 商品模块调用流程

    • 首页调用 openURL("jd://product/detail?id=123")
    • 拦截器检查登录态
    • 执行商品模块的 routerHandle_Product_detail:
    • 创建并跳转到商品详情页
    • 返回执行结果
  4. 高级功能

    • 路由调试工具
    • 动态路由更新
    • AOP 性能监控

这种设计实现了:

  • 自动化注册:模块接口自动发现
  • 安全调用:拦截器统一处理权限
  • 灵活扩展:支持多种场景
  • 高效解耦:模块间无直接依赖

问题:可以省去registerURL:handler: 方法,改为在 openURL: 方法中“直接解析”吗?

1. 关于 registerURL:handler: 方法

这个方法是 JDRouter 乃至所有路由组件的基石。它的核心作用是在路由表(一个字典)中建立一条映射规则,将某个特定的 URL 模式(Pattern)与一个处理该 URL 的代码块(Handler/Block)关联起来。

简单来说:它告诉路由系统:“当有人想打开这个 URL 时,请执行我给你的这段代码”。

2. 工作流程
  1. 注册阶段(App 启动时): 各个模块在自己的 +load 或初始化方法中,调用 registerURL:handler:,传入 URLhandler
  2. 存储: Router 将这对 URL: Handler 以键值对的形式存储在一个内部的、全局的字典(路由表)中。
  3. 调用阶段(运行时):openURL: 被调用时,Router 会用传入的 URL 去路由表中查找匹配的 handler
  4. 执行: 如果找到,则执行该 handler Block,并将参数传递给它;如果没找到,则触发降级策略(如打开 H5 页)。
3. “先注册,后使用”模式的优点
  • 解耦(Decoupling)
    这是最核心的优点。调用方(如订单模块)和目标方(如商品详情页)完全不需要相互引用或导入头文件。它们唯一的联系就是一个约定好的字符串 URL(如 jd://product/detail)。订单模块只知道这个 URL,并不知道背后是哪个 Class、哪个方法在处理它。这极大降低了模块间的依赖关系,使得模块可以独立开发、测试和替换。

  • 集中式管理(Centralized Management)
    路由表成了一个中心化的配置中心。你可以通过打印路由表,一目了然地看到整个 App 所有注册的、可被调用的界面和服务。这对于调试、新人熟悉项目、以及实现动态路由(服务端下发配置)至关重要。

  • 灵活性(Flexibility)
    一个 URL 的 Handler 可以执行任何操作,而不仅仅是创建 VC 和跳转。
    跳转页面:最常用的功能。
    执行业务逻辑:如 jd://log/event?name=click_buy,触发一个埋点日志。
    获取服务/对象:如 jd://service/userInfo,返回当前用户信息的对象。
    动态替换:基于 AB 测试或灰度发布,同一个 URL 在不同条件下可以注册不同的 Handler,指向不同的页面或实现。

  • 安全与降级(Safety & Degradation)
    Router 在 openURL: 时可以先进行统一拦截(Interceptor),进行登录态、权限等检查。如果检查不通过,可以中断跳转。
    如果没有找到注册的 Handler,可以统一降级到一个默认的 H5 页面,保证链接不会失效,提升了应用的鲁棒性。

  • 参数标准化(Parameter Standardization)
    Handler 的签名是统一的 (NSDictionary *params)。这使得参数的传递和解析变得标准化。Router 可以负责将 URL 中的 Query String、openURL: 方法传入的 params 字典、甚至全局上下文参数进行合并,再交给 Handler,简化了开发。

4. 对比方案:直接解析 URL 并执行方法

“在 openURL: 中直接解析 URL,并查找目标类直接执行方法”,这通常被称为 “基于反射(Reflection)的方案”可以实现吗?可以。 但这是一种弱类型、高风险、难维护的方案,在大型项目中被认为是一种反模式(Anti-Pattern)。

如何实现?
假设 URL 格式为:jd://TargetClass/actionName?key1=value1&key2=value2
openURL: 方法中:

  1. 解析出 TargetClass(目标类名)和 actionName(方法名)。
  2. 使用 NSClassFromString(TargetClass) 获取类。
  3. 使用 NSSelectorFromString(actionName) 获取方法选择器。
  4. 使用 performSelector:NSInvocation 来调用方法。

为什么在大型项目中不推荐?

特性 注册模式 (JDRouter) 反射模式 对比分析
安全性 。只能调用已注册的、预期内的功能。 极低。任何类和方法都可以被调用,容易引发安全隐患和崩溃。 反射模式就像把家门钥匙给了所有人,而注册模式只给你想给的人。
可维护性 。路由表清晰,易于查找和管理。 。字符串散落在代码各处,无法集中管理,重构和查找困难。 想象一下,项目有几百个这样的字符串调用,改一个类名或方法名将是灾难。
类型安全 中高。Handler Block 的参数虽然是字典,但内部实现可以强类型检查。 。所有参数都被打包成字符串,方法调用时无法进行编译期类型检查。 反射方案在编译期无法发现错误,所有错误(如方法不存在、参数类型错误)都会在运行时导致崩溃。
灵活性 。Handler 可以做任何事情,不限于方法调用。 。只能调用对象的方法,无法执行复杂的逻辑或条件判断。 注册模式的一个 Handler 里可以写任意代码,而反射模式只能调用一个现存的方法。
解耦程度 。调用方对目标方无感知。 。调用方需要知道目标类的字符串名称和方法名的字符串名称,这本身就是一种耦合。 反射模式并未真正解耦,只是将“硬依赖”变成了“软依赖”(字符串依赖),依赖关系依然存在且更难追踪。
性能 。一次注册,之后就是高效的字典查询。 。每次调用都需要进行字符串解析、运行时查找等操作,性能开销更大。 注册模式用空间(内存中的路由表)换时间(调用时的速度),是更优的选择。

一个简单的反射实现示例(及其问题)

- (BOOL)openURL:(NSString *)URLString {
    // 1. 解析 URL(非常脆弱的解析)
    NSArray *components = [URLString.path componentsSeparatedByString:@"/"]; 
    NSString *targetClassName = components[1];
    NSString *actionName = components[2];
    
    // 2. 获取类和选择器(极不安全)
    Class targetClass = NSClassFromString(targetClassName);
    SEL actionSelector = NSSelectorFromString(actionName);
    
    if (!targetClass || !actionSelector) {
        return NO; // 容易崩溃,因为方法可能需要参数
    }
    
    // 3. 创建对象并调用方法(问题重重)
    id target = [[targetClass alloc] init];
    
    // 如何传递参数?参数是什么类型?方法返回值如何处理?
    // 下面这行代码几乎一定会崩溃,因为方法大概率需要参数
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [target performSelector:actionSelector]; 
    #pragma clang diagnostic pop
    
    return YES;
}

上述代码的问题:

  • 参数传递:无法优雅地传递 URL 中携带的参数。
  • 初始化方法:默认调用 init,但目标类可能需要一个特定的初始化方法(如 initWithProductId:)。
  • 返回值:无法处理目标方法的返回值。
  • 内存管理performSelector: 可能导致内存泄漏问题。
  • 方法签名:无法处理需要多个参数或复杂参数的方法。

所以,不建议省略注册方法而采用纯反射的方案。

registerURL:handler: 方法通过引入一个中间层(路由表)标准化协议(URL -> Handler),完美地解决了模块间调用时的耦合安全管理问题。

而纯粹的反射方案,虽然看起来“更动态”,但其带来的不安全性、脆弱的字符串依赖、极差的可维护性和高昂的运行时开销,使得它完全不适合大型、正规的工程项目。

相关文章

网友评论

      本文标题:OC 版的 JDRouter

      本文链接:https://www.haomeiwen.com/subject/mceynjtx.html