IOS组件化方案

前言


应用在逐步迭代中,代码体积越来越大。现有的工程代码虽然已经按照模块分门别类存放,但是其引用文件还是很随意,代码间的耦合性高,使得代码“牵一发而动全身”,使开发人员对源代码的维护带来极大的不便。因此,为了今后更好的增删和重用代码,需要将各个业务逻辑拆分,实现组件化。

demo地址:demo地址

组件化简介


组件化就是将各个业务逻辑代码或者功能代码拆分出来,形成独立的一部分。组件化的最初目的是代码重用,比如一个弹窗提示功能会在很多项目很多页面调用,它的样式相对固定,使用频率高,因此可以提取出来作为一个组件,其他项目只要引用该组件就能在自己的页面中调用,这样可以减少大量的重复代码。其特点是高内聚,即脱离掉主工程代码也能编译通过,低耦合,从主工程中剥离简单容易。

组件化方案


基本概念

组件化应当是可以完全剥离出主程序,并可以独立编译通过并且运行,但是实际项目中,特别是没有组件化架构的项目想要中途将业务逻辑拆分出来还是有一定困难。因此在方案上我进行了调整,允许初期组件化各个组件对主程序有依赖,如下图所示:


业务逻辑.png

总结下方案设计大致有如下要求:

  • 各个组件可以脱离主程序独立成块并编译,但允许存在少量依赖。
  • 主程序脱离各个组件可以独立运行,对组件没有依赖。

组件化实现


采用静态库脱离主程序

网络上有很多组件化的方法,经过多次尝试,发现都有一定弊端,而且适合新项目,对已有项目不友好。最后,我采取静态库加工程组的方式进行组件化。

所谓工程组,就是将多个项目工程关联到一起,由.xcworkspace文件管理。常用的cocoaPods就是用这种形式。

工程管理.png

建多个项目文件的好处是可以将组建的代码和主程序代码完全剥离开来,做到高内聚的特点。

下面我将演示如何创建一个静态库工程。

1、在文件夹中创建新项目


创建静态库1.png

2、创建项目的时候选取项目组


选择项目组.png

3、静态库项目中将编译格式切换成static Library

选择编译模式.png

4、创建资源文件target

创建资源文件.png

创建资源文件2.png
创建资源文件3.png
修改资源文件属性.png

5、将静态包引入主工程,并设置先后编译顺序

5关联静态库.png
关联静态库2.png

按照以上方法配置,一个组件化就完成,后续可以在NMModelA.xcodeproj中编写组件代码。NMModelAResource targets主要是为了存放一些资源文件,比如图片资源、xib编译后的nib文件。每次改动图片或者xib都需要把新新生成的资源文件替换掉主程序中的资源文件 过程如下图所示:

将变动的资源文件复制到主工程.png

每次添加新资源文件或者新建xib记得从NMModelA target删除,添加到 NMModelAResource中,否则实际包会生成2份或多份图片,造成包大小变大

删除图片.png
添加图片.png

使用静态库加工程组的方式的好处:

  • 高内聚,低耦合

由于工程的限制,主程序的代码无法直接引用各个组件的代码,组件中的代码也无法直接引用主程序,真正实现了组件化的概念。

  • 编译调试方便

在验证组件化功能的时候可以直接编译主程序,在组件工程里下断点,就像编译一个程序一样,简单方便。

  • 静态库只需要.h不需要.m就能编译

前面我们说到,一些没有组件化架构的项目想要完全去除依赖非常困难,因此组件化程序往往需要引用主程序的头文件。使用静态库可以很好解决该问题,只要在静态库中添加需要引用的.h文件,而不用添加.m文件就能编译通过,并打成静态包,可以作为临时过渡期的方法。该方案同样适用于解决库冲突问题。

  • 图片资源、本地化文件与主程序共用

将图片资源、本地化文件添加到主程序中,可以实现本地化、图片资源共用。

  • 完成组件代码测试后可以打成.framework包引入工程

静态库文件不会暴露源代码,因此可以大大减少编程人员误修改组件代码,导致出错的问题。

组件与主程序之间的交互


既然组件代码与主程序不能直接交互,那么怎么做到一些常用操作,比如界面跳转呢?
答案是RunTime。object使用runTime可以很好解决耦合问题,甚至做到不变动代码,删除组件,主程序成功编译并且不崩溃的神奇效果。以下是总体设计思想:

总体设计思路.png

  • 主程序和组件代码只能通过各自的接口文件获取数据

  • 接口程序间通过runtime调用方法

  • 由于runtime performSelector方法的参数个数限制和回调类型限制,这里规定接口最终回调类型必须是对象,参数传id,多个参数用NSDictionary封装传递

这里我创建了一个接口基本类HSLinkClass,所有的接口都是继承自该类,主要提供了对象方法和类方法的调用。

.h文件

/**
 回调block

 @param retunValue 该方法return 的值
 @param performSuccess 该方法是否执行成功
 @param classIsLoad 该类是否加载
 */
