ssiOS应用架构谈 本地持久化方案及动态部署

本文转载至 http://casatwy.com/iosying-yong-jia-gou-tan-ben-di-chi-jiu-hua-fang-an-ji-dong-tai-bu-shu.html

 

Date  Mon 12 October 2015 Tags iOS / architect / thoughts

iOS应用架构谈 开篇 
iOS应用架构谈 view层的组织和调用方案 
iOS应用架构谈 网络层设计方案 
iOS应用架构谈 本地持久化方案及动态部署javascript




前言

 

嗯,大家要的大招。跟着这篇文章一块儿也发布了CTPersistanceCTJSBridge这两个库,但愿你们在实际使用的时候若是遇到问题,就给我提issue或者PR或者评论区。每个issue和PR以及评论我都会回复的。html

 

持久化方案无论是服务端仍是客户端,都是一个很是值得讨论的话题。尤为是在服务端,持久化方案的优劣每每都会在必定程度上影响到产品的性能。然而在客户端,只有为数很少的业务需求会涉及持久化方案,并且在大多数状况下,持久化方案对性能的要求并非特别苛刻。因此我在移动端这边作持久化方案设计的时候,考虑更多的是方案的可维护和可拓展,而后在此基础上才是性能调优。这篇文章中,性能调优不会单独开一节来说,而会穿插在各个小节中,你们有心的话能够重点看一下。java

持久化方案对整个App架构的影响和网络层方案对整个架构的影响相似,通常都是致使整个项目耦合度高的罪魁祸首。而我也是一如既往的去Model化的实践者,在持久层去Model化的过程当中,我引入了Virtual Record的设计,这个在文中也会详细描述。ios

这篇文章主要讲如下几点:git

 

  1. 根据需求决定持久化方案
  2. 持久层与业务层之间的隔离
  3. 持久层与业务层的交互方式
  4. 数据迁移方案
  5. 数据同步方案

 

另外,针对数据库存储这一块,我写了一个CTPersistance,这个库目前可以完成大部分的持久层需求,同时也是个人Virtual Record这种设计思路的一个样例。这个库能够直接被cocoapods引入,但愿你们使用的时候,可以多给我提issue。这里是CTPersistance Class Referencegithub




根据需求决定持久化方案



在有须要持久化需求的时候,咱们有很是多的方案可供选择:NSUserDefault、KeyChain、File,以及基于数据库的无数子方案。所以,当有须要持久化的需求的时候,咱们首先考虑的是应该采用什么手段去进行持久化。web



NSUserDefault数据库

 

通常来讲,小规模数据,弱业务相关数据,均可以放到NSUserDefault里面,内容比较多的数据,强业务相关的数据就不太适合NSUserDefault了。另外我想吐槽的是,天猫这个App实际上是没有一个通过设计的数据持久层的。而后天猫里面的持久化方案就很混乱,我就见到过有些业务线会把大部分业务数据都塞到NSUserDefault里面去,当时看代码的时候我特么就直接跪了。。。问起来为何这么作?结果说由于写起来方便~你妹。。。跨域



keychain数组

 

Keychain是苹果提供的带有可逆加密的存储机制,广泛用在各类存密码的需求上。另外,因为App卸载只要系统不重装,Keychain中的数据依旧可以获得保留,以及可被iCloud同步的特性,你们都会在这里存储用户惟一标识串。因此有须要加密、须要存iCloud的敏感小数据,通常都会放在Keychain。



文件存储

 

文件存储包括了Plist、archive、Stream等方式,通常结构化的数据或者须要方便查询的数据,都会以Plist的方式去持久化。Archive方式适合存储平时不太常用但很大量的数据,或者读取以后但愿直接对象化的数据,由于Archive会将对象及其对象关系序列化,以致于读取数据的时候须要Decode很花时间,Decode的过程能够是解压,也能够是对象化,这个能够根据具体<NSCoding>中的实现来决定。Stream就是通常的文件存储了,通常用来存存图片啊啥的,适用于比较常用,然而数据量又不算很是大的那种。



数据库存储

 

数据库存储的话,花样就比较多了。苹果自带了一个Core Data,固然业界也有无数替代方案可选,不过真正用在iOS领域的除了Core Data外,就是FMDB比较多了。数据库方案主要是为了便于增删改查,当数据有状态类别的时候最好仍是采用数据库方案比较好,并且尤为是当这些状态类别都是强业务相关的时候,就更加要采用数据库方案了。由于你不可能经过文件系统遍历文件去甄别你须要获取的属于某个状态类别的数据,这么作成本就太大了。固然,特别大量的数据也不适合直接存储数据库,好比图片或者文章这样的数据,通常来讲,都是数据库存一个文件名,而后这个文件名指向的是某个图片或者文章的文件。若是真的要作全文索引这种需求,建议最好仍是挂个API丢到服务端去作。



总的说一下

 

NSUserDefault、Keychain、File这些持久化方案都很是简单基础,分清楚何时用什么就能够了,不要像天猫那样乱写就好。并且在这之上并不会有更复杂的衍生需求,若是真的要针对它们写文章,无非就是写怎么储存怎么读取,这个你们随便Google一下就有了,我就不浪费笔墨了。因为大多数衍生复杂需求都是经过采用基于数据库的持久化方案去知足,因此这篇文章的重点就数据库相关的架构方案设计和实现。若是文章中有哪些问题我没有写到的,你们能够在评论区提问,我会一一解答或者直接把遗漏的内容补充在文章中。




持久层实现时要注意的隔离



在设计持久层架构的时候,咱们要关注如下几个方面的隔离:

 

  1. 持久层与业务层的隔离
  2. 数据库读写隔离
  3. 多线程控制致使的隔离
  4. 数据表达和数据操做的隔离

 

1. 持久层与业务层的隔离



关于Model

 

在具体讲持久层下数据的处理以前,我以为须要针对这个问题作一个完整的分析。

View层设计中我分别提到了胖Model瘦Model的设计思路,并且告诉你们我更加倾向于胖Model的设计思路。在网络层设计里面我使用了去Model化的思路设计了APIMananger与业务层的数据交互。这两个看似矛盾的关于Model的设计思路在我接下来要提出的持久层方案中实际上是并不矛盾,并且是相互配合的。在网络层设计这篇文章中,我对去Model化只给出了思路和作法,相关的解释并很少,是由于要解释这个问题涉及面会比较广,写的时候并不认为在那篇文章里作解释是最好的时机。因为持久层在这里胖Model去Model化都会涉及,因此我以为在讲持久层的时候解释这个话题会比较好。

我在跟别的各类领域的架构师交流的时候,发现你们都会或多或少地混用ModelModel Layer的概念,而后每每致使你们讨论的问题最后都不在一个点上,说Model的时候他跟你说Model Layer,那好吧,我就跟你说Model Layer,结果他又在说Model,因而问题就讨论不下去了。我以为做为架构师,若是不分清楚这两个概念,确定是会对你设计的架构的质量有很大影响的。

若是把Model说成Data Model,而后跟Model Layer放在一块儿,这样就可以很容易区分概念了。



Data Model

 

Data Model这个术语针对的问题领域是业务数据的建模,以及代码中这一数据模型的表征方式。二者相辅相承:由于业务数据的建模方案以及业务自己特色,而最终决定了数据的表征方式。一样操做一批数据,你的数据建模方案基本都是细化业务问题以后,抽象得出一个逻辑上的实体。在实现这个业务时,你能够选择不一样的表征方式来表征这个逻辑上的实体,好比字节流(TCP包等),字符串流(JSON、XML等),对象流。对象流又分通用数据对象(NSDictionary等),业务数据对象(HomeCellModel等)。

前面已经遍历了全部的Data Model的形式。在习惯上,当咱们讨论Model化时,都是单指对象流中的业务数据对象这一种。然而去Model化就是指:更多地使用通用数据对象去表征数据,业务数据对象不会在设计时被优先考虑的一种设计倾向。这里的通用数据对象能够在某种程度上理解为范型。



Model Layer

 

Model Layer描述的问题领域是如何对数据进行增删改查(CURD, Create Update Read Delete),和相关业务处理。通常来讲若是在Model Layer中采用瘦Model的设计思路的话,就差很少到CURD为止了。胖Model还会关心如何为须要数据的上层提供除了增删改查之外的服务,并为他们提供相应的解决方案。例如缓存、数据同步、弱业务处理等。



个人倾向

 

我更加倾向于去Model化的设计,在网络层我设计了reformer来实现去Model化。在持久层,我设计了Virtual Record来实现去Model化。

