iOS 的组件化开发
在一个APP开发过程中,如果项目较小且团队人数较少,使用最基本的MVC、MVVM开发就已经足够了,因为维护成本比较低。
但是当一个项目开发团队人数较多时,因为每个人都会负责相应组件的开发,常规开发模式耦合会越来越严重,而且导致大量代码冲突,会使后期维护和升级过程中代码“牵一发而动全身”,额外带来很大的工作量,并且会导致一些潜在的BUG。
在这时,组件化开发就派上很大用场了,所谓的组件化开发,就是把APP根据业务拆分为各独立的组件,各个组件相互写作,组成完整的APP。
一、各组件的引入
关于组件的拆分,就根据具体项目进行拆分,假如APP被拆分了AModule、BModule、CModule,那么,应该如何引入这些组件呢?你可能会想到APP的入口AppDelegate。在平时开发中,AppDelegate中往往初始化了好多组件,比如推送、统计等组件,这样就会导致AppDelegate的臃肿。
所以,我们可以增加一个ModuleManager
,专门用来初始化各组件。 首先增加一个 ModuleProtocol
:
#import <Foundation/Foundation.h> @import UIKit; @import UserNotifications; @protocol ModuleProtocol <UIApplicationDelegate, UNUserNotificationCenterDelegate> @end
我们在ModuleManager
中hook住UIApplicationDelegate
和 UNUserNotificationCenterDelegate
中的方法,使相应的组件中实现了对应方法,在相应时机就会调用组建里的对应方法:
#import "ModuleManager.h" #import "AppDelegate.h" #import <objc/runtime.h> #define ALL_MODULE [[ModuleManager sharedInstance] allModules] #define SWIZZLE_METHOD(m) swizzleMethod(class, @selector(m),@selector(module_##m)); @interface ModuleManager () @property (nonatomic, strong) NSMutableArray<id<ModuleProtocol>> *modules; @end @implementation ModuleManager + (instancetype)sharedInstance { ...... } - (NSMutableArray<id<ModuleProtocol>> *)modules { ...... } - (void)addModule:(id<ModuleProtocol>) module { ...... } - (void)loadModulesWithPlistFile:(NSString *)plistFile { ...... } - (NSArray<id<ModuleProtocol>> *)allModules { ...... } @end @implementation AppDelegate (Module) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; SWIZZLE_METHOD(application:willFinishLaunchingWithOptions:); SWIZZLE_METHOD(application:didFinishLaunchingWithOptions:); ...... }); } static inline void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector) { ...... } - (BOOL)module_application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions { BOOL result = [self module_application:application willFinishLaunchingWithOptions:launchOptions]; for (id<ModuleProtocol> module in ALL_MODULE) { if ([module respondsToSelector:_cmd]) { [module application:application willFinishLaunchingWithOptions:launchOptions]; } } return result; } - (BOOL)module_application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { BOOL result = [self module_application:application didFinishLaunchingWithOptions:launchOptions]; for (id<ModuleProtocol> module in ALL_MODULE) { if ([module respondsToSelector:_cmd]) { [module application:application didFinishLaunchingWithOptions:launchOptions]; } } return result; } ...... @end
ModuleManager.h
:
#import <Foundation/Foundation.h> #import "ModuleProtocol.h" @interface ModuleManager : NSObject + (instancetype)sharedInstance; - (void)loadModulesWithPlistFile:(NSString *)plistFile; - (NSArray<id<ModuleProtocol>> *)allModules; @end
之后我们通过一个 ModulesRegister.plist
文件管理需要引入的组件:
如上图,假如我们要引入AModule、BModule、CModule,那么这三个Module只需要实现协议ModuleProtocol
,然后实现AppDelegate
中对应的方法,在对应方法中初始化自身即可: AModule.h
:
#import <Foundation/Foundation.h> #import "ModuleProtocol.h" @interface AModule : NSObject<ModuleProtocol> @end
AModule.m
:
#import "AModule.h" @implementation AModule - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{ //初始化AModule return YES; } @end
之后在AppDelegate
的load
方法中通过ModulesRegister.plist
引入各组件即可:
@implementation AppDelegate + (void)load { //load modules NSString* plistPath = [[NSBundle mainBundle] pathForResource:@"ModulesRegister" ofType:@"plist"]; [[ModuleManager sharedInstance] loadModulesWithPlistFile:plistPath]; } - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { ...... } @end
这样,各组件的开发者在自己的组件中初始化自己,其他人需要使用时只需要加入ModulesRegister.plist
文件中即可。
二、组件间协作
简单来看,假设APP的每个页面就是一个组件,假如我们的APP有AViewController、BViewController、CViewController、DViewController、EViewController,各ViewController必然设置各种相互跳转。那么,我们APP的跳转逻辑可能是下面这个样子:
为了解决这种复杂的耦合关系,我们可以增加一个Router中间层去管理各ViewController之间的跳转关系(也就是实际开发中组件间相互调用的关系)。
所以,根据需要,我开发并开源了一个支持URL Rewrite的iOS路由库— FFRouter,通过FFRouter去管理各ViewController之间的跳转关系:
这样,各ViewController之间的跳转关系就变的清晰了许多。
FFRouter通过提前注册对应的URL,之后就直接通过打开URL去控制各ViewController之间的跳转(或各组件间的调用)。 FFRouter支持组件间传递非常规对象,如UIImage等,并支持获取组件返回值。 基本使用如下:
/** 注册 url @param routeURL 要注册的 URL @param handlerBlock URL 被 Route 后的回调 */ + (void)registerRouteURL:(NSString *)routeURL handler:(FFRouterHandler)handlerBlock; /** 注册 URL,通过该方式注册的 URL 被 Route 后可返回一个 Object @param routeURL 要注册的 URL @param handlerBlock URL 被 Route 后的回调,可在回调中返回一个 Object */ + (void)registerObjectRouteURL:(NSString *)routeURL handler:(FFObjectRouterHandler)handlerBlock; /** 判断 URL 是否可被 Route(是否已经注册) @param URL 要判断的 URL @return 是否可被 Route */ + (BOOL)canRouteURL:(NSString *)URL; /** Route 一个 URL @param URL 要 Router 的 URL */ + (void)routeURL:(NSString *)URL; /** Route 一个 URL,并带上额外参数 @param URL 要 Router 的 URL @param parameters 额外参数 */ + (void)routeURL:(NSString *)URL withParameters:(NSDictionary<NSString *, id> *)parameters; /** Route 一个 URL,可获得返回的 Object @param URL 要 Router 的 URL @return 返回的 Object */ + (id)routeObjectURL:(NSString *)URL; /** Route 一个 URL,并带上额外参数,可获得返回的 Object @param URL 要 Router 的 URL @param parameters 额外参数 @return 返回的 Object */ + (id)routeObjectURL:(NSString *)URL withParameters:(NSDictionary<NSString *, id> *)parameters; /** Route 一个未注册 URL 时回调 @param handler 回调 */ + (void)routeUnregisterURLHandler:(FFRouterUnregisterURLHandler)handler; /** 取消注册某个 URL @param URL 要被取消注册的 URL */ + (void)unregisterRouteURL:(NSString *)URL; /** 取消注册所有 URL */ + (void)unregisterAllRoutes; /** 是否显示 Log,用于调试 @param enable YES or NO,默认为 NO */ + (void)setLogEnabled:(BOOL)enable;
而且参考天猫的方案增加了URL Rewrite功能: 可以使用正则
添加一条 Rewrite 规则,例如: 要实现打开 URL:https://www.taobao.com/search/原子弹
时,将其拦截,改用本地已注册的 URL:protocol://page/routerDetails?product=原子弹
打开。 首先添加一条 Rewrite 规则:
[FFRouterRewrite addRewriteMatchRule:@"(?:https://)?www.taobao.com/search/(.*)" targetRule:@"protocol://page/routerDetails?product=$1"];
之后在打开URL:https://www.taobao.com/search/原子弹
时,将会 Rewrite 到URL:protocol://page/routerDetails?product=原子弹
。
[FFRouter routeURL:@"https://www.taobao.com/search/原子弹"];
可以通过以下方法同时增加多个规则:
+ (void)addRewriteRules:(NSArray<NSDictionary *> *)rules;
其中 rules 格式必须为以下格式:
@[@{@"matchRule":@"YourMatchRule1",@"targetRule":@"YourTargetRule1"}, @{@"matchRule":@"YourMatchRule2",@"targetRule":@"YourTargetRule2"}, @{@"matchRule":@"YourMatchRule3",@"targetRule":@"YourTargetRule3"},]
Rewrite 规则中的保留字:
- 通过
$scheme
、$host
、$port
、$path
、$query
、$fragment
获取标准 URL 中的相应部分。通过$url
获取完整 URL - 通过
$1
、$2
、$3
...获取matchRule
的正则中使用圆括号取出的参数 $
:原变量的值、$$
:原变量URL Encode后的值、$#
:原变量URL Decode后的值
例如: https://www.taobao.com/search/原子弹
对于Rewrite 规则(?:https://)?www.taobao.com/search/(.*)
$1=原子弹 $$1=%e5%8e%9f%e5%ad%90%e5%bc%b9
同样,https://www.taobao.com/search/%e5%8e%9f%e5%ad%90%e5%bc%b9
对于Rewrite 规则(?:https://)?www.taobao.com/search/(.*)
$1=%e5%8e%9f%e5%ad%90%e5%bc%b9 $#1=原子弹
考虑到经常用路由配置UIViewController
之间的跳转,所以增加了额外的工具FFRouterNavigation
来更方便地控制UIViewController
之间的跳转。
三、其他组件化方案
目前这种组件化方案参考了蘑菇街、天猫、京东的的实现方案。除这种方案外,Casa(查看文章)之前提出了解耦程度更高的方案,这种方案组件仍然使用中间件通信,但中间件通过 runtime 接口解耦,然后使用 target-action 简化写法,通过 category 分离组件接口代码。 但是,这种方案虽然解耦程度更高,但是也增加了组件化的成本,综合考虑,直接使用中间件通信的方式更好一点。具体哪种方案好,也就仁者见仁、智者见智了~
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Hystrix Semaphore timeout
When to use semaphore For Thread isolation. there is a thread context switch cost. but this is almost can be ignored in most application. see Thread pool For circuits that wrap very low-latency requests (such as those that primarily hit in-memory caches) the overhead can be too high and in those cases you can use another method such as tryable semaphores which, while they do not allow for timeouts, provide most of the resilience benefits without the overhead. The overhead in general, however, is...
- 下一篇
Play For Scala 开发指南 - 第9章 Json 开发
Play Json 简介 Play 内置了一套JSON库,以帮助开发者简化JSON操作。目前Play的JSON库包含以下功能: Json对象与字符串之间互转 Json对象和Case Class之间互转 Json数据校验 Json格式之间互转 Play的JSON库并不依赖于Play环境,可以单独使用,通过如下方式可以将它引入到自己的项目: libraryDependencies += "com.typesafe.play" %% "play-json" % playVersion 基本JSON类型 所有的基本JSON类型都继承自JsValuetrait。Play JSON 库提供的基本类型如下: JsString JsNumber JsBoolean JsObject JsArray JsNull 在日程开发中,我们很少跟这些JSON基本类型打交道。因为在Play中对于基本类型T(例如 String, Int, ...)以及Seq[T]已经提供了默认的隐式转换, 可以自动将其转换成对应的JSON类型,例如: //基本类型值 Json.obj("name" -> JsString(...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Linux系统CentOS6、CentOS7手动修改IP地址
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS6,CentOS7官方镜像安装Oracle11G
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- 设置Eclipse缩进为4个空格,增强代码规范
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- 2048小游戏-低调大师作品