iOS 模块化探索

[toc]

现状

杂乱的调用.png
  1. 编译
  2. 开发效率(编译;模糊、不便捷的已有能力)
  3. 代码混乱,层次不明,沉重的冗余,杂乱的引用
  4. 测试

模块化

业务模块化、功能组件化


理想.png

模块化方案对比维度

  1. 使用是否方便
    1.1 快速调用(接口可读性)
    1.2 传参方便、规范
    1.3 便捷的管理和维护

  2. 解耦程度
    2.1 能否完全解耦,是否需要额外依赖
    2.2 新的同样功能的模块能否快速替换现有模块

  3. 模块更新是否快捷
    3.1 新版模块快速被其它业务模块使用
    3.2 模块接口变更时能否被其它模块感知

  4. 回滚要方便
    4.1 当发现某个模块不能上线需要紧急回滚到上个版本时改动范围是否可控。

  5. 改造项目所需工作量
    5.1 需要投入多少人力能将现有的项目实现模块化。

  6. 性能
    6.1 模块间方法频繁调用性能要可控

基本原理

类型I:操作映射型
image.png
类型II:反射型
反射架构.png
类型III:抽象型
抽象型架构.png

实例分析

1. MGJRouter
MGJRouter架构.png

ModuleA注册

/// 例1 普通
[MGJRouter registerURLPattern:@"mgj://foo/bar" toHandler:^(NSDictionary *routerParameters) {
    NSLog(@"routerParameters[@"userInfo"] = %@", routerParameters[@"userInfo"]);
}];

/// 例2 block参数
[MGJRouter registerURLPattern:@"mgj://requestUserInfo" toHandler:^(NSDictionary *routerParameters) {
   NSString *url = routerParameters[@"url"];
   [MJHttpManager.manager requestUserInfoWithUrl:url completion:^(NSDictionary * _Nullable userInfo, NSError *_Nullable error) {
        void (^completion)(id result) = routerParameters[MGJRouterParameterCompletion];
        if (completion) {
            completion(userInfo);
         }
    }];
}];

/// 例3 返回NSObject
[MGJRouter registerURLPattern:@"mgj://loginViewController" toObjectHandler:^(id)(NSDictionary *routerParameters) {
     MGJLoginViewController *loginVC = [[MGJLoginViewController alloc] init];
     return loginVC;
}];

/// 例4
#define TEMPLATE_URL @"mgj://search/:keyword"
[MGJRouter registerURLPattern:TEMPLATE_URL  toHandler:^(NSDictionary *routerParameters) {
    NSLog(@"search[keyword]:%@", routerParameters[@"keyword"]); // Hangzhou
}];

ModuleB调用

/// 例1 普通
[MGJRouter openURL:@"mgj://foo/bar" withUserInfo:@{@"user_id": @1998} completion:nil];

/// 例2 block参数
[MGJRouter openURL:@"mgj://requestUserInfo?url=xxxx" withUserInfo:nil completion:^(NSDictionary *result){
    NSLog(@"用户信息 = %@",result);
}];

/// 例3 返回NSObject
MGJLoginViewController *loginVC = [MGJRouter objectForURL:@"mgj://loginViewController"];
if ([loginVC isKindOfClass:[MGJLoginViewController class]]) {
    NSLog(@"同步获取 登录VC 成功");
} else {
    NSLog(@"同步获取 登录VC 失败");
}

/// 例4
[MGJRouter openURL:[MGJRouter generateURLWithPattern:TEMPLATE_URL parameters:@[@"Hangzhou"]]];

核心

