iOS进阶-架构-组件化

1,486 阅读12分钟

1. 组件化的由来

1.1 为什么要做组件化

早期项目建立之初工程一般较为简单,但是随着项目的慢慢的扩展、功能逐渐的增多,界面VC等之间的通讯满天飞,还有最主要的就是,大的项目一般都不会是一个人做的,如何做到组员高效率的开发、减少沟通成本等一系列的问题。这时开发人员就会意识到组件化的重要性;

1.2 组件化的好处

  • 模块间解耦
  • 模块重用
  • 提高团队协作开发效率
  • 单元测试

1.3 每个项目都需要组件化吗

由于做组件化也是需要时间和成本的,如果你的项目满足下面几点,那就暂时把组件化放一放。

  • 项目较小,模块间交互简单,耦合少
  • 模块没有被多个外部模块引用,只是一个单独的小模块
  • 模块不需要重用,代码也很少被修改
  • 团队规模很小

2. 组件化划分

要做组件化,那么如何划分组件就是开发人员首先需要考虑的问题,组件划分的颗粒度和解耦合程度,会直接影响你组件化的开发,以及项目的以后的走势。

2.1 划分依据

上图把整个工程划分成了4块:业务模块、通用模块、基础模块、管理工具

  • 业务模块

在业务模块这层,一般用业务去划分组件,比如首页、我的、关注。。。

  • 通用模块

在业务模块这层,一般会从业务模块下沉一些通用的常用控件、数据管理。。。

  • 基础模块

在基础模块这层,一般会把一些宏定义、分类等不怎么变化的基础内容放到这层

  • 管理模块

而以上这些个组件又是通过管理模块中个管理工具去控制,一般用CoCoaPods/Cathage

2.2 划分注意点

上面的划分依据只是满足通用的工程架构,你也可以多分几层,但是在划分时,你需要注意以下几点

  • 依赖下沉:如果同级模块中出现相互依赖部分,那么要这部分抽出来,下沉到下一个模块
  • 依赖不可倒置:要上层依赖下层进行开发,下层不能依赖上层

3. Cocoapods 管理组件

3.1 Cocoapods下载组件原理

Cocoapods 如何下载对应的组件?在我们pod install的时候,Cocoapods 根据组件名字比如AFNetworking在本地Specs文件中找到相应的文件夹,之后找到对应的版本josn文件

josn文件里面包含了AFNetworkinggithub上的下载地址,最后根据这个下载地址下载到本地项目中
注意如果组件名字有依赖到其他组件,那么josn文件里面会有dependencies的信息;

3.2 Cocoapods 管理私有组件

既然Cocoapods可以管理第三方的组件,那当然可以管理自己本地划分出来的私有组件,我们可以将项目根据上面所说的划分依据,拆分成若干个组件,这几个组件都通过Cocoapods来管理,需要使用的时候,就通过Cocoapods来组合,项目就像搭积木一样方便快捷,至于私有组件如何去具体管理,您可以参考这篇文章使用Cocoapods创建私有库。下面是我对步骤的简单总结

  1. 创建模板库 pod lib create KTestBaseModle
  2. 复制私有组件文件到replaceme.m所属文件夹
  3. pod install Example文件夹
  4. 将这个私有库上传到远程git库中,其他程序员就只要使用pod install就能使用你写的组件了

3.2.1 Cocoapods 管理私有组件注意点

  • 先从基础模块开始创建私有组件,防止后期的上层模块又要拆分出来
  • 如果组件有下层依赖,那就需要在 pod ->xxx.podspec文件中添加依赖,以及头文件
s.dependency 'AFNetworking', '~> 2.3'
s.dependency 'Masonry'
s.dependency 'KMacro'

prefix_header_contents = '#import "AfNetworking.h",#import "Masonry.h",#import "KMacro.h"'

AfNetworking,Masonry这种三方库cocoapods能够被识别从而下载下来,而KMacro这种是你自己的私有组件,那就需要在Podfile文件中添加索引路径

use_frameworks!

platform :ios, '8.0'

target 'KTestBaseModle_Example' do
  pod 'KTestBaseModle', :path => '../'
  //私有组件添加索引路径,我这里是本地路径 如果是网上的 就需要换成网上的路径
  Pod 'KMacro', :path => '../../KMacro'

  target 'KTestBaseModle_Tests' do
    inherit! :search_paths

    
  end
end
  • 如果私有库使用还涉及到一些资源文件:图片,json,xib...,那么在工程里使用的时候需要新的代码路径,下面是加载一张图片的示例代码,当然每个资源这么写显得很麻烦,你可以自己写个宏做个封装
