iOS底层系列30 -- 组件化

  • 在iOS中常见的组件化方案有如下几种:
    • URL--Scheme:URL页面路由;
    • Target--Action:CTMediator
    • Protocol--Class:BeeHive
  • 实施组件化最主要的目的是为了实现模块之间的解耦,让各个模块依赖于中介者,通过中介者实现模块之间的通信以及传参;

URL--Scheme路由

  • 首先介绍一下iOS中的NSURL的部分属性,代码实现如下:
- (void)viewDidLoad {
    [super viewDidLoad];
//    self.title = @"First";
    self.view.backgroundColor = [UIColor cyanColor];
    [self addBtn];
    NSString *urlString = @"https://www.baidu.com/link?url=dAATY&eqid=81692b77c2ac&pwd=123456";
    NSURL *url = [NSURL URLWithString:urlString];
    
    NSLog(@"scheme = %@",url.scheme);
    NSLog(@"host = %@",url.host);
    NSLog(@"path = %@",url.path);
    NSLog(@"query = %@",url.query);
    NSLog(@"absoluteURL = %@",url.absoluteURL);
}
  • 控制台输出结果如下:
image.png
  • URL--Scheme路由的第三方库有:JLRoutes,MGJRouter,下面详细介绍JLRoutes的工作原理;
JLRoutes的源码分析
  • JLRoutes使用的步骤主要分为两步:
    • 第一步:注册页面路由URL,其中主要包含url与block回调;
    • 第二步:触发事件,传入页面路由,跳转到指定页面;
第一步:注册页面路由URL
  • 在注册页面路由时,首先调用JLRoutes的+ (instancetype)routesForScheme:(NSString *)scheme类方法,创建JLRoutes实例对象,实现如下:
+ (instancetype)routesForScheme:(NSString *)scheme
{
    JLRoutes *routesController = nil;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        routeControllersMap = [[NSMutableDictionary alloc] init];
    });
    
    NSLog(@"%@",routeControllersMap[scheme]);
    if (!routeControllersMap[scheme]) {
        routesController = [[self alloc] init];
        routesController.scheme = scheme;
        routeControllersMap[scheme] = routesController;
    }
    
    routesController = routeControllersMap[scheme];
    
    return routesController;
}
  • 根据路由URL的scheme创建对应的JLRoutes实例对象;
  • routeControllersMap是静态全局HashMap对象,内部存储的是shceme--JLRoutes的键值对,其关系见下图所示:
image.png
  • 然后JLRoutes调用addRoute:handler注册路由block回调,核心实现如下:
- (void)addRoute:(NSString *)routePattern priority:(NSUInteger)priority handler:(BOOL (^)(NSDictionary<NSString *, id> *parameters))handlerBlock
{
    NSArray <NSString *> *optionalRoutePatterns = [JLRParsingUtilities expandOptionalRoutePatternsForPattern:routePattern];
    JLRRouteDefinition *route = [[JLRRouteDefinition alloc] initWithScheme:self.scheme pattern:routePattern priority:priority handlerBlock:handlerBlock];
    
    if (optionalRoutePatterns.count > 0) {
        // there are optional params, parse and add them
        for (NSString *pattern in optionalRoutePatterns) {
            [self _verboseLog:@"Automatically created optional route: %@", route];
            JLRRouteDefinition *optionalRoute = [[JLRRouteDefinition alloc] initWithScheme:self.scheme pattern:pattern priority:priority handlerBlock:handlerBlock];
            [self _registerRoute:optionalRoute];
        }
        return;
    }
    
    [self _registerRoute:route];
}
  • 重点在于:将scheme,route路由和block回调 封装成一个JLRRouteDefinition实例对象,然后将其保存在JLRoutes的属性mutableRoutes路由数组中,关系图见如下绿色部分:
image.png
  • 第一步注册页面路由的逻辑就结束了,其本质是将页面路由与回调block封装成一个JLRRouteDefinition实例对象,然后保存在JLRoutes的属性数组mutableRoutes中;
