iOS组件化构思

断断续续看了几篇有关组件化的文章,记录一下自己学习后的一些想法,同时构思一下自己做组件化如何去做,也是对学习内容一种总结。

组件拆分

流程

  1. 先拆分基础组件为现有业务和业务组件服务;
  2. 新业务开发遵循组件化规范进行,再重构现有业务向组件化过渡;

根据现有业务画了一下结构图。

结构图

基础组件为业务组件服务,对于第三方开源库最好封装成基础组件(不直接在业务组件中使用,防止替换第三方库引起的变动)。

各个业务组件通过实现公共Protocol并关联protocol-class,组件间通信调用Mediator对应目标组件的Category中的方法,而Mediator通过protocol获取对应implClass调用protocol中的方法。代码参考
当然也有其它方案,下面#跨组件调度#做了些说明

服务化AppDelegate将UIApplationDelegate的实现拆分到各个service中,方便分类管理各种不同业务,实现服务的可插拔,同时方便为各个业务组件中注册service监测生命周期。

服务化AppDelegate

首先了解下Objective-C中的消息转发机制,在一个函数找不到时,Objective-C提供了三种方式去补救:

  1. 调用resolveInstanceMethod给个机会让类添加这个实现这个函数
  2. 调用forwardingTargetForSelector让别的对象去执行这个函数
  3. 调用methodSignatureForSelector和forwardInvocation灵活的将目标函数以其他形式执行。

如果没找到目标方法,才调用doesNotRecognizeSelector抛出异常

14760921853007.png

了解这个机制,可以创建一个AppDelegate不实现任何UIApplicationDelegate方法的类,Class load时注册各种service,系统调用AppDelegate时通过forwardInvocation转发给实现了UIApplicationDelegate方法的service执行。这样可以灵活的在各个组件中执行UIApplicationDelegate方法,或者维护一个公用service库。
具体实现可以参考这个

- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSMethodSignature *signature = anInvocation.methodSignature;
    NSUInteger argCount = signature.numberOfArguments;
    __block BOOL returnValue = NO;
    NSUInteger returnLength = signature.methodReturnLength;
    void * returnValueBytes = NULL;
    if (returnLength > 0) {
        returnValueBytes = alloca(returnLength);
    }
    
    [self.servicesMap enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, id<MLAppService> _Nonnull obj, BOOL * _Nonnull stop) {
        if ( ! [obj respondsToSelector:anInvocation.selector]) {
            return;
        }
        
        // check the signature
        NSAssert([[self objcTypesFromSignature:signature] isEqualToString:[self objcTypesFromSignature:[(id)obj methodSignatureForSelector:anInvocation.selector]]],
                 @"Method signature for selector (%@) on (%@ - `%@`) is invalid. \
                 Please check the return value type and arguments type.",
                 NSStringFromSelector(anInvocation.selector), obj.serviceName, obj);
        
        // copy the invokation
        NSInvocation *invok = [NSInvocation invocationWithMethodSignature:signature];
        invok.selector = anInvocation.selector;
        // copy arguments
        for (NSUInteger i = 0; i < argCount; i ++) {
            const char * argType = [signature getArgumentTypeAtIndex:i];
            NSUInteger argSize = 0;
            NSGetSizeAndAlignment(argType, &argSize, NULL);
            
            void * argValue = alloca(argSize);
            [anInvocation getArgument:&argValue atIndex:i];
            [invok setArgument:&argValue atIndex:i];
        }
        // reset the target
        invok.target = obj;
        // invoke
        [invok invoke];
        
        // get the return value
        if (returnValueBytes) {
            [invok getReturnValue:returnValueBytes];
            returnValue = returnValue || *((BOOL *)returnValueBytes);
        }
    }];
    
    // set return value
    if (returnValueBytes) {
        [anInvocation setReturnValue:returnValueBytes];
    }
}

跨组件调度

目前了解到的主要有三种方案。

  • 注册URL Block (蘑菇街/滴滴/贝贝等);

[Router registerURLPattern:@"xxx://detail?id=:id" toHandler:^(NSDictionary *routerParameters) {
NSNumber *id = routerParameters[@"id"];
}]

   或者另一个变种

[Router registerURLPattern:@"xxx://detail" params:@{@"id":@(123)} toHandler:^(NSDictionary *routerParameters) {
NSNumber *id = routerParameters[@"id"];
}]

这种方式需要在启动的时候注册URL--Block,同时URL和params属于都有硬编码问题,业务调用的时候对URL和params不明确,需要统一维护URL和params,如果URL和params变动也不方便改动。

