VirtualView 的重构之路(一)node
VirtualView 是 Tangram 2.0 库中的一个重要组成部分:若是说 Tangram 1.0 解决了 UI 的动态化布局及回收重用问题,那么 Tangram 2.0 所包含的 VirtualView 更进一步的解决了动态化下发新组件的问题。git
用一张图来解释 VirtualView 的主要功能:提供了用 XML 去书写 UI 组件的方案,而后动态化下发编译好的二进制文件,最后再利用客户端内置的 SDK 来解析展现这些 UI 组件。github
有关 Tangram 2.0 更多的介绍能够参考《猫客页面内组件的动态化方案-Tangram 2.0》,如下是 Tangram 2.0 的主要开源库:缓存
首先要给你们介绍下咱们为何要使用二进制文件,主要是考虑如下的几点:安全
而后就是介绍下二进制文件的格式:bash
有关文件格式更详细的介绍能够参照《VirtualView Android实现详解(一)》,本文的重点仍是介绍重构的思路,以及最新版 VirtualView-iOS 里模板加载模块的详细实现。数据结构
能够从上文的模板文件格式里看到,一个二进制模板主要包含了如下几块内容:多线程
旧的模板加载模块工做模式大体以下图所示:异步
能够看到整个模板加载功能分红了两个模块:模板加载模块和建立组件树模块。函数
模板加载模块加载了模板二进制文件,可是只解析了其中的基础信息和字符串信息并存储下来,组件树结构信息仍使用二进制原样缓存下来。
建立组件树模块在建立新的组件时向模板加载模块拿须要的组件树结构信息,进行解析后建立对应的组件树,并进行属性的设置,设置字串属性期间还会向模板加载模块拿须要的字符串信息。
整体来讲设计是能够知足需求的,可是设计上仍然是存在一些缺陷或者不灵活之处:
首先模板加载模块一个模块负责了两个功能:解析模板信息;管理和存储已加载的模板列表。并且还附带了字串映射表等一些功能,没有作到功能单一。这样会致使模板的解析和管理功能相互耦合,若是不进行剥离之后两块代码就会耦合愈来愈严重。因此咱们须要把模板加载模块分离成模板解析模块、模板管理模块两块。
建立组件树模块会向模板加载模块直接拿本身须要的数据信息,这样随着代码的堆积,两个模块会日渐耦合加剧,往后任何一方的修改都不可避免的要修改另外一方的代码。面对这样的状况,建议抽象出双方须要通讯的数据的接口,这样只要双方都实现了定义好的通讯接口,内部实现怎么修改都不会影响另外一方。
解析工做应该放到一个独立的模块里处理。目前的模板是二进制格式的,可是不排除之后会出现其它格式的模板文件的可能性。若是新增一种模板文件格式,就要从新写两个配套的模块,这是十分不科学的。
另外一方面的缘由就是这种分段读取的模式,致使每次建立新组件的时候都须要重复解析一次组件树结构信息的二进制数据,这也是耗费性能的一个不合理点。
由于以上第1点和第3点,致使解析模板的代码要么和别的功能耦合,要么分散到了别处,最终的结果就是没办法对解析模板的代码进行异步调用。因此为了异步化加载模板的目标,须要把全部解析模板的代码集中到一个模块中,方便进行异步调用。这是一个由目标肯定代码结构设计的典型例子。
基于上面咱们要解决的4个方向,首先咱们须要对原来的模块进行拆分和组合:
这样咱们就会获得咱们须要完成的三个独立的模块。
而后为了模块间的通信,咱们须要定义出来一个中间数据接口:
因此最终总的设计结构大致就是这样。
对应 VirtualView-iOS 库里的 VVTemplateLoader 类。这里我把它设计成了一个基类,基类中定义方法进行加载,最终能够吐出模板解析后的中间数据。这样的好处就是针对不一样类型的模板,咱们基于这个基类实现不一样的解析逻辑,就能够供其它模块无缝切换使用了。目前来讲实现了一个二进制模板的读取类,那就是 VVTemplateBinaryLoader。
解析基础数据、字串数据及组件树信息的解析代码所有被集中到这个模块里完成,保证类似功能的高度内聚,也使得模块的功能独立单一。
保证加载解析模板的功能是个纯函数式的过程,没有任何反作用。这要归功于把模板管理和存储的功能都移动到了模板管理模块。没有反作用使得解析逻辑能够被异步调用,有关线程的管理就也能够放在管理模块里进行了。
加载完的模板都由模板管理模块进行统一存储管理,这个类就是 VVTemplateManager。这个类里还有作的一件主要的事情就是异步加载模板的线程管理工做。你们知道异步和多线程常常遇到的一个问题就是数据同步和操做互斥等问题,问了处理这个问题,VVTemplateManager 采用了最简单的方案,就是将异步加载完模板获得的中间数据,所有放在主线程统一加入到缓存字典中。例如存储数据的这一段代码:
void (^action)(void) = ^{
[self.versions setObject:version forKey:type];
[self.creaters setObject:creater forKey:type];
};
if ([NSThread isMainThread]) {
action();
} else {
dispatch_sync(dispatch_get_main_queue(), action);
}
复制代码
这是一段很常见的强制进行主线程调用的代码。为何这里要作一次判断呢?那是由于在主线程直接用 dispatch_sync 去再次调用主线程,会进入线程死锁。
另一个重要的逻辑就是将异步队列中还没有加载完成的模板在必要时进行提早加载。由于咱们把模板放到异步线程队列里去加载,有时候并不能肯定在使用到这个模板的时候它就必定被加载完了。因此代码里有这么一段逻辑:
if ([self.loadedTypes containsObject:type] == NO && _operationQueue) {
// Try to find unloaded template in queue and load it immediately.
BOOL isFirst = YES;
for (NSOperation *operation in _operationQueue.operations.reverseObjectEnumerator) {
if ([operation.name isEqualToString:type]) {
if (isFirst) {
[operation main];
isFirst = NO;
}
[operation cancel];
}
}
}
复制代码
若是已加载的模板里没有包含咱们要使用的 type
,那么就尝试从当前的异步读取队列里找一找有没有对应的 type
,对队列里最后一个知足条件的任务进行当即调用,保证对应模板被当即加载,而后把异步队列里的对应任务都取消掉。
因此说使用 VirtualView-iOS 时,能够放心的把全部的模板所有放到异步线程去加载,而不用担忧后续的调用会出问题。
组件树的重要数据就两个,组件树种每个节点上组件的 class 以及这个组件的属性列表。组件自己是树状结构的,因此中间数据固然也是树状结构会最匹配。因此设计出来的最终中间数据结构就是 VVNodeCreater 和 VVPropertySetter:
@interface VVNodeCreater : NSObject
@property (nonatomic, copy, nullable) NSString *nodeClassName;
@property (nonatomic, strong, nonnull) NSMutableArray<VVPropertySetter *> *propertySetters;
@property (nonatomic, strong, nonnull) NSMutableArray<VVNodeCreater *> *subCreaters;
@end
复制代码
@interface VVPropertySetter : NSObject
@property (nonatomic, assign, readonly) int key;
@property (nonatomic, strong, readonly, nullable) NSString *name;
@end
复制代码
这里最先设计的时候打算他们只用来存储结构和数据,可是后来发现他们本身自己用自递归的方式建立组件树会无比的方便,因此他们同时负责了缓存中间数据及一键建立组件树的功能!
也是由于建立组件树这个模块的功能单独拎出来太过于轻量化了,因此最终的实现中就把它和中间数据的模型直接融合了。融合了以后他们两个类每一个类的代码也才五六十行,因此说一开始的设计也的确有点过分设计了。
VVPropertySetter 也采用了设计成基类的方式,这样不一样类型的属性值就能够经过重载分别实现 VVPropertyIntSetter、VVPropertyFloatSetter 和 VVPropertyStringSetter 来实现。这样作一方面可使得逻辑能够不经过一大堆 if...else...
或者 switch...case...
来写得很难看,使得 VVNodeCreater 在调用时使用统一的入口方便调用,并且另外一方面也是更加方便后续字符串表达式等功能的扩展。关于字符串表达式的实现原理会在后续的文章里继续说明。
至此,VirtualView-iOS 模板加载功能的设计及实现细节也介绍的差很少了,但愿对你们了解 VirtialView 及重构的思路有必定帮助。
在接下来,我还会陆续介绍其余模块设计及重构的思路,敬请期待。