NSString *bundlePath = [[NSBundle bundleForClass:[self class]].resourcePath stringByAppendingPathComponent:@"/KTestBaseModle.bundle"];
NSBundle *resurce_bundle = [NSBundle bundleWithPath:bundlePath];
self.imageView.image =  [UIImage imageNamed:@"testImage.png" inBundle:resurce_bundle compatibleWithTraitCollection:nil];

并且在xxx.podspec文件文件中还需要给到图片路径(资源路径)

   s.resource_bundles = {
     'KTestBaseModle' => ['KTestBaseModle/Assets/*.png']
   }

4. 组件之间的通讯

4.1 模块耦合

在上面的内容中我们聊了组件划分以及具体的管理操作,但是组件化工作里面还有一个重要的内容:组件间的通讯。日常开发中,模块之间的必然需要相互通讯,但是这样会带来不同模块之间的耦合,就像下图所示,最起码,。模块之间你得导入头文件吧

为了避免这种模块耦合,开发人员提出了使用一个中间件来解耦,所有的模块都之和中间件通讯,消息都只由一个中间件去发送,如下图Mediator就是一个中间件

4.1.1 中间件注意点

  • 使用字符串去去确定一个类,因为字符串可以解耦合,中间件不需要导入对应的头文件;
  • 模块和中间件之间使用中间件的分类去管理和调用组件

5 组件化业内方案

目前业内有三种主流的组件方案:MGJRouterCTMediatorBeeHive

5.1 MGJRouter

这个组件是蘑菇接开发团队提出的,基于路由和协议思想设计的,但是由于其内存常驻、字符串硬编码、解耦能力有限等局限性,这里不再讨论。

5.2 CTMediator

该方案基于Mediator(中间件)和Target-Action模式组件化,通过运行时完成调用。简单来说:Mediator维护着若干个category,一个category对应一个target,一个target可以包含多个action。 可以这么理解:一个A业务组件包含一个category组件,这个category组件中有个Mediator的category,这个category中有一个target,这个target属于对应的A业务组件,target中会有若干接口方法,用来其他业务组件通过这个category获取到A业务组件中的业务。 所有组件都通过组件自带的Target-Action来响应,也就是说,模块与模块之间的接口被固化在了Target-Action这一层。

5.2.1 核心方法

基于runtime

  • - (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
- (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];
    NSLog(@"返回值的类型 %s",retType);

    if (strcmp(retType, @encode(void)) == 0) {
        // 通过NSMethodSignature对象创建NSInvocation对象,NSMethodSignature为方法签名类
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        // 设置消息参数
        [invocation setArgument:&params 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:&params 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:&params 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:&params 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:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        NSUInteger result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }
    
// 当你确定编译器的警告对你来说没有什么用处的时候,为了避免心烦,
// 你可以使用#pragma clang diagnostic ignored “xxx” 这样的语句来忽略掉相应的警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}

5.3 BeeHive

CTMediator更多的是一对一的调用模式,但是如果换成一对多的调用方式,比如在用户登录成功的时候,好多的页面需要调整,这时CTMediator就会显得不那么方便了。对于这种情况,阿里的组件化方案 :BeeHive 可能会更加适合。

5.3.1 问题

我们知道在appdelegate中,包含了下面这些时机方法

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;
- (void)applicationDidEnterBackground:(UIApplication *)application;
- (void)applicationWillEnterForeground:(UIApplication *)application;
...

但是这些时机需要开发人员去做主动获取,缺少主动下发能力。eg:某个vc需要监测applicationDidEnterBackground时机,就要在改方法中去做一个消息的通知,无法做到自动下发;

5.3.2 骚气的BeeHive

根据源码,我们看看BeeHive是怎么做的

  • 用自定义的BHAppDelegate去替换原有的AppDelegate。这样BHAppDelegate就拥有了系统事件收集能力;然后后将这些事件用全局单利对象BHModuleManager去存储起来
//BHAppDelegate 遵循<UIApplicationDelegate>协议,所以拥有协议能力
@interface BHAppDelegate : UIResponder <UIApplicationDelegate>
@end
@implementation BHAppDelegate

@synthesize window;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    //储存事件
    [[BHModuleManager sharedManager] triggerEvent:BHMSetupEvent];
    [[BHModuleManager sharedManager] triggerEvent:BHMInitEvent];
    
    dispatch_async(dispatch_get_main_queue(), ^{
        [[BHModuleManager sharedManager] triggerEvent:BHMSplashEvent];
    });
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000
    if ([UIDevice currentDevice].systemVersion.floatValue >= 10.0f) {
        [UNUserNotificationCenter currentNotificationCenter].delegate = self;
    }
#endif
    
#ifdef DEBUG
    [[BHTimeProfiler sharedTimeProfiler] saveTimeProfileDataIntoFile:@"BeeHiveTimeProfiler"];
#endif
    
    return YES;
}

-(void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL))completionHandler {
    [[BeeHive shareInstance].context.touchShortcutItem setShortcutItem: shortcutItem];
    [[BeeHive shareInstance].context.touchShortcutItem setScompletionHandler: completionHandler];
    [[BHModuleManager sharedManager] triggerEvent:BHMQuickActionEvent];
}
...
...
  • 单利对象BeeHive内部保存一个全局的BHContext对象,BHContext用来保存application、launchOptions、openURLItem、、、application原来的属性和参数(包括方法里的参数)