第二步:触发事件,传入页面路由,跳转到指定页面
  • 假设现在传入的页面路由为RouteOne://push/FirstNextViewController?titleText=11111111111&name=中国,URL的参数属性依次为:
    • sheme为RouteOne
    • host为push
    • path为FirstNextViewController
    • query为titleText=11111111111&name=中国
  • 触发事件,传入页面路由,调用[[JLRoutes routesForScheme:@"RouteOne"]routeURL:url]方法;
    • 首先根据scheme,去routeControllersMap中获取JLRoutes实例对象;
    • 然后JLRoutes会根据入参路由URL,调用routeURL:方法,去路由数组mutableRoutes进行匹配,找到目标JLRRouteDefinition对象,然后执行JLRRouteDefinition中的block回调,核心代码实现如下:
- (BOOL)_routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters executeRouteBlock:(BOOL)executeRouteBlock{
    if (!URL) {
        return NO;
    }
    [self _verboseLog:@"Trying to route URL %@", URL];
    
    BOOL didRoute = NO;
    JLRRouteRequest *request = [[JLRRouteRequest alloc] initWithURL:URL alwaysTreatsHostAsPathComponent:alwaysTreatsHostAsPathComponent];
    
    for (JLRRouteDefinition *route in [self.mutableRoutes copy]) {
        // check each route for a matching response
        JLRRouteResponse *response = [route routeResponseForRequest:request decodePlusSymbols:shouldDecodePlusSymbols];
        if (!response.isMatch) {
            continue;
        }
        [self _verboseLog:@"Successfully matched %@", route];
        if (!executeRouteBlock) {
            return YES;
        }
        NSMutableDictionary *finalParameters = [NSMutableDictionary dictionary];
        [finalParameters addEntriesFromDictionary:response.parameters];
        [finalParameters addEntriesFromDictionary:parameters];
        [self _verboseLog:@"Final parameters are %@", finalParameters];
        
        //回调handler
        didRoute = [route callHandlerBlockWithParameters:finalParameters];
        
        //匹配 成功 终止循环
        if (didRoute) {
            // if it was routed successfully, we're done
            break;
        }
    }
    if (!didRoute) {
        [self _verboseLog:@"Could not find a matching route"];
    }
    if (!didRoute && self.shouldFallbackToGlobalRoutes && ![self _isGlobalRoutesController]) {
        [self _verboseLog:@"Falling back to global routes..."];
        didRoute = [[JLRoutes globalRoutes] _routeURL:URL withParameters:parameters executeRouteBlock:executeRouteBlock];
    }
    if (!didRoute && executeRouteBlock && self.unmatchedURLHandler) {
        [self _verboseLog:@"Falling back to the unmatched URL handler"];
        self.unmatchedURLHandler(self, URL, parameters);
    }
    return didRoute;
}
  • 可以看到入参URL路由,会首先被封装成一个JLRRouteRequest对象,然后在JLRoutes实例对象的路由集合数组mutableRoutes中,进行遍历匹配,本质是将JLRRouteDefinitionJLRRouteRequest进行匹配,将两者封装成JLRRouteResponse对象,若匹配成功,就会执行block回调方法,关系见下图红色部分;
image.png
  • 实际开发中,注册路由的代码如下所示:
[[JLRoutes routesForScheme:"RouteOne"] addRoute:@"/push/:controller"handler:^BOOL(NSDictionary<NSString *,id> * _Nonnull parameters) {
    //根据触发事件 传入的页面路由 创建指定的页面控制器
    Class class = NSClassFromString(parameters[@"controller"]);
    UIViewController *nextVC = [[class alloc] init];
    //将页面路由中的参数 传递给页面控制器,实现了页面传参
    [self paramToVc:nextVC param:parameters];
    //获取当前导航控制器
    UIViewController *currentVc = [self currentViewController];
    [currentVc.navigationController pushViewController:nextVC animated:YES];       
    return YES;
}];
  • [self paramToVc:nextVC param:parameters] 实现了将页面路由中的参数,通过Runtime运行时传递给页面控制器,非常巧妙!!!