+ (void)openURL:(NSString *)URL withUserInfo:(NSDictionary *)userInfo completion:(void (^)(id result))completion
{
    URL = [URL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSMutableDictionary *parameters = [[self sharedInstance] extractParametersFromURL:URL matchExactly:NO];
    
    [parameters enumerateKeysAndObjectsUsingBlock:^(id key, NSString *obj, BOOL *stop) {
        if ([obj isKindOfClass:[NSString class]]) {
            parameters[key] = [obj stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        }
    }];
    
    if (parameters) {
        MGJRouterHandler handler = parameters[@"block"];
        if (completion) {
            parameters[MGJRouterParameterCompletion] = completion;
        }
        if (userInfo) {
            parameters[MGJRouterParameterUserInfo] = userInfo;
        }
        if (handler) {
            [parameters removeObjectForKey:@"block"];
            handler(parameters);
        }
    }
}

优点:

  1. 核心代码量少且简白,便于维护、定位问题。
  2. 适合应用于简单页面级解耦。

缺点:

  1. 注册、调用、传参存在大量硬编码,而且接口也太松散了,官方建议参考例4来规避这个问题,但是TEMPLATE_URL又应该定义在哪里,业务模块怎样暴露自己的能力?而且也只能解决部分问题。
  2. 由于多了注册操作,在哪里以及什么时候注册又是个问题。使用block的形式注册,也容易循环引用。
  3. block作为参数的调用很不智能,block只能有一个为NSDictionary类型的参数。
  4. 同步返回数据很不友好,只能返回id类型的数据(例3)。
  5. 模块接口的声明者和模块接口的实现者方相互无法感知对方的变动。
2. CTMediator
CTMediator官方Demo.png
CTMediator架构.png

Adapter层是CTMediator的分类,增加业务模块的过程也就是增加分类的过程。
ModuleB调用形式:

/// 例1
[[CTMediator sharedInstance] CTMediator_showAlertWithMessage:@"弹窗" cancelAction:nil confirmAction:^(NSDictionary *info) {
      // Just do it
}];
/// 例2
UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];

Adapater_A层:

- (void)CTMediator_showAlertWithMessage:(NSString *)message cancelAction:(void(^)(NSDictionary *info))cancelAction confirmAction:(void(^)(NSDictionary *info))confirmAction
{
    NSMutableDictionary *paramsToSend = [[NSMutableDictionary alloc] init];
    if (message) {
        paramsToSend[@"message"] = message;
    }
    if (cancelAction) {
        paramsToSend[@"cancelAction"] = cancelAction;
    }
    if (confirmAction) {
        paramsToSend[@"confirmAction"] = confirmAction;
    }
    [self performTarget:@"Module_A"
                 action:@"showAlert"
                 params:paramsToSend
      shouldCacheTarget:NO];
}

CTMediator核心方法:

// 核心接口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;
// 核心方法
- (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:&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);
    }
    ……
}

ModuleA实现:

@interface Module_A : NSObject
- (id)showAlert:(NSDictionary *)params;
@end

@implementation Module_A
- (id)showAlert:(NSDictionary *)params
{
    UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        CTUrlRouterCallbackBlock callback = params[@"cancelAction"];
        if (callback) {
            callback(@{@"alertAction":action});
        }
    }];
    
    UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"confirm" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        CTUrlRouterCallbackBlock callback = params[@"confirmAction"];
        if (callback) {
            callback(@{@"alertAction":action});
        }
    }];
    
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"alert from Module A" message:params[@"message"] preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:cancelAction];
    [alertController addAction:confirmAction];
    [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alertController animated:YES completion:nil];
    return nil;
}
@end

优点:

  1. 核心代码量少,便于维护
  2. 业务模块间无需硬编码即可实现调用
  3. 业务库无需向CTMediator注册

缺点:

  1. Adapater层是个分类,不同业务模块间方法名不能同名
  2. safePerformAction:target:params:的实现方式导致业务库的方法只能是methodName:形式且参数必须为NSDictionary类型
  3. Adapater层及传参扔采用硬编码方式调用导致业务库接口变更Adapater层无法感知(无编译错误),运行时才会Crash
3. Poseidon(改良版BeeHive)
Poseidon Demo.png

Poseidon架构.png

模块调用

// 例1
id<HomeModuleProtocol> homeModule = [PDModuleManager.manager moduleInstanceForModuleProtocol:@protocol(HomeModuleProtocol)];
UIViewController *vc = homeModule.homeViewController;
[self presentViewController:vc animated:YES completion:nil];

// 例2
id<CameraModuleProtocol> cameraModule = [PDModuleManager.manager moduleInstanceForModuleProtocol:@protocol(CameraModuleProtocol)];
[cameraModule pickAnPhotoWithCompletion:^(NSData *imageData, NSError * _Nullable error) {
  if (error) {
      NSLog(@"Error: %@", error.localizedDescription);
      UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:error.localizedDescription delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
      [alert show];
  } else {
      self.imageView.image = [UIImage imageWithData:imageData];
  }
}];

// 例3
UIViewController *vc = PDModule(HomeModuleProtocol).homeViewController;
[self presentViewController:vc animated:YES completion:nil];

接口层

@protocol CameraModuleProtocol <PDModule>
- (void)pickAnPhotoWithCompletion:(void(^)(NSData *imageData, NSError *error))completion;
@end

模块实现

@interface CameraModule : NSObject <CameraModuleProtocol>
@end