由于具体的Model是一种很容易引入耦合的作法,在尽量弱化Model概念的同时,就可以为引入业务和对接业务提供充分的空间。同时,也能经过去Model的设计达到区分强弱业务的目的,这在未来的代码迁移和维护中,是相当重要的。不少设计很差的架构,就在于架构师并无认识到区分强弱业务的重要性,因此就致使架构腐化的速度很快,愈来愈难维护。

因此说回来,持久层与业务层之间的隔离,是经过强弱业务的隔离达到的。而Virtual Record正是由于这种去Model化的设计,从而达到了强弱业务的隔离,进而作到持久层与业务层之间既隔离同时又能交互的平衡。具体Virtual Record是什么样的设计,我在后面会给你们分析。




2. 数据库读写隔离



在网站的架构中,对数据库进行读写分离主要是为了提升响应速度。在iOS应用架构中,对持久层进行读写隔离的设计主要是为了提升代码的可维护性。这也是两个领域要求架构师在设计架构时要求侧重点不一样的一个方面。

在这里咱们所谓的读写隔离并非指将数据的读操做和写操做作隔离。而是以某一条界限为准,在这个界限之外的全部数据模型,都是不可写不可修改,或者修改属性的行为不影响数据库中的数据。在这个界限之内的数据是可写可修改的。通常来讲咱们在设计时划分的这个界限会和持久层与业务层之间的界限保持一致,也就是业务层从持久层拿到数据以后,都不可写不可修改,或业务层针对这一数据模型的写操做、修改操做都对数据库文件中的内容不产生做用。只有持久层中的操做才可以对数据库文件中的内容产生做用。

在苹果官方提供的持久层方案Core Data的架构设计中,并无针对读写做出隔离,数据的结果都是以NSManagedObject扔出。因此只要业务工程师稍微一不当心动一下某个属性,NSManagedObjectContext在save的时候就会把这个修改给存进去了。另外,当咱们须要对全部的增删改查操做作AOP的切片时,Core Data技术栈的实现就会很是复杂。

总体上看,我以为Core Data相对大部分需求而言是过分设计了。我当时设计安居客聊天模块的持久层时就采用了Core Data,而后为了读写隔离,将全部扔出来的NSManagedObject都转为了普通的对象。另外,因为聊天记录的业务至关复杂,使用Core Data以后为了完成需求不得不引入不少Hack的手段,这种作法在必定程度上下降了这个持久层的可维护性和提升了接手模块的工程师的学习曲线,这是不太好的。在天猫客户端,我去的时候天猫这个App就已经属于基本毫无持久层可言了,比较混乱。只能依靠各个业务线各显神通去解决数据持久化的需求,难以推进统一的持久层方案,这对于项目维护尤为是跨业务项目合做来讲,基本就和车祸现场没啥区别。我如今已经从天猫离职,读者中如果有阿里人想升职想刷存在感拿3.75的,能够考虑给天猫搞个统一的持久层方案。

读写隔离还可以便于加入AOP切点,由于针对数据库的写操做被隔离到一个固定的地方,加AOP时就很容易在正确的地方放入切片。这个会在讲到数据同步方案时看到应用。




3. 多线程致使的隔离



Core Data

 

Core Data要求在多线程场景下,为异步操做再生成一个NSManagedObjectContext,而后设置它的ConcurrencyTypeNSPrivateQueueConcurrencyType,最后把这个Context的parentContext设为Main线程下的Context。这相比于使用原始的SQLite去作多线程要轻松许多。只不过要注意的是,若是要传递NSManagedObject的时候,不能直接传这个对象的指针,要传NSManagedObjectID。这属于多线程环境下对象传递的隔离,在进行架构设计的时候须要注意。



SQLite

 

纯SQLite其实对于多线程却是直接支持,SQLite库提供了三种方式:Single ThreadMulti ThreadSerialized

Single Thread模式不是线程安全的,不提供任何同步机制。Multi Thread模式要求database connection不能在多线程中共享,其余的在使用上就没什么特殊限制了。Serialized模式顾名思义就是由一个串行队列来执行全部的操做,对于使用者来讲除了响应速度会慢一些,基本上就没什么限制了。大多数状况下SQLite的默认模式是Serialized

根据Core Data在多线程场景下的表现,我以为Core Data在使用SQLite做为数据载体时,使用的应该就是Multi Thread模式。SQLite在Multi Thread模式下使用的是读写锁,并且是针对整个数据库加锁,不是表锁也不是行锁,这一点须要提醒各位架构师注意。若是对响应速度要求很高的话,建议开一个辅助数据库,把一个大的写入任务先写入辅助数据库,而后拆成几个小的写入任务见缝插针地隔一段时间往主数据库中写入一次,写完以后再把辅助数据库删掉。

不过从实际经验上看,本地App的持久化需求的读写操做通常都不会大,只要注意好几个点以后通常都不会影响用户体验。所以相比于Multi Thread模式,Serialized模式我认为是性价比比较高的一种选择,代码容易写容易维护,性能损失不大。为了提升几十毫秒的性能而牺牲代码的维护性,我是以为划不来的。



Realm

 

关于Realm我还没来得及仔细研究,因此说不出什么来。




4. 数据表达和数据操做的隔离



这是最容易被忽视的一点,数据表达和数据操做的隔离是否可以作好,直接影响的是整个程序的可拓展性。

长久以来,咱们都很习惯Active Record类型的数据操做和表达方式,例如这样:

 

Record *record = [[Record alloc] init]; record.data = @"data"; [record save]; 

 

或者这种:

 

Record *record = [[Record alloc] init]; NSArray *result = [record fetchList]; 

 

简单说就是,让一个对象映射了一个数据库里的表,而后针对这个对象作操做就等同于针对这个表以及这个对象所表达的数据作操做。这里有一个很差的地方就在于,这个Record既是数据库中数据表的映射,又是这个表中某一条数据的映射。我见过不少框架(不只限于iOS,包括Python, PHP等)都把这二者混在一块儿去处理。若是按照这种不恰当的方式来组织数据操做和数据表达,在胖Model的实践下会致使强弱业务难以区分从而形成很是大的困难。使用瘦Model这种实践自己就是我认为有缺点的,具体的我在开篇中已经讲过,这里就不细说了。

强弱业务不能区分带来的最大困难在于代码复用和迁移,由于持久层中的强业务对View层业务的高耦合是没法避免的,然而弱业务相对而言只对下层有耦合关系对上层并不存在耦合关系,当咱们作代码迁移或者复用时,每每但愿复用的是弱业务而不是强业务,若此时强弱业务分不开,代码复用就无从谈起,迁移时就倍加困难。

另外,数据操做和数据表达混在一块儿会致使的问题在于:客观状况下,数据在view层业务上的表达方式多种多样,有多是个View,也有多是个别的什么对象。若是采用映射数据库表的数据对象去映射数据,那么这种多样性就会被限制,实际编码时每到使用数据的地方,就不得很少一层转换。

我认为之因此会产生这样很差的作法缘由在于,对象对数据表的映射和对象对数据表达的映射结果很是类似,尤为是在表达Column时,他们几乎就是如出一辙。在这里要作好针对数据表或是针对数据的映射要作的区分的关键要点是:这个映射对象的操做着手点相对数据表而言,是对内仍是对外操做。若是是对内操做,那么这个操做范围就仅限于当前数据表,这些操做映射给数据表模型就比较合适。若是是对外操做,执行这些操做时有可能涉及其余的数据表,那么这些操做就不该该映射到数据表对象中。

所以实际操做中,我是以数据表为单位去针对操做进行对象封装,而后再针对数据记录进行对象封装。数据表中的操做都是针对记录的普通增删改查操做,都是弱业务逻辑。数据记录仅仅是数据的表达方式,这些操做最好交付给数据层分管强业务的对象去执行。具体内容我在下文还会继续说。




持久层与业务层的交互方式



说到这里,就不得不说CTPersistanceVirtual Record了。我会经过它来说解持久层与业务层之间的交互方式。

 

                 -------------------------------------------
                 |                                         |
                 |  LogicA     LogicB            LogicC    |    ------------------------------->    View Layer
                 |     \         /                 |       |
                 -------\-------/------------------|--------
                         \     /                   |
                          \   / Virtual            | Virtual
                           \ /  Record             | Record
                            |                      |
                 -----------|----------------------|--------
                 |          |                      |       |
  Strong Logics  |     DataCenterA            DataCenterB  |
                 |        /   \                    |       |
-----------------|-------/-----\-------------------|-------|    Data Logic Layer   ---
                 |      /       \                  |       |                         |
   Weak Logics   | Table1       Table2           Table     |                         |
                 |      \       /                  |       |                         |
                 --------\-----/-------------------|--------                         |
                          \   /                    |                                 |--> Data Persistance Layer
                           \ / Query Command       | Query Command                   |
                            |                      |                                 |
                 -----------|----------------------|--------                         |
                 |          |                      |       |                         |
                 |          |                      |       |                         |
                 |      DatabaseA              DatabaseB   |  Data Operation Layer ---
                 |                                         |
                 |             Database Pool               |
                 -------------------------------------------



