*#*#112#*#*我进去搞了一下我的世界内存不足512了。怎么办好呢?

一水的技术博客
前段时间看到一个关于AppDelegate瘦身的文章 。想起来自己在这方面做得一些事情,拿出来一起分享一下。我写了一个单独的类库来处理这个问题: 。
使用可以直接:
1pod &MRAppDelegateComponents&
关于AppDelegate碰到的问题不再赘述。但是我们可以把问题再抽象一层,AppDelegate碰到的问题本质上是 AppDelegate 作为整个App的一个入口,特别容易承载过多的业务逻辑,从而变成一个臃肿的类。这样的问题同样也存在于 ViewController 中。现在我们只讨论 AppDelegate 的解耦问题。关于 ViewController 的解耦问题可以参考我的另外一个库 。所谓解耦,就是把不同的代码按照其职责,拆解到不同的单元或者模块中(具体是什么,要看拆解的手段)。
而拆解之后必然首先要保障的是原有的业务逻辑能够顺利执行。而且各个业务逻辑之间如果有交互,还能够顺利的完成。
之前我们看到的方案主要是有两类:
一类注重于分发,比如 FRDModuleManager 和 JSDecoupledAppDelegate。 通过一定的机制,将Delegate分发到对应执行的业务单元上执行。而各个业务单元之间执行可以做到不互斥。但是要穷举所有的方法,进行转发也是个麻烦事。
一种是Category。这种虽然可以拆解,但是由于Category的技术限制,同一个方法不能出现在不同的Category中。结果导致了互斥。也就是说,同一个方法你只能写一份。不然就会出问题。
而当我们仔细去分析 AppDelegate 中的方法的时候,会发现有些方法是有返回值的:
1- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchO
这里自然就会延伸出一个问题,这个返回值怎么处理?
比如在多个模块中都需要对 application: didFinishLaunchingWithOptions: 做出一定的动作。一般情况下我们都会丢弃掉返回值。直接按照一个固定的返回输出 return YES;。
而当同一个函数都有一定的返回值的时候,我们自然会想到一个设计模式:责任链模式。所有的相同调用处在一个链条上,当前处理单元会根据上一个处理单元的结果来做些动作,并且能够决定是否继续往下去处理。
到这里我们把解决方案再明确一下:把 AppDelegate 的调用解耦成一个 责任链 模型。并且满足以下特征:
每个模块都可以无限制的实现 AppDelegate 的方法。
每个模块将会实现多个 AppDelegate 的函数以完成一定的业务逻辑,比如对于URLScheme的处理。
对于有返回值的函数,将其调用关系转化成一个 责任链。 能够让调用者之间根据返回值交互。
下面先看一下解决方案,下面是拆解出来的两个逻辑单元 (代码可以从) 中的 Demo 中找到,只要把这两个类创建好,关键的是实现协议 MRApplicationDelegateInjectionProtocol,框架会自动执行你重载的方法:
12345678910111213141516#import &MRAppDelegateComponents/MRAppDelegateComponents.h&@interface MRAppDelegateLogic1 :NSObject
&UIApplicationDelegate, MRApplicationDelegateInjectionProtocol&@end@implementation MRAppDelegateLogic1- (BOOL) application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
SEL sel = @selector(application:didFinishLaunchingWithOptions:);
if (__MRSuperImplatationCurrentCMD__(sel)) {
MRPrepareSendSuper(BOOL, id, id);
MRSendSuperSelector(sel, application, launchOptions);
NSLog(@&MRAppDelegateLogic1 handle&);
return YES;}@end
12345678910111213141516#import &MRAppDelegateComponents/MRAppDelegateComponents.h&@interface MRAppDelegateLogic2 :NSObject &UIApplicationDelegate, MRApplicationDelegateInjectionProtocol&@end@implementation MRAppDelegateLogic2- (BOOL) application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
SEL sel = @selector(application:didFinishLaunchingWithOptions:);
if (__MRSuperImplatationCurrentCMD__(sel)) {
MRPrepareSendSuper(BOOL, id, id);
MRSendSuperSelector(sel, application, launchOptions);
NSLog(@&MRAppDelegate Logic2 handle magic&);
return YES;}@end
先讲解一下用法,然后我们再去解释实现原理。以 MRAppDelegateLogic2 为例。我们可以看到,我新建了一个类 MRAppDelegateLogic2 并且重载了 application:didFinishLaunchingWithOptions: 方法。 然后在函数实现体里面有几个比较有意思的东西。
我首先定义了当前函数的一个变量。然后通过一个我写的宏去判断其父类有没有实现该方法,如果有就调用,如果没有则不调用。然后执行一段自定义的逻辑。这里忽略了父类方法的返回值。当然你也可以处理。
当你运行Demo的时候,你会你的逻辑正常的输出了。
12:MRAppDelegateLogic1 handle:MRAppDelegate Logic2 handle magic
这里是怎么实现的呢?使用了,我之前写的一个基础类库 。
Hook UIApplication 的 setDelegate 方法
搜寻所有遵循了我们自定义的协议 (MRApplicationDelegateInjectionProtocol)的类
把类中同名方法的逻辑,注入到上一个类中,生成一个新的元类,重复直到所有的类都注入完成
将 AppDeletge 的 isa 指针指向这个新的元类
这样就可以完成类的拆解了。同时,满足了我们最开始说的几个诉求。在 MRLogicInjection中我对于LogicInjection的东西做了一些深入的说明。其他的细节可以参考
的 ReadMe 文件。
同时,我们也会发现。这样的处理策略其实是实现了一套可以将一个类拆解成多个类,然后再合并到一起的能力。当然使用OC原始的继承策略可以实现。但是当处理Delegate的时候原生的继承就比较尴尬了。比如UITextView的delegate其中还实现了UIScrollDelegate。两个delegate揉在一起,当你想在另外一个类监听Scroll的变化的时候,就会影响原始的delegate,毕竟delegate的指针只有一个。你继续使用刚刚说的这套机制,一样可以处理UITextView的delegte。
总之 MRAppDelegateComponents 是 基于 MRLogicInjection 对delegate 进行解耦和拆解模块的一个 实例。你要解决 AppDeleagte 的问题可以直接只用这个类库。如果想处理其他 Delegate 的问题,可以参考这个类库的实现处理。
包体积优化收益统计:采样接口:
var_0 (post)
direct (get)
var_1 (post)
最开始优化传输包体积的目标来自于,网络速度优化。希望在带宽恒定的情况下,通过降低传输包体积来达到优化传输速率的目标。传输包大小,主要影响TCP传输的socket package数量。具体可以参考文章。而TCP层传输的最大TCP包大小恒定,因而包体积对于速率的优化,只有当传输包大小达到一个特殊的边界值的时候才会对速度产生影响。但是可以肯定的是,当传输包体积越小,就越有可能传输较少的TCP socket package,就越有可能获得更快的速度。
而我们很容易意识到一个问题,传输包体积不是你说减,说减就能减的。必须在保证业务正常的情况下,才能去做优化。优化传输包体积首先我们需要量化优化目标。
我们为了与服务器进行一次数据交互过程,会制定上行协议和下行协议。而在执行协议的过程中,必然传输的数据中有一部分是业务逻辑直接相关的,而另外的是无直接关系的。比如支付接口,我们将和支付相关的订单信息等称之为业务逻辑直接相关的,而http层的一些header和cookies则称之为与业务逻辑无直接关系的。这里说上述问题是为了区分两个概念直接有效信息与辅助信息。为了让直接有效的支付信息能够在OP服务器发挥作用,我们添加了非常多的辅助信息,为了让支付信息能够顺利在OP中执行。
于是我们可以定义一个信息负荷的概念:
在一次数据交互的过程中,信息的整体数据量与有效信息部分的数据量之间的比率
因而我们很明显的知道信息负荷与以下因素有关:
绝对信息负荷:信息的表达方式(json,pb还是urlencode,同样是int32类型,在不同的表达方式下消耗的数据量不一样)
相对信息负荷;为传输有效信息而支付的辅助信息的多少
我们定义一个信息的标注负荷为其使用标准类型表示时所占用的数据量
而我们优化传输包体积的目标就是为了尽最大可能降低信息负荷。在保证业务逻辑正常的情况下,寻找降低信息负荷的方式。
采样目前主要接口信息负荷
负荷主要组成部分分析目前主要业务参数为字符型表示,接口直接相关的数据,是第一部分负荷。该部分负荷,在目前阶段我们先假定不可压缩,是一个恒定的值。称之为信息固有负荷。
设固有负荷为Ifix,信息负荷为Iall
而称多余付出的资源为冗余负荷
1Ix = Iall - Ifix
而假设信息固有负荷不可降低的情况下,通过降低冗余负荷是一条可选路径。
冗余负荷组成分析某某系统需要的公共参数而在采样的几个接口中统计公共参数大小占比如下:
业务参数大小(与Body大小差异来自于=、&等拼接符号和数据处理的增幅)
公共参数占比
可以很直接的看出来,造成目前接口上行信息负荷较大的大头来自于公共参数部分。因而公共参数优化,将能够带来较大收益。
目前在上行接口参数中,公共从参数如下:
长度(Bytes)
可以看到var_4,var_3,var_5三个参数几乎占了公共参数的全部,换句话说,这三个参数是造成信息负荷较高的直接原因。其中var_3和var_5是一个值组成,而var_4是多值组成。
假设对于var_4,var_3,var_5做Session话缓存处理在理想状况下可以
理想目标优化大小
理想优化空间
而若按照理想状态下优化则可得到信息负荷优化为:
业务参数大小
目标优化大小
可得信息符合优化理想效果为:
原始信息负荷
理想状态下优化后信息负荷
var_4内容分析
/应用的显示名
应用的var_7 identifier
设备唯一标识
pass生成的设备唯一标识
用户指纹信息是否存在
——华丽的分割线———
网络接入点
上图也可以比较直接的看到非易变性数据在整个var_4中也占据了相当大的部分,对此部分作出Session化优化,之后将会有明显的收益。
而且var_4中cuid_1参数与外层公共参数var_2重叠,可以考虑去掉一份
HTTP层造成的冗余负荷HTTP头部参数采样的三个接口中纯HTTP头部的大小如下(对于Get请求,从URL中剔除业务参数和公共参数大小)
Header大小
减去业务参数大小
Header大小(减去业务部分)均方差为33.1,大小抖动范围为+-4.5%。因而可以推测,HTTP头部大小是个相对稳定的冗余负荷。仔细分析一下头部中的参数分布:
var_5-Length
Value-Length
Total-Length
POST /_u/wireless/card_check HTTP/1.1
10.95.40.58
Content-Type
application/x-www-form- charset=utf-8
Connection
keep-alive
Content-Encoding
Proxy-Connection
keep-alive
var_12=_6_R_R__c02f_Y; var_13={“group_smallflow”:”smallflow”,”group_umoney”:”umoney_test”,”group_smallflow_uri”:””}; token=9e30c9aeb71; var_14=7A7E38E7E0BDF0EC2BD693A8D6B8B22B:FG=1; var_15=HlqM2hsbjZmS014ZmxOQlBGajlqTmluZ2g2Rn5oUjV4Q0hYSG1HLTRPbU40NUpYQVFBQUFBJCQAAAAAAAAAAAEAAABeqMoHeWlzaHVpbGl1bmlhbgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI1Wa1eNVmtXa
User-Agent
BaiduWallet-5.9.1.0-IOS-test_828_1472_iPhone_9.3.2_9.3.2_1.0_1.0
Content-Length
Accept-Language
Accept-Encoding
gzip, deflate
其中绝大部分为HTTP层控制信息,是进行HTTP层传输必须配置的参数:
Content-Type
Connection
Proxy-Connection
User-Agent
Content-Length
Accept-Language
Accept-Encoding
而参数Cookies为我们业务需求而产生的参数,并且在整体的头部信息中占到约59%的比例,也是不小的一块开支。因而按照对于公共参数的优化思路,首先寻找最优优化效果(对Cookies做全量剔除):
Cookies理想优化
原始信息负荷
优化后信息负荷
HTTP Cokkies分析
_15_N_R_10_0303_c02f_Y
var_12=_6_R_R__c02f_Y; var_13={“group_smallflow”:”smallflow”,”group_umoney”:”umoney_test”,”group_smallflow_uri”:””}; token=9e30c9aeb71; var_14=7A7E38E7E0BDF0EC2BD693A8D6B8B22B:FG=1; var_15=HlqM2hsbjZmS014ZmxOQlBGajlqTmluZ2g2Rn5oUjV4Q0hYSG1HLTRPbU40NUpYQVFBQUFBJCQAAAAAAAAAAAEAAABeqMoHeWlzaHVpbGl1bmlhbgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI1Wa1eNVmtXa
7A7E38E7E0BDF0EC2BD693A8D6B8B22B:FG=1
HlqM2hsbjZmS014ZmxOQlBGajlqTmluZ2g2Rn5oUjV4Q0hYSG1HLTRPbU40NUpYQVFBQUFBJCQAAAAAAAAAAAEAAABeqMoHeWlzaHVpbGl1bmlhbgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI1Wa1eNVmtXa
其中var_14与var_15为登陆信息,而心在登陆信息绝大部分接口已经在公共参数中带上去了,这两个参数应该剔除掉,但是因为部分后台接口还会检查因而还留着,建议后台排查对应的接口,进行接口升级,剔除对于这两个参数的依赖。
而var_13做为AB测试使用的参数,目前在小流量中使用,这里也可以看一下是否可以使用其他方式来提供小流量分流操作,而不是使用var_13。
var_12这个参数未知,不知道种的,得排查一下。
方案:提出一份Uaser-Agent
###Session化方案
策略1 Cookies策略,通过服务器和客户端维护相同的头部信息MD5来进行头部压缩缓存合法性策略:客户端控制,最长10分钟内中内有效,10S分钟后采取更新策略
策略2 使用HTTP2头部压缩策略在HTTP1.1情况下使用HPACK算法传输头部,在HTTP2情况下默认使用默认算法,不做头部压缩处理
效果参考:
提高绝对信息负荷信息表达方式造成的冗余负荷(json与base64)在第一轮加密之前:var_4数据原始数据长度:259
第一轮加密之后:var_4中Value大小:672
使用JSON表达之后:,var_5大小:70,整体大小890
使用Base64之后:整体大小1188
使用URLEncode之后大小:1190
信息负荷增幅
var_4中真正需要传输的数据为259Bytes,在经过加密,JSON化,Base64,URLEncode之后,数据量激增为1190。信息负荷提高459.5%。接近五倍。因此var_4的处理方式需要升级。同时,提出疑问Base64处理的意义何在?
####var_4升级策略一
JSON化后直接加密
信息负荷增幅
调整顺序之后优化到:
####var_4升级策略二
Base64的必要性?
去掉Base64之后知道可以在前一步之上节省33%的体积,AES加密中,还有而外的一次Base64,这样实际上数据经过了两次base64处理,去掉一步之后,能有较好收益。
空值优化在接口中存在类似于下述形式的参数传输:
123{ &user&:&&}
只有var_5而没有。
总结主体策略分成两个:
剔除无用数据传输
头部中无用数据提出
防止空值传输
压缩有价值数据占用带宽
Session化处理和头部压缩
加密流程编程
iOS端 架构设计目标,从质量出发质量做为衡量一个工程重要的指标,日益在钱包iOS端SDK与app的设计和编码中显的越发重要。而质量往往是一个宏观的标准,其为从代码细节中涌现出来的宏观综合指标。目前业界确保代码和工程质量,比较通用的途径就是为整个成功设计良好的架构。搭建一个稳定可迭代的基础,而后添其骨肉。而iOS端的架构设计也是从此处着眼,以求iOS端SDK与app达到一个高质量。
方法论(1) make it work , keep it simple在让程序能够工作的同事,控制程序的复杂性。而让程序能够工作为先,优化为后。
(2)职责隔离
产品隔离(SafePay,首页,app等产品在业务逻辑上无耦合)
机制同策略分离
业务相关模块(策略)与业务无关模块(机制)分离
领域服务与业务分离
(3)复杂性控制
保持模块间良好的依赖关系
(4)流程建模通过流程建模对业务模块进行流程梳理,而后通过职责切分来划分模块和层次。
(5)简单可组合模块和组件尽可能使用组合式复用的。而非强制的使用类继承等方式。
实施策略简单可依赖的小组件
纵向分层,横向分区。
流程建模,职责划分,拆解模块和组件。
职责单一,不做过多的事情
接口清晰,可组合
对变化封闭,对修改开放
清晰的数据流程
清晰健壮的数据流转
业务逻辑MVVM
业务策略和流程尽可能结构归一,避免过于差异化
在质量和效率之间权衡
在完美方案和业务现状间权衡
对未来的方案修改风险作出一定的预期
代码&约定&配置&文档
代码即注释,代码自注释,代码本身能够阐释业务逻辑,而不是依赖文档和其他
能写成代码的就不要做成团队内部的约定, 写死的代码大于一切
不要重复造轮子,尽量使用开源代码
开源代码的查找,分析和优劣比较
对开源代码可控
开源代码 -& 设计思想&经验提炼
一、模块与结构使用领域建模、机制与策略分析的原理。将整体的功能进行纵向分层。
###目录划分根据上述模块划分,将整体目录结构划分如下:
这里需要特别强调的一点是:产品隔离。为了实现某些特殊的产品逻辑而独立出来的功能,不要在整个SDK项目中使用。比如application机制,是真对钱包首页产品宿主需要对不同的业务进行打包而诞生的,没有很强的必要,耦合在SafePay或者MiniSDK等产品当中。不同产品线的功能逻辑,尽量做到隔离,不要互相混淆。造成维护成本增大,更有甚者造成运行时的数据干扰。
demos 用于承载调试和测试的app
products 用于承载各个产品模块的代码,目前知道的有聚合收银台,钱包首页产品,核心支付产品还有精简收银台产品
services 针对我们目前的业务,抽离出来的和业务相关的公用模块,比如下单,BScanC,转账等等
程序公用功能,在该目录下面的模块和代码尽量做到业务逻辑无关。可被其他app和模块复用。
二、数据流向数据是一个app的核心内容。数据流的走向与数据流转过程,决定了app绝大部分结构。我们通过流程建模与职责划分,同时借鉴业内比较推崇的MVVM思想。将数据流规划如下(也规定了各个层次的职责):
三、开发流程本地源码引用调试我们采用了模块化开发的思路。而在具体落实的时候,这里需要考量:需不需要将每个模块都作为一个单独部署的工程来处理啊。鉴于目前单独部署所需人力成本较大,而目前团队规模较小的现状,因而建议还是采用源码打包的方式,通过分目录的方式来实现模块化。
使用CocoaPods开发模式CI与CD持续集成持续交付代码Snippet与规范共享四、业务相关基础功能模块###(1)UIStack
系统VC堆栈管理,同时负责首页模块中各个application的UI相关的生命周期的通知。
###(2)Application机制
为了满足首页需要定制打包的需求。
四、实施计划1. MVVM改造
网络库与模型解析适配,暂且使用ASI,在此基础上封装请求和模型解析。
业务模块向MVVM改造
2. 开发流程改造
根据新的模块划分,进行目录规整
CI与CD配套设施到位。
We Build SDK 或者 SDK
写在第一版基于新的研发流程和架构的COS SDK发布之际
上半年的时间,我们在SDK的制作上面趟了不少坑,或许也埋下了不少坑。跌跌撞撞中也有一些小的经验。现在总结一下在制作一个的SDK道路上作出的一些尝试和那些解决了的问题。
一个新的命题制作SDK和之前写APP很大的不同首先来自于使用你产品的用户不一样了。APP都是终端用户在用,用户基本上都是和界面在交互。点点点,戳戳戳,摸摸摸。而SDK是开发者在用,他们的操作是:下载某某SDK,引入SDK,调用函数接口完成功能。使用的用户发生改变,我们的产品就不能简单的按照之前制作APP的一套标准来搞。
举个简单的例子:刚来的时候,发现我们在测试SDK的时候,测试的同学是在我们提供的Demo基础上进行测试,仍旧使用针对APP的那套测试方法,一个按钮点击之后是否符合预期。而比较尴尬的是,之前的SDK在放出去之后多多少少出现了一些质量上的问题。而针对于SDK比较标准的做法应该是UT(UnitTest)通过单元测试来保障SDK的质量。这就是区别。
在写APP的时候,我们还有UI/UE的同学帮我们去设计用户的交互界面。而SDK产品,我们就是UI和UE。需要我们自己去设计API(application program interface),把脸面先弄好。APP用户不关心你的代码写的好不好,而SDK用户则完全不一样:他们也是CodeFarmer。如果你的代码写的很挫,是会被骂的很惨的。而且将令对方开始怀疑你的产品质量,从而影响他们的决策要不要用你的产品。
做APP的时候我们可以比较容易的把需求拆解成很多个自需求,然后让不同的同学相对独立的去开发,而且很多基本上都是业务逻辑的开发。而SDK得制作,则显得怎么说呢之前接触过的项目都有点像是小作坊生产。产品来了一个需求,然后leader就会把这个需求分配给A同学。剩下的就是A同学去完成了。然后下一个需求可能给了B同学,也可能给了C同学。他们之间从来没有探讨过完成这个需求对我们目前系统的影响,或者在目前的系统中有没有更好的解决方案。所以代码朝着很多方向“无序生长”。时间一长,就开始出现各种稀奇古怪的问题。但是只要业务和产品能够持续往前推进,代码的腐朽则显得不是那么那么重要。而一个SDK产品,如果你的代码都腐朽了那么你还剩下啥?
类似这样的问题不一而足。
而现在制作SDK,我们将面对一个新的命题:如何制作一个很靠谱的SDK。
或者一个最起码看起来很牛逼的SDK,如果装的时间久了,那么我们就有可能是一个很牛逼的SDK。
怎么样一个靠谱的SDK首先要解决的问题是如何定义什么样才算一个靠谱的SDK?
我无法给出一个准确的定义,但是对于什么样才算一个靠谱的SDK我有很多美好的想象。
一个靠谱的SDK应该首先能够完成他声称的功能。
一个靠谱的SDK应该是一个持续稳定输出的队友。不会中途挂机–Crash。坑死你
一个靠谱的SDK应该有足够的兼容性,在尽可能多的设备上跑的起来。
一个靠谱的SDK应该用起来很爽,接口简单明了,能够快速上手。粑粑啦啦能量!!!
一个靠谱的SDK应该没有那么多复杂的东西,一起看起来都很简单,简单就意味着好理解。
一个靠谱的SDK应该是有完备的文档,我可以跟着文档走就能使用起来了?
一个靠谱的SDK应该……
好吧,是因为有太多的要求可以挂在这个“什么样才算一个靠谱的SDK”上面了,所以这里只能写一点点了。
研发流程流程在整个研发过程起到的作用,反正我是耳朵起茧子了。也就不赘述了。直接切入我们是做的。如下图:
主要分成了四大块:
我们在整体的研发流程当中,特意强调了“设计”的重要性。在一个新的SDK需求到达的时候,首先要进行架构和接口设计。给出这个SDK的”结构蓝图”。而后,我们会根据蓝图进行代码”实施“。当然在实施的过程中我们会依赖一些工具化的手段来保障代码质量,以及结构和接口还原的准确性。最后我们会有一道质量阀,当这个版本的SDK满足这个质量阀。就开始后面的发布流程。
如果你做过“数学建模”,可能会觉得这个流程很熟悉。首先针对问题抽象出模型,然后根据模型构建解决方案,之后验证好解决方案的正确性就可以发布了。
这是一个非常普通的研发流程,可能在任何一个书本上都能看到。但是从自己的工作经验来看,真正困难的是实施下来。因为中间会碰到一些坑啊。
设计与架构很重要
在写代码的时候我们应该80%的时间在思考,20%的时间在Coding(甚至更少)
架构设计模块化–处理代码的复用问题关于模块化是个老生常谈的话题,基本上稍微大一点的工程都会采用模块化的方案。在iOS开发上cocoapods已经变成了事实上的模块化基础工具。因而,我们在实施模块化的时候,自然也采取了cocoapods作为基础工具链之一的方案。关于cocoapods基础概念就不多说了,它和其他语言的包管理工具npm、spm、pip等大同小异。主要解决了依赖管理和模块引入的问题。
我们借助cocoapods将模块拆成了一个个的podspec。然后使用本地相对路径引用的方式,来引入模块。比如我们的一个模块长这个样子:
12345678├── COSV4.podspec└── Pod
└── Classes
├── COSClient.h
├── Request
├── .....
└── Util
├── COSClientDefine.h
而在引入的时候,直接在Podfile中使用:
1pod &COSV4&, :path =& &../../Libs/COSV4&
依照这种模块的管理方式我们将,产品拆成了各种基础模块和业务模块。而产品的主工程文件只是一个空壳。这种处理策略同时也避免了恼人的xcodeproj工程文件冲突问题。因为主工程文件主要是维护了一个Podfile文件来管理其依赖的模块的引用。
同时,这也很好的解决了我们同时维护多个产品时的代码复用。我们将所有的产品都放在了同一个git仓库中。起了一个看起来很唬人的名字“OneForAll”(来自于动漫《我的英雄学院》)。所有的模块或者产品通过最原始的“目录”方式进行隔离。
12345678910111213├── BisnessComponents (业务模块)│
├── COSXML(每一个业务都是一个Podspec)│
├── EnterpriseCloudDrive│
└── FaceInUI├── Libs (公共基础库)│
└── COSV4 (每一个库都是一个Podspec)│
├── COSV4.podspec│
└── Pod|
|---....├── Products
(产品,在该目录下面建立产品输出)│
└── COSV4DemoAppOld├── README.md└── build.sh
在Products目录下面放的就各种产品的工程配置,而其他目录下面则是拆成了podspec模块的基础类库或者业务。
多仓库VS单一仓库先说结果,在经历了一系列的尝试、摸索、填坑之后,我们选择了单一仓库这种方式处理代码管理。
最开始,代码没有模块复用,都是堆在SVN的不同的目录下面。因为同时维护多个产品,里面充斥着大量的Contrl+C和Control+V的代码。为了避免这种情况,我们很自然的引入了模块化处理。我们使用原始的“发布更新”这种策略处理。每一个产品都是独立的仓库,而且每一个模块都是单独的仓库。我们建立了一个。在一个模块修改之后,发布到该仓库。然后其他的产品pod update拉取更新。
但是,立刻就陷入了维护的噩梦。很多基础类库都不是很稳定,经常变动,而且很多时候是在维护某个产品的时候,需要修改基础类库。这个时候,就得跑到另外一个工程修改基础类库,发布。然后再跑回来更新产品依赖。毕竟我们人力有限,把时间花在这种维护的事情上,实在是心痛。
于是立刻开始着手寻找解决方案,首先要解决的事同时开发多个类库的问题,尤其是同时修改开发调试基础类库。经过一圈研究我们使用了pod的本地相对引用的策略。决定了这个策略之后,剩下的一个问题是。怎么去管理其他的pod的git仓库。
在处理这个问题的时候,前期调研不足。首先上了gitsubmodule的方案来维护,最开始开心的用了一个月。但是时间一长就开始逐步的暴露问题:各仓库版本很难保持一致。具体解释一下这个问题。当我们A产品发布了V1.1.1版本的时候,我们会在A产品的git仓库上打上tagv1.1.1。为的是出现问题能够还原当时的代码环境修改问题。但是啊,很尴尬的是我们发现:他引用的gitsubmodules都没有打tag,而且没有地方记录v1.1.1其他submodule当时的commit信息。这就悲催了。
那么就要解决这个问题了,总不能无法修改问题吧。而android源码管理工具repo则正好解决了这个问题。在综合考虑了可能的一些潜在代码管理问题之后。认为repo能够很好的满足我们目前的需求:多git仓库版本一致性管理。于是我就开始了基于repo的改革。repo满足了基本的管理需求,但是发现还是有些不顺手的地方。于是在repo的基础上我构建了自己的工具(欢迎围观,可能对于我们目前不适合,但是当团队规模扩大的时候,这套东西或许适合你们)。一个iOS多模块管理ToolKit。怎么说呢,当时写这个的时候。觉得还是很不错的,除了基本的git多仓库的管理功能。我甚至把git.gode.oa打通了。在创建模块的时候,会自动化的在git.code.ao上的创建对应的git仓库,还有把merge-reques的功能也脚本化了。为啥这么感慨,因为我现在切到单一仓库后,这套工具就暂时弃之不用了。也是心血啊。
为啥要切到“单一仓库”?因为管理模式更简单啊。你可能会问,你这么折腾其他同事怎么办。好吧,我很坦白的说,当我折腾到单一仓库的时候,产品基本上都是我一个人在维护。影响范围那是相当有限。而正是因为团队规模较小,才发现了一个问题:适合才是最重要的。hive的整套管理虽然付出了一些心血,但是发现多个git仓库管理对于团队规模较小而且核心库不稳定的情况非常的不适合。因为你要频繁的去维护多个不同的仓库。虽然使用hive就是敲一些命令的事情,但是终归是没有在同一个仓库中直接commit一下来的轻松啊。思前想后,为了降低管理和维护成本。痛下决心签到了单一仓库管理。
开心一刻:说点大家喜闻乐见的,我们团队目前虚位以待iOS和Android的兄弟,欢迎自荐或者推荐。
只是单纯的因为“简单”啊。
GitFlow VS TBD切到单一git仓库之后,那么必然要选取一个合适的分支管理策略。一般情况下有两个选择“GitFlow”或者“TBD(Trunk base develop)”。严格执行了一段时间的gitflow之后,发现还是啰嗦。关于为啥啰嗦,可以参考这篇文章:。总之就是你需要维护很多的分支,而且修改的代码无法及时同步到其他同事那里。这个时候我们有两个人了。
开心一刻:说点大家喜闻乐见的,我们团队目前虚位以待iOS和Android的兄弟,欢迎自荐或者推荐。
关于GitFlow就不多介绍了,着重说一下我们目前在使用的
一个软体开发的分支模型,也被称作mainline
同一个产品开发的所有人员共享一个Repository,有一个trunk,单一Developer或是Developer团队可以有自己的private branch,所有修改最后都会回到主干
只有在Release时才会有官方的分支,一般Developer不能对Release Branch作动作,只有Release Engineer可以更动Release Branch,当Release Branch完成它的任务,就会被砍掉。
Google与Facebook都采用这种分支模型
我们在使用上结合了gitflow的工具链,因为没有找到tbd相关的工具。gitflow则是一大堆。于是我们把master分支删掉了!是的删掉了。留下了develop分支作为trunk分支。然后所有的修改都在develop上进行。当发布或者过往版本修bug的时候,就用gitflow的工具创建一个小的临时分支。
一个没有持续集成和持续测试的代码管理不是一个真正的TBD。这一块在后面讲我们怎么去建立起来的。
怎样拆分模块
机制与策略分离
前面说到我们使用了模块化的策略,那么这就会自然引申出一个问题:你凭啥分的模块。简单点说就是:机制与策略分离。这是Linux设计哲学之一,所为机制就是能够被某一类需求公用的基础设施,所为策略则是这种基础设施的应用。举个形象点的例子:
1f(x) = x x属于R
就是一个机制,而
1f(1) = 1, x=1
就是一个策略。机制是对一类问题的归纳,是针对变化的归纳。之前想过这个问题写了一个,也是类似的思路。于是像网络处理、日志、文件基本管理、权限管理….这些都是基础机制,COS对象存储就是个策略。
所以要把这些基础的机制拆出来作为单独的podspec使用,因为他们通用性强啊。但是还会有一个粒度的问题。当然你可以把所有的基础功能类库,本着单一职责原则,拆的很细。网络一个库,甚至URLEncode一个库,日志一个库…..然后你就有了非常多的基础类库。然后噩梦就来了。东西越多你的管理成本就越大,虽然表面看起来符合了单一职责等设计原理。但是吧,维护是个噩梦啊。你还要考虑他们之间的版本用来。实话是:我这样干了,几乎拆除了几十个基础模块??♀。然后又乖乖的把他们合到了“QCloudCore”里面。
简单的才是最好的
干完之后思考,其实模块的粒度并不是越细越好。需要适度的折中。在一个维护成本和设计原则中间取合适的中间值。
模块间通信先定义一下模块间通信的概念:A模块对B模块产生了依赖则需要进行模块间”通信“。而这里所谓的依赖可以分为以下两个层次:
所谓接口依赖,就是我要引用你的头文件调用你的函数接口,比如打日志。所谓功能以来就是业务逻辑上处理的过程中,付款完了要跳转到另外一个页面,而这个页面是啥其实付款不需要关心。针对接口依赖的处理,这个没啥好的办法,只能乖乖的引用头文件。不过这里在引用的时候,我们不再推荐使用类似于pch这种全量引入的策略。而是用到啥引用啥。不要引用一堆不需要的东西,拖慢了编译速度。
而针对功能依赖这个就可以开始解耦了,我把自己之前写的一个拖了进来。作为功能依赖的方案。解决依赖的资源的定位和转发的功能。 这是支持基于标准URL进行Native页面间跳转的Objective-C实现。方便您构页面之间高内聚低耦合的开发模式。他的核心思想是把每一个页面当成一个资源,通过标准的URL协议(统一资源定位符)来定位到每一个可触达的页面(资源)。
处在流程中的架构设计和接口设计上面说的是一些影响比较大的基础的技术方案。而接下来说处在最开始流程中的设计我么所做的事情。
在设计这块我们主要是做了两方面的设计:
接口设计,也就是传说中的API
你可能会问,产品驱动跑哪里去了。好吧,有些时候技术类的产品只是高速你我要一个直播的SDK,然后剩下的就要靠你自己去想象了。很多时候,技术类的产品只会告诉你,我们当下需要什么功能。但是关于功能背后的稳定性、可扩展性、易用性等等,可能都不会有所设计。而这些就是我们在设计阶段要完成的事情。姑且把这个阶段的设计当成另外一种“产品需求文档”。
架构设计这个词吧,比较空泛。首先具化一下,就是盖房子的“建筑图纸”啊。要表明用什么材料,楼宇的承重结构之类的啊。所以在架构设计的时候,我们会强调两个东西:
所谓的静态结构就是类图,比如这样的:
这个具体的知识可以谷歌UML。他的主要作用是设计出一个SDK或者某块业务逻辑的静态结构:抽象出来的主要的类、类和类之间的关系。关于类和类之间的关系如何管理,这里有本书可以看看《元素模式》。基本上在下一阶段,代码实施的时候,主要的框架都是从这个图中来的。当然不排除一些修修改改。
我们在做这个事情的时候遇到的最大的问题是,我们之前都没有干过这个事情啊。什么UML了都是在现学。之前写APP业务逻辑,都是想到哪写到哪,很任性很随意的。而现在,把一部分时间从Coding中强制抽离出来,做设计的事情。刚开始的时候,战友们多少都有些抗拒。毕竟和之前的开发体验有些不一样。这个真的克服克服就好了。
而动态结构,则是程序跑起来会是什么样子。主要是一些流程图,数据流向图之类的。这块的东西倒是没静态结构强调的那么严。因为其实动态结构和具体的业务逻辑关系比较大。这个都是到了具体的技术细节的时候才会用,比如在管理某个链接的时候会要求出一个状态机:
我们发现,当强制从Coding中抽离出来的时候。对待需求的视野也顺便提高了一些,一些原先可能想的比较少的东西,开始变得重要起来。能够开始从一个宏观的角度思考的需求。将一些之前缺失的质量约束纳入进来,比如扩展性,比如代码的复用。当你被逼着思考这些问题的时候,这些问题才不会在Coding了N久之后被新来的同学吐槽。
APISDK类产品API接口的可读性是非常重要的衡量SDK好坏的指标。无论你的功能多么牛逼哄哄,别人就是看不懂你的接口,也就无从谈起使用你的SDK。只说以下文字抄袭自。自己重新写一边真的是没必要了。
好API的6个特质
API之于程序员就如同图形界面之于普通用户(end-user)。API中的『P』实际上指的是『程序员』(Programmer),而不是『程序』(Program),强调的是API是给程序员使用的这一事实。
在第13期Qt季刊,Matthias 的关于API设计的文章中提出了观点:API应该极简(minimal)且完备(complete)、语义清晰简单(have clear and simple semantics)、符合直觉(be intuitive)、易于记忆(be easy to memorize)和引导API使用者写出可读代码(lead to readable code)。
极简极简的API是指每个class的public成员尽可能少,public的class也尽可能少。这样的API更易理解、记忆、调试和变更。
完备完备的API是指期望有的功能都包含了。这点会和保持API极简有些冲突。如果一个成员函数放在错误的类中,那么这个函数的潜在用户就会找不到,这也是违反完备性的。
语义清晰简单就像其他的设计一样,我们应该遵守最少意外原则(the principle of least surprise)。好的API应该可以让常见的事完成的更简单,并有可以完成不常见的事的可能性,但是却不会关注于那些不常见的事。解决的是具体问题;当没有需求时不要过度通用化解决方案。(举个例子,在Qt 3中,QMimeSourceFactory不应命名成QImageLoader并有不一样的API。)
符合直觉就像计算机里的其他事物一样,API应该符合直觉。对于什么是符合直觉的什么不符合,不同经验和背景的人会有不同的看法。API符合直觉的测试方法:经验不很丰富的用户不用阅读API文档就能搞懂API,而且程序员不用了解API就能看明白使用API的代码。
易于记忆为使API易于记忆,API的命名约定应该具有一致性和精确性。使用易于识别的模式和概念,并且避免用缩写。
引导API使用者写出可读代码代码只写一次,却要多次的阅读(还有调试和修改)。写出可读性好的代码有时候要花费更多的时间,但对于产品的整个生命周期来说是节省了时间的。
最后,要记住的是,不同的用户会使用API的不同部分。尽管简单使用单个Qt类的实例应该符合直觉,但如果是要继承一个类,让用户事先看好文档是个合理的要求。
向优秀的设计看齐,我们做了下面的事情代码规范这是又一个之前过度流于口头的东西,但是我们就偏偏不信邪。讨论并书写了自己的代码规范,每一位同学都参与进来。然后互相监督。
接口层使用DSL自动化生成当写到第二个SDK的时候,就开始发现。其实很大一部分都是针对于网络接口的封装。雷同的东西太多。而我又是一个非常懒得人,当同一件事情做第二次的时候,内心是抗拒的。也是就写了一个工具.
另外一点这样可以强制保持接口层的一致性。还极大的降低了出错的概率。之前写网络接口最大的尴尬就是入参的关键字拼错了,还费时费力的去调试。
通过API定义文件生成各语言版本SDK 【变色龙】可变成任何一种环境颜色
以COS XML的版本为例,只要根据我们自定义的一套简单的(真的是简单极了)书写网络接口层的描述文件。然后运行命令就可以了。
比如描述文件是这样的:
12345678910model XXXXResponse {
string x [xx]}request Image (UP.url DOWN.json){
string pathxxx
response XXXXResponse
method xx/xxx
server http://xxxxxx.com}
然后运行命令:
1chameleon oc request.model --outType=bwmodel -d .
接口层的代码就自动生成了。关于具体的实现原理还是参考这个项目的介绍吧。
文档注释接口层尽量文档化,使用标注的apple的标注来写。他还支持markdown。把设计意图和接口功能描述清楚。
文档标准输出接口文档化之后,一个好处就是你可以输出标准的apple的docset或者dash可以使用的docset。综合比较了各种文档生成工具之后,我们使用了作为文档输出工具。用以输出docset文档。用户导入之后就可以非常方便的查看各个类的使用。
接入CodeCC服务持续代码检查所有的SDK产品都需要接入CodeCC进行静态代码检查。建立了一个叫做OneForAll的Xcode工程,将所有的模块都引入进来。以此为基础在CodeCC上对所有的代码进行静态检查。
实施这个环节都是一些琐碎的事情了,主要是围绕着Coding和CodeReview展开。我们没有使用任何强制性的CodeReview的工具。用的是滞后CR的策略。每一次提交git平台都会给项目组的成员发邮件,收到邮件之后,大家会去看一下。有任何问题或者不对的地方,通知修改者,修改后再提交一个commit。
一个牛逼的SDK 应该是什么样子?可能是那个样子吧。虽然现在不是那个样子,但是我们要先装的像那个样子,装的时间久了,我们就成那个样子了。
而整个流程的中的每一个环节都在做着质量保障的事情,无论是刚开始的架构和API设计,还是CR,还是代码规范。这是一个贯穿始终的话题。在这里单独摘出来说的就是自动化测试吧。
单元测试与覆盖率作为一个SDK,单元测试应该是一个必须的选项!!是的必须的选项。之前写demo让测试同学基于demo测试,发现其实很多问题都测不出来。质量一直得不到有效的保障。而单元测试就显得格外重要。
对于绝大部分的接口和业务场景,都写TestCase。同时异常和边界情况也可以去检查。以COS XML为例:
上图是我们跑的自动化测试的截屏。最开始我们是自己搭建了jenkins环境来跑自动化测试。但是后来发现公司有个平台,就将自动化测试迁移到这上面来了。
打包&源码&发布
源码or not 源码,this is a qustion
作为SDK类产品,用户的使用方式会有多种多样,就拿iOS来说,用户可以使用以下方式:
静态链接库
动态链接库
为了兼顾用户不同的引入方式,我们写了一个新的工具。可以支持各种各样不同形式的产品打包输出。可以是framewrok,也可以是.a库文件。这个工具是在改造了的基础上构建而来。
1qscaffold build -i podspecs.rclt -n QCloudCore
一条命令可以将该库和库对应的依赖全部打包出来。
打包是发布的一种方式,主要是提供给希望直接使用编译成品的用户。而很多用户则希望能够使用cocoapods这样的工具来自动引入我们的产品,于是我们也会在cocoapods等标准的管理台上发布自己的产品。
发布版本管理因为我们采用了模块化的处理策略,那么在发布的时候,就会遇到发布多个模块的问题。一旦发布了多个模块,模块之间的版本的依赖关系就必须很好的维护起来。避免依赖地狱。这个问题是这样子的:
我们发布了产品A的1.0.0版本,此时A产品依赖B库的1.0.0版本。但是我们在写依赖的时候是用的宽松依赖:
1A.dependency &B&
而在产品C的迭代过程中我们更新了B库到2.0.0版本,同时也发布了适配该调整的A产品1.0.0。
而用户测还是在使用A1.0.0这个时候,只要他pod update一下。将会拉取到:
12A 1.0.0B 2.0.0
然后就出现各种诡异的编译问题。而一旦出现这种问题,我们这边也很难还原用户当时的代码环境去调试问题。那么这里就得使用严格版本依赖这种策略。在每个模块发布的时候,对于每一个依赖关系都需要制定。
而这种口头的规定,在发布的流程中可能因为人为的疏忽就忘记掉了。于是在qscaffold工具里面加入了codeversion命令。来强制化处理版本。这个命令一方面会将podspec文件根据依赖关系转化成严格依赖的版本,一方面会在对应的模块中加入版本管理的代码:
1234567891011121314#import &Foundation/Foundation.h&#import &QCloudCore/QCloudCoreVersion.h&#ifndef QCloudCOSXMLModuleVersion_h#define QCloudCOSXMLModuleVersion_h#define QCloudCOSXMLModuleVersionNumber 1001//dependency#if QCloudCoreModuleVersionNumber != 1001
#error &库QCloudCOSXML依赖QCloudCore最小版本号为0.1.1,当前引入的QCloudCore版本号过低,请及时升级后使用&#endif//FOUNDATION_EXTERN NSString * const QCloudCOSXMLModuleVFOUNDATION_EXTERN NSString * const QCloudCOSXMLModuleN
如果依赖的版本不对编译直接报错!!!
死程序不说谎
标准化发布流程每一个产品的发布流程都写成文档样式的CheckList。每一次发布都跟着文档走。发布的时候,内容要包括这几个:
framework等库的集成方式
cocoapods 发布
同时还需要修改仓库中的CHANGLOG.md等。跟着CheckList走主要是为了防止在发布的过程中出错。导致发布出去的产品和我们开发的不一样。后面这块也在规划做成命令行的工具,加入到qscaffold的工具里面。
标准化工具链
工欲上其事必先利其器
在整个过程中,我们构建了自己的一套工具链。如果你有类似的需求,可以围观一下,或者有好的建议可以提issues给我。
纪念一下马上要被废弃的Hive
新的工具链qscffold和qricker
开心一刻:说点大家喜闻乐见的,我们团队目前虚位以待iOS和Android的兄弟,欢迎自荐或者推荐。
iOS架构设计(解耦的尝试)之UI样式复用与布局管理
该系列文章是2016年折腾的一个总结,对于这一年中思考和解决的一些问题做一些梳理和总结。Talk is cheap show me the code.
本来只是想写写ElementKit中对于MVVM的实践来着,结果发现这一年做的一些事情中,还有值的继续说的。而且基本上都是围绕着解耦和复用的主题。而且很多都是在日常的开发中常见的问题和其解决方案。也就继续写写,算是抛砖引玉,在闷头设计开发之后与业界交流一下。
这一次来说一下在日常的开发中最常见的两个问题:
一个是样式,颜色、背景、font。。。。
一个是布局管理
其中的成果自然也是搞成了类库方便使用(Talk is cheap show me the code.):和。
样式和布局基本上是在iOS编程中最繁重的两个工作,之所以说繁重,
一是因为常见,绝大多数iOSCoder每写一个业务逻辑,都得和这两个事情纠缠上半天,去还原UI/UE同学的设计。
二是因为UI样式千变万化很难像其他业务逻辑那样用类继承了之类的策略来处理。每次都得蛮干。
再一次提一句话『懒是第一生产力』,有压迫的地方就有反抗,有繁重的地方就想着偷懒。所以得需要一个有效途径来降低UI工作的繁重。
抽象的威力其实我一直希望自己的文章能够道业双关。在说明一个问题的解决方案的同时,也能够解释清楚自己设计一个类库或者做一个技术决定背后的东西:知识背景和思维方法。还原整个从问题发现到问题解决方案的过程。所以,发现之前的几篇文章中多多少少有一些东扯西扯的东西。自然这篇文章也难免其俗:)。这一次要扯的是抽象。之所以要说这个是因为,在解决UI工作繁重的问题中,就是用的这个最基本的方法。
删繁就简三秋树,霜叶红于二月花
从小老师就耳提面命:要看透问题的表象看本质。起初不解,随着干Coder时间长了,愈发察觉要想偷懒就得去解决本质问题,而不是围绕着问题的表层转来转去。而进一步说,不能停留在使用简单抽象出来的概念解决问题的层次,应该进一步使用一些高级抽象。就拿布局这个事情来说吧。
原始抽象第一层的抽象是对现实世界在数字世界的描述,用坐标系统。用数学语言来描述布局这个事情。其实抽象到这一步也是一个非常大的进步了。在iOS中也就是我们常用的spring&struct的布局系统。通过frame等来标注一个view来控制平面布局,通过View-Tree来控制Z轴方向上的布局。
基于这种抽象的情况下,我们做的事情就是精准的描述每一个View在坐标系统中的位置。这个就是那个繁重的工作。在layoutsubviews函数里面里面,各种计算大小和偏移量。熟悉iOS的同学,随意看一下自己View里面layoutSubviews函数的大小就可以感受到其繁重。
描述抽象抽象这个东西有意思的地方就在于他是可以递归使用的。在第一层抽象得到的概念基础上,进行第二层抽象;在第二层抽象得到的概念基础上,进行第三层抽象…..如此反复。当然,Apple牛逼的工程师们也会意识到spring&struct的繁重,于是就有了Autolayout(自动布局系统)。
Autolayout是基于描述的抽象,更多的是描述元素与元素之间的位置关系。通过解n元一次方程组来确定元素的位置(在坐标系统中的frame)。而这种基于模式的抽象,要比刚才第一层的抽象要高级很多。在这种抽象概念下,我们发现我们写的布局的代码有了一个大幅度的减少。原先需要拼了老命计算view frame的情况变成了,描述view之间关系的过程。原先需要非常多代码才能完成的任务,现在只要简单的写一句AView在x轴方向上距离BView固定间隔为10就可以完成了。随着代码量的降低,我们的对于布局系统理解成本都在降低。
复杂性由固有复杂性和认知复杂性组成。
而基于描述的抽象,因为是对第一层抽象概念的操作,忽略了第一层抽象的很多细节,很大程度上降低了布局系统的认知复杂度。无论从代码量上还是我们的认知成本上,都让我们可以『偷懒』了。
任务抽象而有了基于描述抽象的Autolayout之后,我们会发现其实很多任务还是很繁重,比如我们要写个List布局,不可能每次都把List中每个元素的位置关系描述一边啊。比如我们要写个Collection的布局,也不可能每次都把每个元素的位置关系都描述一边啊…..
于是真对某些特定任务,会使用任务抽象。把List布局抽象成UITableView,把Collection布局抽象成UICollectionView,把线性布局抽象成UIStackView。。。。。而且Apple的工程师们也会在这条路上越走越远。在近几个版本的iOS-SDK更新中,我们也看到了更多的这种布局View的出现。相信以后也会更多。
而这种类型的View在简化编程工作这件事情上要比autolayout来的更加厉害。可以随意感受一下,我原先为了实现一个TableView所写的代码量。就知道当我们把通用任务抽象出来的时候,能偷多大的懒了。
而关于这种任务抽象我们听到的最多就是面试中经常被问及的GoF设计模式。在想想应用设计模式时的爽,也就知道抽象这个工具的确很好用。在这里强烈推荐一本书。个人认为这本提供了一整套的瑞士军刀,来帮你进行抽象或者去设计『设计模式』。
….当然这里还会存在更高层次的抽象。不过嘛,抽象并非银弹。并不是说一味的抽象下去就能够得到极致的『偷懒』。随着抽象层次提高,概念密度也在提高,而那些在这个过程中所忽略的特殊场中的细节,将变得难以还原。有些时候,处理问题反而更加困难,编码量却在增加。老生常谈的问题啊:度。当抽象层次满足业务需求和业务发展的时候,就可以临时先止步了。
DZGeometryTools简单描述了一下抽象的威力之后,开始切入如何使用部分。顺着刚才的话题讲布局的事情。我自己写界面的时候也是尽可能的”偷懒”。于是就有了这个库。主要是用了描述抽象和任务抽象两种技术。看起来忽悠人,其实实现部分一看就明白了。无非是将常用的一些任务转换成了函数而已。其实在CoreGraphics框架中又一个CGGeometry.h文件,其中提供了很多方便操作CGRect等几何类型的函数。而在DZGemetryTools中所做的工作可以看做是对CGGeometry的一个扩展。
DZGeometryTools.h中主要是对几何类型的操作通过抽象我将比较常见的几何操作归结为以下几种比较基础的操作:
间距计算操作
margin施加操作
现在把他们提取出来,基本上就是完成了一个工具集的构建。而后在这些工具集上,就可以来描述每个Rect之间的关系,这个有点类似于autolayout,不过是提前收工算好了,而autolayout是自动计算并赋值的。贴段真实使用中代码来感受一下:
12345678910111213141516171819- (void) layoutSubviews{
[super layoutSubviews];
CGSize imageSize = {62*2, 84};
CGRect contentRect = CGRectCenterSubSize(self.bounds, CGSizeMake(20, 20));
CGRect textR
CGRect imageR
CGRectDivide(contentRect, &textRect, &imageRect, 30, CGRectMaxYEdge);
imageRect = CGRectCenter(imageRect, imageSize);
CGRect imgRs[2];
CGRectHorizontalSplit(imageRect, imgRs, 2, 0);
_indicatorImageView.frame = imgRs[0];
_powerImageView.frame = imgRs[1];
_textLabel.frame = textR
_backgroundView.frame = self.
_lastTimeLabel.frame = imageR}
其中使用到了CGRectCenterSubSize来对contentRect做了margin计算,并用CGRectHorizontalSplit对rect坐了纵向均分的操作,这些都是描述UI元素之间相对位置关系的关系,我们通过手工计算来完成了对于UI元素的布局操作。这些函数都没有采用直接操作view.frame的方式,只进行了几何运算,计算出了UI元素坐标。之所以采取这种方式,是因为想基于目前的抽象层次应该是针对于几何概念的操作。对于UI元素的操作,已经简化成了一个赋值操作,没有太大必要去优化处理了。而相较于以前的编码量和思维量来说,基于目前构建的这个工具集来进行编码已经省了不少力气了。体力活少了。
DZLayoutMacros.h 常用的任务在这个文件里面依旧是一些常用的布局工具集,不过和上面不一样的是采用了另外的表达方式:宏。针对于一些比较简单的任务,做了抽象。比如:
y依赖于顶部元素,并且尽可能填充满width的布局
顶部固定高度,铺满width的布局
//底部固定高度,铺满width的布局
小结这些函数是我在日常的编码中抽象出来的一些比较基础的工具。没有完整的模型,是比较零散的收集吧,更多的东西还是看库中代码的注释。这是个遗憾,没有统一的模型。一直都想搞一个类似于autolayout或者flexbox之类的布局系统,能够更加方面的来完成布局的操作。这个工作看来得放到17年做了,惟愿完成。
StyleSheet据说一个终端开发人员将会有70%以上的时间在和UI打交道。自己想想也对,貌似有很大一部分时间花费在了调整UI样式,addSubView还有layout上面。猛然间就发现自己的代码中有大量这种东西存在
1234567891011121314151617181920212223242526self.label.layer.cornerRadius = 3;self.label.textColor = [UIColor darkTextColor];self.label.font = [UIFont systemFontOfSize:13];self.label.backgroundColor = [UIColor greenColor];self.label.layer.borderWidth = 2;self.label.layer.borderColor = [UIColor redColor].CGCself.label2.layer.cornerRadius = 3;self.label2.textColor = [UIColor darkTextColor];self.label2.font = [UIFont systemFontOfSize:13];self.label2.backgroundColor = [UIColor greenColor];self.label2.layer.borderWidth = 2;self.label2.layer.borderColor = [UIColor redColor].CGCself.button.layer.cornerRadius = 3;self.button.backgroundColor = [UIColor greenColor];self.button.layer.borderWidth = 2;self.button.layer.borderColor = [UIColor redColor].CGCself.aView.layer.cornerRadius = 3;self.aView.backgroundColor = [UIColor greenColor];self.aView.layer.borderWidth = 2;self.aView.layer.borderColor = [UIColor redColor].CGC......
上面的代码是为了实现这样的效果而写的代码。
很多几乎是一毛一样的代码,充斥着整个APP。自己花在这些样式调整上的时间也非常多。为了实现一个样式效果,需要配置各种各样的属性。而且很多界面中这些样式都是一样的。于是又是无数次的重复上面的工作。oy my god!时间啊,就这样流走了。做为一个懒人,就会发问有没有一种可以少写点代码的方式呢?你可以写一个子类嘛,但是会有类污染的问题,单纯为了一个公有样式,就创建个子类有点大材小用。那写一批样式渲染的函数呗,恩这个注意不错,但是细想一下工作量也不小,而且不通用。于是,花了几天的时间我写了StyleSheet这个库。为了的就是来简化UI样式的编码。
通过上述描述我们可以发现,原始的写UI样式的问题:
繁琐的代码,大量重复性的工作
样式无法共享,每一个View都需要重新进行样式赋值。
而StyleSheet的设计目标就是:
样式配置轻便化,能够使用更加少的代码来描述View的样式
样式在View之间的共享.不止是相同类的实例之间的共享,甚至是跨类的共享。
So,先看看上述代码使用StyleSheet之后的效果:
123456789101112self.label.style = DZLabelStyleMake(
style.backgroundColor = [UIColor greenColor];
style.cornerRedius = 3;
style.borderColor = [UIColor redColor];
style.borderWidth = 2;
style.textStyle.textColor = [UIColor darkTextColor];
style.textStyle.font = [UIFont systemFontOfSize:13];);self.label2.style = self.label.self.aView.style = self.label.[self.button.style copyAttributesWithStyle:self.label.style];
设计与使用基础抽象模型很简单,就是要让界面上关于展示的属性可以被组合使用。而我们所谓的样式,其实也就是各种基础属性组合出来的结果。基于这个模型,在设计StyleSheet的时候故意淡化了被渲染的View的类型的概念,任何一种类型的Style可以对任何类型的View进行渲染,但是必须是这种类型的View支持Style所指称的属性。比如你可以使用真对Button设计的DZButtonStateStyle来渲染一个UILabel,但由于UILabel不支持DZButtonStateStyle中的渲染属性,所以渲染结果是无效的。
但是当使用DZButtonStyle(继承自DZViewStyle)来渲染UILabel的时候,会使用DZButtonStyle中其父类的某些渲染属性,来渲染UILabel的父类UIView所支持的那些属性。
使用直接使用Style对View进行渲染:1234567891011DZLabelStyle* style =DZLabelStyleMake(
style.backgroundColor = [UIColor greenColor];
style.cornerRedius = 3;
style.borderColor = [UIColor redColor];
style.borderWidth = 2;
style.textStyle.textColor = [UIColor darkTextColor];
style.textStyle.font = [UIFont systemFontOfSize:13];);[style decorateView:self.label];
直接渲染的好处是,不用再次生成Style对象,更加方便样式在多个View之间渲染。
赋值渲染对UIKit中常用的一些组件进行了扩张为他们增利了style属性,直接进行style属性的赋值,会出发一次渲染操作。当第一次调用style属性的时候,会自动生成一个zeroStyle并赋值。self.label.style =或者self.label.style = DZLabelStyleMake(
style.backgroundColor = [UIColor greenColor];
style.cornerRedius = 3;
tyle.borderColor = [UIColor redColor];
style.borderWidth = 2;
style.textStyle.textColor = [UIColor darkTextColor];
style.textStyle.font = [UIFont systemFontOfSize:13];);当进行赋值渲染的时候,会将Style的Copy后的实例与当前View绑定,当更改Style的属性的时候,对应View的样式会立刻改变。
通用样式的共享使用原有的配置,进行通用样式的共享是个非常困难的事情,基本上都是体力活,靠人力来维护。我们的代码中会掺杂大量的用于配置样式的代码,而且是独立且散在。现在你可以通过StyleSheet解决:定义共享的样式:
123456789//在头文件中使用 xxx.h 声明一个公有样式EXTERN_SHARE_LABEL_STYLE(Content)//在实现文件中使用 xxx.m ,实现一个公有样式IMP_SHARE_LABEL_STYLE(Content,
style.backgroundColor = [UIColor clearColor];
style.cornerRedius = 2;
style.textStyle.textColor = [UIColor redColor];)
(1)使用共享样式,方式一self.label.style =
DZStyleContent();
(2)使用共享样式,方式二(推荐)很多时候, 如果不需要进一步更改样式,可以不采复制赋值的方式来进行渲染,可以直接使用:[DZStyleContent() decorateView:self.label];只进行渲染,而不进行复制。好了,现在可以尝试着换这种方式来写UI样式了。
iOS架构设计解耦的尝试之VC逻辑AOP切割
该系列文章是2016年折腾的一个总结,对于这一年中思考和解决的一些问题做一些梳理和总结
上一篇文章中提到要说一下全局UI堆栈是怎么维护的。要写的时候发现,这个东西背后还有一个更有意思的东西:使用AOP对VC的业务逻辑进行切割。在DZURLRoute中所使用到的全局UI堆栈就是基于该思想构建出来的。这一部分的成果在库中总结成了Code(Talk is cheap. Show me the code)。而我们在中提到了通过MVVM来进行解耦,而这篇文章我们又通过另外一种方式AOP来尝试进行解耦。感觉这一年在疯狂的解耦:)。
AOP先从AOP说起,其实在之前的文章中或者开发的库中已经涉及到过很多次。比如对于Instance进行逻辑注入的库,基于MRLogicInjection的应用方案用于相应区域扩展的、用于放重复点击的、用于界面上红点提醒的。这一年对于AOP也算有了一个比较深入的实践。而这次要说的VC逻辑切割,其实也算是AOP的一个实践。说句题外话,Objective-C是门神奇的语言,他提供的动态性,让我们可以对其进行很多有意思的改造,把OC改造成一个更好用的工具。而对其进行AOP改造就是我发现的非常有意思的一个事情。
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
上面这段文字摘自。对于AOP做了一个非常好的解释,点击链接可以进去看看具体的内容。关于AOP只简短的说一下我自己的理解,以作补充。
OC本来是个OOP的语言,我们通过封装、继承、多态来组织类之间的静态结构,然后去实现我们的业务逻辑。只不过有些时候,严格遵循OOP的思想去设计继承结构,会产生非常深的继承关系。这势必要增加整个系统的理解复杂度。而这并不是我们希望的。另外一点,我们讲究设计的时候能够满足开闭原则,对变化是开放的,对于修改是封闭的。然而当我们的类继承结构比较复杂的时候,就很难做到这一点。我们先来看一个比较Common的例子:
123456└── Object
└── biont
├── Animal
├── cat
└── dog
└── plant
我们现在要构建一个用于描述生物的系统(精简版),第一版我们做出了类似于上面的类结构。我们在Animal类中写了cat和dog的公有行为,在cat和dog中各自描述了他们独有的行为。这个时候突然发现我们多了一个sparrow物种。但是呢我们在Animal中描述的是动物都有四条腿,而sparrow只有两条腿,于是原有的类结构就不能满足现在的需求了,就得改啊。
123456789└── Object
└── biont
├── Animal
├── flying
└── sparrow
└── reptile
├── cat
└── dog
└── plant
为了能够引入sparrow我们修改了Animal类,将四条腿的描述放到了reptile类中,并修改了Cat和Dog的继承关系。修改的变动量还是不小的。引入了两个新类,并对原有三个进行比较大的改动。
而如果用AOP的话我们会怎么处理这个事情呢?切割和组合。
我们会将四条腿独立出来,爬行切割出来,两条腿切割出来,会飞切割出来。。。然后dog就是四条腿爬行的动物。sparrow就是两条腿会飞的动物。没有了层次深的类继承结构。更多的是组合,而一个具体的类更像是一个容器,用来容纳不同的职责。当把这些不同的职责组合在一起的时候就得到了我们需要的类。AOP则提供一整套的瑞士军刀,指导你如何进行切割,并如何进行组合。这也是我认为AOP的最大魅力。
对VC进行逻辑切割和组合类似于上面我们提到的例子,我们在写ViewController的业务逻辑的时候,也有可能造成非常深的继承结构。而我们其实发现在众多的业务逻辑中,有些东西是可以单独抽离出来的。比如:
我们会在页面第一次viewWillAppear的时候刷新一次数据,这个在TableViewController会这样,在CollectionViewController的时候也会这样。
我们会在生命周期打Log,对用户的使用路径进行上报。
有些事情我们通过类继承来做了,比如打Log,找一个根类,在里面把打Log的逻辑写了。但是当发现在继承树的末端有一个ViewController不需要打Log的时候就尴尬了。得大费周折的去改类结构,来适配这个需求。但是,如果这些业务逻辑像是积木一样,需要的时候拿过来用,不需要的时候不管他,多好。这样需要打Log的时候,拿过来一个打Log的积木堆进去,不需要的时候把打Log的积木拿走。
职责编程界面(API)而这就是AOP,面向切面编程。我们在ViewController上所选择进行逻辑编制的切面就是UIViewController的各种展示回调:
1234- (void)viewWillAppear:(BOOL)animated- (void)viewDidAppear:(BOOL)animated- (void)viewWillDisappear:(BOOL)animated- (void)viewDidDisappear:(BOOL)animated
选择这四个函数做为切面是因为在实际的编程过程中发现我们绝大多数的业务逻辑的起点都在这里面,还有一些在viewDidLoad里面。不过按照语义来讲,viewDidLoad中应该是更多的对于VC中属性变量的初始化工作,而不是业务逻辑的处理。在DZViewControllerLifeCircleAction的设计的时候,我们更多的是关注到ViewController的展示周期内会做的一些事情。就像:
在页面第一次显示的时候进行数据加载
每一次展示的时候增加xxx的观察者通知,在不展示的时候移除
在页面第一次展示的时候执行特殊的动作
构建特殊的页面逻辑
。。。。。。
对应的我们在抽象出来的职责基类DZViewControllerLifeCircleBaseAction中提供了具体的编程接口:
123456789101112131415161718192021222324/** When a instance of UIViewController&s view will appear , it will call this method. And post the instance of UIViewController @param vc the instance of UIViewController that will appear @param animated
appearing is need an animation , this will be YES , otherwise NO. */- (void) hostController:(UIViewController*)vc viewWillAppear:(BOOL)/** When a instance of UIViewController&s view did appeared. It will call this method, and post the instance of UIViewController which you can modify it. @param vc the instance of UIViewController that did appeared @param animated appearing is need an animation , this will be YES, otherwise NO. */- (void) hostController:(UIViewController*)vc viewDidAppear:(BOOL)/** When a instance of UIViewController will disappear, it will call this method, and post the instance of UIViewController which you can modify it. @param vc the instance of UIViewController that will disappear @param animated dispaaring is need an animation , this will be YES, otherwise NO. */- (void) hostController:(UIViewController*)vc viewWillDisappear:(BOOL)/** When a UIViewController did disappear, it will call this method ,and post the instance of UIViewController which you can modify it. @param vc the instance of UIViewControll that did disppeared. @param animated disappearing is need an animation, this will be YES, otherwise NO. */- (void) hostController:(UIViewController*)vc viewDidDisappear:(BOOL)
一个独立的职责可以继承基类创建一个子类,重载上述编程接口,进行逻辑编制。在展示周期内去写自己都有的逻辑。这里建议将这些逻辑尽可能的切割成粒度较小的逻辑单元。在自己的时间中发现,相对较小的粒度可以获得更高的业务逻辑隔离和解耦效果。
在后续版本中也会考虑增加其他函数切入点的支持。
职责注入与删除编程界面而所有的这些职责,可以分成两类:
通用职责,表现为所有的UIViewController都会有的职责,比如日志Log。
专用职责,比如一个UITableViewController,需要在展示时才注册xxx通知。
因而,在ViewController中设计职责容器的时候,也对应的设计了两个职责容器:
DZViewControllerGlobalActions()用来承载通用职责可以通过接口:
1234567891011121314151617/** This function will remove the target instance from the global cache . Global action will be call when every UIViewController appear. if you want put some logic into every instance of UIViewController, you can user it.
@param action the action that will be rmeove from global cache. */FOUNDATION_EXTERN void DZVCRemoveGlobalAction(DZViewControllerLifeCircleBaseAction* action);/** This function will add an instance of DZViewControllerLifeCircleBaseAction into the global cache. Global action will be call when every UIViewController appear. if you want put some logic into every instance of UIViewController, you can user it.
@param action the action that will be insert into global cache */FOUNDATION_EXTERN void DZVCRegisterGlobalAction(DZViewControllerLifeCircleBaseAction* action);
来增加或者删除职责。
专用职责容器可以通过下述接口进行添加或者删除职责:
12345678910111213141516@interface UIViewController (appearSwizzedBlock)/** add an instance of DZViewControllerLifeCircleBaseAction to the instance of UIViewController or it&s subclass. @param action the action that will be inserted in to the cache of UIViewController&s instance. */- (DZViewControllerLifeCircleBaseAction* )registerLifeCircleAction:(DZViewControllerLifeCircleBaseAction *)/** remove an instance of DZViewControllerLifeCircleBaseAction from the instance of UIViewController or it&s subclass. @param action the action that will be removed from cache. */- (void) removeLifeCircleAction:(DZViewControllerLifeCircleBaseAction *)@end
使用举例LogAction先拿我们刚才一直再说的Log的例子来说,我们可以写一个专门打Log的Action:
12345678910111213141516171819202122@interface DZViewControllerLogLifeCircleAction : DZViewControllerLifeCircleBaseAction@end@implementation DZViewControllerLogLifeCircleAction+ (void) load{
DZVCRegisterGlobalAction([DZViewControllerLogLifeCircleAction new]);}- (void) hostController:(UIViewController *)vc viewDidDisappear:(BOOL)animated{
[super hostController:vc viewDidDisappear:animated];
[TalkingData trackPageBegin:YHTrackViewControllerPageName(vc)];
}- (void) hostController:(UIViewController *)vc viewDidAppear:(BOOL)animated{
[super hostController:vc viewDidAppear:animated];
[TalkingData trackPageEnd:YHTrackViewControllerPageName(vc)];}@end
在该类Load的时候将该Action注册到通用职责容器中,这样所有的ViewController都能够打Log了。如果某一个ViewController不需要打Log可以直接选择屏蔽掉该Action。
UIStack好了,这个才是最终要说的正题。扯了半天,其实就是为了说这个全局的展示的UIStack是怎么维护的。首先要说明的是,此处的UIStack所维护的内容的是正在展示的ViewController的堆栈关系,而不是keywindow上ViewController的叠加关系。
当一个ViewController展示的时候他就入栈,当一个ViewController不在展示的时候就出栈。
因而在该UIStack中的内容是当前整个APP正在展示的ViewController的堆栈。而他的实现原理就是继承DZViewControllerLifeCircleBaseAction并在viewAppear的时候入栈,在viewDisAppear的时候出栈。
1234567891011121314151617181920212223242526272829303132@implementation DZUIStackLifeCircleAction+ (void) load{
DZUIShareStack = [DZUIStackLifeCircleAction new];
DZVCRegisterGlobalAction(DZUIShareStack);}- (void) hostController:(UIViewController *)vc viewDidAppear:(BOOL)animated{
[super hostController:vc viewDidAppear:animated];
if (vc) {
[_uiStack addPointer:(void*)vc];
}}//出栈- (void) hostController:(UIViewController *)vc viewDidDisappear:(BOOL)animated{
[super hostController:vc viewDidDisappear:animated];
NSArray* allObjects = [_uiStack allObjects];
for (int i = (int)allObjects.count-1; i &= 0; i--) {
id object = allObjects[i];
if (vc == object) {
[_uiStack replacePointerAtIndex:i withPointer:NULL];
[_uiStack compact];}....@end
同样也注册为一个通用职责。上面这两个例子下来,就已经在ViewController中加入了两个通用职责了。而这些职责之间都是隔离的,是代码隔离的那种!!!
执行一次的Action, 专用职责的例子在ViewController编程的时候,我们经常会写一些类似于_firstAppear这样的BOOL类型的变量,来标记这个VC是第一次被展示,然后做一些特定的动作。其实这个就是在VC所有的展示周期内只做一次的操作,真对这个需求我们可以写一个这样的Action:
123456789101112131415161718192021222324252627282930313233343536373839/** The action block to handle ViewController appearing firstly. @param vc The UIViewController tha appear @param animated It will aminated paramter from the origin SEL paramter. */typedef void (^DZViewControllerOnceActionWhenAppear)(UIViewController* vc, BOOL animated);/** when a ViewController appear firstly , it will do something . This class is design for this situation */@interface DZVCOnceLifeCircleAction : DZViewControllerLifeCircleBaseAction/** The action block to handle ViewController appearing firstly. */@property (nonatomic, strong) DZViewControllerOnceActionWhenAppear actionB /** Factory method to reduce an instance of DZViewControllerOnceActionWhenAppear @param block The handler to cover UIViewController appearing firstly @return an instance of DZViewControllerOnceActionWhenAppear */+ (instancetype) actionWithOnceBlock:(DZViewControllerOnceActionWhenAppear) /** a once action is an class that handle some logic once when one instance of UIViewController appear. It need a block to exe the function. @param
the logic function to exe @return an instance of DZVCOnceLifeCircleAction */- (instancetype) initWithBlock:(DZViewControllerOnceActionWhenAppear)@end
该Action默认包含在DZViewControllerLifeAction库中了。当有VC需要这种指责的时候直接注入就行了,例如:
123[tableVC registerLifeCircleAction:[DZVCOnceLifeCircleAction actionWithOnceBlock:^(UIViewController *vc, BOOL animated) {
[[DZContactMonitor userMonitor] asyncLoadSystemContacts];
其他上面我们举了通用职责和专用职责的例子,都还算是比较简单的例子。其实,就是希望把职责拆解成粒度更小的单元。然后组合使用。而在我的APP中还有更加复杂的关于应用ViewController的AOP的例子。我把一个整个逻辑模块,比如弹幕功能做为了一个逻辑单元,基于DZViewControllerLifeAction来写,当某个界面需要弹幕的时候,就当做专用职责进行逻辑注入。而这样一来,发现你完全可以复用一整块原先可能完全不能复用的逻辑。在解耦和复用这条路上,这种方式算是目前我做的比较疯狂的事情了。非常有意思。
iOS架构设计解耦的尝试之模块间通信
改系列文章是2016年折腾的一个总结,对于这一年中思考和解决的一些问题做一些梳理和总结。
前两篇文章主要是说了业务逻辑接口还有模块化的事情。随着系统内部逻辑单元(可能是模块,也可能是为了解耦拆解出来用来承载职责的类等常见的实现)的增多。势必会引入另外的一个问题,就是逻辑单元之间的交互增加和逻辑单元之间通信成本的提高。在,一文中我们在将整个业务逻辑层从MCV向MVVM演变的时候也遇到了这个问题,当时是本着作孽自己造轮子的心态,通过构建EventBus组件来解决。同样,针对于逻辑单元之间通信成本增加的问题,也需要寻找一个合适的解决方案。
问题场景描述在一文中,描述一种进行系统模块拆解和管理的思路。将职能不同的业务,拆解成了独立的模块。而且每个模块通过代码隔离,做到了互相之间影响的最小化。但是他们之间怎么交互呢?换种说法就是业务模块应该暴漏什么样的外部接口,以方便其他业务模块来调用?
在终端上业务逻辑主要是围绕着界面展开的。在iOS中的表现就是各式各样的ViewController。而在以往的编码实践中,所谓业务模块间的交互就是VC之间的相互调用。我们常见的是这样的:
1234UIViewController* aVC = [UIViewController new];//配置aVC需要参数....[self.navigationController pushViewController:aVC animated:YES];
或者这样的
1234UIViewController* aVC = [UIViewController new];//配置aVC需要的参数...[self presentViewController:aVC animated:YES];
我们通过对于指定业务模块的代码级引用来调用对方的服务。而这种依赖属于接口依赖,稍微符合接口隔离的设计。但不是一个符合迪米特法则(最小知识法则)的设计。调用方由于对于服务提供方有接口依赖,因而就造成了以下的潜在问题:
链接过程中必须引入服务提供方所在的模块,无法提供打包过程中动态下掉某些业务模块的需求。
服务提供方类接口变动,将直接调用方。
调用方知道了服务提供方的太多有冗余信息。
这些问题,我们通过代码级别的模块隔离基本上解决了。正如前文所说,你要真正把模块之间的交互影响降低的最小,最好的解决方案就是建造『信息孤岛』,而信息孤岛就会造成模块之间『鸡犬之声相闻老死不相往来』,这也非我等所愿。他们之间还要保持一个最小的通信,来完成服务的调用。这样我们在代码隔离之后,就要解决两个问题:
模块发现,就是说我一个模块怎么被其他模块发现,或者说我一个模块做些什么事情,外部模块使用的时候,才能知道有我的存在。
服务调用,模块做为服务提供方,需要能够真实的提供所标称的服务。
机制与策略分离解决这两个问题,我们首先要说一个观点就是机制与策略分离。我们希望设计的是一整套能够满足上述要求的协议,其次才是实现,最后才是在我们的APP中的具体应用。这也是我这一年来的一个非常重要的总结。并且在逐渐开源出来的一些库中也体现着这个设计。具体说一下,所谓机制即是抽象出来的规则,比如:
1f(x)=x^2 x属于R
所谓策略即是在具体场景中的应用,比如当x=2的时候:
1f(2)=4 x=2
很明显刚才说的三个层次中协议与实现做成了一个机制与策略分离。而实现与应用又组成了另外的一个机制与策略分离。我比较喜欢这种嵌套的解决方案,你解决了一个通用性的问题,然后嵌套使用,就能够解决更多的问题,只需要付出少量的思维成本。
协议是问题解决方案的描述,或者说要解决这个问题大家都应该遵守的规则。就像网络的tcp协议,你要基于tcp通信你就需要遵循这个协议。
实现是针对于某类环境的实施方案,比如linux上对于TCP的实现还有windows上对于TCP的实现。虽然都是一个协议,但是大家的实现方式不一样,有基于c写的,有基于c++写的.
而应用是真对具体的问题域提出的实施方案,比如我们做了一个哟呵校园的聊天软件使用了tcp进行socket通信。
解决方案设计模块间通信协议URL那我们首先要做的就是针对模块间通信问题构思一个协议。一个为了解决模块间通信问题大家都遵守的规则。其实关于这个问题在今年下半年,业界飘来一股router风。大家都在模块化之后的通信问题上作出了不同的尝试。而且甚至为此进行了一场博客间的辩论。仔细分析一下,就能发现大家虽各有意见,但是基本上都同意使用URL的方案来解决这个问题。所争执的不同在于实现方案上的差异。而此处的URL正是我们所谓的协议部分。
(或称统一资源定位器/定位地址、URL地址等[1],英语:Uniform / Universal Resource Locator,常缩写为URL),有时也被俗称为网页地址(网址)。如同在网络上的门牌,是因特网上标准的资源的地址(Address)。它最初是由蒂姆·伯纳斯-李发明用来作为万维网的地址。现在它已经被万维网联盟编制为因特网标准RFC 1738。
为何如此?
回到最开始我们描述的问题中第一点: 模块发现。其实也就是模块这种资源的定位问题,这个和URL设计的初衷是不谋而合的。URL整套的设计思路就是在整体的互联网中解决信息孤岛,让各个信息孤岛之间能够进行资源发现和资源调用而设计。而我们目前所要处理的模块间通信问题,其实是这个宏大问题域的一个子集。因而选用URL协议,是一个非常顺理成章的事情。另外一点,这里真心没必要重新造一个类似于URL的协议的轮子出来的。URL协议中能够非常完美的解决这个问题。
在iOS中基于URL协议的模块间通信实现其实业界这个route的实现已经有千千万万了,为啥我还要再写一个?一个是因为原有的一些库的模型和我所想象的不吻合,一个是因为我实现不想削足适履去适配他们的模型。所以本着造轮子的作孽心态还是自己写。其实也不是非常复杂。
URL协议解决了模块发现的问题,但是是个静态的txt,并不具备exe的能力。我们可以通过定义一个类似于:
1yoho://innerfuction/viewcontroller/showuserinfo?uid=22&xx=33
来让一个模块对外宣称支持显示用户详细信息的服务。但是我们要如何使用这个服务呢?很多Route的实现是通过URL直接将对应的ViewController返回,然后由调用方再去调用接口配置ViewController,而后调用方进行push或者present。而我认为这种方式不是很合理,做为调用发应该尽可能少的知道服务提供方的信息。服务怎么被弹起,应该是由服务方自己决定的,而不是调用方。最好只知道一个URL还有支持什么样的服务就好了,最好能把交互接口精简、精简、再精简。
而思考了一下很多route库之所以没能够做到模块只对外暴漏URL就可以的一个很重要的原因,就是在ViewController被弹出的时候,iOS需要一个调起的ViewController
1234UIViewController* aVC = [UIViewController new];//配置aVC需要参数....[self.navigationController pushViewController:aVC animated:YES];
就是必须知道当前界面是在哪里,你才能去push下一个界面。只有知道这个self.navigationController上下文信息才行。所以很多事情只能在调用方来处理。我觉得这种方式制约着被调用方需要拿到服务提供方的一个实例才行。每一个问题背后都有一个解决方案。于是我在自己造的轮子中使用了全局UI堆栈的方式解决了这个问题。
通过构造了被调用的上下文信息类DZURLRequestContext,用于携带调用方的上下文信息来解决这个问题。上下文信息中携带了当前UI的堆栈信息,能够方便定位用哪个VC做为起点,来弹出下一个页面。当然使用这个context还有另外一个原因就是因为URL中能够传输的参数类型是受限的,只能传输NSString类型,对于一个实例则不能传输。为了传输实例参数也需要这样的一个context环境。
这样在调用一个页面的服务的时候,就能够做到如下所示极致简单:
1[[DZURLRoute defaultRoute] routeURL:DZURLRouteQueryLink(kYHURLSacnQRCode, @{})];
当然该库首先是解决了通过URL调用的问题,而后才是上面说的这些优化问题。同时,也针对很多不同的应用场景提供了解决方案。更加具体的信息可以参考。
而具体的应用问题,就是APP内部自己的事情了,不展开叙述。基本上都是调用库接口的事情,没有太多的表述价值。
Others做个预告吧,也算是对自己的一个敦促,下一篇说一下在DZURLRoute中关于UI堆栈的问题是怎么解决的。
iOS 多模块管理为了增强模块复用性和屏蔽其他模块的影响等因素,往往会采取将一个iOS项目拆解成多个子工程的方式,每个模块一个子工程。然后用一个大的工程胶合起来。这样子模块在其他的项目中还可以复用。有些团队为了团队之间代码维护方便,也会采取这样的策略。甚至,把每个模块做为一个单独的repo来管理。这样单个项目中就会有多个工程。就拿最近业务做得项目来说已经拆成了60多个工程。而这60多个冲程都是不同

我要回帖

更多关于 我的世界内存溢出 的文章

 

随机推荐