iOS模块化开发浅析

背景:由于目前所在公司的iOS项目的依赖管理是比较原始的状态,但是APP功能又是越来越复杂的,这就带来的很多问题,比如开发时编译时间过长、模块间耦合严重、模块依赖混乱等。最近又听说这个项目中的部分功能可能需要独立出一个新APP,本着“Don't repeat yourself”的原则,我们试着抽离出原项目中的各个模块,并在新的APP中集成这些模块。

最近算是初步完成了新APP的模块化,也算是从中总结了一些经验拿出来分享一下。同时也完成了一个模块化框架TinyPart欢迎star。

模块划分

做模块化还是要结合实际业务,对目前APP的功能做一个模块划分,在划分模块的时候还需要关注模块之间的层级。

比如说,在我们项目中,模块被分成了3个层级:基础层、中间层、业务层。基础层模块比如像网络框架、持久化、Log、社交化分享这样的模块,这一层的模块我们可以称之为组件,具有很强的可重用性。中间层模块可以有登录模块、网络层、资源模块等,这一层模块有一个特点是它们依赖着基础组件但又没有很强的业务属性,同时业务层对这层模块的依赖是很强的。业务层模块,就是直接和产品需求对应的模块了,比如类似朋友圈、直播、Feeds流这样的业务功能了。

代码隔离

模块化首先要做的是代码层面上独立,任意一个基础模块都是可以独立编译的,底层模块绝对不能有对上层模块的代码依赖,还要确保未来也不会再出现这样的代码。

在这里我们选择使用CocoaPods来确保模块间代码隔离,基础和中间层模块是一定会做成标准的私有pods组件,加入到私有pods仓库。业务层的模块,则不一定非要加到私有pods仓库中,也可以使用submodule + local pods的方案。这样做有两个原因:其一是业务模块的改动往往比较频繁,如果是标准的私有pods组件则需要频繁的操作pod install或者pod update;其二是如果是local pod会直接引用对应仓库的源文件,在主工程对pods工程下业务模块的改动就是直接对其git仓库的改动,没有了频繁的pod repo push和pod install操作。

依赖管理

选择使用CocoaPods另外一个重要原因就是,可以通过它来管理模块间的依赖,之前项目各个功能之所以难以复用的重要原因之一就是没有声明依赖。这里的依赖不仅仅是A模块依赖B模块这样的事情,还可以是A模块运行需要的所有工程配置,比如A模块工程需要添加一个GCC_PREPROCESSOR_DEFINITIONS预处理宏才能正常编译。因此,个人认为模块依赖声明非常重要,即便没有像CocoaPods这样的管理工具,也应该有相关文档来说明每个内部模块或者SDK的依赖。

CocoaPods的方便之处就在于你必须把你模块的依赖列出来,否则是无法通过pod spec lint过程的,并且所有的依赖项也都是必须是pods仓库。除此以外,依赖的集成也是自动化的,CocoaPods可以自动地添加工程配置和依赖组件。

模块集成

在完成上述两个步骤以后,模块化工程的构建工作基本就结束了,接下来我们探讨一下如何在工程中更好地使用这些模块。为此我们写了一个组件化的开源方案 TinyPart。

一般来说,模块初始化需要在APP启动或者UI初始化附近的时机来完成,有时候各个模块的启动顺序可能也是有讲究的,这些初始化逻辑我们往往会加入到AppDelegate这个类里。过一段时间我们会发现,AppDelegate这个类变得臃肿不堪,逻辑复杂,难以维护。在TinyPart中,Module的声明协议包含了UIApplicationDelegate,这就意味着每一个模块都可以实现有一套自己的UIApplicationDelegate协议,并且它们之间调用顺序是可以自定义的。

@interface TPLShareModule : NSObject <TPModuleProtocol>
@end
@implementation TPLShareModule
TP_MODULE_ASYNC

TP_MODULE_PRIORITY(TPLSHARE_MODULE_PRIORITY)

- (void)moduleDidLoad:(TPContext *)context {
  [WXApi registerApp:APPID];
}

- (BOOL)application:(UIApplication *)application
      openURL:(NSURL *)url
 sourceApplication:(NSString *)sourceApplication
     annotation:(id)annotation {
  return [WXApi handleOpenURL:url delegate:self];
}

- (BOOL)application:(UIApplication *)app
      openURL:(NSURL *)url
      options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
  return [WXApi handleOpenURL:url delegate:self];
}
@end