//传参数
-(void)paramToVc:(UIViewController *)v param:(NSDictionary<NSString *,NSString *> *)parameters{
    //runtime将参数传递至需要跳转的控制器
    unsigned int outCount = 0;
    objc_property_t * properties = class_copyPropertyList(v.class , &outCount);
    for (int i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        NSString *key = [NSString stringWithUTF8String:property_getName(property)];
        NSString *param = parameters[key];
        if (param != nil) {
            [v setValue:param forKey:key];
        }
    }
}
  • 获取当前导航控制器的代码如下:
- (UIViewController *)currentViewController{
    UIViewController * currVC = nil;
    UIViewController * Rootvc = self.window.rootViewController ;
    do {
        if ([Rootvc isKindOfClass:[UINavigationController class]]) {
            UINavigationController * nav = (UINavigationController *)Rootvc;
            UIViewController * v = [nav.viewControllers lastObject];
            currVC = v;
            Rootvc = v.presentedViewController;
            continue;
        }else if([Rootvc isKindOfClass:[UITabBarController class]]){
            UITabBarController * tabVC = (UITabBarController *)Rootvc;
            currVC = tabVC;
            Rootvc = [tabVC.viewControllers objectAtIndex:tabVC.selectedIndex];
            continue;
        }
    } while (Rootvc!=nil);
    
    return currVC;
}

Target-Action -- CTMediator

  • 我们知道模块之间的耦合是因为A模块在跳转B模块时,在A模块中需要实例化B模块,造成AB模块之间的耦合绑定;
  • CTMediator的工作原理在于:将B模块的实例化,单独放在一个类ClassA的自定义一个方法MethodA中,然后利用CTMediator的分类,在调用方传入参数params,通过perform:selector去调用ClassA中MethodA方法,并将参数params传递过去,原理如下图所示:
image.png
  • 代码实现如下:A模块 跳转 B模块(GoodsDetailViewController)
  • 第一步:CTMediator分类方法,需传入调用方的参数params;
@implementation CTMediator (TAGoodsDetail)

- (UIViewController *)goodsDetailViewControllerWithGoodsId:(NSString *)goodsId goodsName:(NSString *)goodsName{
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    params[@"goodsId"] = goodsId;
    params[@"goodsName"] = goodsName;
    //核心 利用消息发送的原理 去调用TAGoodsDetail类的GoodsDetailViewController方法
    return [self performTarget:@"TAGoodsDetail" action:@"GoodsDetailViewController" params:params shouldCacheTarget:NO];
}
@end
  • 第二步:提供实例化B模块的类(Target_TAGoodsDetail)和方法(Action_GoodsDetailViewController:)
#import "Target_TAGoodsDetail.h"
#import "TAGoodsDetailViewController.h"

@implementation Target_TAGoodsDetail

- (UIViewController *)Action_GoodsDetailViewController:(NSDictionary *)params{
    TAGoodsDetailViewController *goodsDetailVC = [[TAGoodsDetailViewController alloc] init];
    goodsDetailVC.goodsId = params[@"goodsId"];
    goodsDetailVC.goodsName = params[@"goodsName"];
    return goodsDetailVC;
}
@end
  • 第三步:调用方A模块,调用CTMediator分类中方法,并传入页面参数给B模块;
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    NSDictionary *goodsItem = self.dataSource[indexPath.row];
    UIViewController *goodsDetailVC = [[CTMediator sharedInstance] goodsDetailViewControllerWithGoodsId:goodsItem[@"goodsId"] goodsName:goodsItem[@"goodsName"]];
    
    if (goodsDetailVC) {
        [self.navigationController pushViewController:goodsDetailVC animated:YES];
    }
}