typedef void (^plugInCallBackblock)(id retunValue, BOOL performSuccess,BOOL classIsLoad);

@interface HSLinkClass : NSObject
/**
 调用对象方法

 @param className 类名
 @param selectStr 方法名
 @param params 参数字典
 @param block 回调函数
 */
- (void)hsObjectMethodClassName:(NSString *)className performSelectorStr:(NSString *)selectStr param:(id)params block:(plugInCallBackblock)block;

/**
 调用类方法

 @param className 类名
 @param selectStr 方法名
 @param params 参数字典
 @param block 回调函数
 */
- (void)hsClassMethodClassName:(NSString *)className performSelectorStr:(NSString *)selectStr param:(id)params block:(plugInCallBackblock)block;

/**
 初始方法
 */
+ (instancetype)shareObject;

@end

.m文件

@implementation HSLinkClass

/**
 初始方法

 */
+ (instancetype)shareObject{
    return [[self alloc] init];
}

/**
 调用对象方法

@param className 类名
@param selectStr 方法名
@param params 参数字典
@param block 回调函数
*/
- (void)hsObjectMethodClassName:(NSString *)className performSelectorStr:(NSString *)selectStr param:(id)params block:(plugInCallBackblock)block{

    Class classObj = NSClassFromString(className);
    if (classObj == nil) {
        if (block) {
            block(nil,NO,NO);
        }
    }
    id object = [[classObj alloc] init];
    if ([object respondsToSelector:NSSelectorFromString(selectStr)]) {
        id returnValue = nil;
        if (params != nil) {
            returnValue = [object performSelector:NSSelectorFromString(selectStr) withObject:params];
            if (block) {
                block(returnValue,YES,YES);
            }
        }
        else {
             returnValue = [object performSelector:NSSelectorFromString(selectStr)];
            if (block) {
                block(returnValue,YES,YES);
            }
        }
    }
    else {
        if (block) {
            block(nil,NO,YES);
        }
    }
}

/**
 调用类方法
 
 @param className 类名
 @param selectStr 方法名
 @param params 参数字典
 @param block 回调函数
 */
- (void)hsClassMethodClassName:(NSString *)className performSelectorStr:(NSString *)selectStr param:(NSDictionary *)params block:(plugInCallBackblock)block{
    Class classObj = NSClassFromString(className);
    if (classObj == nil) {
        if (block) {
            block(nil,NO,NO);
        }
    }
   if ([classObj respondsToSelector:NSSelectorFromString(selectStr)]) {
        id returnValue = nil;
        if (params != nil) {
            returnValue = [classObj performSelector:NSSelectorFromString(selectStr) withObject:params];
        if (block) {
            block(returnValue,YES,YES);
        }
    }
    else {
        returnValue = [classObj performSelector:NSSelectorFromString(selectStr)];
        if (block) {
            block(returnValue,YES,YES);
        }
    }
}
else {
        if (block) {
            block(nil,NO,YES);
        }
    }
}
@end

通过Runtime可以判断该模块是否加载,在基本类中做了多种情况保护防止其崩溃。

主程序和组件程序都需要新建一个HSLinkClass的子类作为接口,也称之为中间层,分别提供索取方法供应方法。组件的索取方法就是主程序的供应方法,反之也一样。

下面是主程序跳转到视频扥类页面的例子:

主程序接口代码:


索取方法.png

组件接口代码:


提供方法.png

主程序代码:


主程序代码.png

建议以及优化


  • 每次xib变动或者添加图片都得手动复制到主程序中去十分麻烦,这里可以学习cocoapods的做法,写一个脚本放到Build Phases中,可以减少不少操作

  • 接口基类目前只是实现了基本方法用runtime调用,还有很大的提升空间,比如增加属性判断组件是否加载

总结


组件化的确会增加一定工作量,原来#import一个文件就能解决的事,现在需要多好几步操作。不过,也正是因为之前的随意,导致后续代码功能变更,牵一发而动全身,痛苦万分。吃苦在前,享乐在后才是正确的工作态度。

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

推荐阅读更多精彩内容

  • 最近在思考团队扩张及项目数量增加的情况下,如何持续保障团队高效产出的问题,很自然的想到了组件化这个话题。重翻了前段...
    其实也没有阅读 8,138评论 4 20
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,598评论 25 707
  • 三年学会说话,一辈子学会闭嘴。 两性关系是世界上最难的课题,连个人的相处模式很难短时间改变。很多人一直想改变婚姻活...
    自由的U0阅读 300评论 0 0
  • 在深邃的夜晚 我梦见了过往 在有星星的夜空 我看到了银河的遥远 模糊的想象中看不清未来 没有人想到哪里是最终的家园...
    李诺亚阅读 286评论 0 1
  • 文/泥璐 -01- 好久没刷微博了,今天一打开微博,铺天盖地地向我抛来的都是《那年花开月正圆》中,杜明礼这个角色让...
    泥璐阅读 742评论 20 16