上面的代码是一个微信社交分享模块的初始化内容,同时实现了微信分享所要求的UIApplicationDelegate中的方法。

通信

消息

在面向对象中,消息是一个十分重要的概念,它是对象之前通信的重要方式。但是,在OC中如果想要向一个对象发消息,正常做法就是将改对象类的头文件import进来,这样我们就能够写出[aInstance method]这样的代码了。

然而在模块化中,我们并不希望模块与模块之间相互引用各自的类文件,但是又想要实现通信,那怎么办呢?通过协议来完成。我们知道OC是一个动态语言,方法的调用过程其实是动态的,头文件中消息方法的声明只是为了通过编译前的静态检查。也就是说,我们只要写一个协议来告诉编译器有这么一个方法就可以了,至于实际上究竟有没有这个方法是在消息发过去以后就知道了。既然OC有这个特性,我们甚至可以直接通过类名和方法名向一个对象发送消息,这其实就是网上大部分组件化路由的实现机制。

因此在TinyPart中我们既提供了协议和路由两种模式来调用模块内的服务。

@protocol TestModuleService1 <TPServiceProtocol>
- (void)function1;
@end

@interface TestModuleService1Imp : NSObject <TestModuleService1>
@end

@implementation TestModuleService1Imp
TPSERVICE_AUTO_REGISTER(TestModuleService1) // Service will be registered in "+load" method

- (void)function1 {
  NSLog(@"%@", @"TestModuleService1 function1");
}
@end

上面的代码中,我们定义了一个服务的协议。

#import "TestModuleService1.h"

id<TestModuleService1> service1 = [[TPServiceManager sharedInstance] serviceWithName:@"TestModuleService1"];

[service1 function1];

这里我们只需要import上述协议的头文件,然后就可以向TestModuleService1发消息了。

我们看到上述的跨模块调用方案中,只需要暴露一个协议文件就可以了,下面我们再看一下如何用路由的方式来做到完全不暴露任何头文件。

#import "TPRouter.h"

@interface TestRouter : TPRouter
@end

@implementation TestRouter
TPROUTER_METHOD_EXPORT(action1, {
  NSLog(@"TestRouter action1 params=%@", params);
  return nil;
});

TPROUTER_METHOD_EXPORT(action2, {
  NSLog(@"TestRouter action2 params=%@", params);
  return nil;
});
@end

在这里我们参考了ReactNative的方案,通过一个TPROUTER_METHOD_EXPORT宏来定义一个可供调用的路由服务,同时可以传一个params参数进了。然后我们再来调用这个路由。

[[TPMediator sharedInstance] performAction:@"action1" router:@"Test" params:@{}];

通知

除了上面提到的两种普通的模块通信方案,我们发现在项目中经常会有跨模块的NSNotification,对于这样的观察者模式使用NSNotification来实现是最便捷的方式了。尽管NSNotification可以做到模块间解耦,但是对于通知的管理过于松散会导致散落在各个模块的NSNotification逻辑变得十分复杂,因此我们为TinyPart增加了一种有向通信的方案。

所谓有向通信,则是在NSNotification基础上对通知的传播方向进行了限制,底层模块对上层模块的通知称为广播Broadcast,上层模块对底层模块或者同层模块的通知称为上报Report。这样做有两个好处:一方面更利于通知的维护,另一方面可以帮助我们划分模块层级,如果我们发现有一个模块需要向多个同级模块进行Report那么这个模块很有可能应该被划分到更底层的模块。

用法同NSNotification类似,只不过创建通知的方法是一个链式调用,大概就是这样:

// 发送
TPNotificationCenter *center2 = [TestModule2 tp_notificationCenter];

[center2 reportNotification:^(TPNotificationMaker *make) {
  make.name(@"report_notification_from_TestModule2");
} targetModule:@"TestModule1"];

[center2 broadcastNotification:^(TPNotificationMaker *make) {
  make.name(@"broadcast_notification_from_TestModule2").userInfo(@{@"key":@"value"}).object(self);
}];

// 接收
TPNotificationCenter *center1 = [TestModule1 tp_notificationCenter];
[center1 addObserver:self selector:@selector(testNotification:) name:@"report_notification_from_TestModule2" object:nil];
(0)