我先解释一下这个图:持久层有专门负责对接View层模块或业务的DataCenter,它们之间经过Record来进行交互。DataCenter向上层提供业务友好的接口,这通常都是强业务:好比根据用户筛选条件返回符合要求的数据等。

 

而后DataCenter在这个接口里面调度各个Table,作一系列的业务逻辑,最终生成record对象,交付给View层业务。

 

DataCenter为了要完成View层交付的任务,会涉及数据组装和跨表的数据操做。数据组装由于View层要求的不一样而不一样,所以是强业务。跨表数据操做本质上就是各单表数据操做的组合,DataCenter负责调度这些单表数据操做从而得到想要的基础数据用于组装。那么,这时候单表的数据操做就属于弱业务,这些弱业务就由Table映射对象来完成。

 

Table对象经过QueryCommand来生成相应的SQL语句,并交付给数据库引擎去查询得到数据,而后交付给DataCenter。



DataCenter 和 Virtual Record

 

提到Virtual Record以前必须先说一下DataCenter。

 

DataCenter实际上是一个业务对象,DataCenter是整个App中,持久层与业务层之间的胶水。它向业务层开放业务友好的接口,而后经过调度各个持久层弱业务逻辑和数据记录来完成强业务逻辑,并将生成的结果交付给业务层。因为DataCenter处在业务层和持久层之间,那么它执行业务逻辑所须要的载体,就要既可以被业务层理解,也可以被持久层理解。

CTPersistanceTable就封装了弱业务逻辑,由DataCenter调用,用于操做数据。而Virtual Record就是前面提到的一个既可以被业务层理解,也可以被持久层理解的数据载体。

 

Virtual Record事实上并非一个对象,它只是一个protocol,这就是它Virtual的缘由。一个对象只要实现了Virtual Record,它就能够直接被持久层看成Record进行操做,因此它也是一个Record。连起来就是Virtual Record了。因此,Virtual Record的实现者能够是任何对象,这个对象通常都是业务层对象。在业务层内,常见的数据表达方式通常都是View,因此通常来讲Virutal Record的实现者也都会是一个View对象。

 

咱们回顾一下传统的数据操做过程:通常都是先从数据库中取出数据,而后Model化成一个对象,而后再把这个模型丢到外面,让Controller转化成View,而后再执行后面的操做。

Virtual Record也是同样遵循相似的步骤。惟一不一样的是,整个过程当中,它并不须要一个中间对象去作数据表达,对于数据的不一样表达方式,由各自Virtual Record的实现者本身完成,而不须要把这些代码放到Controller,因此这就是一个去Model化的设计。若是将来针对这个数据转化逻辑有复用的需求,直接复用Virtual Record就能够了,十分方便。

 

用好Virtual Record的关键在于DataCenter提供的接口对业务足够友好,有充足的业务上下文环境。

因此DataCenter通常都是被Controller所持有,因此若是整个App就只有一个DataCenter,这其实并非一个好事。我见过有不少App的持久层就是一个全局单例,全部持久化业务都走这个单例,这是一种很蛋疼的作法。DataCenter也是须要针对业务作高度分化的,每一个大业务都要提供一个DataCenter,而后挂在相关Controller下交给Controller去调度。好比分化成SettingsDataCenterChatRoomDataCenterProfileDataCenter等,另外要要注意的是,几个DataCenter之间最好不要有业务重叠。若是一个DataCenter的业务实在是大,那就再拆分红几个小业务。若是单个小业务都很大了,那就拆成各个Category,具体的作法能够参考个人框架中CTPersistanceTableCTPersistanceQueryCommand的实践。

这么一来,若是要迁移涉及持久层的强业务,那就只须要迁移DataCenter便可。若是要迁移弱业务,就只须要迁移CTPersistanceTable




实际场景



假设业务层此时收集到了用户的筛选条件:

 

NSDictionary *filter = @{ @"key1":@{ @"minValue1":@(1), @"maxValue1":@(9), }, @"key2":@{ @"minValue2":@(1), @"maxValue2":@(9), }, @"key3":@{ @"minValue3":@(1), @"maxValue3":@(9), }, }; 

 

而后ViewController调用DataCenter向业务层提供的接口,得到数据直接展现:

 

/* in view controller */

    NSArry *fetchedRecordList = [self.dataCenter fetchItemListWithFilter:filter] [self.dataList appendWithArray:fetchedRecordList]; [self.tableView reloadData]; 

 

在View层要作的事情其实到这里就已经结束了,此时咱们回过头再来看DataCenter如何实现这个业务:

 

/* in DataCenter */

- (NSArray *)fetchItemListWithFilter:(NSDictionary *)filter { ... ... ... /*  解析filter得到查询所须要的数据  whereCondition  whereConditionParams  假设上面这两个变量就是解析获得的变量  */ ... ... ... /* 告知Table对象查询数据后须要转化成的对象(可选,统一返回对象能够便于归并来自不一样表的数据) */ self.itemATable.recordClass = [Item class]; self.itemBTable.recordClass = [Item class]; self.itemCTable.recordClass = [Item class]; /* 经过Table对象获取数据,此时Table对象内执行的就是弱业务了 */ NSArray *itemAList = [self.itemATable findAllWithWhereCondition:whereCondition conditionParams:whereConditionParams isDistinct:NO error:NULL]; NSArray *itemBList = [self.itemBTable findAllWithWhereCondition:whereCondition conditionParams:whereConditionParams isDistinct:NO error:NULL]; NSArray *itemCList = [self.itemCTable findAllWithWhereCondition:whereCondition conditionParams:whereConditionParams isDistinct:NO error:NULL]; /* 组装数据 */ NSMutableArray *resultList = [[NSMutableArray alloc] init]; [resultList addObjectsFromArray:itemAList]; [resultList addObjectsFromArray:itemBList]; [resultList addObjectsFromArray:itemCList]; return resultList; } 

 

基本上差很少就是上面这样的流程。

通常来讲,架构师设计得差的持久层,都没有经过设计DataCenter和Table,去将强业务和弱业务分开。经过设计DataCenter和Table对象,主要是便于代码迁移。若是迁移强业务,把DataCenter和Table一块儿拿走就能够,若是只是迁移弱业务,拿走Table就能够了。

 

另外,经过代码我但愿向你强调一下这个概念:将Table和Record区分开,这个在我以前画的架构图上已经有所表现,不过上文并无着重强调。其实不少别的架构师在设计持久层框架的时候,也没有将Table和Record区分开,对的,这里我说的框架包括Core Data和FMDB,这个也不只限于iOS领域,CodeIgniter、ThinkPHP、Yii、Flask这些也都没有对这个作区分。(这里吐槽一下,话说上文我还提到Core Data被过分设计了,事实上该设计的地方没设计到,不应设计的地方各类设计往上堆...)

 

以上就是对Virtual Record这个设计的简单介绍,接下来咱们就开始讨论不一样场景下如何进行交互了。

其中咱们最为熟悉的一个场景是这样的:通过各类逻辑组装出一个数据对象,而后把这个数据对象交付给持久层去处理。这种场景我称之为一对一的交互场景,这个交互场景的实现很是传统,就跟你们想得那样,并且CTPersistance的test case里面都是这样的,因此这里我就很少说了。因此,既然你已经知道有了一对一,那么瓜熟蒂落地就也会有多对一,以及一对多的交互场景。

下面我会一一描述Virtual Record是如何发挥虚拟的优点去针对不一样场景进行交互的。




多对一场景下,业务层如何与持久层交互?



多对一场景其实有两种理解,一种是一个记录的数据由多个View的数据组成。例如一张用户表包含用户的全部资料。而后有的View只包含用户昵称用户头像,有的对象只包含用户ID用户Token。然而这些数据都只存在一张用户表中,因此这是一种多个对象的数据组成一个完整Record数据的场景,这是多对一场景的理解之一。

第二种理解是这样的,例如一个ViewA对象包含了一个Record的全部信息,而后另外一个ViewB对象其实也包含了一个Record的全部信息,这就是一种多个不一样对象表达了一个Record数据的场景,这也是一种多对一场景的理解。

同时,这里所谓的交互还分两个方向:存和取。

其实这两种理解的解决方案都是同样的,Virtual Record的实现者经过实现Merge操做来完成record数据的汇总,从而实现存操做。任意Virtual Record的实现者经过Merge操做,就能够将本身的数据交付给其它不一样的对象进行表达,从而实现取操做。具体的实如今下面有具体阐释。




多对一场景下,如何进行存操做?



