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

推荐阅读更多精彩内容

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