相关推荐

  • iOS中关于模块化开发解决方案(纯干货)

    关于iOS模块化开发解决方案网上也有一些介绍,但真正落实在在具体的实例却很少看到,计划编写系统文章来介绍关于我对模块化解决方案的理解,里面会有包含到一些关于解耦.路由.封装.私有Pod管理等内容:并编写的一个实例项目放在git进行开源[jiaModuleDemo],里面现在已经放着一些封装的功能模块:会不断的进行更新,假如你感兴趣可以Star一下,项目也不断的更新完善优化:如果你有更好的方案或者说好的建议可以lssues,我会在短时间进行更新并修改相应的问题: 一:项目中存在的问题 1:当公司里

  • iOS开发中Swift 指纹验证功能模块实例代码

    iOS调用TouchID代码: override func viewDidLoad() { super.viewDidLoad() let context = LAContext() var error: NSError? = nil let canEvaluatePolicy = context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error) as Bool if error

  • 关于iOS GangSDK的使用 为App快速集成社群公会模块

    手上有一个自己开发的小游戏,想加一个家族系统活跃下游戏的氛围,想到这块儿可能会有大量的工作需要自己做,就偷了个懒去网上搜罗了一波,结果惊奇的发现居然真的有类似的服务,并且还是免费的,所以决定入坑尝试一下.这里就我使用的第三方家族系统(GangSDK)做一个简单的记录,方便以后查看. 一.GangSDK介绍 GangSDK是为开发者提供的一套快速接入社群系统的开发框架,主要为了帮助开发者在自己的应用里快速构建社群系统.社群系统包含两大功能:1.为用户们提供自己的社交圈,使他们交流更方便;2.社群建

  • iOS购物分类模块的实现方案

    本文实例分享了iOS购物分类模块的实现方案,供大家参考,具体内容如下 启动 在AppDelegate中创建主视图控制器. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. self.window = [[

  • iOS开发之拦截URL转换成本地路由模块URLRewrite详解

    本文主要给大家介绍了关于iOS拦截URL转换成本地路由模块URLRewrite的相关内容,分享出来供各位iOS开发者们参考学习,下面话不多说了,来一起看看详细的介绍: 需求场景 做过电商App的可能都遇到过这样的需求,在商场首页,各种各样动态的跳转,跳转商品详情.秒杀列表.品牌列表.搜索结果.分类结果页面等等等等.同一个位置,可能今天跳这个商品,明天跳转那个商品,运营配的就是一个web端的URL. 拦截webView里面的URL. 需求分析 拦截各种各样的URL,跳转到指定的原生页面. URL的

  • IOS开发用户登录注册模块所遇到的问题

    最近和另外一位同事负责公司登录和用户中心模块的开发工作,开发周期计划两周,减去和产品和接口的协调时间,再减去由于原型图和接口的问题,导致强迫症纠结症状高发,情绪不稳定耗费的时间,能在两周基本完成也算是个不小的奇迹了.本文就总结一下如何满足产品需要的情况下,高效开发一个登录注册模块. 1.利用继承解决界面重复性功能.通常登录注册会有一个独立的设计,而模块内部会有有相似的背景,相似的导航栏样式,相似返回和退出行为,相似的输入框,按钮样式等. 比如上面的的注册和登录模块,就有相同的返回按钮,相同的背景

  • iOS 模块化之JLRoute路由示例

    JLRoutes是一个调用极少代码 , 可以很方便的处理不同URL schemes以及解析它们的参数,并通过回调block来处理URL对应的操作 , 可以用于处理复杂跳转逻辑的三方库. 1.在日常开发中 , push , present 出现在整个程序的各个地方 , 如果你想快速理清一个项目的整体逻辑 , 非常麻烦 . 大多数情况 , 你得找到代码目录 ,根据层级结构分出关系 , 然后找到对应的push位置 , 寻找下一级页面 , 如果本身项目的目录就非常乱 , 那么如果要了解一个项目的整体跳转

  • iOS模块化开发浅析

    背景:由于目前所在公司的iOS项目的依赖管理是比较原始的状态,但是APP功能又是越来越复杂的,这就带来的很多问题,比如开发时编译时间过长.模块间耦合严重.模块依赖混乱等.最近又听说这个项目中的部分功能可能需要独立出一个新APP,本着"Don't repeat yourself"的原则,我们试着抽离出原项目中的各个模块,并在新的APP中集成这些模块. 最近算是初步完成了新APP的模块化,也算是从中总结了一些经验拿出来分享一下.同时也完成了一个模块化框架TinyPart欢迎star. 模块

  • ES6中module模块化开发实例浅析

    本文实例讲述了ES6中module模块化开发.分享给大家供大家参考,具体如下: 多人开发JavaScript时伴随着命名冲突等问题,先后有了模拟块级作用域.命名空间.模块化开发等方法. 之前,模块化开发一直是由第三方库来模拟的,比较知名的有AMD规范和CMD规范. 两个规范分别对应requirejs和seajs. 而现在,ES6提出了自己的模块化统一标准. 一个ES6的模块是一个包含了js代码的文件.ES6里没有所谓的module关键字,一个模块就是一个普通的脚本文件,除了以下两个区别: 1.

  • IOS多线程开发之线程的状态

    大家都知道,在开发过程中应该尽可能减少用户等待时间,让程序尽可能快的完成运算.可是无论是哪种语言开发的程序最终往往转换成汇编语言进而解释成机器码来执行.但是机器码是按顺序执行的,一个复杂的多步操作只能一步步按顺序逐个执行.改变这种状况可以从两个角度出发:对于单核处理器,可以将多个步骤放到不同的线程,这样一来用户完成UI操作后其他后续任务在其他线程中,当CPU空闲时会继续执行,而此时对于用户而言可以继续进行其他操作:对于多核处理器,如果用户在UI线程中完成某个操作之后,其他后续操作在别的线程中继续

  • iOS Swift开发之日历插件开发示例

    本文介绍了iOS Swift开发之日历插件开发示例,分享给大家,具体如下: 效果图 0x01 如何获取目前日期 关于日期,苹果给出了 Date 类,初始化一个 Date 类 let date = Date() 打印出来就是当前系统的日期和时间 那么如何单独获得当前年份,月份呢? var date: [Int] = [] let calendar: Calendar = Calendar(identifier: .gregorian) var comps: DateComponents = Dat

  • iOS程序开发之使用PlaceholderImageView实现优雅的图片加载效果

    说明 1. PlaceHolderImageView基于SDWebImage编写 2. 给定一个图片的urlString,以及一个placeholderImage就可以优雅的显示图片加载效果 效果 源码 PlaceholderImageView.h/.m // // PlaceholderImageView.h // SDWebImageViewPlaceHorder // // Created by YouXianMing on 16/9/14. // Copyright © 2016年 Yo

  • 使用requirejs模块化开发多页面一个入口js的使用方式

    描述 知道requirejs的都知道,每一个页面需要进行模块化开发都得有一个入口js文件进行模块配置.但是现在就有一个很尴尬的问题,如果页面很多的话,那么这个data-main对应的入口文件就会很多.理论这样其实也没什么,但是到后面用grunt进行合并压缩就会有很多入口js,虽然这个入口js都把配置的模块内容都压缩到里面了,但是各个入口合并压缩后的文件中其实都有很多重合的代码,所以考虑到这个就想到把所以的入口文件都统一了,使用一个,到时候用grunt合并压缩也只有这么一个入口文件,也很方便. 实

  • IOS游戏开发之五子棋OC版

    先上效果图 - 功能展示 - 初高级棋盘切换效果 实现思路及主要代码详解 1.绘制棋盘 利用Quartz2D绘制棋盘.代码如下 - (void)drawBackground:(CGSize)size{ self.gridWidth = (size.width - 2 * kBoardSpace) / self.gridCount; //1.开启图像上下文 UIGraphicsBeginImageContext(size); //2.获取上下文 CGContextRef ctx = UIGraph

  • IOS程序开发之跳转短信发送界面实现发送短信功能

    项目需求:在程序开发中,我们需要在某个程序里面发送一些短信验证(不是接收短信验证,关于短信验证,传送门:http://www.cnblogs.com/wolfhous/p/5096774.html 项目实现: 新建demo,直接看我源码标志. 源码截图 真机截图 就是如此简单,如您有任何问题/建议或者更好的实现方法,联系本人. 可以看我折叠的源码 /** 点击发送短信按钮*/ - (IBAction)sendMessageBut:(id)sender { /** 如果可以发送文本消息(不在模拟器

  • 在Html中使用Requirejs进行模块化开发实例详解

    在前端模块化的时候,不仅仅是js需要进行模块化管理,html有时候也需要模块化管理.这里就介绍下如何通过requirejs,实现html代码的模块化开发. 如何使用requirejs加载html Reuqirejs有一个text的插件,它可以读取指定文件的内容,读取到的内容就是文本. 如何下载text插件 第一种方法,可以通过npm下载: npm install requirejs/text 第二种方法,也可以直接去官方github上面直接下载. 直接拷贝内容到text.js中即可. 如何安装t

随机推荐