*  注册protocol(蘑菇街/BeeHive);

   蘑菇街protocol和BeeHive的方式其实很类似,都是维护一份公共protocol(如果protocol分散定义在各个Module中,调用方就对定义该protocol的Module产生了依赖), 在各个Module实现protocol并通过一个Manager绑定(注册)protocol-class,调用者通过 protocol 获取Class,而调用的接口都已经定义在protocol里了。
   这样好处是没有硬编码,接口调用也比较明确。但是protocol的调用在各个Module中比较分散应该加一层wrapper/Mediator统一调用避免,同一protocol在公用库中容易被其它Module注册(可以通过编程规范约定命名方式避免-同时方便模块迁移)。

*  CTMediator(casatwy);
相比前两种方案,CTMediator不需要手动注册的行为,而相对于蘑菇街protocol的方案个人感觉要简洁一些。但是CTMediator的target-action调用存在硬编码的问题。

so结合CTMediator写了个protocol的[**IFQMediator**](https://github.com/infiniteQin/IFQMediator) 用来解决CTMediator硬编码的问题。


## 集成
无外乎直接集成源码和framework(静态库)两种方式。
 
|   | 源码 | framework  |
| --- | --- | --- |
| 优点 | 方便debug,定位问题 | 编译速度快,对每个组件的代码有很好的保密性 |
| 缺点 | 编译速度慢 | 不方便查看源码,定位问题  |


## pods 库的版本管理
首先先看下pod是如何指定版本的

pod 'AFNetworking' //不显式指定依赖库版本,表示每次都获取最新版本
pod 'AFNetworking', '~>0' //高于0的版本,写这个限制和什么都不写是

pod 'AFNetworking', '~> 0.1.2' //使用大于等于0.1.2但小于0.2的版本
pod 'AFNetworking', '~>0.1' //使用大于等于0.1但小于1.0的版本一个效果,都表示使用最新版本

pod 'AFNetworking', '2.0' //只使用2.0版本
pod 'AFNetworking', '= 2.0' //只使用2.0版本

pod 'AFNetworking', '> 2.0' //使用高于2.0的版本
pod 'AFNetworking', '>= 2.0' //使用大于或等于2.0的版本
pod 'AFNetworking', '< 2.0' //使用小于2.0的版本
pod 'AFNetworking', '<= 2.0' //使用小于或等于2.0的版本

pod 'AFNetworking', :git => 'http://gitlab.xxxx.com/AFNetworking.git', :branch => 'R20161010' //指定分支

pod 'AFNetworking', :path => '../AFNetworking' //指定本地库

s.dependency基本上会是使用最新的,用 `pod 'AFNetworking'`形式指定,特殊情况下才会指定版本使用。方便开发中测试发现最新版本的问题。

[**脚本更新.podSpec文件中的 s.version**](https://github.com/azu/podspec-bump/)

podspec-bump -w
git commit -am "update tag to podspec-bump --dump-version"
git tag "podspec-bump --dump-version"
git push --tags

pod trunk push


##  动态调度和安全

* 安全:基本沿用CTMediator的方案,用前缀区分只服务本地调用的方法;

* 动态调度:相比CTMediator四种不同的切点IFQMediator的切点更少,跨业务组件的动态调用更易实现。只要以category调度方法为切点,就能覆盖远程调用和本地跨模块调用。
至于是启动时下载动态调度列表在调用Mediator+category方法时审查列表,还是Mediator+category调用Api实时审查就看业务需求了。
## 资源跨组件互用问题
网上有同学的观点对于图片或者配置的资源文件单独建一个组件库,这样可以避免资源的重复性。
但是我个人更倾向于各个组件的资源文件自己管理。

* 首先,像大多数基础组件(比如ImagePicker库)都是跨项目使用的,如果用一个公共资源库去维护资源文件不利于多项目使用的情况。试想A项目使用了一个ImagePicker库,而这个库又依赖一个公共资源库,在B项目启动导入ImagePicker库时必须copy一份A项目的公共资源库,而公共资源库又可能包含其它组件库的资源,这时你就要剔除无用的资源文件;
* 另一方面,如果再业务组件跨库资源互用度高的情况下,应该考虑是否可以提取成公用组件(公共UI组件)或者重新考虑下组件划分是否合理。

当然,这主要还是要看自己具体的业务情况选择适用。

## 结束
组件化工作大多数情况都是在业务达到一定水平/协调开发人员达到一定数量,为了解决代码互用、工程管理、协同开发等问题进行的。但个人认为从长期发展来讲是越早进行对后续的开发工作/业务迭代能起到很大助益,就像在做hybrid开发的时候有一个调度中心话,native--webview--weex(RN)的之间页面跳转会非常方便。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容