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 分离组件接口代码。
但是,这种方案虽然解耦程度更高,但是也增加了组件化的成本,综合考虑,直接使用中间件通信的方式更好一点。具体哪种方案好,也就仁者见仁、智者见智了~
iOS 的组件化开发的更多相关文章
- ReactNative新手学习之路04 组件化开发轮播图swiper支持安卓和IOS
react native 新手之路04 组件化开发轮播图swiper支持安卓和IOS npm install react-native-carousel --save git 地址Properties ...
- iOS组件化开发入门 —— 提交自己的私有库
前言:本人也是初次接触组件化开发,感觉现有的资料太繁杂,就简单整理了一下,在此跟大家分享一些入手的经验,主要就是描述cocoapods的私有库封装和提交.组件化开发是个大的议题,涉及到架构思路.设计模 ...
- iOS组件化开发-CocoaPods简介
CocoaPods简介 任何一门开发语言到达一定阶段就会出现第三方的类库管理工具,比如Java的Maven.WEB的Webpack等.在iOS中类库的管理工具-CocoaPods. 利用CocoaPo ...
- iOS 组件化开发之使用CocoaPod制作自己的远程私有库
随着应用需求逐步迭代,应用的代码体积将会越来越大,为了更好的管理应用工程,我们开始借助CocoaPods版本管理工具对原有应用工程进行拆分.但是仅仅完成代码拆分还不足以解决业务之间的代码耦合,为了更好 ...
- iOS组件化开发· 什么是组件化
越来越多公司,开始了组件化,你还要等到什么时候...... 说到开发模式,我们最熟知的开发模式 MVC 或者最近比较热门的MVVM.但是我今天说的组件化的开发,其实MVC不是一类的.它其实是····· ...
- iOS组件化开发一本地环境配置(一)
首先我们要使用pod支持组件化开发 解决CocoaPods慢的方案(gem和pod repo换源) gem换源 $ gem sources --remove https://rubygems.org/ ...
- Webpack+Vue+ES6 前端组件化开发mobile-multi-page应用实战总结
本文版权归博客园和作者吴双本人共同所有 转载和爬虫请注明原文地址 www.cnblogs.com/tdws 一.写在前面 项目上线有一段时间了,一个基于webpack+vue+ES6的手机端多页面应用 ...
- Webpack+Vue+ES6 前端组件化开发mobile-multi-page应用实战总结和踩坑
本文版权归博客园和作者吴双本人共同所有 转载和爬虫请注明原文地址 www.cnblogs.com/tdws 一.写在前面 项目上线有一段时间了,一个基于webpack+vue+ES6的手机端多页面应用 ...
- 大话大前端时代(一) —— Vue 与 iOS 的组件化
序 今年大前端的概念一而再再而三的被提及,那么大前端时代究竟是什么呢?大前端这个词最早是因为在阿里内部有很多前端开发人员既写前端又写 Java 的 Velocity 模板而得来,不过现在大前端的范围已 ...
随机推荐
- BZOJ 2683 简单题 cdq分治+树状数组
题意:链接 **方法:**cdq分治+树状数组 解析: 首先对于这道题,看了范围之后.二维的数据结构是显然不能过的.于是我们可能会考虑把一维排序之后还有一位上数据结构什么的,然而cdq分治却可以非常好 ...
- POJ1502 MPI Maelstrom Dijkstra
题意 给出图,从点1出发,求到最后一个点的时间. 思路 单源最短路,没什么好说的.注意读入的时候的技巧. 代码 #include <cstdio> #include <cstring ...
- Android-加载大图,照片墙的实现
照片墙这种功能现在应该算是挺常见了,在很多应用中你都可以经常看到照片墙的身影.它的设计思路其实也非常简单,用一个GridView控件当作“墙”,然后随着GridView的滚动将一张张照片贴在“墙”上, ...
- 基于JavaSwing的例子-非连接数据库
项目结构: Constant.java package com.mstf.test; import java.io.Serializable; public class Constant implem ...
- 隐私:随机选择 MAC 地址
隐私:随机选择 MAC 地址 从 Android 8.0 开始,Android 设备在未连接到网络的情况下探测新网络时会使用随机 MAC 地址. 在 Android 9 中,您可以启用开发者选项(默认 ...
- Web开发、原生开发、混合开发的区别优势:
一.Web 应用 Web应用本质上是为移动浏览器设计的基于Web的应用,它们是用普通Web开发语言开发的,可以在各种智能手机浏览器上运行. 优点: 支持设备广泛: 较低的开发成本: 可即时上线: 无内 ...
- springMVC+request.session实现用户登录和访问权限控制
用springmvc mybatis实现用户登录登出功能,使用session保持登录状态,并实现禁止未登录的用户访问.感谢谷歌资源,在这里做个学习记录加深自己的印象. 原文在我的https://my. ...
- 作诗(si)[分块]
题目描述 神犇SJY虐完HEOI之后给傻×LYD出了一题: SHY是T国的公主,平时的一大爱好是作诗. 由于时间紧迫,SHY作完诗之后还要虐OI,于是SHY找来一篇长度为N的文章,阅读M次,每次只阅读 ...
- 为OLED屏添加GUI支持3:字库
为OLED屏添加GUI支持3:字库 本文博客链接:http://blog.csdn.net/jdh99,作者:jdh,转载请注明. 环境: 主机:WIN10 开发环境:MDK5.13 MCU:STM3 ...
- POJ 1035-Spell checker(字符串)
题目地址:POJ 1035 题意:输入一部字典.输入若干单词. 若某个单词能在字典中找到,则输出corret.若某个单词能通过 变换 或 删除 或 加入一个字符后.在字典中找得到.则输出这些单词.输出 ...