<CTPersistanceProtocol>提供了- (NSObject <CTPersistanceRecordProtocol> *)mergeRecord:(NSObject <CTPersistanceRecordProtocol> *)record shouldOverride:(BOOL)shouldOverride;这个方法。望文生义一下,就是一个record能够与另一个record进行merge。在shouldOverride为NO的状况下,任何一边的nil都会被另一边不是nil的记录覆盖,若是merge过程当中两个对象都不含有这些空数据,则根据shouldOverride来决定是否要让参数中record的数据覆盖本身自己的数据,若shouldOverride为YES,则即使是nil,也会把已有的值覆盖掉。这个方法会返回被Merge的这个对象,便于链式调用。

举一个代码样例:

 

/*
这里的RecordViewA, RecordViewB, RecordViewC都是符合<CTPersistanceRecordProtocol>且实现了- (NSObject <CTPersistanceRecordProtocol> *)mergeRecord:(NSObject <CTPersistanceRecordProtocol> *)record shouldOverride:(BOOL)shouldOverride方法。 */ RecordViewA *a; RecordViewB *b; RecordViewC *c; ... 收集a, b, c的值的逻辑,我就不写了~ ... [[a mergeRecord:b shouldOverride:YES] mergeRecord:c shouldOverride:YES]; [self.dataCenter saveRecord:a]; 

 

基本思路就是经过merge不一样的record对象来达到获取完整数据的目的,因为是Virtual Record,具体的实现都是由各自的View去决定。View是最了解本身属性的对象了,所以它是有充要条件来把本身与持久层相关的数据取出并Merge的,那么这段凑数据的代码,就相应分散到了各个View对象中,Controller里面就可以作到很是干净,总体可维护性也就提升了。

若是采用传统方式,ViewController或者DataCenter中就会散落不少用于凑数据的代码,写的时候就会出现一大段用于合并的代码,很是难看,还不容易维护。




多对一场景下,如何进行取操做?



其实这样的表述并不恰当,由于不管Virtual Record的实现如何,对象是谁,只要从数据库里面取出数据来,数据就都是可以保证完整的。这里更准确的表述是,取出数据以后,如何交付给不一样的对象。其实仍是用到上面提到的mergeRecord方法来处理。

 

/*
这里的RecordViewA, RecordViewB, RecordViewC都是符合<CTPersistanceRecordProtocol>且实现了- (NSObject <CTPersistanceRecordProtocol> *)mergeRecord:(NSObject <CTPersistanceRecordProtocol> *)record shouldOverride:(BOOL)shouldOverride方法。 */ RecordViewA *a; RecordViewB *b = [[RecordViewB alloc] init]; RecordViewC *c = [[RecordViewC alloc] init]; a = [self.table findLatestRecordWithError:NULL]; [b mergeRecord:a]; [c mergeRecord:a]; return @[a, b, c] 

 

这样就能很容易把a记录的数据交给b和c了,代码观感一样很是棒,并且容易写容易维护。




一对多场景下,业务层如何与持久层交互?



一对多场景也有两种理解,其一是一个对象包含了多个表的数据,另一个是一个对象用于展现多种表的数据,这个代码样例其实文章前面已经有过,这一节会着重强调一下。乍看之下二者并无什么区别,因此我须要指出的是,前者强调的是包含,也就是这个对象是个大熔炉,由多个表的数据组成。

仍是举用户列表的例子:

 

假设数据库中用户相关的表有多张。大多数状况是由于单表Column太多,因此为了提升维护性和查询性能而进行的纵切

 

多说一句,纵切在实际操做时,大多都是根据业务场景去切分红多个不一样的表,分别来表达用户各业务相关的部分数据,因此纵切的结果就是把Column特别多的一张表拆成Column不那么多的好几个表。虽然数据库通过了纵切,可是有的场景仍是要展现完整数据的,好比用户详情页。所以,这个用户详情页的View就有可能包含用户基础信息表(用户名、用户ID、用户Token等)、以及用户详细信息表(用户邮箱地址、用户手机号等)。这就是一对多一个对象包含了多个表的数据的意思。

后者强调的是展现。举个例子,数据库中有三个表分别是:

 

二手房新房租房,它们三者的数据分别存储在三个表里面,这实际上是一种横切

 

横切也是一种数据库的优化手段,横切与纵切不一样的地方在于,横切是在保留了这套数据的完整性的前提下进行的切分,横切的结果就是把一个本来数据量很大的表,分红了好几个数据量不那么大的表。也就是原来三种房子都能用同一个表来存储,可是这样数据量就太大了,数据库响应速度就会降低。因此根据房子的类型拆成这三张表。横切也有根据ID切的,好比根据ID取余的结果来决定分在哪些表里,这种作法比较普遍,由于拓展起来方便,到时候数据表又大了,大不了除数也跟着再换一个更大的数罢了。其实根据类型去横切也能够,只是拓展的时候就不那么方便。

 

刚才扯远了如今我再扯回来,这三张表在展现的时候,只是根据类型的不一样,界面才有稍许不一样而已,因此仍是会用同一张View去展现这三种数据,这就是一对多一个对象用于展现多种表的数据的意思。




一个对象包含了多个表的数据时,如何进行存取操做?



在进行取操做时,其实跟前面多对一的取操做是同样的,用Merge操做就能够了。

 

RecordViewA *a; a = [self.CasaTable findLatestRecordWithError:NULL]; [a mergeRecord:[self.TaloyumTable findLatestRecordWithError:NULL] shouldOverride:YES]; [a mergeRecord:[self.CasatwyTable findLatestRecordWithError:NULL] shouldOverride:YES]; return a; 

 

在进行存操做时,Virtual Record<CTPersistanceRecordProtocol>要求实现者实现- (NSDictionary *)dictionaryRepresentationWithColumnInfo:(NSDictionary *)columnInfo tableName:(NSString *)tableName;这个方法,实现者能够根据传入的columnInfotableName返回相应的数据,这样就可以把这一次存数据时关心的内容提供给持久层了。代码样例就是这样的:

 

RecordViewA *a = ...... ; /* 因为有- (NSDictionary *)dictionaryRepresentationWithColumnInfo:(NSDictionary *)columnInfo tableName:(NSString *)tableName;的实现,a对象本身会提供给不一样的Table它们感兴趣的内容而存储。 因此直接存就行了。 */ [self.CasaTable insertRecord:a error:NULL]; [self.TaloyumTable insertRecord:a error:NULL]; [self.CasatwyTable insertRecord:a error:NULL]; 

 

经过上面的存取案例,你会发现使用Virtual Record以后,代码量一会儿少掉不少,本来那些乱七八糟用于拼凑条件的代码所有被分散进了各个虚拟记录的实现中去了,代码维护所以就变得至关方便。如果采用传统作法,再存取以前少不了要写一大段逻辑,若是涉及代码迁移,这大段逻辑就也得要跟着迁移过去,这就很蛋疼了。




一个对象用于展现多种表的数据,如何进行存取操做?



在这种状况下的存操做其实跟上面同样,直接存。Virtual Record的实现者本身会根据要存入的表的信息组装好数据提供给持久层。样例代码与上一小节的存操做中给出的如出一辙,我就不复制粘贴了。

取操做就不太同样了,不过因为取出时的对象是惟一的(由于一对多嘛),代码也同样十分简单:

 

ViewRecord *a; ViewRecord *b; ViewRecord *c; self.itemATable.recordClass = [ViewRecord class]; self.itemBTable.recordClass = [ViewRecord class]; self.itemCTable.recordClass = [ViewRecord class]; [a = self.itemATable findLatestRecordWithError:NULL]; [b = self.itemBTable findLatestRecordWithError:NULL]; [c = self.itemCTable findLatestRecordWithError:NULL]; 

 

这里的abc都是同一个View,而后itemATableitemBTableitemCTable分别是不一样种类的表。这个例子表示了一个对象如何用于展现不一样类型的数据。若是使用传统方法,这里少不了要写不少适配代码,可是使用Virtual Record以后,这些代码都由各自实现者消化掉了,在执行数据逻辑时能够无需关心适配逻辑。




多对多场景?



其实多对多场景就是上述这些一对多多对一场景的排列组合,实现方式都是如出一辙的,我这里就也很少啰嗦了。




交互方案的总结



在交互方案的设计中,架构师应当区分好强弱业务,把传统的Data Model区分红TableRecord,并由DataCenter去实现强业务,Table去实现弱业务。在这里因为DataCenter是强业务相关,因此在实际编码中,业务工程师负责建立DataCenter,并向业务层提供业务友好的方法,而后再在DataCenter中操做Table来完成业务层交付的需求。区分强弱业务,将TableRecord拆分开的好处在于:

 

  1. 经过业务细分下降耦合度,使得代码迁移和维护很是方便
  2. 经过拆解数据处理逻辑和数据表达形态,使得代码具备很是良好的可拓展性
  3. 作到读写隔离,避免业务层的误操做引入Bug
  4. 为Virtual Record这一设计思路的实践提供基础,进而实现更灵活,对业务更加友好的架构

 