@interface BeeHive : NSObject

//save application global context
@property(nonatomic, strong) BHContext *context;
...
@end

@interface BHContext : NSObject <NSCopying>

//global env
@property(nonatomic, assign) BHEnvironmentType env;

//global config
@property(nonatomic, strong) BHConfig *config;

//application appkey
@property(nonatomic, strong) NSString *appkey;
//customEvent>=1000
@property(nonatomic, assign) NSInteger customEvent;
//全局的application
@property(nonatomic, strong) UIApplication *application;
//launchOptions 
@property(nonatomic, strong) NSDictionary *launchOptions;
//3D-Touch 包含内部属性会被封装成BHShortcutItem
#if __IPHONE_OS_VERSION_MAX_ALLOWED > 80400
@property (nonatomic, strong) BHShortcutItem *touchShortcutItem;
#endif
//OpenURL model
@property (nonatomic, strong) BHOpenURLItem *openURLItem;
...
  • 使用BHModuleProtocol协议去包含个原来application中的方法,比如只要某vc遵循了BHModuleProtocol,他就能接收到里面BHModuleProtocol的方法;
@protocol BHModuleProtocol <NSObject>

@optional
//如果不去设置Level默认是Normal
//basicModuleLevel不去实现默认Normal
- (void)basicModuleLevel;
//越大越优先
- (NSInteger)modulePriority;

- (BOOL)async;
:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions


//这个类似于didFinishLaunchingWithOptions方法,而原方法里的(UIApplication *)application和(NSDictionary *)launchOptions两个参数会被储存在(BHContext *)context里
- (void)modSetUp:(BHContext *)context;
- (void)modInit:(BHContext *)context;
- (void)modSplash:(BHContext *)context;
- (void)modQuickAction:(BHContext *)context;
- (void)modTearDown:(BHContext *)context;
- (void)modWillResignActive:(BHContext *)context;
- (void)modDidEnterBackground:(BHContext *)context;
- (void)modWillEnterForeground:(BHContext *)context;
....
  • 最后当BHApplication在收到系统事件后(eg:WillEnterForeground),会通过BHModuleManager,找到事先实现对应BHModuleProtocol协议的模块集合,然后遍历调用,调用的同时会把全局的context参数一并给模块使用
- (void)handleModuleEvent:(NSInteger)eventType
                forTarget:(id<BHModuleProtocol>)target
           withSeletorStr:(NSString *)selectorStr
           andCustomParam:(NSDictionary *)customParam
{
    //区全局BHContext的对象
    BHContext *context = [BHContext shareInstance].copy;
    context.customParam = customParam;
    context.customEvent = eventType;
    //根据eventType枚举取到对应的方法
    if (!selectorStr.length) {
        selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
    }
    SEL seletor = NSSelectorFromString(selectorStr);
    if (!seletor) {
        selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
        seletor = NSSelectorFromString(selectorStr);
    }
    //id<BHModuleProtocol> moduleInstance 是都是遵守协议的,所以能够执行协议中的方法
    NSArray<id<BHModuleProtocol>> *moduleInstances;
    if (target) {
        moduleInstances = @[target];
    } else {
        moduleInstances = [self.BHModulesByEvent objectForKey:@(eventType)];
    }
    //最后遍历调用
    [moduleInstances enumerateObjectsUsingBlock:^(id<BHModuleProtocol> moduleInstance, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([moduleInstance respondsToSelector:seletor]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            //把参数context 给 moduleInstance
            [moduleInstance performSelector:seletor withObject:context];
#pragma clang diagnostic pop
            
            [[BHTimeProfiler sharedTimeProfiler] recordEventTime:[NSString stringWithFormat:@"%@ --- %@", [moduleInstance class], NSStringFromSelector(seletor)]];
            
        }
    }];
}