Protocol -- Class -- BeeHive

  • 面向协议编程,使用BeeHive地方库的主要步骤分为两步:
    • 第一步:注册,包括Module模块注册和Services服务注册;
    • 第二步:触发事件,调用指定模块与服务;
Module注册
  • Module注册分为以下三种方式:
    • BHAnnotation.h文件中的宏定义#define BeeHiveMod(name)进行注册;
    • Load类方法注册;
    • 读取本地Plist文件进行注册;
以BHAnnotation中的宏定义注册Module
  • 以阿里巴巴官方提供的Demo进行分析,其中ShopModule就是以BHAnnotation中的宏定义进行注册的,即@BeeHiveMod(ShopModule)BeeHiveMod宏定义如下:
#define BeeHiveMod(name) \
class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";
  • 内部又使用了BeeHiveDATA宏,其定义如下:
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
  • @BeeHiveMod(ShopModule)最终会被表示成此形式:class BeeHive; char * kShopModule_mod ____attribute((used, section("__DATA,"BeehiveMods" "))) = ""ShopModule""
  • 此形式的代码,会在 App编译阶段 将ShopModule这个类名写入到Mach-O文件的 __DATA(数据)段中,并以 BeehiveMods 作为key标识位置
  • BHAnnotation.m文件中,看到以__attribute__((constructor))修饰的函数方法initProphet(),此方法会在App运行时调用(且在main函数之间),实现如下:
__attribute__((constructor)) void initProphet() {
    _dyld_register_func_for_add_image(dyld_callback);
}
  • 内部调用dyld系统函数_dyld_register_func_for_add_image,此函数会在dyld加载连接镜像文件时频繁调用,传入的回调函数dyld_callback,实现如下所示:
image.png
  • 其中访问Mach-O文件,读取Module数据的是调用BHReadConfiguration函数,其具体实现如下:
image.png
  • 上述的逻辑关系总结如下所示:
  • 现在马上进入Module的注册流程,主要涉及的类class为BHModuleManager,顾名思义Module的管理者;
Services注册
  • Services的注册有以下几种方式:
    • BHAnnotation.h文件中的宏定义#define BeeHiveService(servicename,impl)进行注册;
以BHAnnotation中的宏定义注册
  • 以阿里巴巴官方提供的Demo进行分析,其中BHViewController就是以宏进行注册的,即@BeeHiveService(HomeServiceProtocol,BHViewController),其中宏定义如下:
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";
  • @BeeHiveService(HomeServiceProtocol,BHViewController),进行宏替换后的形式为:class BeeHive; char * kHomeServiceProtocol_service __attribute((used, section("__DATA,"BeehiveServices" "))) = "{ \""HomeServiceProtocol"\" : \""BHViewController"\"}"
  • 此形式的代码,会在 App编译阶段 将"{ \""HomeServiceProtocol"\" : \""BHViewController"\"}"写入到Mach-O文件的 __DATA(数据)段中,并以 BeehiveServices 作为key标识位置
  • 然后在App运行时阶段,读取Mach-O文件中的Services数据进行注册,逻辑与注册Module的逻辑基本相同,逻辑关系如下所示:
image.png
  • 接下来进入Services的注册流程,主要涉及的类class为BHServiceManager,顾名思义Services的管理者,注册服务的核心函数如下所示:
image.png
  • 上述的关系如下图所示:
image.png
Service服务的调用
  • 通过BHServiceManager调用函数createService:传入指定的协议,可获取遵循当前指定协议的服务模块(页面),具体实现如下:
Snip20220310_14.png
  • 目前主要是用的是Service的注册与调用
    • 在App编译期时,往Mach-O文件的数据段,写入需要注册的Service服务即protocol-class的键值对关系;
    • 在App运行时,读取Mach-O文件中写入的Service服务,进行服务注册,全部保存在BHServiceManager的属性字典中;
    • 调用服务,通过调用BHServiceManager的createService函数,传入指定协议,获取指定模块页面;

参考文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容