任何不区分强弱业务的架构都是架构师在耍流氓,嗯。

在具体与业务层交互时,采用Virtual Record的设计思路来设计Record,由具体的业务对象来实现Virtual Record,并以它做为DataCenter和业务层之间的数据媒介进行交互。而不是使用传统的数据模型来与业务层作交互。

使用Virtual Record的好处在于:

 

  1. 将数据适配和数据转化逻辑封装到具体的Record实现中,能够使得代码更加抽象简洁,代码污染更少
  2. 数据迁移时只须要迁移Virtual Record相关方法便可,很是容易拆分
  3. 业务工程师实现业务逻辑时,能够在不损失可维护性的前提下,极大提升业务实现的灵活性

 

这一部分还顺便提了一下横切纵切的概念。原本是打算有一小节专门写数据库性能优化的,不过事实上移动App场景下数据库的性能优化手段不像服务端那样丰富多彩,不少牛逼技术和参数调优手段想用也用不了。差很少就只剩下数据切片的手段比较有效了,因此性能优化这块感受没什么好写的。其实你们了解了切片的方式和场景,就足以根据本身的业务场景去作优化了。再使用一下Instrument的Time Profile再配合SQLite提供的一些函数,就足以找到慢在哪儿,而后去作性能调优了。但若是我把这些也写出来,就变成教你怎么使用工具,感受这个太low写着也不起劲,你们有兴趣搜使用手册下来看就行。




数据库版本迁移方案



通常来讲,具备持久层的App同时都会附带着有版本迁移的需求。当一个用户安装了旧版本的App,此时更新App以后,若数据库的表结构须要更新,或者数据自己须要批量地进行更新,此时就须要有版本迁移机制来进行这些操做。然而版本迁移机制又要兼顾跨版本的迁移需求,因此基本上大方案也就只有一种:创建数据库版本节点,迁移的时候一个一个跑过去。

数据迁移事实上实现起来仍是比较简单的,作好如下几点问题就不大了:

 

  1. 根据应用的版本记录每一版数据库的改变,并将这些改变封装成对象
  2. 记录好当前数据库的版本,便于跟迁移记录作比对
  3. 在启动数据库时执行迁移操做,若是迁移失败,提供一些降级方案

 

CTPersistance在数据迁移方面,凡是对于数据库本来没有的数据表,若是要新增,在使用table的时候就会自动建立。所以对于业务工程师来讲,根本不须要额外多作什么事情,直接用就能够了。把这部分工做放到这里,也是为数据库版本迁移节省了一些步骤。

 

CTPersistance也提供了Migrator。业务工程师能够本身针对某一个数据库编写一个Migrator。这个Migrator务必派生自CTPersistanceMigrator,且符合<CTPersistanceMigratorProtocol>,只要提供一个migrationStep的字典,以及记录版本顺序的数组。而后把你本身派生的Migrator的类名和对应关心的数据库名写在CTPersistanceConfiguration.plist里面就能够。CTPersistance会在初始数据库的时候,根据plist里面的配置对应找到Migrator,并执行数据库版本迁移的逻辑。

 

在版本迁移时要注意的一点是性能问题。咱们通常都不会在主线程作版本迁移的事情,这天然没必要说。须要强调的是,SQLite自己是一个容错性很是强的数据库引擎,所以差很少在执行每个SQL的时候,内部都是走的一个Transaction。当某一版的SQL数量特别多的时候,建议在版本迁移的方法里面本身创建一个Transaction,而后把相关的SQL都包起来,这样SQLite执行这些SQL的时候速度就会快一点。

其余的彷佛并无什么要额外强调的了,若是有没说到的地方,你们能够在评论区提出来。




数据同步方案



数据同步方案大体分两种类型,一种类型是单向数据同步,另外一种类型是双向数据同步。下面我会分别说说这两种类型的数据同步方案的设计。




单向数据同步



单向数据同步就是只把本地较新数据的操做同步到服务器,不会从服务器主动拉取同步操做。

 

好比即时通信应用,一个设备在发出消息以后,须要等待服务器的返回去知道这个消息是否发送成功,是否取消成功,是否删除成功。而后数据库中记录的数据就会随着这些操做是否成功而改变状态。可是若是换一台设备继续执行操做,在这个新设备上只会拉取旧的数据,好比聊天记录这种。但对于旧的数据并无删除或修改的需求,所以新设备也不会问服务器索取数据同步的操做,因此称之为单向数据同步。

 

单向数据同步通常来讲也不须要有job去作定时更新的事情。若是一个操做迟迟没有收到服务器的确认,那么在应用这边就能够认为这个操做失败,而后通常都是在界面上把这些失败的操做展现出来,而后让用户去勾选须要重试的操做,而后再从新发起请求。微信在消息发送失败的时候,就是消息前面有个红色的圈圈,里面有个感叹号,只有用户点击这个感叹号的时候才从新发送消息,背后不会有个job一直一直跑。

 

因此细化需求以后,咱们发现单向数据同步只须要作到可以同步数据的状态便可。




如何完成单向数据同步的需求



添加identifier



添加identifier的目的主要是为了解决客户端数据的主键和服务端数据的主键不一致的问题。因为是单向数据同步,因此数据的生产者只会是当前设备,那么identifier也理所应当由设备生成。当设备发起同步请求的时候,把identifier带上,当服务器完成任务返回数据时,也把这些identifier带上。而后客户端再根据服务端给到的identifier再更新本地数据的状态。identifier通常都会采用UUID字符串。




添加isDirty



isDirty主要是针对数据的插入和修改进行标识。当本地新生成数据或者更新数据以后,收到服务器的确认返回以前,isDirty置为YES。当服务器的确认包返回以后,再根据包里提供的identifier找到这条数据,而后置为NO。这样就完成了数据的同步。

然而这只是简单的场景,有一种比较极端的状况在于,当请求发起到收到请求回复的这短短几秒间,用户又修改了数据。若是按照当前的逻辑,在收到请求回复以后,这个又修改了的数据的isDirty会被置为NO,因而这个新的修改就永远没法同步到服务器了。这种极端状况的简单处理方案就是在发起请求到收到回复期间,界面上不容许用户进行修改。

若是但愿作得比较细致,在发送同步请求期间依旧容许用户修改的话,就须要在数据库额外增长一张DirtyList来记录这些操做,这个表里至少要有两个字段:identifierprimaryKey。而后每一次操做都分配一次identifier,那么新的修改操做就有了新的identifier。在进行同步时,根据primaryKey找到原数据表里的那条记录,而后把数据连同identifier交给服务器。而后在服务器的确认包回来以后,就只要拿出identifier再把这条操做记录删掉便可。这个表也能够直接服务于多个表,只是还须要额外添加一个tablename字段,方便发起同步请求的时候可以找获得数据。




添加isDeleted



当有数据同步的需求的时候,删除操做就不能是简单的物理删除了,而只是逻辑删除,所谓逻辑删除就是在数据库里把这条记录的isDeleted记为YES,只有当服务器的确认包返回以后,才会真正把这条记录删除。isDeleted和isDirty的区别在于:收到确认包后,返回的identifier指向的数据若是是isDeleted,那么就要删除这条数据,若是指向的数据只是新插入的数据和更新的数据,那么就只要修改状态就行。插入数据和更新数据在收到数据包以后作的操做是相同的,因此就用isDirty来区分就足够了。总之,这是根据收到确认包以后的操做不一样而作的区分。二者都要有,缺一不可。




在请求的数据包中,添加dependencyIdentifier



在我看到的不少其它数据同步方案中,并无提供dependencyIdentifier,这会致使一个这样的问题:假设有两次数据同步请求一块儿发出,A先发,B后发。结果反而是B请求先到,A请求后到。若是A请求的一系列同步操做里面包含了插入某个对象的操做,B请求的一系列同步操做里面正好又删除了这个对象,那么因为到达次序的前后问题错乱,就致使这个数据没办法删除。

这个在移动设备的使用场景下是很容易发生的,移动设备自己网络环境就多变,先发的包反然后到,这种状况出现的概率仍是比较大的。因此在请求的数据包中,咱们要带上上一次请求时一系列identifier的其中一个,就能够了。通常都是选择上次请求里面最后的那一个操做的identifier,这样就能表征上一次请求的操做了。

