当前位置: 首页 > 科技观察

设计一个简单的iOS架构

时间:2023-03-19 10:57:43 科技观察

前言就像“100个读者有100个哈姆雷特”一样,不同的软件工程师对架构的理解有不同的看法。架构设计往往是一个权衡取舍的过程,每个架构师都要考虑各种因素,比如团队成员的技术水平、具体的业务场景、项目的成长阶段、开发周期等。这篇文章讲了作者的一些架构概念,以及我是如何设计一个简单的iOS架构的。iOS架构DEMO一、关于组件化组件化似乎是项目发展壮大后必然要做的事情。让各业务线的工程师无需过多关注其他业务线的代码,有效提升团队整体效率。但是,实施组件化的时机是在需求相对稳定,产品闭环形成之后。所以本文不会应用组件化,这里简单说说业界的组件化解决方案。组件化的核心问题是组件之间如何通信。“软件工程中的所有问题都可以通过间接的中间层来解决。”很自然地使用了中介模型:虽然这样可以统一组件之间的通信请求,但是并没有避免Mediator与目标组件之间的耦合。ModuleA项目还是ModuleB需要导入。所以关键问题是解耦:要实现Mediator和target组件之间的解耦,需要实现它们之间的间接调用(图中虚线)。既然是间接调用,就需要映射机制。在iOS开发中,业界大概有3种应对方式。(1)使用URL->Block解耦,简单来说就是将组件的调用代码放到block中,然后以URL为key,block为value,存储在一个全局的hash容器中。组件通过一个URL(比如"native/id=10/type=1")向Mediator发起请求,Mediator找到对应的代码块并执行。如此一来,Mediator与目标组件之间的耦合就被解耦了(参见博客:蘑菇街App的组件化之路)。这个方案有很多缺陷:组件越多,常驻内存越多;复杂的URL解析逻辑;URL不能表达特定语言相关的对象类型。所以这种方式不适合组件化解耦。(2)使用Protocol解耦阿里的BeeHive是本方案的一个很好的实践。看了源码,它的大致工作原理是这样的:注册Protocol对应的组件,和上面说的URL->Block方法完全一样。但是这里是Protocol->Module;当组件申请访问时,导入相应的Protocol,通过Mediator获取相应的组件对象。由于协议的表达可以支持所有的对象类型,所以这种方式基本上可以解决组件之间的通信需求。BeeHive有几种注册组件的方法。一种是在动态链接时监听图片二进制文件加载时的回调,通过修改代码段判断对应的模块进行注册;二是在+load方法中注册;第三种是异步注册,但是这个方法有个问题。当组件用户要使用组件时,组件还没有注册成功。BeeHive还为组件设置了优先级的概念。它通过数组维护优先级排序。在源码中可以看到一些数组排序的逻辑,带来了相当多的时间复杂度高的操作。因此,如果组件数量过多,会延长动态链接库的过程。BeeHive为了让每个组件都有自己的app生命周期、3Dtouch等功能,BeeHive会将这些系统级的事件发给每个组件,大量的方法调用损失不说,就必须让入口文件AppDelegate继承来自BeeHive的BHAppDelegate,笔者觉得侵入性太强,开发者在需要重写AppDelegate方法时,还要注意调用super,可以说是非常不优雅。在基于协议的组件化方案中,组件用户可以直接获取目标组件的实例,因此用户可能会修改实例,这可能会导致安全问题。(3)使用Target-Action解耦CasaTaloyum前辈的iOS应用架构,说说组件化方案。为此,制定了最佳实践。Mediator使用Target-Action间接调用目标组件,无需特殊注册。组件维护者需要做一个Mediator分类,通过硬编码调用目标组件,然后组件使用者只需要依赖这个分类。打包好的Mediator源码只有简单的200+行代码,非常通俗易懂。这也让开发者对组件化的实现更有信心,不会因为基础设施的错误而束手无策。总结以上关于组件化的简单表述,仅代表作者个人观点。由于作者并没有真正实现组件化,所以理解可能有误。笔者设计的iOS架构虽然没有应用组件化,但是给我们的架构设计带来了前瞻性的指导,这一点非常重要。2、模块化思维划分文件在团队开发中,总会有一些文件或代码在项目开发后期难以管理。造成这种情况的主要原因通常是项目开发过程中对文件的管理过于随意。开发人员应尽量将所有代码文件归于模块,而不是模棱两可的文件。笔者这里所说的模块都是具有特定含义的模块,比如图像处理模块、字体处理模块,而不是Public、Common等非特定的代码文件。试想一下,在多人开发中,当大家觉得有些代码不知道如何归类时,就会丢到Public中。当有一天你想整理这个Public时,你会发现无从下手;或者当你需要迁移项目中的某个业务模块时,你会迁移一些模块,当这个模块有意义时(比如图像处理模块),你的迁移成本会很低,但是当这个断开连接的模块是Public时,时间成本可能比你想象的要高。估计你会把它完全复制过来,污染新项目。全局公共文件是垃圾代码的来源。笔者认为几乎所有的代码都可以归为模块。大致梳理了一个文件分类,当然这个分类比较灵活,只是分为模块:-GeneralModules放项目特有的通用配置模块(比如通用颜色模块,通用字体模块)-ToolModules放工具模块(比如系统信息Module)-PackageModules放一些基于业务的包(比如提示框模块,加载菊花模块)-BusinessModules放业务模块(比如购物车,个人中心)详情可以查看作者的DEMO。3、减少全局宏的使用很多时候,太多的宏会让项目变得很凌乱。每个开发人员都将宏添加到全局文件中,但它通常只是一段简单的代码。笔者认为在开发中应尽量少用宏。原因如下:宏在预编译阶段被实际代码替换,存在效率问题。使用宏的地方可能只需要一块内存,但是替换宏后,就开辟了多个(这种情况下宏应该替换成常量)可能存在宏命名冲突宏封装过多导致难以理解和调试代码迁移需要处理全局宏实际上,必须使用宏的地方并不多。比如需要定义一个全局的导航栏字体,方便使用。通用字体的配置参数可以作为A模块:@interfaceYBGeneralFont:NSObject/**导航栏标题字体*/+(UIFont*)navigationBarTitleFont;@end或使用常量代替宏:.hFOUNDATION_EXTERNNSString*constkNotify_xxx;//xxxnotificationkey.mNSString*constkNotify_xxx=@"kNotify_xxx";这样也方便转换思路,毕竟swift里面没有宏。4.去基类设计在代码设计中,要尽量避免使用基类,也就是说,你不应该总是要求开发者去继承你的基类的功能。使用基类会造成不可避免的耦合,阻碍业务的长远发展(当然有些情况下可以使用基类)。其实用基类就好了。如果把大量的业务逻辑放到基类中,那将是灾难的开始。试想一下,当一个项目的新成员看到几千行基类代码时,TA是什么感受?另一种场景,当项目中的某个模块需要迁移到其他项目中,或者需要将其他项目合并到当前项目中时,基类的合并会非常头疼,其断开的模块和代码会驱动你疯了。那么,类的工具方法应该放在哪里呢?所有类的统一配置应该放在哪里呢?package模块的个性化定制应该怎么做?装饰模式类的工具方法在逻辑上可以抽取出来作为一个模块,但是有些场景可能会显得不够简洁。其实只要关注iOS官方API,不难发现装饰模式的大量应用。使用几个类别将大量的方法按照功能进行归类就会清晰优雅:@interfaceUIViewController(YBGeneral)/**基本配置*/-(void)YBGeneral_baseConfig;@end@interfaceUIViewController(YBGeneralBackItem)/**配置通用系统导航栏后退按钮*/-(void)YBGeneral_configBackItem;/**重写此方法自定义系统导航栏后退按钮点击事件*/-(void)YBGeneral_clickBackItem:(UIBarButtonItem*)item;@end不过应该需要注意的是,在定义类别的时候,一定要加前缀,避免方法覆盖。AOP面向方面编程在iOS领域的经典应用是使用Runtime去Hook方法:@implementationUIViewController(YBGeneralHook)+(void)load{[selfYBGeneralHook_exchangeImplementationsWithOriginSel:@selector(viewDidLoad)customSel:@selector(YBGeneralHook_viewDidLoad)];}+(无效)YBGeneralHook_exchangeImplementationsWithOriginSel:(SEL)originSelcustomSel:(SEL)customSel{Methodorigin=class_getInstanceMethod(self,originSel);Methodcustom=class_getInstanceMethod(self,customSel);if(origin&&custom){method_exchangeImplementations(origin,custom);}}-(void)YBGeneralHook_viewDidLoad{NSLog(@"Enter:%@",self);[selfYBGeneral_baseConfig];if(self.navigationController&&[self.navigationController.viewControllersindexOfObject:self]!=0){[selfYBGeneral_configBackItem];}[代码中的selfYBGeneralHook_viewDidLoad];}@end统一配置了UIViewController系统导航栏的返回按钮。注意这里调用的业务配置方法都是定义在UIViewController类中的。如果某些导航栏需要配置返回按钮,你可以扩展一个属性来控制它。面向协议的设计模式对于一些封装的组件,考虑使用协议进行个性化定制,继承是最坏的解决方案,而不是最好的解决方案。定义一个符合组件自定义协议的属性是一个常见的解决方案:@property(nonatomic,strong)idstrategy;不同的属性作为不同的策略,组件内部通过调用相应的协议方法实现自定义。而当用户想要改变策略时,只需要改变这个属性即可。将面向协议的设计模式与策略模式相结合是一种很好的做法。5.MVC?最有价值球员?MVVM?毒蛇?业务的具体架构模式是很多开发者头疼的问题,因为有时它可以让复杂的业务变得更清晰,有时又因为胶水代码过多而显得臃肿。其实为什么要严格遵守架构模型呢?为什么每个业务模块的架构模型要完全一样?笔者认为,正确的架构思想一定要立足于业务。不同的模块,不同的业务线可以有不同的架构,只要架构足够清晰,不至于晦涩难懂。粗略设计了架构主旋律:DataCenter负责数据的采集、处理、缓存等,Model设计为“瘦”Model,便于复用和迁移;另外考虑到数据源的数量可能会非常庞大??,如果Model设计的太“肥”,会造成更多的内存占用。View负责数据的展示,可以根据业务情况权衡是否需要ViewModel来处理界面逻辑。ViewController充当DataCenter和View之间的桥梁。目前笔者设计的项目并不是很复杂。在大多数情况下,上述结构就足够了。如果某个页面的功能太多,可以提取一些额外的模块。比如DataCenter处理过于复杂,那么数据处理和缓存提取:xxxDataProcesser,xxxDataCache。这些都是灵活的,只需要按照模块化的思想抽取出来,ViewController的代码相信不会太多。关于响应式框架Reactivecocoa功能强大,笔者之前也用过,但属于重量级框架,学习成本有点高。可能会因为团队成员的理解不够,造成难以定位的错误。美团的EasyReact似乎是一个福音。浏览了一下源码,发现质量真的很高,表演处理的也很细腻。基于图论算法的处理也很不错,项目侵入性很大。但缺点是太新,需要开发社区一定时间的验证。暂时,笔者持观望态度。结束语本文只是经过笔者的思考,对一个项目架构进行了简单的设计。还有很多地方需要改进和补充。具体细节还可以根据具体情况进行修改。Demo只是一个原型,希望与广大读者朋友交流。