整个过程如下图,注意:图中的通知就是遍历调用

5.3.3 BeeHive 如何建立协议和控件(eg:vc)联系的?

上面说的是appdelegate事件如何被主动下发,即一对多的操作,那一对一的操作又是如何实现的呢?

下面以在官方提供的样例代码中BHUserTrackViewController交易vc说明

id<UserTrackServiceProtocol> v4 = [[BeeHive shareInstance] createService:@protocol(UserTrackServiceProtocol)];

BHViewControllerinit()方法中有这么一段代码,只通过UserTrackServiceProtocol就能取到v4(BHUserTrackViewController)的vc对象,那UserTrackServiceProtocoBHUserTrackViewController是如何绑定了呢?继续看源码

5.3.3.1 存
#import "BHUserTrackViewController.h"
#import "BeeHive.h"
#import "BHService.h"

//这个宏是关键
@BeeHiveService(UserTrackServiceProtocol,BHUserTrackViewController)
//BHUserTrackViewController 遵循 UserTrackServiceProtocol
@interface BHUserTrackViewController()<UserTrackServiceProtocol>

@end
@implementation BHUserTrackViewController
+(BOOL)singleton {
    return NO;
}

@end

我们看到有这一句@BeeHiveService(UserTrackServiceProtocol,BHUserTrackViewController)看上去就是绑定操作,那就继续深究,查看这个宏

//使用__attribute() 函数可以在编译其就能将数据写入到mach-o data段
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))

//建立 servicename 和 impl联系
#define BeeHiveService(servicename,impl) \
class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}";

源码中使用了一个 __attribute()C函数宏在项目编译期就能将protocol和vc对应关系写入mach-o的data段

5.3.3.2 取
//C函数 会在dyld - doModinitFunctions函数后自动调用
__attribute__((constructor))
void initProphet() {
    //在编译期对_dyld_register_func_for_add_image()进行监听,用dyld_callback这个函数保存监听后的动作
    _dyld_register_func_for_add_image(dyld_callback);
}

#### dyld_callback
static void dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide) {
    //从Mach-o DATA段取出之前写入的集合BHReadConfiguration()这个方法是关键
    NSArray *mods = BHReadConfiguration(BeehiveModSectName, mhp);
    for (NSString *modName in mods) {
        Class cls;
        if (modName) {
            cls = NSClassFromString(modName);
            
            if (cls) {
                [[BHModuleManager sharedManager] registerDynamicModule:cls];
            }
        }
    }
    
    
    //这个是从
    NSArray<NSString *> *services = BHReadConfiguration(BeehiveServiceSectName,mhp);
    for (NSString *map in services) {
        NSData *jsonData =  [map dataUsingEncoding:NSUTF8StringEncoding];
        NSError *error = nil;
        id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
        if (!error) {
            if ([json isKindOfClass:[NSDictionary class]] && [json allKeys].count) {
                
                NSString *protocol = [json allKeys][0];
                NSString *clsName  = [json allValues][0];
                
                if (protocol && clsName) {
                    [[BHServiceManager sharedManager] registerService:NSProtocolFromString(protocol) implClass:NSClassFromString(clsName)];
                }
                
            }
        }
    }
}

##### BHReadConfiguration() 
NSArray<NSString *>* BHReadConfiguration(char *sectionName,const struct mach_header *mhp)
{
    NSMutableArray *configs = [NSMutableArray array];
    unsigned long size = 0;
    //又从DATA 取出来
#ifndef __LP64__
    uintptr_t *memory = (uintptr_t*)getsectiondata(mhp, SEG_DATA, sectionName, &size);
#else
    const struct mach_header_64 *mhp64 = (const struct mach_header_64 *)mhp;
    uintptr_t *memory = (uintptr_t*)getsectiondata(mhp64, SEG_DATA, sectionName, &size);
#endif
    
    unsigned long counter = size/sizeof(void*);
    for(int idx = 0; idx < counter; ++idx){
        char *string = (char*)memory[idx];
        NSString *str = [NSString stringWithUTF8String:string];
        if(!str)continue;
        
        BHLog(@"config = %@", str);
        if(str) [configs addObject:str];
    }
    return configs;
}

又通过监听_dyld_register_func_for_add_image()方法在回调方法dyld_callback()里从之前的mach-o DATA段取出对应集合,让后保存在本地内存中。预编译时期的存,和dyld链接时期的取--内存动态注入,真是太骚了!

写在最后

如果文章对您有帮助,烦请点个赞,小弟在此谢过了!

参考 www.jianshu.com/p/6ee3e8a50…