服务端这边也要记录最近的100个请求包里面的最后一个identifier。之因此是100条纯属只是拍脑壳定的数字,我以为100条差很少就够了,客户端发请求的时候denpendency应该不会涉及到前面100个包。服务端在收到同步请求包的时候,先看denpendencyIdentifier是否已被记录,若是已经被记录了,那么就执行这个包里面的操做。若是没有被记录,那就先放着再等等,等到条件知足了再执行,这样就能解决这样的问题。

之因此不用更新时间而是identifier来作标识,是由于若是要用时间作标识的话,就是只能以客户端发出数据包时候的时间为准。但有时不一样设备的时间不必定彻底对得上,多少会差个几秒几毫秒,另外若是同时有两个设备发起同步请求,这两个包的时间就都是同样的了。假设A1, B1是1号设备发送的请求,A2, B2,是2号设备发送的请求,若是用时间去区分,A1到了以后,B2说不定就直接可以执行了,而A1还没到服务器呢。

固然,这也是一种极端状况,用时间的话,服务器就只要记录一个时间了,凡是依赖时间大于这个时间的,就都要再等等,实现起来就比较方便。可是为了保证bug尽量少,我认为依赖仍是以identifier为准,这要比以时间为准更好,并且实现起来其实也并无增长太多复杂度。




单向数据同步方案总结



  1. 改造的时候添加identifier,isDirty,isDeleted字段。若是在请求期间依旧容许对数据作操做,那么就要把identifier和primaryKey再放到一个新的表中
  2. 每次生成数据以后对应生成一个identifier,而后只要是针对数据的操做,就修改一次isDirty或isDeleted,而后发起请求带上identifier和操做指令去告知服务器执行相关的操做。若是是复杂的同步方式,那么每一次修改数据时就新生成一次identifier,而后再发起请求带上相关数据告知服务器。
  3. 服务器根据请求包的identifier等数据执行操做,操做完毕回复给客户端确认
  4. 收到服务器的确认包以后,根据服务器给到的identifier(有的时候也会有tablename,取决于你的具体实现)找到对应的记录,若是是删除操做,直接把数据删除就好。若是是插入和更新操做,就把isDirty置为NO。若是有额外的表记录了更新操做,直接把identifier对应的这个操做记录删掉就行。




要注意的点



在使用表去记录更新操做的时候,短期以内颇有可能针对同一条数据进行屡次更新操做。所以在同步以前,最好可以合并这些相同数据的更新操做,能够节约服务器的计算资源。固然若是你服务器强大到不行,那就无所谓了。




双向数据同步



双向数据同步多见于笔记类、日程类应用。对于一台设备来讲,不光本身会往上推数据同步的信息,本身也会问服务器主动索取数据同步的信息,因此称之为双向数据同步。

举个例子:当一台设备生成了某时间段的数据以后,到了另一台设备上,又修改了这些旧的历史数据。此时再回到原来的设备上,这台设备就须要主动问服务器索取是否旧的数据有修改,若是有,就要把这些操做下载下来同步到本地。

双向数据同步实现上会比单向数据同步要复杂一些,并且有的时候还会存在实时同步的需求,好比协同编辑。因为自己方案就比较复杂,另一定要兼顾业务工程师的上手难度(这主要看你这个架构师的良心),因此要实现双向数据同步方案的话,仍是颇有意思比较有挑战的。




如何完成双向数据同步的需求



封装操做对象



这个其实在单向数据同步时多少也涉及了一点,可是因为单向数据同步的要求并不复杂,只要告诉服务器是什么数据而后要作什么事情就能够了,却是不必将这种操做封装。在双向数据同步时,你也得解析数据操做,因此互相之间要约定一个协议,经过封装这个协议,就作到了针对操做对象的封装。

这个协议应当包括:

 

  1. 操做的惟一标识
  2. 数据的惟一标识
  3. 操做的类型
  4. 具体的数据,主要是在Insert和Update的时候会用到
  5. 操做的依赖标识
  6. 用户执行这项操做时的时间戳

 

分别解释一下这6项的意义:




  1. 操做的惟一标识

 

这个跟单向同步方案时的做用同样,也是在收到服务器的确认包以后,可以使得本地应用找到对应的操做并执行确认处理。



  1. 数据的惟一标识

 

在找到具体操做的时候执行确认逻辑的处理时,都会涉及到对象自己的处理,更新也好删除也好,都要在本地数据库有所体现。因此这个标识就是用于找到对应数据的。



  1. 操做的类型

 

操做的类型就是DeleteUpdateInsert,对应不一样的操做类型,对本地数据库执行的操做也会不同,因此用它来进行标识。



  1. 具体的数据

 

当更新的时候有Update或者Insert操做的时候,就须要有具体的数据参与了。这里的数据有的时候不见得是单条的数据内容,有的时候也会是批量的数据。好比把全部10月1日以前的任务都标记为已完成状态。所以这里具体的数据如何表达,也须要定一个协议,何时做为单条数据的内容去执行插入或更新操做,何时做为批量的更新去操做,这个本身根据实际业务需求去定义就行。



  1. 操做的依赖标识

 

跟前面提到的依赖标识同样,是为了防止先发的包后到后发的包先到这种极端状况。



  1. 用户执行这项操做的时间戳

 

因为跨设备,又由于旧数据也会被更新,所以在必定程度上就会出现冲突的可能。操做数据在从服务器同步下来以后,会存放在一个新的表中,这个表就是待操做数据表,在具体执行这些操做的同时会跟待同步的数据表中的操做数据作比对。若是是针对同一条数据的操做,且这两个操做存在冲突,那么就以时间戳来决定如何执行。还有一种作法就是直接提交到界面告知用户,让用户作决定。




新增待操做数据表和待同步数据表



前面已经部分提到这一点了。从服务器拉下来的同步操做列表,咱们存在待执行数据表中,操做完毕以后若是有告知服务器的需求,那就等因而走单向同步方案告知服务器。在执行过程当中,这些操做也要跟待同步数据表进行匹配,看有没有冲突,没有冲突就继续执行,有冲突的话要么按照时间戳执行,要么就告知用户让用户作决定。在拉取待执行操做列表的时候,也要把最后一次操做的identifier丢给服务器,这样服务器才能返回相应数据。

待同步数据表的做用其实也跟单向同步方案时候的做用相似,就是防止在发送请求的时候用户有操做,同时也是为解决冲突提供方便。在发起同步请求以前,咱们都应该先去查询有没有待执行的列表,当待执行的操做列表同步完成以后,就能够删除里面的记录了,而后再把本地待同步的数据交给服务器。同步完成以后就能够把这些数据删掉了。所以在正常状况下,只有在待操做待执行的操做间会存在冲突。有些从道理上讲也算是冲突的事情,好比获取待执行的数据比较晚,但其中又和待同步中的操做有冲突,像这种极端状况咱们其实也无解,只能由他去,不过这种状况也是属于比较极端的状况,发生概率不大。




什么时候从服务器拉取待执行列表



  1. 每次要把本地数据丢到服务器去同步以前,都要拉取一次待执行列表,执行完毕以后再上传本地同步数据
  2. 每次进入相关页面的时候都更新一次,看有没有新的操做
  3. 对实时性要求比较高的,要么客户端本地起一个线程作轮询,要么服务器经过长连接将待执行操做推送过来
  4. 其它我暂时也想不到了,具体仍是看需求吧




双向数据同步方案总结



  1. 设计好同步协议,用于和服务端进行交互,以及指导本地去执行同步下来的操做
  2. 添加待执行待同步数据表记录要执行的操做和要同步的操做




要注意的点



我也见过有的方案是直接把SQL丢出去进行同步的,我不建议这么作。最好仍是将操做和数据分开,而后细化,不然检测冲突的时候你就得去分析SQL了。要是这种实现中有什么bug,解这种bug的时候就要考虑先后兼容问题,机制重建成本等,由于贪图一时偷懒,到最后其实得不偿失。




总结



这篇文章主要是基于CTPersistance讲了一下如何设计持久层的设计方案,以及数据迁移方案和数据同步方案。

着重强调了一下各类持久层方案在设计时要考虑的隔离,以及提出了Virtual Record这个设计思路,并对它作了一些解释。而后在数据迁移方案设计时要考虑的一些点。在数据同步方案这一节,分开讲了单向的数据同步方案和双向的数据同步方案的设计,然而具体实现仍是要依照具体的业务需求来权衡。

但愿你们以为这些内容对各自工做中遇到的问题可以有所价值,若是有问题,欢迎在评论区讨论。

