ios组件化_ios 组件化 target-action 组件之间的通信 - CSDN
  • iOS 组件化方案

    2018-04-18 11:22:48
    主要是参考了如下几篇文章,另外零零散散的也看了一些其他资料,但是大多都是相似的蘑菇街组件化之路iOS应用架构谈 组件化方案iOS 组件化 —— 路由设计思路分析滴滴iOS的组件化实践与优化iOS组件化方案iOS 组件化...

    前言

    这篇文章主要是我近段时间针对市面上存在的一些组件化方案的调研之后,再经过自己的反思和总结写的,博客中部分文字和图借鉴自下面的博客。各位看官大爷就当做一篇读书笔记来看即可,主要是参考了如下几篇文章,另外零零散散的也看了一些其他资料,但是大多都是相似的

    看上去各家都是各显神通,都有自己的技术方案,但是实际上都可以归类到如下两种方案:

    利用runtime实现的target-action方法

    利用url-scheme方案

    目前市面上流行的组件化方案都是通过url-scheme实现的,包括很多开源的组件化的库都是如此,只有casa的方案独树一帜,是通过Target-Action实现的

    URL-Scheme库:

    1. JLRoutes

    2. routable-ios

    3. HHRouter

    4. MGJRouter

    Target-Action库:

    1. CTMediator

    上面这些第三方组件库的具体对比,大家可以参考霜神的这篇博客:

    iOS 组件化 —— 路由设计思路分析

    URL-Sheme方案一般都是各个组件把自己可以提供的服务通过url的形式注册到一个中心管理器,然后调用发就可以通过openURL的方式来打开这个url,然后中心管理器解析这个url,把请求转发到相应的组件去执行

    Target-Action方案利用了OC的runtime特性,无需注册,直接在原有的组件之外加一层wrapper,把对外提供的服务都抽离到该层。然后通过runtime的TARGET performSelector:ACTION withObject:PARAMS找到对应的组件,执行方法和传递参数。

    就我个人而言,我还是比较推荐target-action方案,具体原因我们下面会进一步分析

    为何要组件化

    在做一件事之前我们一般都要搞清楚为什么要这么做,好处是什么,有哪些坑,这样才会有一个整体的认识,然后再决定要不要做。同样我们也要搞清楚到底需不需要实施组件化,那么就要先搞清楚什么是组件

    组件的定义

    组件是由一个或多个类构成,能完整描述一个业务场景,并能被其他业务场景复用的功能单位。组件就像是PC时代个人组装电脑时购买的一个个部件,比如内存,硬盘,CPU,显示器等,拿出其中任何一个部件都能被其他的PC所使用。

    所以组件可以是个广义上的概念,并不一定是页面跳转,还可以是其他不具备UI属性的服务提供者,比如日志服务,VOIP服务,内存管理服务等等。说白了我们目标是站在更高的维度去封装功能单元。对这些功能单元进行进一步的分类,才能在具体的业务场景下做更合理的设计。

    组件化的优点

    纵观目前的已经在实施组件化的团队来看,大家的一般发展路径都是:前期项目小,需要快速迭代抢占市场,大家都是用传统的MVC架构去开发项目。等到后期项目越来越大,开发人数越来越多,会发现传统的开发方式导致代码管理混乱,发布、集成、测试越来越麻烦,被迫走向组件化的道路。

    其实组件化也不是完全必须的,如果你的团队只是开发一个小项目,团队人数小于10个人,产品线也就是两三条,那么完全可以用传统开发方式来开发。但是如果你的团队在不断发展,产品线也越来越多的时候,预计后期可能会更多的时候,那么最好尽早把组件化提上议程。

    摘自casa的建议:

    组件化方案在App业务稳定,且规模(业务规模和开发团队规模)增长初期去实施非常重要,它助于将复杂App分而治之,也有助于多人大型团队的协同开发。但组件化方案不适合在业务不稳定的情况下过早实施,至少要等产品已经经过MVP阶段时才适合实施组件化。因为业务不稳定意味着链路不稳定,在不稳定的链路上实施组件化会导致将来主业务产生变化时,全局性模块调度和重构会变得相对复杂。

    其实组件化也没有多么高大上,和我们之前说的模块化差不多,就是把一些业务、基础功能剥离,划分为一个个的模块,然后通过pods的方式管理而已,同时要搭配一套后台的自动集成、发布、测试流程

    一般当项目越来越大的时候,无可避免的会遇到如下的痛点:

    代码冲突多,编译慢。

    每一次拉下代码开发功能,开发完成准备提交代码时,往往有其他工程师提交了代码,需要重新拉去代码合并后再提交,即使开发一个很小的功能,也需要在整个工程里做编译和调试,效率较低。

    迭代速度慢,耦合比较严重,无法单独测试。

    各个业务模块之间互相引入,耦合严重。每次需要发版时,所有的业务线修改都需要全部回归,然后审查看是否出错,耗费大量时间。业务线之间相互依赖,可能会导致一个业务线必须等待另外一个业务线开发完某个功能才可以接着开发,无法并行开发。还有一个问题,就是耦合导致无法单独测试某个业务线,可能需要等到所有业务线开发完毕,才能统一测试,浪费测试资源

    为了解决上述痛点,组件化应运而生,总体来说,组件化就是把整个项目进行拆分,分成一个个单独的可独立运行的组件,分开管理,减少依赖。 完成组件化之后,一般可达到如下效果:

    1. 加快编译速度,可以把不会经常变动的组件做成静态库,同时每个组件可以独立编译,不依赖于主工程或者其他组件

    2. 每个组件都可以选择自己擅长的开发模式(MVC / MVVM / MVP)

    3. 可以单独测试每个组件

    4. 多条业务线可以并行开发,提高开发效率

    如何组件化

    当我们确定需要对项目进行组件化了,我们第一个要解决的问题就是如何拆分组件。这是一个见仁见智的问题,没有太明确的划分边界,大致做到每个组件只包含一个功能即可,具体实施还是要根据实际情况权衡。

    当我们写一个类的时候,我们会谨记高内聚,低耦合的原则去设计这个类,当涉及多个类之间交互的时候,我们也会运用SOLID原则,或者已有的设计模式去优化设计,但在实现完整的业务模块的时候,我们很容易忘记对这个模块去做设计上的思考,粒度越大,越难做出精细稳定的设计,我暂且把这个粒度认为是组件的粒度。

    组件可以是个广义上的概念,并不一定是页面跳转,还可以是其他不具备UI属性的服务提供者,比如日志服务,VOIP服务,内存管理服务等等。说白了我们目标是站在更高的维度去封装功能单元,把多个功能单元组合在一起形成一个更大的功能单元,也就是组件。对这些功能单元进行进一步的分类,才能在具体的业务场景下做更合理的设计。

    下面的组件划分粒度,大家可以借鉴一下

    组件化前后对比

    iOS里面的组件化主要是通过cocopods把组件打包成单独的私有pod库来进行管理,这样就可以通过podfile文件,进行动态的增删和版本管理了。

    下面是链家APP在实行组件化前后的对比

    可以看到传统的MVC架构把所有的模块全部糅合在一起,是一种分布式的管理方法,耦合严重,当业务线过多的时候就会出现我们上面说的问题。 而下图的组件化方式是一种中心Mediator的方式,让所有业务组件都分开,然后都依赖于Mediator进行统一管理,减少耦合。

    组件化后,代码分类也更符合人类大脑的思考方式 

    组件化方案对比分析

    组件化如何解决现有工程问题

    传统模式的组件之间的跳转都是通过直接import,当模块比较少的时候这个方式看起来没啥问题。但到了项目越来越庞大,这种模式会导致每个模块都离不开其他模块,互相依赖耦合严重。这种方式是分布式的处理方式,每个组件都是处理和自己相关的业务。管理起来很混乱,如下图所示:

    (借用霜神的几张图) 

    那么按照人脑的思维方式,改成如下这种中心化的方式更加清晰明了: 

    但是上面这个图虽然看起来比刚开始好了许多,但是每个组件还是和mediator双向依赖,如果改成如下图所示就完美了: 

    这个时候看起来就舒服多了,每个组件只需要自己管好自己就完了,然后由mediator负责在各个组件中间进行转发或者跳转,perfect 那么如何实现这个架构呢?只要解决下面两个问题就好了:

    1. mediator作为中间件,需要通过某种方式找到每个组件,并能调用组件的方法

    2. 每个组件如何得知其他组件提供了哪些方法?只有这样才可以调用对方嘛

    原始工程

    假设我们现有工程里面有两个组件A、B,功能很简单,如下所示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #import @interface A_VC : UIViewController
    -(void)action_A:(NSString*)para1;
    @end
     
    ==================================
     
    #import "A_VC.h"
     
    @implementation A_VC
     
    -(void)action_A:(NSString*)para1 {
        NSLog(@"call action_A %@",para1);
    }
     
    @end
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #import @interface B_VC : UIViewController
     
    -(void)action_B:(NSString*)para1 para2:(NSInteger)para2;
     
    @end
     
    ====================
     
    #import "B_VC.h"
     
    @implementation B_VC
     
    -(void)action_B:(NSString*)para1 para2:(NSInteger)para2{
        NSLog(@"call action_B %@---%zd",para1,para2);
    }
     
    @end

    如果是传统做法,A、B要调用对方的功能,就会直接import对方,然后初始化,接着调用方法。现在我们对他们实行组件化,改成如上图所示的mediator方式

    target-action方案

    该方案借助OC的runtime特性,实现了服务的自动发现,无需注册即可实现组件间调用。不管是从维护性、可读性、扩展性方面来讲,都优于url-scheme方案,也是我比较推崇的组件化方案,下面我们就来看看该方案如何解决上述两个问题的

    Demo演示

    此时A、B两个组件不用改,我们需要加一个mediator,代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    #import @interface Mediator : NSObject
     
    -(void)A_VC_Action:(NSString*)para1;
    -(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2;
    + (instancetype)sharedInstance;
     
    @end
     
    ===========================================
     
    #import "Mediator.h"
     
    @implementation Mediator
     
    + (instancetype)sharedInstance
    {
        static Mediator *mediator;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            mediator = [[Mediator alloc] init];
        });
        return mediator;
    }
     
     
    -(void)A_VC_Action:(NSString*)para1{
        Class cls = NSClassFromString(@"A_VC");
        NSObject *target = [[cls alloc]init];
        [target performSelector:NSSelectorFromString(@"action_A:") withObject:para1];
    }
     
     
    -(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2{
        Class cls = NSClassFromString(@"B_VC");
        NSObject *target = [[cls alloc]init];
        [target performSelector:NSSelectorFromString(@"action_B:para2:") withObject:para1 withObject:para2];
    }
     
    @end

    组件B调用组件A,如下所示:

    1
        [[Mediator sharedInstance]A_VC_Action:@"参数1"];

    组件A调用组件B,如下所示:

    1
        [[Mediator sharedInstance]B_VC_Action:@"参数1" para2:123];

    此时已经可以做到最后一张图所示的效果了,组件A,B依赖mediator,mediator不依赖组件A,B(也不是完全不依赖,而是把用runtime特性把类的引用弱化为了字符串)

    反思

    看到这里,大概有人会问,既然用runtime就可以解耦取消依赖,那还要Mediator做什么?我直接在每个组件里面用runtime调用其他组件不就完了吗,干嘛还要多一个mediator?

    但是这样做会存在如下问题:

    1. 调用者写起来很恶心,代码提示都没有, 参数传递非常恶心,每次调用者都要查看文档搞清楚每个参数的key是什么,然后自己去组装成一个 NSDictionary。维护这个文档和每次都要组装参数字典很麻烦。

    2. 当调用的组件不存在的时候,没法进行统一处理

    那么加一个mediator的话,就可以做到:

    1. 调用者写起来不恶心,代码提示也有了, 参数类型明确。

    2. Mediator可以做统一处理,调用某个组件方法时如果某个组件不存在,可以做相应操作,让调用者与组件间没有耦合。

    改进

    聪明的读者可能已经发现上面的mediator方案还是存在一个小瑕疵,受限于performselector方法,最多只能传递两个参数,如果我想传递多个参数怎么办呢?

    答案是使用字典进行传递,此时我们还需要个组件增加一层wrapper,把对外提供的业务全部包装一次,并且接口的参数全部改成字典。 假设我们现在的B组件需要接受多个参数,如下所示:

    1
    2
    3
    -(void)action_B:(NSString*)para para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
        NSLog(@"call action_B %@---%zd---%zd----%zd",para1,para2,para3,para4);
    }

    那么此时需要对B组件增加一层wrapper,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #import @interface target_B : NSObject
    -(void)B_Action:(NSDictionary*)para;
     
    @end
     
    =================
    #import "target_B.h"
    #import "B_VC.h"
     
    @implementation target_B
     
    -(void)B_Action:(NSDictionary*)para{
        NSString *para1 = para[@"para1"];
        NSInteger para2 = [para[@"para2"]integerValue];
        NSInteger para3 = [para[@"para3"]integerValue];
        NSInteger para4 = [para[@"para4"]integerValue];
        B_VC *VC = [B_VC new];
        [VC action_B:para1 para2:para2 para3:para3 para4:para4];
    }
    @end

    此时mediator也需要做相应的更改,由原来直接调用组件B,改成了调用B的wrapper层:

    1
    2
    3
    4
    5
    -(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
        Class cls = NSClassFromString(@"target_B");
        NSObject *target = [[cls alloc]init];
        [target performSelector:NSSelectorFromString(@"B_Action:") withObject:@{@"para1":para1, @"para2":@(para2),@"para3":@(para3),@"para4":@(para4)} ];
    }

    现在的组件A调用组件B的流程如下所示:

    此时的项目结构如下:

    继续改进

    做到这里,看似比较接近我的要求了,但是还有有点小瑕疵:

    1. Mediator 每一个方法里都要写 runtime 方法,格式是确定的,这是可以抽取出来的。

    2. 每个组件对外方法都要在 Mediator 写一遍,组件一多 Mediator 类的长度是恐怖的。

    接着优化就是casa的方案了,我们来看看如何改进,直接看代码:

    针对第一点,我们可以抽出公共代码,当做mediator:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    #import "CTMediator.h"
    #import @interface CTMediator ()
     
    @property (nonatomic, strong) NSMutableDictionary *cachedTarget;
     
    @end
     
    @implementation CTMediator
     
    #pragma mark - public methods
    + (instancetype)sharedInstance
    {
        static CTMediator *mediator;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            mediator = [[CTMediator alloc] init];
        });
        return mediator;
    }
     
    /*
     scheme://[target]/[action]?[params]
     
     url sample:
     */
     
    - (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
    {
        NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
        NSString *urlString = [url query];
        for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {
            NSArray *elts = [param componentsSeparatedByString:@"="];
            if([elts count] < 2continue;
            [params setObject:[elts lastObject] forKey:[elts firstObject]];
        }
     
        // 这里这么写主要是出于安全考虑,防止黑客通过远程方式调用本地模块。这里的做法足以应对绝大多数场景,如果要求更加严苛,也可以做更加复杂的安全逻辑。
        NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""];
        if ([actionName hasPrefix:@"native"]) {
            return @(NO);
        }
     
        // 这个demo针对URL的路由处理非常简单,就只是取对应的target名字和method名字,但这已经足以应对绝大部份需求。如果需要拓展,可以在这个方法调用之前加入完整的路由逻辑
        id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO];
        if (completion) {
            if (result) {
                completion(@{@"result":result});
            else {
                completion(nil);
            }
        }
        return result;
    }
     
    - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
    {
     
        NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
        NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
        Class targetClass;
     
        NSObject *target = self.cachedTarget[targetClassString];
        if (target == nil) {
            targetClass = NSClassFromString(targetClassString);
            target = [[targetClass alloc] init];
        }
     
        SEL action = NSSelectorFromString(actionString);
     
        if (target == nil) {
            // 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
            return nil;
        }
     
        if (shouldCacheTarget) {
            self.cachedTarget[targetClassString] = target;
        }
     
        if ([target respondsToSelector:action]) {
            return [self safePerformAction:action target:target params:params];
        else {
            // 有可能target是Swift对象
            actionString = [NSString stringWithFormat:@"Action_%@WithParams:", actionName];
            action = NSSelectorFromString(actionString);
            if ([target respondsToSelector:action]) {
                return [self safePerformAction:action target:target params:params];
            else {
                // 这里是处理无响应请求的地方,如果无响应,则尝试调用对应target的notFound方法统一处理
                SEL action = NSSelectorFromString(@"notFound:");
                if ([target respondsToSelector:action]) {
                    return [self safePerformAction:action target:target params:params];
                else {
                    // 这里也是处理无响应请求的地方,在notFound都没有的时候,这个demo是直接return了。实际开发过程中,可以用前面提到的固定的target顶上的。
                    [self.cachedTarget removeObjectForKey:targetClassString];
                    return nil;
                }
            }
        }
    }
     
    - (void)releaseCachedTargetWithTargetName:(NSString *)targetName
    {
        NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
        [self.cachedTarget removeObjectForKey:targetClassString];
    }
     
    #pragma mark - private methods
    - (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
    {
        NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
        if(methodSig == nil) {
            return nil;
        }
        const char* retType = [methodSig methodReturnType];
     
        if (strcmp(retType, @encode(void)) == 0) {
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
            [invocation setArgument:¶ms atIndex:2];
            [invocation setSelector:action];
            [invocation setTarget:target];
            [invocation invoke];
            return nil;
        }
     
        if (strcmp(retType, @encode(NSInteger)) == 0) {
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
            [invocation setArgument:¶ms atIndex:2];
            [invocation setSelector:action];
            [invocation setTarget:target];
            [invocation invoke];
            NSInteger result = 0;
            [invocation getReturnValue:&result];
            return @(result);
        }
     
        if (strcmp(retType, @encode(BOOL)) == 0) {
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
            [invocation setArgument:¶ms atIndex:2];
            [invocation setSelector:action];
            [invocation setTarget:target];
            [invocation invoke];
            BOOL result = 0;
            [invocation getReturnValue:&result];
            return @(result);
        }
     
        if (strcmp(retType, @encode(CGFloat)) == 0) {
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
            [invocation setArgument:¶ms atIndex:2];
            [invocation setSelector:action];
            [invocation setTarget:target];
            [invocation invoke];
            CGFloat result = 0;
            [invocation getReturnValue:&result];
            return @(result);
        }
     
        if (strcmp(retType, @encode(NSUInteger)) == 0) {
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
            [invocation setArgument:¶ms atIndex:2];
            [invocation setSelector:action];
            [invocation setTarget:target];
            [invocation invoke];
            NSUInteger result = 0;
            [invocation getReturnValue:&result];
            return @(result);
        }
     
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        return [target performSelector:action withObject:params];
    #pragma clang diagnostic pop
    }
     
    #pragma mark - getters and setters
    - (NSMutableDictionary *)cachedTarget
    {
        if (_cachedTarget == nil) {
            _cachedTarget = [[NSMutableDictionary alloc] init];
        }
        return _cachedTarget;
    }
     
    @end

    针对第二点,我们通过把每个组件的对外接口进行分离,剥离到多个mediator的category里面,感官上把本来在一个mediator里面实现的对外接口分离到多个category里面,方便管理

    下面展示的是个组件B添加的category,组件A类似

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #import "CTMediator.h"
     
    @interface CTMediator (B_VC_Action)
    -(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4;
     
    @end
     
    ====================
    #import "CTMediator+B_VC_Action.h"
     
    @implementation CTMediator (B_VC_Action)
    -(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
        [self performTarget:@"target_B" action:@"B_Action" params:@{@"para1":para1, @"para2":@(para2),@"para3":@(para3),@"para4":@(para4)} shouldCacheTarget:YES];
    }
    @end

    此时调用者只要引入该category,然后调用即可,调用逻辑其实和上面没有拆分出category是一样的。此时的项目结构如下:

    URL-Scheme方案

    这个方案是流传最广的,也是最多人使用的,因为Apple本身也提供了url-scheme功能,同时web端也是通过URL的方式进行路由跳转,那么很自然的iOS端就借鉴了该方案。

    如何实现

    Router实现代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    #import typedef void (^componentBlock) (NSDictionary *param);
     
    @interface URL_Roueter : NSObject
    + (instancetype)sharedInstance;
    - (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk;
    - (void)openURL:(NSString *)url withParam:(id)param;
    @end
     
    ====================
     
     
    #import "URL_Roueter.h"
     
    @interface URL_Roueter()
    @property (nonatomic, strong) NSMutableDictionary *cache;
    @end
     
     
    @implementation URL_Roueter
     
    + (instancetype)sharedInstance
    {
        static URL_Roueter *router;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            router = [[URL_Roueter alloc] init];
        });
        return router;
    }
     
     
     
    -(NSMutableDictionary *)cache{
        if (!_cache) {
            _cache = [NSMutableDictionary new];
        }
        return _cache;
    }
     
     
    - (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk {
        [self.cache setObject:blk forKey:urlPattern];
    }
     
    - (void)openURL:(NSString *)url withParam:(id)param {
        componentBlock blk = [self.cache objectForKey:url];
        if (blk) blk(param);
    }
     
     
    @end

    组件A

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    #import "A_VC.h"
    #import "URL_Roueter.h"
     
    @implementation A_VC
     
    //把自己对外提供的服务(block)用url标记,注册到路由管理中心组件
    +(void)load{
        [[URL_Roueter sharedInstance]registerURLPattern:@"test://A_Action" toHandler:^(NSDictionary* para) {
            NSString *para1 = para[@"para1"];
            [[self new] action_A:para1];
        }];
    }
     
     
    -(void)viewDidLoad{
        [super viewDidLoad];
        UIButton *btn = [UIButton new];
        [btn setTitle:@"调用组件B" forState:UIControlStateNormal];
        btn.frame = CGRectMake(10010010050);
        [btn addTarget:self action:@selector(btn_click) forControlEvents:UIControlEventTouchUpInside];
        [btn setBackgroundColor:[UIColor redColor]];
     
        self.view.backgroundColor = [UIColor blueColor];
        [self.view addSubview:btn];
     
    }
     
    //调用组件B的功能
    -(void)btn_click{
        [[URL_Roueter sharedInstance]openURL:@"test://B_Action" withParam:@{@"para1":@"PARA1", @"para2":@(222),@"para3":@(333),@"para4":@(444)}];
    }
     
     
    -(void)action_A:(NSString*)para1 {
        NSLog(@"call action_A: %@",para1);
    }
     
    @end

    组件B实现的代码类似,就不在贴了。上面都是简化版的实现,不过核心原理是一样的。

    从上面的代码可以看出来,实现原理很简单:每个组件在自己的load方面里面,把自己对外提供的服务(回调block)通过url-scheme标记好,然后注册到URL-Router里面。

    URL-Router接受各个组件的注册,用字典保存了每个组件注册过来的url和对应的服务,只要其他组件调用了openURL方法,就会去这个字典里面根据url找到对应的block执行(也就是执行其他组件提供的服务)

    存在的问题

    通过url-scheme的方式去做组件化主要存在如下一些问题:

    需要专门的管理后台维护

    要提供一个文档专门记录每个url和服务的对应表,每次组件改动了都要即使修改,很麻烦。参数的格式不明确,是个灵活的 dictionary,同样需要维护一份文档去查这些参数。

    内存问题

    每个组件在初始化的时候都需要要路由管理中心去注册自己提供的服务,内存里需要保存一份表,组件多了会有内存问题。

    混淆了本地调用和远程调用

    url-scheme是Apple拿来做app之间跳转的,或者通过url方式打开APP,但是上述的方案去把他拿来做本地组件间的跳转,这会产生问题,大概分为两点:

    • 远程调用和本地调用的处理逻辑是不同的,正确的做法应该是把远程调用通过一个中间层转化为本地调用,如果把两者两者混为一谈,后期可能会出现无法区分业务的情况。比如对于组件无法响应的问题,远程调用可能直接显示一个404页面,但是本地调用可能需要做其他处理。如果不加以区分,那么久无法完成这种业务要求。

    • 远程调用只能传能被序列化为json的数据,像 UIImage这样非常规的对象是不行的。所以如果组件接口要考虑远程调用,这里的参数就不能是这类非常规对象,接口的定义就受限了。出现这种情况的原因就是,远程调用是本地调用的子集,这里混在一起导致组件只能提供子集功能(远程调用),所以这个方案是天生有缺陷的

    • 理论上来讲,组件化是接口层面的东西,应该用语言自身的特性去解决,而url是用于远程通信的,不应该和组件化扯上关系

    改进

    针对上述第二点描述的无法传递常规对象的问题,蘑菇街做了改进,通过protocol转class的方式去实现,但是我想说这种实现办法真是越高越复杂了。具体看代码就知道了

    protocolMediator实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    功能:通过protocol的字符串存储class
     
    #import @interface ProtocolMediator : NSObject
    + (instancetype)sharedInstance;
    - (void)registerProtocol:(Protocol *)proto forClass:(Class)cls;
    - (Class)classForProtocol:(Protocol *)proto;
     
    @end
     
    ============
     
    #import "ProtocolMediator.h"
     
    @interface ProtocolMediator()
    @property (nonatomic,strong) NSMutableDictionary *protocolCache;
     
    @end
    @implementation ProtocolMediator
     
     
    + (instancetype)sharedInstance
    {
    static ProtocolMediator *mediator;  
    static dispatch_once_t onceToken;  
    dispatch_once(&onceToken, ^{  
        mediator = [[ProtocolMediator alloc] init];
    });
    return mediator;  
    }
     
    -(NSMutableDictionary *)protocolCache{
        if (!_protocolCache) {
            _protocolCache = [NSMutableDictionary new];
        }
        return _protocolCache;
    }
     
    - (void)registerProtocol:(Protocol *)proto forClass:(Class)cls {
        [self.protocolCache setObject:cls forKey:NSStringFromProtocol(proto)];
    }
     
    - (Class)classForProtocol:(Protocol *)proto {
        return self.protocolCache[NSStringFromProtocol(proto)];
    }
     
     
    @end

    commonProtocol实现:

    1
    2
    3
    4
    5
    6
    7
    8
    功能:所有需要传递非常规参数的方法都放在这里定义,然后各个组件自己去具体实现(这里为了演示方便,使用的常规的字符串和int类型。当然也可以传递UIImage等非常规对象)
     
    #import @protocol A_VC_Protocol -(void)action_A:(NSString*)para1;
     
    @end
     
    @protocol B_VC_Protocol -(void)action_B:(NSString*)para para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4;
    @end

    组件A实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    #import #import "CommonProtocol.h"
     
    @interface A_VC : UIViewController@end
     
     
    =============================
     
    #import "A_VC.h"
    #import "ProtocolMediator.h"
     
     
    @implementation A_VC
     
    //注册自己的class
    +(void)load{
        [[ProtocolMediator sharedInstance] registerProtocol:@protocol(A_VC_Protocol) forClass:[self class]];
    }
     
     
    //调用组件B,先通过protocol字符串取出类class,然后再实例化之调用组件B的方法    
    -(void)btn_click{
        Class cls = [[ProtocolMediator sharedInstance] classForProtocol:@protocol(B_VC_Protocol)];
        UIViewController *B_VC = [[cls alloc] init];
        [B_VC action_B:@"param1" para2:222 para3:333 para4:444];
    }
     
     
    -(void)action_A:(NSString*)para1 {
        NSLog(@"call action_A: %@",para1);
    }
     
    @end

    组件B实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    #import #import "CommonProtocol.h"
     
     
    @interface B_VC : UIViewController@end
     
    =============
     
    #import "B_VC.h"
    #import "ProtocolMediator.h"
     
    @implementation B_VC
     
    +(void)load{
        [[ProtocolMediator sharedInstance] registerProtocol:@protocol(B_VC_Protocol) forClass:[self class]];
    }
     
     
    -(void)btn_click{
        Class cls = [[ProtocolMediator sharedInstance] classForProtocol:@protocol(A_VC_Protocol)];
        UIViewController *A_VC = [[cls alloc] init];
        [A_VC action_A:@"param1"];
    }
     
     
    -(void)action_B:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
        NSLog(@"call action_B: %@---%zd---%zd---%zd",para1,para2,para3,para4);
    }
     
    @end

    原理和缺点

    每个组件先通过 Mediator 拿到其他的组件对象class,然后在实例化该class为实例对象,再通过该对象去调用它自身实现的protocol方法,因为是通过接口的形式实现的方法,所以任何类型参数都是可以传递的。

    但是这会导致一个问题:组件方法的调用是分散在各地的,没有统一的入口,也就没法做组件不存在时的统一处理。

    从上面的实现就可以看出来A调用B不是直接通过mediator去调用,而是先通过mediator生成其他组件的对象,然后自己再用该对象去调用其他组件的方法,这就导致组件方法调用分散在各个调用组件内部,而不能像target-action方案那样对所有组件的方法调用进行统一的管理。

    再者这种方式让组件同时依赖两个中心:ProtocolMediator和CommonProtocol,依赖越多,后期扩展和迁移也会相对困难。

    并且这种调用其他组件的方式有点诡异,不是正常的使用方法,一般都是直接你发起一个调用请求,其他组件直接把执行结果告诉你,但是这里确实给你返回一个组件对象,让你自己在用这个对象去发起请求,这操作有点蛋疼。。。

    总结

    其实蘑菇街的url-scheme加上protocol-class方案一起提供组件间跳转和调用会让人无所适从,使用者还要区分不同的参数要使用的不同的方法,而target-action方案可以用相同的方法来传递任意参数。综上所述,target-action方案更优。

    Demo下载

    1. url-scheme

    2. protocol-class

    3. target-action



      原文地址:http://www.cocoachina.com/ios/20180417/23059.html


    展开全文
  • iOS App组件化开发实践

    2018-03-13 10:53:15
    iOS App组件化开发实践前因其实我们这个7人iOS开发团队并不适合组件化开发。原因是因为性价比低,需要花很多时间和经历去做这件事,带来的收益并不能彻底改变什么。但是因为有2~3个星期的空档期,并不是很忙;另外...

    iOS App组件化开发实践


    前因

    其实我们这个7人iOS开发团队并不适合组件化开发。原因是因为性价比低,需要花很多时间和经历去做这件事,带来的收益并不能彻底改变什么。但是因为有2~3个星期的空档期,并不是很忙;另外是可以用在一个全新的App上。所以决定想尝试下组件化开发。

    所谓尝试也就是说:去尝试解决组件化开发当中的一些问题。如果能解决,并且有比较好的解决方案,那就继续下去,否则就放弃。

    背景

    脱离实际情况去谈方案的选型是不合理的。

    所以先简单介绍下背景:我们是一家纳斯达克交易所上市的科技企业。我们公司还有好几款App,由不同的几个团队去维护,我们是其中之一。我们这个团队是一个7人的iOS开发小团队。作者本人是小组长。

    之前的App已经使用了模块化(CocoaPods)开发,并且已经使用了 二进制化 方案。App已经在使用自动化集成。

    虽然要开发一个新App,但是很多业务和之前的App是一样的或者相似的。

    为什么要写这篇博客?

    想把整个过程记录下来,方便以后回顾。

    我们的思路和解决方案不一定是对的或者是最好的。所以希望大家看了这篇博客之后,能给我们提供很多建议和别的解决方案,让我们可以优化使得这个组件化开发的方案能变得更加好。

    技术栈

    • gitlab
    • gitlab-runner
    • CocoaPods
    • CocoaPods-Packager
    • fir
    • 二进制化
    • fastlane
    • deploymate
    • oclint
    • Kiwi

    成果

    使用组件化开发App之后:

    • 代码提交更规范,质量提高。体现在测试人员反馈的bug明显减少。
    • 编译加快。在都是源码的情况下:原App需要150s左右整个编译完毕,然后开发人员才可以开始调试。而现在组件化之后,某个业务组件只需要10s~20s左右。在依赖二进制化组件的情况下,业务组件编译速度一般低于10s。
    • 分工更为明确,从而提升开发效率。
    • 灵活,耦合低。
    • 结合MVVM。非常细致的单元测试,提高代码质量,保证App稳定性。体现在测试人员反馈的bug明显减少。
    • 回滚更方便。我们经常会发生业务或者UI变回之前版本的情况,以前我们都是checkout出之前的代码。而现在组件化了之后,我们只需要使用旧版本的业务组件Pod库,或者在旧版本的基础上再发一个Pod库。
    • 新人更容易上手。

    对于我来说:

    • 更加容易地把控代码质量。
    • 更加容易地知道小组成员做了些什么。
    • 更加容易地分配工作。
    • 更加容易地安排新成员。

    解耦

    我们的想法是这样的,就算最后做不成组件化开发,把这些应该重用的代码抽出来做成Pod库也没有什么影响。所以优先做了这一步。

    哪些东西需要抽成Pod库?

    我们之前的App已经使用了模块化(CocoaPods化)开发。我们已经把会在App之间重用的Util、Category、网络层和本地存储等等这些东西抽成了Pod库。还有些一些和业务相关的,比如YTXChart,YTXChartSocket;这些也是在各个App之间重用的。

    所以得出一个很简单的结论:要在App之间共享的代码就应该抽成Pod库,把它们作为一个个组件。

    我们去仔细查看了原App代码,发现很多东西都需要重用而我们却没有把它们组件化。

    为什么没有把这些代码组件化?

    因为当时没想好怎么解耦,举个例子。

    有一个类叫做YTXAnalytics。是依赖UMengAnalytics来做统计的。 它的耦合是在于一个方法。这个方法是用来收集信息的。它依赖了User,还依赖了currentServerId这个东西。

    + (NSDictionary*)collectEventInfo:(NSString*)event withData:(NSDictionary*)data
    {
    .......
        return @{
            @"event" : event,
            @"eventType" : @"event",
            @"time" : [[[NSDate date] timeIntervalSince1970InMillionSecond] stringValue],
            @"os" : device.systemName,
            @"osVersion" : device.systemVersion,
            @"device" : device.model,
            @"screen" : screenStr,
            @"network" : [YTXAnalytics networkType],
            @"appVersion" : [AppInfo appVersion],
            @"channel" : [AppInfo marketId],
            @"deviceId" : [ASIdentifierManager sharedManager].advertisingIdentifier.UUIDString,
            @"username" : objectOrNull([YTXUserManager sharedManager].currentUser.username),
            @"userType" : objectOrNull([[YTXUserManager sharedManager].currentUser.userType stringValue]),
            @"company" : [[ServiceProvider sharedServiceProvider].currentServerId stringValue],
            @"ip" : objectOrNull([SSNetworkInfo currentIPAddress]),
            @"data" : jsonStr
        };
    }

    解决方案是,搞了一个block,把获取这些信息的责任丢出来。

    [YTXAnalytics sharedAnalytics].analyticsDataBlock = ^ NSDictionary *() {
            return @{
                     @"appVersion" : objectOrNull([PBBasicProviderModule appVersion]),
                     @"channel" : objectOrNull([PBBasicProviderModule marketId]),
                     @"username" : objectOrNull([PBUserManager shared].currentUser.username),
                     @"userType" : objectOrNull([PBUserManager shared].currentUser.userType),
                     @"company" : objectOrNull([PBUserManager shared].currentUser.serverId),
                     @"ip" : objectOrNull([SSNetworkInfo currentIPAddress])
                     };
        };

    我们的耦合大多数都是这种。解决方案都是弄了一个block,把获取信息的职责丢出来到外面。

    我们解耦的方式就是以下几种:

    1. 把它依赖的代码先做成一个Pod库,然后转而依赖Pod库。有点像是“依赖下沉”。
    2. 使用category的方式把依赖改成组合的方式。
    3. 使用一个block或delegate(协议)把这部分职责丢出去。
    4. 直接copy代码。copy代码这个事情看起来很不优雅,但是它的好处就是快。对于一些不重要的工具方法,也可以直接copy到内部来用。

    初始化

    AppDelegate充斥着各种初始化。 比如我们自己的代码。已经只是截取了部分!

    [self setupScreenShowManager];
    
        //event start
        [YTXAnalytics createYtxanalyticsTable];
        [YTXAnalytics start];
        [YTXAnalytics page:APP_OPEN];
        [YTXAnalytics sharedAnalytics].analyticsDataBlock = ^ NSDictionary *() {
            return @{
                     @"appVersion" : objectOrNull([AppInfo appVersion]),
                     .......
                     @"ip" : objectOrNull([SSNetworkInfo currentIPAddress]),
                     };
        };
    
        [self registerPreloadConfig];
        //Migrate UserDefault 转移standardUserDefault到group
        [NSUserDefaults migrateOldUserDefaultToGroup];
        [ServiceProvider sharedServiceProvider];
    
        [YTXChatManager sharedYTXChatManager];
        [ChartSocketManager sharedChartSocketController].delegate = [ChartProvider sharedChartProvider];
    
        //初始化最初的行情集合
        [[ChartProvider sharedChartProvider] addMetalList:[ChartSocketManager sharedChartSocketController].quoteList];
    
        //初始化环信信息Manager
        [YTXEaseMobManager sharedManager];

    比如第三方:

    //注册环信
        [self setupEaseMob:application didFinishLaunchingWithOptions:launchOptions];
    
        //Talking Data
        [self setupTalkingData];
        [self setupAdTalkingData];
        [self setupShareSDK];
        [self setupUmeng];
        [self setupJSPatch];
        [self setupAdhocSDK];
        [YTXGdtAnalytics communicateWithGdt];//广点通

    首先这些初始化的东西是会被各个业务组件都用到的。

    那我组件化开发的时候,每一个业务组件如何保证我使用这些东西的时候已经初始化过了呢?难道每一个业务组件都初始化一遍?有参数怎么办,能不能使用单例?

    但问题是第三方库基本都需要注册一个AppKey,我每一个业务组件里都写一份?那样肯定不好,那我配置在主App里的info.plist里面,每一个业务组件都初始化一下好了,也不会有什么副作用。但这样感觉不优雅,而且有很多重复代码。万一某个AppKey或重要参数改了,那每一个业务组件岂不是都得改了。这样肯定不行。另外一点,那我的业务组件必须依赖主App的内容了。无论是在主App里调试还是把主App的info.plist的相关内容拷贝过来使用。

    更关键的是有一些第三方的库需要在application: didFinishLaunchingWithOptions:时初始化。

    //初始化环信,shareSDK, 友盟, Talking Data等
    [self setupThirdParty:application didFinishLaunchingWithOptions:launchOptions];

    有没有更好的办法呢?

    首先我写了一个 YTXModule 。它利用runtime,不需要在AppDelegate中添加任何代码,就可以捕获App生命周期。

    在某个想获得App生命周期的类中的.m中这样使用:

    YTXMODULE_EXTERN()
    {
        //相当于load
        isLoad = YES;
    }
    + (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions
    {
        //实现一样的方法名,但是必须是静态方法。
        return YES;
    }

    分层

    因为在解决初始化问题的时候,要先设计好层级结构。所以这里突然跳转到分层。

    上个图:

    我们自己定了几个原则。

    • 业务组件之间不能有依赖关系。
    • 按照图示不能跨层依赖。
    • 所谓弱业务组件就是包含着少部分业务,并且可以在这个App内的各个业务组件之间重用的代码。
    • 要依赖YTXModule的组件一定要以Module结尾,而且它一定是个业务组件或是弱业务组件。
    • 弱业务组件以App代号开头(比如PB),以Module结尾。例:PBBasicProviderModule。
    • 业务组件以App代号开头(比如PB)BusinessModule结尾。例:PBHomePageBusinessModule。

    业务组件之间不能有依赖关系,这是公认的的原则。否则就失去了组件化开发的核心价值。

    弱业务组件之间也不应当有依赖关系。如果有依赖关系说明你的功能划分不准确。

    初始化

    我们约定好了层级结构,明确了职责之后。我们就可以跳回初始化的设计了。

    创建一个PBBasicProviderModule弱业务组件。

    • 它通过依赖YTXModule来捕捉App生命周期。
    • 它来负责初始化自己的和第三方的东西。
    • 所有业务组件都可以依赖这个弱业务组件。
    • 它来保证所有东西一定是是初始化完毕的。
    • 它来统一管理。
    • 它来暴露一些类和功能给业务组件使用。

    反正就是业务组件中依赖PBBasicProviderModule,它保证它里面的所有东西都是好用的。

    因为有了PBBasicProviderModule,所以才让我更明确了弱业务组件这个概念。

    因为我们懒,如果把PBBasicProvider定义为业务组件。那它和其他业务组件之间的通信就必须通过Bus、Notification或协议等等。

    但它又肯定是业务啊。因为那些AppKey肯定是和这个App有关系的,也就是App的相关配置和参数也可以说是业务;我需要初始化设置那些Block依赖User信息、CurrentServerId等等肯定都是业务啊。

    那只好搞个弱业务出来啊。因为我不能打破这个原则啊:业务组件之间不能互相依赖。

    再进一步分清弱业务组件和业务组件。

    业务组件里面基本都有:storyboard、nib、图片等等。弱业务组件里面一般没有。这不是绝对的,但一般情况是这样。

    业务组件一般都是App上某一具体业务。比如首页、我、直播、行情详情、XX交易大盘、YY交易大盘、XX交易中盘、资讯、发现等等。而弱业务组件是给这些业务组件提供功能的,自己不直接表现在App上展示。

    我们还可以创建一些弱业务组件给业务组件提供功能。当然了,不能够滥用。需要准确划分职责。

    最后,代码大概是这样的:

    @implementation PBBasicProviderModule
    
    YTXMODULE_EXTERN()
    {
    
    }
    
    + (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions
    {
        [self setupThirdParty:application didFinishLaunchingWithOptions:launchOptions];
        [self setupBasic:application didFinishLaunchingWithOptions:launchOptions];
    
        return YES;
    }
    
    + (void) setupThirdParty:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self setupEaseMob:application didFinishLaunchingWithOptions:launchOptions];
            [self setupTalkingData];
            [self setupAdTalkingData];
            [self setupShareSDK];
            [self setupJSPatch];
            [self setupUmeng];
    //        [self setupAdhoc];
        });
    }
    
    + (void) setupBasic:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
        [self registerBasic];
    
        [self autoIncrementOpenAppCount];
    
        [self setupScreenShowManager];
    
        [self setupYTXAnalytics];
    
        [self setupRemoteHook];
    }
    
    + (YTXAnalytics) sharedYTXAnalytics
    {
        return ......;
    }
    ......

    设想

    这个PBBasicProviderModule简直就是个大杂烩啊,把很多以前写在AppDelegate里的东西都丢在里面了。毫无优雅可言。

    的确是这样的,感觉没有更好的办法了。

    既然已经这样了。我们可不可以大胆地设想一下:每个开发者开发自己负责的业务组件的时候不需要关心主App。

    因为我知道美团的组件化开发必须依赖主App的AppDelegate的一大堆设置和初始化。所以干脆他们就直接在主App中集成调试,他们通过二进制化和去Pod依赖化的方式让主App的构建非常快。

    所以我们是不是可以继续污染这个PBBasicProviderModule。不需要在主App项目里的AppDelegate写任何初始化代码?基本或者尽量不在主App里写任何代码?改依赖主App变为依赖这个弱业务组件?

    按照这个思路我们搬空了AppDelegate里的所有代码。比如一些初始化App样式的东西、初始化RootViewController等等这些都可以搬到一个新的弱业务组件里。

    而业务组件其实根本不需关心这个弱业务组件,开发人员只需要在业务组件中的Example App中的AppDelegate中初始化自己业务组件的RootViewController就好了。

    其他的事情交给这个新的弱业务组件就好了。而主App和Example App只要在Podfile中依赖它就好了。

    所以最后的设想就是:开发者不会去改主App项目,也不需要知道主App项目。对于开发者来说,主App和业务组件之间是隔绝的。

    有一个更大的好处,我只要更换这个弱业务组件,这个业务组件就能马上适配一个新App。这也是某种意义上的解耦。

    Debug/Release

    谁说不用在主App里的AppDelegate写任何代码的,打脸。。。

    我们在对二进制Pod库跑测试的发现,源码能过,二进制(.a)不能过。百思不得其解,然后仔细查看代码,发现是这个宏的锅:

    #ifdef DEBUG
    
    #endif

    DEBUG在编译阶段就已经决定了。二进制化的时候已经编译完成了。 而我们的代码中充满着#ifdef DEBUG 就这样这样。那怎么办,这是二进制化的锅。但是我们的二进制化已经形成了标准,大家都自觉会这么做,怎么解决这个问题呢。

    解决方案是:

    创建了一个PBEnvironmentProvider。大家都去依赖它。

    然后原来判断宏的代码改成这样:

    if([PBEnvironmentProvider testing])
    {
    //...
    }

    在主App的AppDelegate中这样:

    #if DEBUG && TESTING
    //PBEnvironmentProvider提供的宏
    CONFIG_ENVIRONMENT_TESTING
    #endif

    原理是:如果AppDelegate有某个方法(CONFIG_ENVIRONMENT_TESTING宏会提供这个方法),[PBEnvironmentProvider testing]得到的结果就是YES。

    为什么要写在主App里呢?其实也可以丢在PBBasicProviderModule里面,提供一个方法啊。

    因为主App的AppDelegate.m是源码,未经编译。另外注意TESTING这个宏。我们可以在xcode设置里加一个macro参数TESTING,并且修改为0的情况下,能够生成一个实际是DEBUG的App但里面内容却是线上的内容。

    这个需求是来自于我们经常需要紧急通过xcode直接build一个app到手机上以解决或确认线上的问题。

    虽然打脸了,但是也还好,以后也不用改了。再说这个是特殊需求。除了这个之外,主App没有其他代码了。

    业务组件间通信

    我们解决了初始化和解耦的问题。接下来只要解决组件间通信的问题就好了。

    然后我找了几个第三方库,选用了 MGJRouter 。本来直接依赖它就好了。

    后来觉得都使用Block的方式会导致这样的代码,全部堆在了一个方法里:

    + (void) setupRouter
    {
    ......
    [MGJRouter registerURLPattern:@"mgj://foo/a" toHandler:^(NSDictionary *routerParameters) {
        NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]);
    }];
    [MGJRouter registerURLPattern:@"mgj://foo/b" toHandler:^(NSDictionary *routerParameters) {
        NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]);
    }];
    ......
    }

    这样感觉很不爽。那我干脆就把MGJRouter代码复制了下来,把Block改成了@selector。并且把它直接加入了 YTXModule 里面。并且使用了宏,让结果看起来优雅些。代码看起来是这样的:

    //在某个类的.m里,其实并不需要继承YTXModule也可以使用该功能
    YTXMODULE_EXTERN_ROUTER_OBJECT_METHOD(@"object1")
    {
        YTXMODULE_EXAPAND_PARAMETERS(parameters)
        NSLog(@"%@ %@", userInfo, completion);
        isCallRouterObjectMacro2 = YES;
        return @"我是个类型";
    }
    
    YTXMODULE_EXTERN_ROUTER_METHOD(@"YTX://QUERY/:query")
    {
        YTXMODULE_EXAPAND_PARAMETERS(parameters)
        NSLog(@"%@ %@", userInfo, completion);
        testQueryStringQueryValue = parameters[@"query"];;
        testQueryStringNameValue = parameters[@"name"];
        testQueryStringAgeValue = parameters[@"age"];
    }

    调用的时候看起来是这样的:

    [YTXModule openURL:@"YTX://QUERY/query?age=18&name=CJ" withUserInfo:@{@"Test":@1} completion:nil];
    
     NSString * testObject2 = [YTXModule objectForURL:@"object1" withUserInfo:@{@"Test":@2}];

    通信问题解决了。其实页面跳转问题也解决了。

    页面跳转

    页面跳转解决方案与业务组件之间通信问题是一样的。

    但是需要注意的是,你一个业务组件内部的页面跳转也请使用URL+Router的方式跳转,而不要自己直接pushViewController。

    这样的好处是:如果将来某些内部跳转页面需要给其他业务组件调用,你就不需要再注册个URL了。因为本来就有。

    是否去Model化

    去Model化主要体现在业务组件间通信,要不要传一个Model过去(传过去的Dictionary中的某个键是Model)。

    如果去Model化,这个业务组件的开发者如何确定Dictionary里面有哪些内容分别是什么类型呢?那需要有个地方传播这些信息,比如写在头文件,wiki等等。

    如果不去Model化的话,就需要把这个Model做成Pod库。两个业务组件都去依赖它。

    最后决定不去Model。因为实际上有一些Model就是在各个业务组件之间公用的(比如User),所以肯定就会有Model做成Pod库。我们可以把它做成重Model,Model里可以带网络请求和本地存储的方法。唯一不能避免的问题是,两个业务组件的开发者都有可能去改这个Model的Pod库。

    信息的披露

    跳转的页面需要传哪些参数? 业务组件之间传递数据时候本质的载体是什么?

    不同业务开发者如何知晓这些信息。

    使用去Model化和不使用去Model化,我们都有各自的方案。

    去Model化,则披露头文件,在头文件里面写详细的注释。

    如果不去Model化,则就看Model就可以了。如有特殊情况,那也是文档写在头文件内。

    总结的话:信息披露的方式就是把注释文档写在头文件内。

    组件的生命周期

    业务组件的生命周期和App一样。它本身就是个类,只暴露类方法,不存在需要实例,所以其实不存在生命周期这个概念。而它可以使用类方法创建很多ViewController,ViewController的生命周期由App管理。哪怕这些ViewController之间需要通信,你也可以使用Bus/YTXModule/协议等等方式来做,而不应该让业务组件这个类来负责他们之间的通信;也不应该自己持有ViewController;这样增加了耦合。

    弱业务组件的生命周期由创建它的对象来管理。按需创建和ARC自动释放。

    基础功能组件和第三方的生命周期由创建它的对象来管理。按需创建和ARC自动释放。

    版本规范

    我们自己定的规则。

    所有Pod库都只依赖到minor

    "~> 2.3"

    主App中精确依赖到patch

    "2.3.1"

    主App中的业务组件版本号的Main.Minor要和主App版本保持一致。

    参考: Semantic Versioning RubyGems Versioning Policies

    二进制化

    二进制化我认为是必须的,能够加快开发速度。

    而我使用的这个 二进制方案

    有个坑就是在gitlab-runner上在二进制和源码切换时,经常需要pod cache clean --all,test/lint/publish才能成功。而每次pod cache clean --all之后CocoaPods会去重新下载相关的pod库,增加了时间和不必要的开销。

    我们现在通过podspec中增加preserve_paths和执行download_zip.sh解决了cache的问题。原理是让pod cache既有源码又有二进制.a。具体可以看ytx-pod-template项目中的 Name.podspec 和 download_zip.sh 。

    二进制化还得注意宏的问题。小心使用宏,尤其是#ifdef。避免源码和二进制代码运行的结果不一样。

    集成调试

    集成调试很简单。每一个业务组件在自己的Example App中调试。

    这个业务组件的podspec只要写清楚自己依赖的库有哪些。剩下的其他业务组件应该写在Example App的Podfile里面。

    依赖的Pod库都是二进制的。如有问题可以装源码(IS_SOURCE=1 pod install)来调试。

    开发人员其实只需要关心自己的业务组件,这个业务组件是自洽的。

    公共库谁来维护的问题

    这个问题在我们这种小Team不存在。没有仔细地去想过。但是只要做好代码准入(Test/Lint/Code Review)和权限管理就应该不会存在大的问题。

    单元测试

    单元测试我们用的是 Kiwi 。 结合MVVM模式,对每一个业务组件的ViewModel都进行单元测试。每次push代码,gitlab-runner都会自动跑测试。一旦开发人员发现测试挂了就能够及时找到问题。也可以很容易的追溯哪次提交把测试跑挂了。

    这也是我们团队的强制要求。没有测试,测试写的不好,测试挂了,直接拒绝merge request。

    lint

    对每一个组件进行lint再发布,保证了正确性。这也是一步强制要求。

    lint的时候能够发现很多问题。通常情况下不允许warning出现的。如果不能避免(比如第三方)请用--allow-warnings。

    pod lib lint --sources=$SOURCES --verbose --fail-fast --use-libraries

    统一的网络服务和本地存储方式

    这个就很简单。把这两个部分抽象成几个Pod库供所有业务组件使用就好了。 我们这边分别是三个Pod库:

    • YTXRequest
    • YTXRestfulModel
    • NSUserDefault+YTX

    其他一些内容

    ignore了主App中的Podfile.lock尽量避免冲突。

    主App Archive的时候要使用源码,而不是二进制。

    后期可以使用oclint和deploymate检查代码。

    使用fastlane match去维护开发证书。

    一些需要从plist或者json读取配置的Pod库模块,要注意读出来的内容最好要加一个namespace。namespace可以是这个业务组件的名字。

    业务组件读取资源文件的区别

    #从main bundle中取。如果图片希望在storyboard中被找到,使用这种方式。
    s.resource = ["#{s.name}/Assets/**"]
    
    #只是希望在我这个业务组件的bundle内使用的plist。作为配置文件。这是官方推荐方式。
    s.resource_bundles = {
      "{s.name}/" => ["{s.name}/Assets/config.plist"]
    }

    持续集成

    原来的App就是持续集成的。想当然的,我们希望新的组件化开发的App也能够持续集成。

    Podfile应该是这样的:这里面出现的全是私有Pod库。

    pod 'YTXRequest', '2.0.1'
    pod 'YTXUtilCategory', '1.6.0'
    
    pod 'PBBasicProviderModule', '0.2.1'
    pod 'PBBasicChartAndSocketModule', '0.3.1'
    pod 'PBBasicAppInitModule', '0.5.1'
    ...
    
    pod 'PBBasicHomepageBusinessModule', '1.2.15'
    pod 'PBBasicMeBusinessModule', '1.2.10'
    pod 'PBBasicLiveBusinessModule', '1.2.1'
    pod 'PBBasicChartBusinessModule', '1.2.6'
    pod 'PBBasicTradeBusinessModule', '1.2.7'
    ...

    如果Pod依赖的东西特别特别多,比如100多个。另外又必须依赖主App做集成调试。 你也可以用这种方案:把你所有的Pod库的依赖都展开写到主App的Podfile中。而发布Pod库时podspec中不带任何的依赖的。这样就避免了pod install的时候解析依赖特别耗时的问题。

    各个脚本都在这个 ytx-pod-template 。先从.gitlab-ci.yml看起。

    我们持续集成的工具是gitlab runner。

    持续集成的整个流程是:

    第一步:

    使用template创建Pod。像这样:

    pod lib create <Pod库名称> --template-url="http://gitlab.baidao.com/pods/ytx-pod-template"

    第二步:

    创建dev分支。用来开发。

    第三步:

    每次push dev的时候会触发runner自动跑Stage: Init Lint(中的test)

    第四步:

    1.准备发布Pod库。修改podspec的版本号,打上相应tag。 2.使用merge_request.sh向master提交一个merge request。

    第五步:

    1.其他有权限开发者code review之后,接受merge request。 2.master合并这个merge request 3.master触发runner自动跑Stage: Init Package Lint ReleasePod UpdateApp

    第六步:

    如果第五步正确。主App的dev分支会收到一个merge request,里面的内容是修改Podfile。 图中内容出现了AFNetworking等是因为这个时候在做测试。

    第七步:

    主App触发runner,会构建一个ipa自动上传到 fir 。

    Init

    • 初始化一些环境。
    • 打印一些信息。

    Package

    • 二进制化打包成.a

    Lint

    • Pod lib lint。二进制和源码都lint。
    • 测试。
    • 以后考虑加入oclint和deploymate。

    ReleasePod

    • 把相关文件zip后,传到静态服务器库。以提供二进制化下载包。
    • pod repo push。发布该Pod库。

    ReleasePod的时候不允许Pod库出现警告。

    UpdateApp

    • 下载App代码
    • 修改Podfile文件。如果匹配到pod库文件名则修改,否则添加。
    • 生成一个merge request到主App的dev分支。

    关于gitlab runner。

    stage这个功能非常的厉害。强烈推荐。

    每一个stage可以跑在不同的runner上。每一个stage失败了可以单独retry。而某一个stage里面的任务可以并行执行:(test和lint就是并行的)

    转载自:http://www.chinaznyj.com/ZhiNengYun/1590.html###


    展开全文
  • iOS组件化及架构设计

    2019-01-03 10:32:06
    关于组件化 网上组件化的文章很多。很多文章一提到组件化,就会说解耦,一说到解耦就会说路由或者runtime。好像组件化 == 解耦 == 路由/Runtime,然而这是一个非常错误的观念。持有这一观点的人,没有搞清楚在组件...

    关于组件化

    网上组件化的文章很多。很多文章一提到组件化,就会说解耦,一说到解耦就会说路由或者runtime。好像组件化 == 解耦 == 路由/Runtime,然而这是一个非常错误的观念。持有这一观点的人,没有搞清楚在组件化中什么是想要结果,什么是过程。

    组件化和解耦

    大家不妨先思考两个问题:

    1、为何要进行组件化开发?

    2、各个组件之间是否一定需要解耦?

    采用组件化,是为了组件能单独开发,单独开发是结果。要让组件能单独开发,组件必须职责单一,职责单一需要用到重构和解耦的技术,所以重构和解耦是过程。那解耦是否是必须的过程?不一定。比如UIKit,我们用这个系统组件并没有使用任何解耦手段。问题来了,UIKit苹果可以独立开发,我们使用它为什么没用解耦手段?答案很简单,UIKit没有依赖我们的代码所以不用解耦。

    PS:我这里不纠结组件、服务、模块、框架的概念,网上对这些概念的定义五花八门,实际上把简单的事说复杂了。我这里只关心一件事,这一部分代码能否独立开发,能就叫组件,不能我管你叫什么

    我们之所以要解耦才能独立开发,通常是出现了循环依赖。这时候当然可以无脑的用路由把两个组件的耦合解开,也可以独立开发。然而,这样做只是把强引用改成了弱引用,代码还是烂代码。站在重构的角度来说,A、B组件循环依赖就是设计有问题,要么应该重构A、B让依赖单向;要么应该抽离一个共用组件C,让A、B组件都只依赖于C。

    如果我们每个组件都只是单向依赖其他组件,各个组件之间也就没有必要解耦。再换个角度说,如果一个组件职责不单一,即使跟其他组件解耦了,组件依然不能很好的工作。如何解耦只是重构过程中可选手段,代码设计的原则如依赖倒置、接口隔离、里氏替换,都可以指导我们写出好的组件。

    所以在组件化中重要的是让组件职责单一,职责单一的重要标志之一就是没有组件间的循环依赖

    架构图

    一般来讲,App的组件可以分为三层,上层业务组件、中层UI组件、底层SDK组件

    同一层之间的组件互相独立,上层的组件耦合下层的组件。一般来讲,底层SDK组件和中层UI组件都是独立的功能,不会出现同层耦合。

    架构图

    业务组件解耦

    上层业务组件之间的解耦,采用依赖注入的方式实现。每个模块都声明一个自己依赖的协议,在App集成方里去实现这些协议。

    我之前的做法是每个模块用协议提供自己对外的能力,其他模块通过协议来访问它。这样做虽然也可以解耦,但是维护成本很高,每个模块都要去理解其他模块。同时也引入了其他模块自己用不到的功能,不符合最小依赖的原则。

    使用依赖注入,APP集成方统一去管理各个模块的依赖,每个模块也能单独编译,是业务层解耦的最佳实践。

    包管理

    要解除循环依赖,引入包管理技术cocoapods会让我们更有效率。pod不允许组件间有循环依赖,若有pod install时就会报错。

    cocoapods,提供私有pod repo,使用时把自己的组件放在私有pod repo里,然后在Podfile里直接通过pod命令集成。一个组件对应一个私有pod,每个组件依赖自己所需要的三方库。多个组件联合开发的时候,可以再一个podspec里配置子模块,这样在每个组件自己的podspec里,只需要把子模块里的pod依赖关系拷贝过去就行了。

    在多个组件集成时会有版本冲突的问题。比如登录组件(L)、广告组件(A)都依赖了埋点组件(O),L依赖O的1.1版本,A依赖O的1.2版本,这时候集成就会报错。为了解决这个错误,在组件间依赖时,不写版本号,版本号只在APP集成方写。即podfile里引用所有组件,并写上版本号,.podspec里不写版本号。

    这样做既可以保证APP集成方的稳定性,也可以解决组件依赖的版本冲突问题。这样做的坏处是,所有组件包括App集成方,在使用其他组件时,都必须使用其他组件最新的API,这会造成额外的升级工作量。如果不想接受组件升级最新api的成本,可以私有化一个三方库自己维护。

    组件开发完毕后告诉集成方,目前的组件稳定版本是多少,引用的三方库稳定版本集成方自己去决定

    推荐的组件版本号管理方式

    另一种版本管理的方式,是在podspec里写依赖组件的版本号,podfile里不写组件依赖的版本,然后通过内部沟通来解决版本冲突的问题。我认为虽然也能做,但有很多弊端。

    1.作为App集成方,没办法单独控制依赖的三方库版本。三方库升级会更复杂

    2.每个依赖的三方库,都应该做了完整的单元测试,才能被集成到App中。所以正确的逻辑不是组件内测试过三方库没问题就在组件内写死版本号,而是这个三方库经过我们测试后,可以在我们系统中使用XX版本。

    3.在工程中就没有一个地方能完整知道所有的pod组件,而App集成方有权利知道这一点

    4.沟通成本高

    不推荐的方式

    顺便说一句,基础组件库可以通过pod子模块单独暴露独立功能,较常用。

    以上,就是组件化的所有东西。你可能会奇怪,解耦在组件化过程中有什么用。答案是解耦是为了更好的实现组件的单一职责,解耦的作用在架构设计中谈。需要再次强调,组件化 ≠ 解耦。

    如果非要给组件化下一个定义,我的理解是:

    组件化意味着重构,目的是让每个组件职责单一。在结构上,每个组件都最小依赖它所需要的东西。


    关于架构设计

    在我看来,iOS客户端架构主要为了解决两个问题,一是解决大型项目分组件开发的效率的问题,二是解决单进程App的稳定性的问题。

    设计到架构设计的都是大型App,小型App主要是业务的堆叠。很多公司在业务初期都不会考虑架构,在业务发展到一定规模的时候,才会重新审视架构混乱带来的开发效率和业务稳定性瓶颈。这时候就会引入组件化的概念,我们常常面临的是对已有项目的组件化,这一过程会异常困难。

    组件拆分原则

    对老工程的组件拆分,我的办法是,从底层开始拆。SDK>  模块 > 业务 。如果App没有SDK可以抽离,就从模块开始拆,不要为了抽离SDK而抽离。常见的误区是,大家一拿到代码就把公共函数提出来作为共用框架,起的名字还特别接地气,如XXCommon。

    事实上,这种框架型SDK,是最鸡肋的组件,原因是它实用性很小,无非就是减少了点冗余代码。而且在架构能力不强的情况下,它很容易变成“垃圾堆”,什么东西都想往里面放,后面越来越庞大。所以,开始拆分架构的时候,尽量以业务优先,比如先拆分享模块。

    如果两个组件中有共同的函数,前期不要想着提出来,改个名字让它冗余是更好的办法。如果共同耦合的是一个静态库,可以利用动态库的隔离性封装静态库,具体方法可以网上找。

    响应式

    基础组件常常要在系统启动时初始化,或者接受App生命周期时间。这就引出了个问题,如何给appDelegate瘦身?比如我们现在有两个基础组件A、B,他们都需要监听App生命周期事件,传统的做法是,A、B两个组件都提供一些函数在appDelegate中调用。但这样做的坏处是,如果某一天我不想引入B组件了,还得去改appDelegate代码。理想的方式是,基础组件的使用不需要在appDelegate里写代码

    为了实现基础组件与appDelegate分离,得对appDelegate改造。首先得提出一个观点,苹果的appDelegate设计的有问题,它在用代理模式解决观察者模式的问题。在《设计模式》中,代理模式的设计意图定义是:为其他对象提供一种代理以控制对这个对象的访问。反过来看appDelegate你会发现,它大部分代理函数都没有办法控制application,如applicationDidBecomeActive。applicationDidBecomeActive这种事件常常需要多个处理者,这种场景用观察者模式更适合。而openURL需要返回BOOL值,才需要使用代理模式。App生命周期事件虽然可以用监听通知获取,但用起来不如响应式监听信号方便。

    基于响应式编程的思想,我写了一个TLAppEventBus,提供属性来监听生命周期事件。我并不喜欢庞大的ReactiveObjectC,所以我通过category实现了简单的响应式,用户只需要监听需要的信号即可。在TLAppEventBus里,我默认提供了8个系统事件用来监听,如果有其他的系统事件需要监听,可以使用扩展的方法,给TLAppEventBus添加属性(见文末Demo)。

    路由

    对于Appdelegate中的openURL的事件,苹果使用代理模式并没有问题,但我们常常需要在openURL里面写if-else区分事件的处理者,这也会造成多个URL处理模块耦合在Appdelegate中。我认为appdelegate中的openURL应该用路由转发的方式来解耦。

    openURL代理需要同步返回处理结果,但网上开源的路由框架能同步返回结果的。所以我这边实现了一个能同步返回结果的路由TLRouter,同时支持了注册scheme。注册scheme这一特性,在第三方分享的场景下会比较有用(见文末Demo)。

    另外,网上大部分方案都搞错了场景。以蘑菇街的路由方案为例(好像iOS的路由就是他们提出来的?),蘑菇街认为路由主要有两个作用,一是发送数据让路由接收者处理,二是返回对象让路由发送者继续处理。我不禁想问,这是路由吗?不妨先回到URL的定义

    URL: 统一资源标识符(Uniform Resource Locator,统一资源定位符)是一个用于标识某一互联网资源名称的字符串

    openURL就是在访问资源,在浏览器中,openURL意味着打开一个网页,openURL的发起者并不关心打开的内容是什么,只关心打开的结果。所以苹果的openURL Api 就只返回了除了结果YES/NO,没有返回一个对象。所以,我对openURL这一行为定义如下

    openURL:访问资源,返回是否访问成功

    那把蘑菇街的路由,返回的对象改成BOOL值就可以了么?我认为还不够。对于客户端的路由,使用的实际上是通知的形式在解耦,带来的问题是路由的注册代码散落在各地,所以路由方案必须要配路由文档,要不然开发者会不知道路由在干嘛。

    有没有比文档更好的方式呢?我的思路是:用schema区分路由职责

    系统的openURL只干了两件事:打开App和打开网页

    [[UIApplicationsharedApplication] openURL:[NSURLURLWithString:@"weixin://"]]; // 打开App

    [[UIApplicationsharedApplication] openURL:[NSURLURLWithString:@"https://www.baidu.com"]];//打开网页

    两者的共性是页面切换。所以我这边设计的路由openURL,只扩充了controller跳转的功能,比如打开登录页

    [TLRouter openURL:@"innerJump://account/login"];

    只扩充了controller跳转的功能好处是让路由的职责更单一,同时也更符合苹果对openURL的定义。工程师在看到url schema的时候就知道他的作用,避免反复查看文档。

    对于数据的传递,我认为不应该用路由的方式。相比路由,通过依赖注入传入信号是更好的选择。

    App配置

    有时候我们需要组件的跨App复用,在App集成组件时,能够不改代码只改配置是最理想的方式。使用组件+plist配置是一个方案,具体做法是把A组件的配置放在A.plist中,在A组件内写死要读取A.plist。

    以配置代替硬编码,防止对代码的侵入,是一个很好的思路。设想一下,如果我们可以通过配置在决定App是否使用组件、也可通过配置来改变组件和app所需的参数,那运维可以代替app开发来出包,这对效率和稳定性都会有提升。为了实现这一效果,我使用了OC的runtime来动态注册组件。需要在didfinishLaunch初始化的组件,可以实现代理 - (void)initializeWhenLaunch; 这样,自动初始化函数,就可以通过runtime+plist里配置的class name自动初始化。组件需要初始化的代码,可以在自己的initializeWhenLaunch里做。

    由于路由只扩充了controller跳转的功能,所以路由注册这一行为也可进行一次抽象,把不同的部分放在plist配置文件,相同的放到runtime里做。这样做还有个好处是,程序内的路由跳转在一个plist里可以都可以看到

    appdelegate改造后示例


    iOS解耦工具Tourelle

    Tourelle,是根据上面的思路写的一个开源项目 https://github.com/zhudaye12138/Tourelle,可以通过pod集成  pod 'Tourelle'。下面介绍一下他的使用方式

    TLAppEventBus

    TLAppEventBus通过接收系统通知来获取app生命周期事件,收到生命周期事件后改变对应属性的值。默认提供了didEnterBackground等八个属性,可以使用响应式函数来监听 

    - (void)observeWithBlock:(TLObservingBlock)block; 

        [TLAppEventBus.shared.didBecomeActive observeWithBlock:^(idnewValue) {

            //do some thing

        }];

    需要注意,如果在其它地方使用observeWithBlock,需要设置属性的owner,否则没有办法监听到。这里不用单独设置是因为在TLAppEventBus里已设置好

    TLAppEventBus使用前需要调用 - (void)start; 如果需要监听更多的事件,可以调用

    - (void)startWithNotificationMap:(NSDictionary *)map; 

      NSMutableDictionary *defaultMap = [NSMutableDictionary dictionaryWithDictionary:[TLAppEventBus defaultNotificationMap]]; //获取默认map

        [defaultMapsetObject:KDidChangeStatusBarOrientation forKey:UIApplicationWillChangeStatusBarOrientationNotification]; //添加新的事件

        [TLAppEventBus.shared startWithNotificationMap:defaultMap];//开启EventBus

    添加新事件需要用分类添加TLAppEventBus的属性,添加后就可正常使用了

    -(void)setDidChangeStatusBarOrientation:(NSNotification*)didChangeStatusBarOrientation {

        objc_setAssociatedObject(self, (__bridge const void *)KDidChangeStatusBarOrientation , didChangeStatusBarOrientation, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    }

    -(NSNotification*)didChangeStatusBarOrientation {

        returnobjc_getAssociatedObject(self, (__bridge const void *)KDidBecomeActive);

    }

    TLRouter

    路由支持两种注册方式,一种只写schema,一种写url路径

     [TLRouter registerURL:@"wx1234567://" hander:^(TLRouterURL *routeURL, void (^callback)(BOOL result)) {       

            //do something     

    }]//注册schema

    [TLRouter registerURL:@"InnerJump://account/login" hander:^(TLRouterURL *routeURL, void (^callback)(BOOL result)) {

                    //do something

     }]//注册url路径

    支持同步 & 异步获取返回值,其中异步转同步内部通过semaphore实现

    +(void)openURL:(NSString*)url callback:(void(^)(BOOLresult))callback;

    +(BOOL)openURL:(NSString*)url;

    另外openURL除了支持url中带参数,也支持参数放在字典中

    +(BOOL)openURL:(NSString*)url param:(NSDictionary *)param;

    TLAppLaunchHelper

    TLAppLaunchHelper有两个函数,一个用来初始化组件。该函数会读取AutoInitialize.plist中的classes,通过runtime + 自动初始化协议完成初始化

    -(void)autoInitialize;

    AutoInitialize.plist

    另一个函数用来自动注册路由,该函数会读取AutoRegistURL.plist完成路由注册。其中controller代表类名,params代表默认参数,如果openURL传的参数与默认参数不符合,路由会报错

    -(void)autoRegistURL;

     

     AutoRegistURL.plist

     

    路由注册时,并不决定controller跳转的方式。注册者只是调用presentingSelf方法,跳转方式由controller中presentingSelf方法决定。

    -(BOOL)presentingSelf {

        UINavigationController *rootVC = (UINavigationController *) APPWINDOW.rootViewController;

        if(rootVC) {

            [rootVCpushViewController:self animated:YES];

            returnYES;

       }

        return NO;

    }

     

    耦合检测工具

    针对既有代码的组件化重构,我这边开发了一个耦合检测工具,目前只支持OC。

    耦合检测工具的原理是这样:工具认为工程中一级文件夹由组件构成,比如A工程下面有aa、bb、cc三个文件夹,aa、bb、cc就是三个待检测的组件。耦合检测分三步,第一步通过正则找到组件内.h文件中所有关键字(包括函数、宏定义和类)。第二步通过找到的组件内关键字,再通过正则去其它组件的.m中找是否使用了该组件的关键字,如果使用了,两个组件就有耦合关系。第三步,输出耦合检测报告

    代码:开源中....

    总结

    本文给出了组件化的定义:组件化意味着重构,目的是让每个组件职责单一以提升集成效率。包管理技术Pod是组件化常用的工具,iOS组件依赖及组件版本号确定,都可以用pod实现。整个iOS工程的组件通常分为3层,业务组件、模块组件和SDK组件。在老工程重构时,优先抽离SDK组件,切记不要写XXCommon让它变成垃圾堆。

    关于解耦的技术,appldegate适合用观察者模式替换代理模式,路由只用来做controller之间的跳转,上层业务组件的解耦靠依赖注入而不是全用路由。工程的组件和路由都可通过runtime + 配置的形式自动注册,这样做维护和集成都会很方便。

    Demo地址:https://github.com/zhudaye12138/Tourelle



    作者:朱大爷12138
    链接:https://www.jianshu.com/p/d88aef8e29a4
    來源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

    展开全文
  • 模块间相互不干扰,就有了,今天组件化之路。 一、组件化的目的。 说是组件化,其实更多的是模块化,对模块之间相互之间不干扰,可以单独打包,测试,且相同模块不同项目之间的移植便利许多。和Android的插件化...

    前言:公司一年多的小项目,进行项目拆分,要求是每个业务模块都可以单独打包。在开发过程中,如:酒店模块,只修改酒店单元,测试也只测试酒店部分。模块间相互不干扰,就有了,今天组件化之路。

    一、组件化的目的。
    说是组件化,其实更多的是模块化,对模块之间相互之间不干扰,可以单独打包,测试,且相同模块不同项目之间的移植便利许多。和Android的插件化大同小异,尤其是使用了cocoapods管理。

    二、插件化需要使用到哪些技术。
    在考虑拆分时候,考虑使用的是类似携程的主项目引用子项目的模式,但是考虑到尝试使用cocoapods 管理未果,还有就是考虑到项目之间的引用比较混乱,就使用了类似百度或者阿里的插件化方案。即将主项目变成壳工程,将其余模块都变成私有pods的形式引用。
    这时候我们就会考虑到使用什么技术去让模块间相互调用。git上有各自router ,可以考虑的也有有多,包括ct的,MGJ ,阿里的Been。这个着重谈一下,CTMediator 和MGJRouter.

    casatwy是通过CTMediator类实现组件化的,在此类中对外提供明确参数类型的接口,接口内部通过performTarget方法调用服务方组件的Target、Action。由于CTMediator类的调用是通过runtime主动发现服务的,所以服务方对此类是完全解耦的。但如果CTMediator类对外提供的方法都放在此类中,将会对CTMediator造成极大的负担和代码量。解决方法就是对每个服务方组件创建一个CTMediator的Category,并将对服务方的performTarget调用放在对应的Category中,这些Category都属于CTMediator中间件,从而实现了感官上的接口分离。但是实际过程中,还是通过CTMediator 对象,用runtime 创建对应的方法,然后当有这个对应方法时,之间运行实现方法。

    蘑菇街的组件化方案,更像是一个中间件,中间件的注册用一个字典来保持,拿到url 将url 逐层拆分,然后将一个block 和url、回调的block 对应保存。当使用open url的时候,运行对应的Block 和回调的block 。

    组件化过程中如何管理。
    因为MGJRouter 需要一个注册的过程,并且需要知道注册的Url 才能调用。所有需要有一个保存的过程,这个创建一个文档用来保存对应的名称 url 用途,传参数等。另外,项目中使用私有pods 可以很容易的区分版本,只要保存podfile对应的私有pods 的版本号即可。即使私有私有pods 已经上传至服务端。

    三、项目的基本架构
    这里写图片描述

    每个BU相互独立,但是唯一没有解耦的是,相互的之间的调用需要注册,如果使用的是CTMediator可剩下注册这步,完全解耦。

    展开全文
  • iOS 组件化开发

    2018-11-14 14:36:25
    在一个APP开发过程中,如果项目较小且团队人数较少,使用最基本的MVC、MVVM开发就已经足够了,因为维护成本比较低。 但是当一个项目开发团队人数较...在这时,组件化开发就派上很大用场了,所谓的组件化开发,就是把...
  • iOS组件化方案对比

    2019-05-17 14:09:56
    随着公司业务的不断发展,项目的功能越来越复杂,各个业务代码耦合也越来越多,代码量也是急剧增加,传统的MVC或者MVVM架构已经无法高效的管理工程代码,因此需要用一种技术来更好地管理工程,而组件化(也可称为...
  • iOS组件化之具体实现

    2018-11-16 14:36:19
    组件主要分三类:基础功能组件,基础UI组件,产品业务组件
  • iOS组件化

    2019-12-31 18:16:27
    组件化这个东西其实并不复杂,他就是种思路,本质上是一种 app 架构思路,说穿了很简单的,难在组件化改造的时候,真正写起代码会出现不少棘手的问题。组件化是一种 app 架构,他的发展也是沿着正常的技术发展脉络来...
  • 组件化在业界已经炒的水深火热,关于组件化的好处和组件化的方案网上已经有大篇的文章了。笔者通过拆分一个现有的demo来简单聊一下项目实施组件化的过程(将分为上、中、下三篇)。...《iOS组件化(下篇)-加载XIB、图
  • iOS 组件化,插件化设计思路分析 前言 随着用户的需求越来越多,对App的用户体验也变的要求越来越高。为了更好的应对各种需求,开发人员从软件工程的角度,将App架构由原来简单的MVC变成MVVM,VIPER等...
  • iOS组件化(上篇)- 拆分基础组件 http://www.jianshu.com/p/760d6cd46719 iOS组件化(中篇)-拆分业务组件 http://www.jianshu.com/p/e6e84688f0b8 iOS组件化(下篇)-加载XIB、图片资源 ...
  • 经过前两篇文章的学习,相信对组件化开发有了大致的了解,那我们这篇文章就来讲讲资源文件的加载吧 这里我新建了一个LXFMain组件库,主要是用来显示TabBar的玩意,然后再进行组件化抽离出来,其中的过程这里不再...
  • ios组件化之私有组件创建以及使用 直接进入主题,如有不对处,希望大家指出! 需要:cocoapods,码云账号,SourceTree(可无) 开始创建一个组件 随意找个地方创建一个文件夹,用来做本地仓库,主要存放...
  • 组件化介绍 需求来源 随着项目规模不断扩大,业务模块增多,开发过程中会有多条产品线(多人或多小组开发不同的功能);如果用传统的开发模式,会导致代码臃肿,编译速度越来越慢,开发效率低下,代码维护成本越来越高. ...
  •   在进行iOS组件化开发时常常遇到某些供应商或者第三方的组件不支持use_framework。具体原因:第三方库很久没人维护了, 某些供应商其实也是把别人的库包了一层。当然了还有一些别的原因。   以友盟为例。传统的...
  • 我们在做组件化之前,必须要弄清楚,我们为什么要组件化,如果没有明显的优点,或者解决我们的所需,我们没有必要组件化。在app迭代如此快速的情况下,耗费时间精力去做这么一件事情到底值不值得? 一、组件化所...
  • 先宝一下组件化的私有荚方案。[ iOS组件化方案选择(http://blog.csdn.net/shangy110/article/details/78937148 )然后确定使用哪个中间件。这里,我着重说一下MGJRouter和CTMediator 那我们用私有pods会有哪些坑...
  • 最近研究了一下项目的组件化,把casa、bang、limboy的有关组件化的博客看了一遍,学到了不少东西,对目前业界的组件化方案有了一定的了解。这些高质量的博客大致讨论了组件化的三种方案:url-block、protocol-class...
  • iOS组件化 打包SDK

    2019-11-16 22:24:12
    项目组件化可能使用到的framework打包流程梳理(本文均已ISHTool_SDK项目为例). framework打包 1.新建工程, 选择 iOS -> Framework & Library -> Cocoa Touch Framework, 进行下一步 2.在新建的framework...
  • iOS组件化开发

    2017-02-16 11:07:14
    为什么要组件化?   组件和组件之间没有明确的约束;   组件单独开发、单独测试,不能揉入主项目中开发,测试也可以针对性的测试;   代码拆分还不足以解决业务之间的代码耦合,为了更好的让...
1 2 3 4 5 ... 20
收藏数 54,149
精华内容 21,659
关键字:

ios组件化