@PD_EXPORT_MODULE(CameraModuleProtocol, CameraModule);
@implementation CameraModule
- (void)pickAnPhotoWithCompletion:(void (^)(NSData *, NSError *))completion {
    [[[ImagePickerController alloc] init] showWithCompletion:completion];
}
@end

核心

// 声明
@PD_EXPORT_MODULE(CameraModuleProtocol, CameraModule);
char * kCameraModuleProtocol __attribute((used, section("__DATA, 'PDModule'"))) = "{\"\"CameraModuleProtocol\"\":\"CameraModule\"\"\"}"

__attribute__((constructor))
void initProphet() {
    _dyld_register_func_for_add_image(__pd_dyld_callback);
}

static void __pd_dyld_callback(const struct mach_header *mhp, intptr_t vmaddr_slide) {
    // read modules
    NSArray<NSString *> *services = __pd_readConfiguration("PDModule",mhp);
    // PDModuleManager register modules
    ........   
}

static NSArray<NSString *>* __pd_readConfiguration(char *sectionName,const struct mach_header *mhp) {
    NSMutableArray *configs = [NSMutableArray array];
    unsigned long size = 0;
    const struct mach_header_64 *mhp64 = (const struct mach_header_64 *)mhp;
    uintptr_t *memory = (uintptr_t *)getsectiondata(mhp64, SEG_DATA, sectionName, &size);
    unsigned long counter = size / sizeof(void *);
    for(int idx = 0; idx < counter; ++idx) {
        char *string = (char *)memory[idx];
        if (!string) { continue; }
        NSString *str = [NSString stringWithUTF8String:string];
        if (!str) {
            continue;
        } else {
            [configs addObject:str];
        }
    }
    return configs;
}

优点

  1. 无硬编码,接口可读性强调用和传参和原生几乎无差,理论上调用效率也比反射方案更高。
  2. 模块接口变动可在编译阶段报错(有利有弊吧)
  3. 核心代码量少且简单,有利于维护

缺点

  1. 建议新建专门遵守协议的类(还算可接受吧,毕竟频率不高)
  2. 接口层和实现层依旧不能相互感知,变动无法通知到对方

补充
模块生命周期、模块保活、 模块优先级、application方法的透传

4. 简白Category型
简易Category型Demo.png
简易Category型架构.png

ModuleB调用

UIViewController *vc = [MIModuleA playAudioViewControllerWithSongID:@"0001"];

声明层

@interface MIModuleA : NSObject
+ (UIViewController *)playerViewControllerWithSongID:(NSString *)songID;
@end

@implementation MIModuleA
/// 默认实现
+ (UIViewController *)playerViewControllerWithSongID:(NSString *)songID {
     return [[UIViewController alloc] init];
}
@end

ModuleA实现

@implementation MIModuleA (implementationA)
+ (UIViewController *)playerViewControllerWithSongID:(NSString *)songID {
     return [[PlayerViewController alloc] initWithSongID:songID];
}
@end

优点:

  1. 开局一个分类方法全靠累加,方法名、传参无需硬编码,调用简单直白,Category的方式理论上效率还可能高些。
  2. 由于过于简白基本无需维护代码。

缺点:

  1. 接口层更改API后模块实现层无法感知修改。
  2. 由于MIModuleA (implementationA)过于单薄,不方便在其中添加复杂逻辑。
  3. 分类重写方法的过程中方法名存在出错而无报错的情况。

难点

  1. 随着项目增长单个子业务模块样例工程所依赖的业务模块就很繁多导致快速编译又变成了奢望
  2. 模块间传通用对象
  3. 怎样快速回滚单个子业务模块
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 感觉我去年11月的时候还不知道啥是组件化和模块化,今年这个时候就可以写这个topic了也是神奇0.0 首先说下在我...
    木小易Ying阅读 4,372评论 0 15
  • iOS原生模块化的探索 大概是去年秋天开始,随着沪江学习的App越来越大,我做了很多模块化的尝试,最近要把沪学的一...
    moubuns阅读 2,764评论 3 8
  • 前言 什么是模块化项目由多个模块构成,模块间解耦、可重用,模块可通过aar依赖。每个模块都可以独立运行。 为什么要...
    朔野阅读 2,380评论 5 11
  • 最近在重构公司的一个项目,准备把项目进行模块化,顺便记录一下在重构过程中的一些感想。 Android模块化设计方...
    王远道呀阅读 1,164评论 1 5
  • 忙了一个多月,一直没时间写文章。终于把项目重构完了,借此机会浅谈一下对Android架构的见解。笔者将会把重构分为...
    Robin_Lrange阅读 9,134评论 1 15