另外,关于动态部署方案,其实直到今天在iOS领域也并无特别好的动态部署方案能够拿出来,我以为最靠谱的其实仍是H5和Native的Hybrid方案。React Native在我看来相比于Hybrid仍是有比较多的限制。关于Hybrid方案,我也提供了CTJSBridge这个库去实现这方面的需求。在动态部署方案这边其实成文已经好久,迟迟不发的缘由仍是由于以为当时并无什么银弹能够解决iOS App的动态部署,另外也有一些问题没有考虑清楚。当初想到的那些问题如今我已经确认无解。当初写的动态部署方案我一直认为它没法做为一个单独的文章发布出来,因此我就把这篇文章也放在这里,权当给各位参考。








iOS动态部署方案



前言

 

这里讨论的动态部署方案,就是指经过不发版的方式,将新的内容、新的业务流程部署进已发布的App。由于苹果的审核周期比较长,并且苹果的限制比较多,业界在这里也没有特别多的手段来达到动态部署方案的目的。这篇文章主要的目的就是给你们列举一下目前业界作动态部署的手段,以及其对应的优缺点。而后给出一套我比较倾向于使用的方案。

其实单纯就动态部署方案来说,没什么太多花头能够说的,就是H五、Lua、JS、OC/Swift这几门基本技术的各类组合排列。写到后面以为,动态部署方案实际上是很是好的用于讲解某些架构模式的背景。通常咱们经验总结下来的架构模式包括但不限于:

 

  1. Layered Architecture
  2. Event-Driven Architecture
  3. Microkernel Architecture
  4. Microservices Architecture
  5. Space-Based Architecture

 

我在开篇里面提到的MVC等方案跟这篇文章中要提到的架构模式并非属于同一个维度的。比较容易混淆的就是容易把MVC这些方案跟Layered Architecture混淆,这个我在开篇这篇文章里面也作过了区分:MVC等方案比较侧重于数据流动方向的控制和数据流的管理。Layered Architecture更加侧重于各分层之间的功能划分和模块协做。

另外,上述五种架构模式在Software Architecture Patterns这本书里有很是详细的介绍,整本书才45页,个把小时就看完了,很是值得看和思考。本文后半篇涉及的架构模式是以上架构模式的其中两种:Microkernel ArchitectureMicroservices Architecture

最后,文末还给出了其余一些关于架构模式的我以为还不错的PPT和论文,里面对架构模式的分类和总结也比较多样,跟Software Architecture Patterns的总结也有些许不同的地方,能够博采众长。




Web App

 

实现方案

 

其实所谓的web app,就是经过手机上的浏览器进行访问的H5页面。这个H5页面是针对移动场景特别优化的,好比UI交互等。



优势

 

  1. 无需走苹果流程,全部苹果流程带来的成本都能避免,包括审核周期、证书成本等。
  2. 版本更新跟网页同样,随时生效。
  3. 不须要Native App工程师的参与,并且市面上已经有不少针对这种场景的框架。



缺点

 

  1. 因为每一页都须要从服务器下载,所以web app重度依赖网络环境。
  2. 一样的UI效果使用web app来实现的话,流畅度不如Native,比较影响用户体验。
  3. 本地持久化的部分很难作好,绕过本地持久化的部分的办法就是提供帐户体系,对应帐户的持久化数据所有存在服务端。
  4. 即时响应方案、远程通知实现方案、移动端传感器的使用方案复杂,维护难度大。
  5. 安全问题,H5页面等因而全部东西都暴露给了用户,若是对安全要求比较高的,不少额外的安全机制都须要在服务端实现。



总结

web app通常是创业初期会重点考虑的方案,由于迭代很是快,并且创业初期的主要目标是须要验证模式的正确性,并不在于提供很是好的用户体验,只须要完成闭环便可。早年facebook曾经尝试过这种方案,最后由于用户体验的问题而宣布放弃。因此这个方案只能做为过渡方案,或者当App不可用时,做为降级方案使用。




Hybrid App

 

经过市面上各类Hybrid框架,来作H5和Native的混合应用,或者经过JS Bridge来作到H5和Native之间的数据互通。



优势

 

  1. 除了要承担苹果流程致使的成本之外,具有全部web app的优点
  2. 可以访问本地数据、设备传感器等



缺点

 

  1. 跟web app同样存在过分依赖网络环境的问题
  2. 用户体验也很难作到很好
  3. 安全性问题依旧存在
  4. 大规模的数据交互很难实现,例如图片在本地处理后,将图片传递给H5



总结

Hybrid方案更加适合跟本地资源交互不是不少,而后主要之内容展现为主的App。在天猫App中,大量地采用了JS Bridge的方式来让H5跟Native作交互,由于天猫App是一个之内容展现为主的App,且营销活动多,周期短,比较适合Hybrid。




React-Native

 

严格来讲,React-Native应当放到Hybrid那一节去讲,单独拎出来的缘由是Facebook自从放出React-Native以后,业界讨论得很是激烈。天猫的鬼道也作了很是多的关于React-Native的分享。

React-Native这个框架比较特殊,它展现View的方式依然是Native的View,而后也是能够经过URL的方式来动态生成View。并且,React-Native也提供了一个Bridge通道来作Javascript和Objective-C之间的交流,仍是很贴心的。

然而研究了一下发现有一个比较坑的地方在于,解析JS要生成View时所须要的View,是要本地可以提供的。举个例子,好比你要有一个特定的Mapview,而且要响应对应的delegate方法,在React-Native的环境下,你须要先在Native提供这个Mapview,而且本身实现这些delegate方法,在实现完方法以后经过Bridge把数据回传给JS端,而后从新渲染。

在这种状况下咱们就能发现,其实React-Native在使用View的时候,这些View是要通过本地定制的,而且将相关方法经过RCT_EXPORT_METHOD暴露给js,js端才能正常使用。在我看来,这里在必定程度上限制了动态部署时的灵活性,好比咱们须要在某个点击事件中展现一个动画或者一个全新的view,因为本地没有实现这个事件或没有这个view,React-Native就显得捉襟见肘。



优势

 

  1. 响应速度很快,只比Native慢一点,比webview快不少。
  2. 可以作到必定程度上的动态部署



缺点

 

  1. 组装页面的元素须要Native提供支持,必定程度上限制了动态部署的灵活性。



总结

 

因为React-Native框架中,由于View的展现和View的事件响应分属于不一样的端,展现部分的描述在JS端,响应事件的监听和描述都在Native端,经过Native转发给JS端。因此,从作动态部署的角度上讲,React-Native只能动态部署新View,不能动态部署新View对应的事件。固然,React-Native自己提供了不少基础组件,然而这个问题仍然仍是会限制动态部署的灵活性。由于咱们在动态部署的时候,大部分状况下是但愿View和事件响应一块儿改变的。

另一个问题就在于,View的原型须要从Native中取,这个问题相较于上面一个问题却是显得不那么严重,只是之后某个页面须要添加某个复杂的view的时候,须要从现有的组件中拼装罢了。

因此,React-Native事实上解决的是如何不使用Objc/Swift来写iOS App的View的问题,对于如何经过不发版来给已发版的App更新功能这样的问题,帮助有限。




Lua Patch

 

大众点评的屠毅敏同窗在基于wax的基础上写了waxPatch,这个工具的主要原理是经过lua来针对objc的方法进行替换,因为lua自己是解释型语言,能够经过动态下载获得,所以具有了必定的动态部署能力。然而iOS系统原生并不提供lua的解释库,因此须要在打包时把lua的解释库编译进app。



优势

 

  1. 可以经过下载脚本替换方法的方式,修改本地App的行为。
  2. 执行效率较高



缺点

 

  1. 对于替换功能来讲,lua是很不错的选择。但若是要添加新内容,实际操做会很复杂
  2. 很容易改错,小问题变成大问题



总结

lua的解决方案在必定程度上解决了动态部署的问题。实际操做时,通常不使用它来作新功能的动态部署,主要仍是用于修复bug时代码的动态部署。实际操做时须要注意的另一点是,真的很容易改错,尤为是你那个方法特别长的时候,因此改了以后要完全回归测试一次。




Javascript Patch

 

这个工做原理其实跟上面说的lua那套方案的工做原理同样,只不过是用javascript实现。并且最近新出了一个JSPatch这个库,至关好用。



优势

 

  1. 同Lua方案的优势
  2. 打包时不用将解释器也编译进去,iOS自带JavaScript的解释器,只不过要从iOS7.0之后才支持。



缺点

 

  1. 同Lua方案的缺点



总结

 

在对app打补丁的方案中,目前我更倾向于使用JSPatch的方案,在可以完成Lua作到的全部事情的同时,还不用编一个JS解释器进去,并且会javascript的人比会lua的人多,技术储备比较好作。




JSON Descripted View

 

其实这个方案的原理是这样的:使用JSON来描述一个View应该有哪些元素,以及元素的位置,以及相关的属性,好比背景色,圆角等等。而后本地有一个解释器来把JSON描述的View生成出来。

