iOS模块化探索实践

背景:因为目前所在公司的iOS项目的依赖管理是比较原始的状态,可是APP功能又是愈来愈复杂的,这就带来的不少问题,好比开发时编译时间过长、模块间耦合严重、模块依赖混乱等。最近又据说这个项目中的部分功能可能须要独立出一个新APP,本着Don't repeat yourself的原则,咱们试着抽离出原项目中的各个模块,并在新的APP中集成这些模块。react

最近算是初步完成了新APP的模块化,也算是从中总结了一些经验拿出来分享一下。git

模块划分

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

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

代码隔离

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

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

依赖管理

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

CocoaPods的方便之处就在于你必须把你模块的依赖列出来,不然是没法经过pod spec lint过程的,而且全部的依赖项也都是必须是pods仓库 。除此之外,依赖的集成也是自动化的,CocoaPods能够自动地添加工程配置和依赖组件。框架

模块集成

在完成上述两个步骤之后,模块化工程的构建工做基本就结束了,接下来咱们探讨一下如何在工程中更好地使用这些模块。为此咱们写了一个组件化的开源方案 TinyPart [https://github.com/RyanLeeLY/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];
复制代码

参考

BeeHive

ReactNative