美文网首页
iOS 组件化路由

iOS 组件化路由

作者: Q14 | 来源:发表于2020-04-09 14:31 被阅读0次

    路由的意义

    路由并非只是指的界面跳转,还包括数据获取等几乎所有业务。

    (一)项目中WKWebview& UIWebView 与原生Native 交互路由

    路由URL 例如 QJ://detail?name=xx&id=xxx

    缺点很明显:字符串 URI 并不能表征 iOS 系统原生类型,要阅读对应模块的使用文档,大量的硬编码
    项目中实现代码大概就是这样

    //先维护一张路由表 
    - (NSDictionary *)getHostDictionary {
    
        static NSDictionary *dicts = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
           dicts = @{
            HOST_LOGOUT:  @"logoutAction:",
            HOST_ORDER_DETAIL:  @"orderDetail:"
          };
        });
       return dicts;
    }
    
    - (UIViewController *)logoutAction:(NSDictionary *)param {
    //做一些登录登出的业务逻辑
    }
    

    通俗来讲
    解析URI ---> 等到 target params ---> 调用原生

    三、组件化的意义

    前面对路由的分析提到了使用目标和参数 (aim/params) 动态定位到具体业务的技术点。实际上在 iOS Objective-C 中大概有反射依赖注入两种思路:

    • aim转化为具体的ClassSEL,利用 runtime 运行时调用到具体业务。
    • 对于代码来说,进程空间是共享的,所以维护一个全局的映射表,提前将aim映射到一段代码,调用时执行具体业务。

    可以明确的是,这两种方式都已经让Mediator免去了对业务模块的依赖:

    16c40c9fb110ace5.png

    而这些解耦技术,正是 iOS 组件化的核心。

    组件化主要目的是为了让各个业务模块独立运行,互不干扰,那么业务模块之间的完全解耦是必然的,同时对于业务模块的拆分也非常考究,更应该追求功能独立而不是最小粒度。

    (一) Runtime 解耦

    为 Router 定义了一个统一入口方法:

    /// 此方法就是一个拦截器,可做容错以及动态调度
    - (id)performTarget:(NSString *)target action:(NSString *)action params:(NSDictionary *)params {
        Class cls; id obj; SEL sel;
        cls = NSClassFromString(target);
        if (!cls) goto fail;
        sel = NSSelectorFromString(action);
        if (!sel) goto fail;
        obj = [cls new];
        if (![obj respondsToSelector:sel]) goto fail;
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        return [obj performSelector:sel withObject:params];
    #pragma clang diagnostic pop
    fail:
        NSLog(@"找不到目标,写容错逻辑");
        return nil;
    }
    复制代码
    

    简单写了下代码,原理很简单,可用 Demo 测试。对于内部调用,为每一个模块写一个分类:

    @implementation BMediator (BAim)
    - (void)gotoBAimControllerWithName:(NSString *)name callBack:(void (^)(void))callBack {
        [self performTarget:@"BTarget" action:@"gotoBAimController:" params:@{@"name":name, @"callBack":callBack}];
    }
    @end
    复制代码
    

    可以看到这里是给BTarget发送消息:

    @interface BTarget : NSObject
    - (void)gotoBAimController:(NSDictionary *)params; 
    @end
    @implementation BTarget
    - (void)gotoBAimController:(NSDictionary *)params {
        BAimController *vc = [BAimController new];
        vc.name = params[@"name"];
        vc.callBack = params[@"callBack"];
        [UIViewController.yb_top.navigationController pushViewController:vc animated:YES];
    }
    @end
    复制代码
    

    为什么要定义分类

    定义分类的目的前面也说了,相当于一个语法糖,让调用者轻松使用,让 hard code 交给对应的业务工程师。

    为什么要定义 Target “靶子”

    • 避免同一模块路由逻辑散落各地,便于管理。
    • 路由并非只有控制器跳转,某些业务可能无法放代码(比如网络请求就需要额外创建类来接受路由调用)。
    • 便于方案的接入和摒弃(灵活性)。

    可能有些人对这些类的管理存在疑虑,下图就表示它们的关系(一个块表示一个 repo):

    image.png

    图中“注意”处箭头,B 模块是否需要引入它自己的分类 repo,取决于是否需要做所有界面跳转的拦截,如果需要那么 B 模块仍然要引入自己的 repo 使用。

    完整的方案和代码可以查看 Casa 的 CTMediator,设计得比较完备,笔者没挑出什么毛病。

    (二) Block 解耦

    下面简单实现了两个方法:

    - (void)registerKey:(NSString *)key block:(nonnull id _Nullable (^)(NSDictionary * _Nullable))block {
        if (!key || !block) return;
        self.map[key] = block;
    }
    /// 此方法就是一个拦截器,可做容错以及动态调度
    - (id)excuteBlockWithKey:(NSString *)key params:(NSDictionary *)params {
        if (!key) return nil;
        id(^block)(NSDictionary *) = self.map[key];
        if (!block) return nil;
        return block(params);
    }
    复制代码
    

    维护一个全局的字典 (Key -> Block),只需要保证闭包的注册在业务代码跑起来之前,很容易想到在+load中写:

    @implementation DRegister
    + (void)load {
        [DMediator.share registerKey:@"gotoDAimKey" block:^id _Nullable(NSDictionary * _Nullable params) {
            DAimController *vc = [DAimController new];
            vc.name = params[@"name"];
            vc.callBack = params[@"callBack"];
            [UIViewController.yb_top.navigationController pushViewController:vc animated:YES];
            return nil;
        }];
    }
    @end
    复制代码
    

    至于为什么要使用一个单独的DRegister类,和前面“Runtime 解耦”为什么要定义一个Target是一个道理。同样的,使用一个分类来简化内部调用(这是蘑菇街方案可以优化的地方):

    @implementation DMediator (DAim)
    - (void)gotoDAimControllerWithName:(NSString *)name callBack:(void (^)(void))callBack {
        [self excuteBlockWithKey:@"gotoDAimKey" params:@{@"name":name, @"callBack":callBack}];
    }
    @end
    复制代码
    

    可以看到,Block 方案和 Runtime 方案 repo 架构上可以基本一致(见图6),只是 Block 多了注册这一步。

    为了灵活性,Demo 中让 Key -> Block,这就让 Block 里面要写很多代码,如果缩小范围将 Key -> UIViewController.class 可以减少注册的代码量,但这样又难以覆盖所有场景。

    注册所产生的内存占用并不是负担,主要是大量的注册可能会明显拖慢启动速度。

    (三) Protocol 解耦

    这种方式仍然要注册,使用一个全局的字典 (Protocol -> Class) 存储起来。

    - (void)registerService:(Protocol *)service class:(Class)cls {
        if (!service || !cls) return;
        self.map[NSStringFromProtocol(service)] = cls;
    }
    - (id)getObject:(Protocol *)service {
        if (!service) return nil;
        Class cls = self.map[NSStringFromProtocol(service)];
        id obj = [cls new];
        if ([obj conformsToProtocol:service]) {
            return obj;
        }
        return nil;
    }
    复制代码
    

    定义一个协议服务:

    @protocol CAimService <NSObject>
    - (void)gotoCAimControllerWithName:(NSString *)name callBack:(void (^)(void))callBack;
    @end
    复制代码
    

    用一个类实现协议并且注册协议:

    @implementation CAimServiceProvider
    + (void)load {
        [CMediator.share registerService:@protocol(CAimService) class:self];
    }
    #pragma mark - <CAimService>
    - (void)gotoCAimControllerWithName:(NSString *)name callBack:(void (^)(void))callBack {
        CAimController *vc = [CAimController new];
        vc.name = name;
        vc.callBack = callBack;
        [UIViewController.yb_top.navigationController pushViewController:vc animated:YES];
    }
    @end
    复制代码
    

    至于为什么要使用一个单独的ServiceProvider类,和前面“Runtime 解耦”为什么要定义一个Target是一个道理。

    使用起来很优雅:

    id<CAimService> service = [CMediator.share getObject:@protocol(CAimService)];
    [service gotoCAimControllerWithName:@"From C" callBack:^{
           NSLog(@"CAim CallBack");
    }];
    复制代码
    

    看起来这种方案不需要硬编码很舒服,但是它有个致命的问题 ——— 无法拦截所有路由方法。

    这也就意味着这种方案做不了自动化动态调用。

    阿里的 BeeHive 是目前的最佳实践。注册部分它可以将待注册的类字符串写入 Data 段,然后在 Image 加载的时候读取出来注册。这个操作只是将注册的执行放到了+load方法之前,仍然会拖慢启动速度,所以这个处理并不是为了提速,而是将注册代码更加优雅的分散到具体业务方。

    为什么 Protocol -> Class 和 Key -> Block 需要注册?

    想象一下,解耦意味着调用方只有系统原生的标识,如何定位到目标业务? 必然有个映射。 而 runtime 可以直接调用目标业务,其它两种方式只有建立映射表。 当然 Protocol 方式也可以不建立映射表,直接遍历所有类,找出遵循这个协议的类也能找到,不过明显这样是低效且不安全的。

    组件化总结

    经过对比作者选择的方式是 target-action + runtime
    本文Demo

    参考文章
    解读 iOS 组件化与路由的本质

    相关文章

      网友评论

          本文标题:iOS 组件化路由

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