这跟React-Native有点儿像,一个是JS转Native,一个是JSON转Native。可是一样有的问题就是事件处理的问题,在事件处理上,React-Native作得相对更好。由于JSON不可以描述事件逻辑,因此JSON生成的View所须要的事件处理都必需要本地事先挂好。



优势

 

  1. 可以自由生成View并动态部署



缺点

 

  1. 天猫实际使用下来,发现仍是存在必定的性能问题,不够快
  2. 事件须要本地事先写好,没法动态部署事件



总结

 

其实JSON描述的View比React-Native的View有个好处就在于对于这个View而言,不须要本地也有一套对应的View,它能够依据JSON的描述来本身生成。然而对于事件的处理是它的硬伤,因此JSON描述View的方案,通常比较适用于换肤,或者固定事件不一样样式的View,好比贴纸。




架构模式

 

其实咱们要作到动态部署,至少要知足如下需求:

  1. View和事件都要可以动态部署
  2. 功能完整
  3. 便于维护

 

我更加倾向于H5和Native以JSBridge的方式链接的方案进行动态部署,在cocoapods里面也有蛮多的JSBridge了。看了一圈以后,我仍是选择写了一个CTJSBridge,来知足动态部署和后续维护的需求。关于这个JSBridge的使用中的任何问题和需求,均可以在评论区向我提出来。接下来的内容,会主要讨论如下这些问题:

  1. 为何不是React-Native或其它方案?
  2. 采用什么样的架构模式才是使用JSBridge的最佳实践?



为何不是React-Native或其余方案?

 

首先针对React-Native来作解释,前面已经分析到,React-Native有一个比较大的局限在于View须要本地提供。假设有一个页面的组件是跑马灯,若是本地没有对应的View,使用React-Native就显得很麻烦。然而一样的状况下,HTML5可以很好地实现这样的需求。这里存在一个这样的取舍在性能和动态部署View及事件之间,选择哪个?

我更加倾向于可以动态部署View和事件,至少后者是可以完成需求的,性能再好,难以完成需求其实没什么意义。然而对于HTML5的Hybrid和纯HTML5的web app之间,也存在一个相同的取舍,可是还要额外考虑一个新的问题,纯HTML5可以使用到的设备提供的功能相对有限,JSBridge可以将部分设备的功能以Native API的方式交付给页面,所以在考虑这个问题以后,选择HTML5的Hybrid方案就显得理所应当了。

在诸多Hybrid方案中,除了JSBridge以外,其它的方案都显得相对过于沉重,对于动态部署来讲,其实须要补充的软肋就是提供本地设备的功能,其它的反而显得较为累赘。



基于JSBridge的微服务架构模式

 

我开发了一个,基于JSBridge的微服务架构差很少是这样的:

 

                                 -------------------------
                                 |                       |
                                 |         HTML5         |
                                 |                       |
                                 | View + Event Response |
                                 |                       |
                                 -------------------------
                                             |
                                             |
                                             |
                                          JSBridge
                                             |
                                             |
                                             |
        ------------------------------------------------------------------------------
        |                                                                            |
        |   Native                                                                   |
        |                                                                            |
        |  ------------   ------------   ------------   ------------   ------------  |
        |  |          |   |          |   |          |   |          |   |          |  |
        |  | Service1 |   | Service2 |   | Service3 |   | Service4 |   |    ...   |  |
        |  |          |   |          |   |          |   |          |   |          |  |
        |  ------------   ------------   ------------   ------------   ------------  |
        |                                                                            |
        |                                                                            |
        ------------------------------------------------------------------------------

 

解释一下这种架构背后的思想:

由于H5和Native之间可以经过JSBridge进行交互,然而JSBridge的一个特征是,只能H5主动发起调用。因此理所应当地,被调用者为调用者提供服务。

另一个想要处理的问题是,但愿可以经过微服务架构,来把H5和Native各自的问题域区分开。所谓区分问题域就是让H5要解决的问题和Native要解决的问题之间,交集最小。所以,咱们设计时但愿H5的问题域可以更加偏重业务,而后Native为H5的业务提供基础功能支持,例如API的跨域调用,传感器设备信息以及本地已经沉淀的业务模块均可以做为Native提供的服务交给H5去使用。H5的快速部署特性特别适合作重业务的事情,Native对iPhone的功能调用能力和控制能力特别适合将其封装成服务交给H5调用。

因此这对Native提供的服务有两点要求:

 

  1. Native提供的服务不该当是强业务相关的,最好是跟业务无关,这样才能方便H5进行业务的组装
  2. 若是Native必定要提供强业务相关的服务,那最好是一个完整业务,这样H5就能比较方便地调用业务模块。

 

只要Native提供的服务符合上述两个条件,HTML5在实现业务的时候,束缚就会很是少,也很是容易管理。

 


 

而后这种方案也会有必定的局限性,就是若是Native没有提供这样的服务,那仍是必须得靠发版来解决。等于就是Native向HTML5提供API,这其实跟服务端向Native提供API的道理同样。

但基于Native提供的服务的通用性这点来看,添加服务的需求不会特别频繁,每个App都有属于本身的业务领域,在同一个业务领域下,其实须要Native提供的服务是有限的。而后结合JSPatch提供的动态patch的能力,这样的架构可以知足绝大部分动态部署的需求。

而后随着App的不断迭代,某些HTML5的实现实际上是能够逐步沉淀为Native实现的,这在必定程度上,下降了App早期的试错成本。




基于动态库的微内核模式

 

我开发了CTDynamicLibKit这个库来解决动态库的调用问题,其实原先的打算是拿动态库作动态部署的,不过我用@念纪 的我的App把这个功能塞进去以后,发现苹果仍是能审核经过的,可是下载下来的动态库是没法加载的。报错以下:

 

error:Error Domain=NSCocoaErrorDomain Code=3587 "The bundle “DynamicLibDemo” couldn’t be loaded because it is damaged or missing necessary resources." (dlopen_preflight(/var/mobile/Containers/Data/Application/61D3BF00-CF8D-4157-A87C-D999905E9040/Library/DynamicLibDemo1.framework/DynamicLibDemo): no suitable image found. Did find: /var/mobile/Containers/Data/Application/61D3BF00-CF8D-4157-A87C-D999905E9040/Library/DynamicLibDemo1.framework/DynamicLibDemo: code signature invalid for '/var/mobile/Containers/Data/Application/61D3BF00-CF8D-4157-A87C-D999905E9040/Library/DynamicLibDemo1.framework/DynamicLibDemo'  ) UserInfo=0x174260b80 {NSLocalizedFailureReason=The bundle is damaged or missing necessary resources., NSLocalizedRecoverySuggestion=Try reinstalling the bundle., NSFilePath=/var/mobile/Containers/Data/Application/61D3BF00-CF8D-4157-A87C-D999905E9040/Library/DynamicLibDemo1.framework/DynamicLibDemo, NSDebugDescription=dlopen_preflight(/var/mobile/Containers/Data/Application/61D3BF00-CF8D-4157-A87C-D999905E9040/Library/DynamicLibDemo1.framework/DynamicLibDemo): no suitable image found. Did find:  /var/mobile/Containers/Data/Application/61D3BF00-CF8D-4157-A87C-D999905E9040/Library/DynamicLibDemo1.framework/DynamicLibDemo: code signature invalid for '/var/mobile/Containers/Data/Application/61D3BF00-CF8D-4157-A87C-D999905E9040/Library/DynamicLibDemo1.framework/DynamicLibDemo' , NSBundlePath=/var/mobile/Containers/Data/Application/61D3BF00-CF8D-4157-A87C-D999905E9040/Library/DynamicLibDemo1.framework, NSLocalizedDescription=The bundle DynamicLibDemocouldnt be loaded because it is damaged or missing necessary resources.} 

 

主要缘由是由于签名没法经过。由于Distribution的App只能加载相同证书打包的framework。在in house和develop模式下,能够使用相同证书既打包App又打包framework,因此测试的时候没有问题。可是在正式的distribution下,这种作法是行不通的。

因此就目前看来,基于动态库的动态部署方案是没办法作到的。




总结

 

我在文中针对业界常见的动态部署方案作了一些总结,而且提供了我本身认为的最佳解决方案以及对应的JSBridge实现。文中提到的方案我已经尽量地作到了全面,若是还有什么我遗漏没写的,你们能够在评论区指出,我把它补上去。





评论系统我用的是Disqus,不按期被墙。因此若是你看到文章下面没有加载出评论列表,翻个墙就有了。




本文遵照CC-BY。 请保持转载后文章内容的完整,以及文章出处。本人保留全部版权相关权利。

个人博客拒绝挂任何广告,若是您以为文章有价值,能够经过支付宝扫描下面的二维码捐助我。

相关文章
相关标签/搜索