嗯,大家要的大招。跟着這篇文章一塊兒也發佈了CTPersistance和CTJSBridge這兩個庫,但願你們在實際使用的時候若是遇到問題,就給我提issue或者PR或者評論區。每個issue和PR以及評論我都會回覆的。javascript
持久化方案無論是服務端仍是客戶端,都是一個很是值得討論的話題。尤爲是在服務端,持久化方案的優劣每每都會在必定程度上影響到產品的性能。然而在客戶端,只有爲數很少的業務需求會涉及持久化方案,並且在大多數狀況下,持久化方案對性能的要求並非特別苛刻。因此我在移動端這邊作持久化方案設計的時候,考慮更多的是方案的可維護和可拓展,而後在此基礎上纔是性能調優。這篇文章中,性能調優不會單獨開一節來說,而會穿插在各個小節中,你們有心的話能夠重點看一下。html
持久化方案對整個App架構的影響和網絡層方案對整個架構的影響相似,通常都是致使整個項目耦合度高的罪魁禍首。而我也是一如既往的去Model化
的實踐者,在持久層去Model化的過程當中,我引入了Virtual Record
的設計,這個在文中也會詳細描述。java
這篇文章主要講如下幾點:ios
另外,針對數據庫存儲這一塊,我寫了一個CTPersistance,這個庫目前可以完成大部分的持久層需求,同時也是個人Virtual Record
這種設計思路的一個樣例。這個庫能夠直接被cocoapods引入,但願你們使用的時候,可以多給我提issue。這裏是CTPersistance Class Reference。git
在有須要持久化需求的時候,咱們有很是多的方案可供選擇:NSUserDefault、KeyChain、File,以及基於數據庫的無數子方案。所以,當有須要持久化的需求的時候,咱們首先考慮的是應該採用什麼手段去進行持久化。github
NSUserDefaultweb
通常來講,小規模數據,弱業務相關數據,均可以放到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一下就有了,我就不浪費筆墨了。因爲大多數衍生複雜需求都是經過採用基於數據庫的持久化方案去知足,因此這篇文章的重點就數據庫相關的架構方案設計和實現。若是文章中有哪些問題我沒有寫到的,你們能夠在評論區提問,我會一一解答或者直接把遺漏的內容補充在文章中。
在設計持久層架構的時候,咱們要關注如下幾個方面的隔離:
在具體講持久層下數據的處理以前,我以爲須要針對這個問題作一個完整的分析。
在View層設計中我分別提到了胖Model
和瘦Model
的設計思路,並且告訴你們我更加傾向於胖Model
的設計思路。在網絡層設計裏面我使用了去Model化
的思路設計了APIMananger與業務層的數據交互。這兩個看似矛盾的關於Model
的設計思路在我接下來要提出的持久層方案中實際上是並不矛盾,並且是相互配合的。在網絡層設計這篇文章中,我對去Model化
只給出了思路和作法,相關的解釋並很少,是由於要解釋這個問題涉及面會比較廣,寫的時候並不認爲在那篇文章裏作解釋是最好的時機。因爲持久層在這裏胖Model
和去Model化
都會涉及,因此我以爲在講持久層的時候解釋這個話題會比較好。
我在跟別的各類領域的架構師交流的時候,發現你們都會或多或少地混用Model
和Model Layer
的概念,而後每每致使你們討論的問題最後都不在一個點上,說Model
的時候他跟你說Model Layer
,那好吧,我就跟你說Model Layer
,結果他又在說Model
,因而問題就討論不下去了。我以爲做爲架構師,若是不分清楚這兩個概念,確定是會對你設計的架構的質量有很大影響的。
若是把Model
說成Data Model
,而後跟Model Layer
放在一塊兒,這樣就可以很容易區分概念了。
Data Model
這個術語針對的問題領域是業務數據的建模,以及代碼中這一數據模型的表徵方式。二者相輔相承:由於業務數據的建模方案以及業務自己特色,而最終決定了數據的表徵方式。一樣操做一批數據,你的數據建模方案基本都是細化業務問題以後,抽象得出一個邏輯上的實體。在實現這個業務時,你能夠選擇不一樣的表徵方式來表徵這個邏輯上的實體,好比字節流
(TCP包等),字符串流
(JSON、XML等),對象流
。對象流又分通用數據對象
(NSDictionary等),業務數據對象
(HomeCellModel等)。
前面已經遍歷了全部的Data Model
的形式。在習慣上,當咱們討論Model化
時,都是單指對象流
中的業務數據對象
這一種。然而去Model化
就是指:更多地使用通用數據對象
去表徵數據,業務數據對象
不會在設計時被優先考慮的一種設計傾向。這裏的通用數據對象能夠在某種程度上理解爲範型。
Model Layer
描述的問題領域是如何對數據進行增刪改查(CURD, C
reate U
pdate R
ead D
elete),和相關業務處理。通常來講若是在Model Layer
中採用瘦Model
的設計思路的話,就差很少到CURD爲止了。胖Model
還會關心如何爲須要數據的上層提供除了增刪改查之外的服務,併爲他們提供相應的解決方案。例如緩存、數據同步、弱業務處理等。
我更加傾向於去Model化
的設計,在網絡層我設計了reformer
來實現去Model化。在持久層,我設計了Virtual Record
來實現去Model化。
由於具體的Model是一種很容易引入耦合的作法,在儘量弱化Model概念的同時,就可以爲引入業務和對接業務提供充分的空間。同時,也能經過去Model的設計達到區分強弱業務的目的,這在未來的代碼遷移和維護中,是相當重要的。不少設計很差的架構,就在於架構師並無認識到區分強弱業務的重要性,因此就致使架構腐化的速度很快,愈來愈難維護。
因此說回來,持久層與業務層之間的隔離,是經過強弱業務的隔離達到的。而Virtual Record
正是由於這種去Model化的設計,從而達到了強弱業務的隔離,進而作到持久層與業務層之間既隔離同時又能交互的平衡。具體Virtual Record
是什麼樣的設計,我在後面會給你們分析。
在網站的架構中,對數據庫進行讀寫分離主要是爲了提升響應速度。在iOS應用架構中,對持久層進行讀寫隔離的設計主要是爲了提升代碼的可維護性。這也是兩個領域要求架構師在設計架構時要求側重點不一樣的一個方面。
在這裏咱們所謂的讀寫隔離並非指將數據的讀操做和寫操做作隔離。而是以某一條界限爲準,在這個界限之外的全部數據模型,都是不可寫不可修改,或者修改屬性的行爲不影響數據庫中的數據。在這個界限之內的數據是可寫可修改的。通常來講咱們在設計時劃分的這個界限會和持久層與業務層之間的界限保持一致,也就是業務層從持久層拿到數據以後,都不可寫不可修改,或業務層針對這一數據模型的寫操做、修改操做都對數據庫文件中的內容不產生做用。只有持久層中的操做纔可以對數據庫文件中的內容產生做用。
在蘋果官方提供的持久層方案Core Data的架構設計中,並無針對讀寫做出隔離,數據的結果都是以NSManagedObject
扔出。因此只要業務工程師稍微一不當心動一下某個屬性,NSManagedObjectContext
在save的時候就會把這個修改給存進去了。另外,當咱們須要對全部的增刪改查操做作AOP的切片時,Core Data技術棧的實現就會很是複雜。
總體上看,我以爲Core Data相對大部分需求而言是過分設計了。我當時設計安居客聊天模塊的持久層時就採用了Core Data,而後爲了讀寫隔離,將全部扔出來的NSManagedObject
都轉爲了普通的對象。另外,因爲聊天記錄的業務至關複雜,使用Core Data以後爲了完成需求不得不引入不少Hack的手段,這種作法在必定程度上下降了這個持久層的可維護性和提升了接手模塊的工程師的學習曲線,這是不太好的。在天貓客戶端,我去的時候天貓這個App就已經屬於基本毫無持久層可言了,比較混亂。只能依靠各個業務線各顯神通去解決數據持久化的需求,難以推進統一的持久層方案,這對於項目維護尤爲是跨業務項目合做來講,基本就和車禍現場沒啥區別。我如今已經從天貓離職,讀者中如果有阿里人想升職想刷存在感拿3.75的,能夠考慮給天貓搞個統一的持久層方案。
讀寫隔離還可以便於加入AOP切點,由於針對數據庫的寫操做被隔離到一個固定的地方,加AOP時就很容易在正確的地方放入切片。這個會在講到數據同步方案時看到應用。
Core Data
Core Data要求在多線程場景下,爲異步操做再生成一個NSManagedObjectContext
,而後設置它的ConcurrencyType
爲NSPrivateQueueConcurrencyType
,最後把這個Context的parentContext設爲Main線程下的Context。這相比於使用原始的SQLite去作多線程要輕鬆許多。只不過要注意的是,若是要傳遞NSManagedObject
的時候,不能直接傳這個對象的指針,要傳NSManagedObjectID
。這屬於多線程環境下對象傳遞的隔離,在進行架構設計的時候須要注意。
SQLite
純SQLite其實對於多線程卻是直接支持,SQLite庫提供了三種方式:Single Thread
,Multi Thread
,Serialized
。
Single Thread
模式不是線程安全的,不提供任何同步機制。Multi Thread
模式要求database connection
不能在多線程中共享,其餘的在使用上就沒什麼特殊限制了。Serialized
模式顧名思義就是由一個串行隊列來執行全部的操做,對於使用者來講除了響應速度會慢一些,基本上就沒什麼限制了。大多數狀況下SQLite的默認模式是Serialized
。
根據Core Data在多線程場景下的表現,我以爲Core Data在使用SQLite做爲數據載體時,使用的應該就是Multi Thread
模式。SQLite在Multi Thread
模式下使用的是讀寫鎖,並且是針對整個數據庫加鎖,不是表鎖也不是行鎖,這一點須要提醒各位架構師注意。若是對響應速度要求很高的話,建議開一個輔助數據庫,把一個大的寫入任務先寫入輔助數據庫,而後拆成幾個小的寫入任務見縫插針地隔一段時間往主數據庫中寫入一次,寫完以後再把輔助數據庫刪掉。
不過從實際經驗上看,本地App的持久化需求的讀寫操做通常都不會大,只要注意好幾個點以後通常都不會影響用戶體驗。所以相比於Multi Thread
模式,Serialized
模式我認爲是性價比比較高的一種選擇,代碼容易寫容易維護,性能損失不大。爲了提升幾十毫秒的性能而犧牲代碼的維護性,我是以爲划不來的。
這是最容易被忽視的一點,數據表達和數據操做的隔離是否可以作好,直接影響的是整個程序的可拓展性。
長久以來,咱們都很習慣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時,他們幾乎就是如出一轍。在這裏要作好針對數據表或是針對數據的映射要作的區分的關鍵要點是:這個映射對象的操做着手點相對數據表而言,是對內仍是對外操做。若是是對內操做,那麼這個操做範圍就僅限於當前數據表,這些操做映射給數據表模型就比較合適。若是是對外操做,執行這些操做時有可能涉及其餘的數據表,那麼這些操做就不該該映射到數據表對象中。
所以實際操做中,我是以數據表爲單位去針對操做進行對象封裝,而後再針對數據記錄進行對象封裝。數據表中的操做都是針對記錄的普通增刪改查操做,都是弱業務邏輯。數據記錄僅僅是數據的表達方式,這些操做最好交付給數據層分管強業務的對象去執行。具體內容我在下文還會繼續說。
說到這裏,就不得不說CTPersistance和Virtual 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去調度。好比分化成SettingsDataCenter
,ChatRoomDataCenter
,ProfileDataCenter
等,另外要要注意的是,幾個DataCenter之間最好不要有業務重疊。若是一個DataCenter的業務實在是大,那就再拆分紅幾個小業務。若是單個小業務都很大了,那就拆成各個Category,具體的作法能夠參考個人框架中CTPersistanceTable
和CTPersistanceQueryCommand
的實踐。
這麼一來,若是要遷移涉及持久層的強業務,那就只須要遷移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
是如何發揮虛擬
的優點去針對不一樣場景進行交互的。
通常來講,具備持久層的App同時都會附帶着有版本遷移的需求。當一個用戶安裝了舊版本的App,此時更新App以後,若數據庫的表結構須要更新,或者數據自己須要批量地進行更新,此時就須要有版本遷移機制來進行這些操做。然而版本遷移機制又要兼顧跨版本的遷移需求,因此基本上大方案也就只有一種:創建數據庫版本節點,遷移的時候一個一個跑過去。
數據遷移事實上實現起來仍是比較簡單的,作好如下幾點問題就不大了:
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通常都會採用UUID字符串。
isDirty主要是針對數據的插入和修改進行標識。當本地新生成數據或者更新數據以後,收到服務器的確認返回以前,isDirty置爲YES。當服務器的確認包返回以後,再根據包裏提供的identifier找到這條數據,而後置爲NO。這樣就完成了數據的同步。
然而這只是簡單的場景,有一種比較極端的狀況在於,當請求發起到收到請求回覆的這短短几秒間,用戶又修改了數據。若是按照當前的邏輯,在收到請求回覆以後,這個又修改了的數據的isDirty會被置爲NO,因而這個新的修改就永遠沒法同步到服務器了。這種極端狀況的簡單處理方案就是在發起請求到收到回覆期間,界面上不容許用戶進行修改。
若是但願作得比較細緻,在發送同步請求期間依舊容許用戶修改的話,就須要在數據庫額外增長一張DirtyList
來記錄這些操做,這個表裏至少要有兩個字段:identifier
,primaryKey
。而後每一次操做都分配一次identifier,那麼新的修改操做就有了新的identifier。在進行同步時,根據primaryKey
找到原數據表裏的那條記錄,而後把數據連同identifier交給服務器。而後在服務器的確認包回來以後,就只要拿出identifier再把這條操做記錄刪掉便可。這個表也能夠直接服務於多個表,只是還須要額外添加一個tablename
字段,方便發起同步請求的時候可以找獲得數據。
當有數據同步的需求的時候,刪除操做就不能是簡單的物理刪除了,而只是邏輯刪除,所謂邏輯刪除就是在數據庫裏把這條記錄的isDeleted記爲YES,只有當服務器的確認包返回以後,纔會真正把這條記錄刪除。isDeleted和isDirty的區別在於:收到確認包後,返回的identifier指向的數據若是是isDeleted,那麼就要刪除這條數據,若是指向的數據只是新插入的數據和更新的數據,那麼就只要修改狀態就行。插入數據和更新數據在收到數據包以後作的操做是相同的,因此就用isDirty來區分就足夠了。總之,這是根據收到確認包以後的操做不一樣而作的區分。二者都要有,缺一不可。
在我看到的不少其它數據同步方案中,並無提供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爲準,這要比以時間爲準更好,並且實現起來其實也並無增長太多複雜度。
要注意的點
在使用表去記錄更新操做的時候,短期以內頗有可能針對同一條數據進行屢次更新操做。所以在同步以前,最好可以合併這些相同數據的更新操做,能夠節約服務器的計算資源。固然若是你服務器強大到不行,那就無所謂了。
雙向數據同步多見於筆記類、日程類應用。對於一臺設備來講,不光本身會往上推數據同步的信息,本身也會問服務器主動索取數據同步的信息,因此稱之爲雙向數據同步。
舉個例子:當一臺設備生成了某時間段的數據以後,到了另一臺設備上,又修改了這些舊的歷史數據。此時再回到原來的設備上,這臺設備就須要主動問服務器索取是否舊的數據有修改,若是有,就要把這些操做下載下來同步到本地。
雙向數據同步實現上會比單向數據同步要複雜一些,並且有的時候還會存在實時同步的需求,好比協同編輯。因爲自己方案就比較複雜,另一定要兼顧業務工程師的上手難度(這主要看你這個架構師的良心),因此要實現雙向數據同步方案的話,仍是頗有意思比較有挑戰的。
這個其實在單向數據同步時多少也涉及了一點,可是因爲單向數據同步的要求並不複雜,只要告訴服務器是什麼數據而後要作什麼事情就能夠了,卻是不必將這種操做封裝。在雙向數據同步時,你也得解析數據操做,因此互相之間要約定一個協議,經過封裝這個協議,就作到了針對操做對象的封裝。
這個協議應當包括:
分別解釋一下這6項的意義:
- 操做的惟一標識
這個跟單向同步方案時的做用同樣,也是在收到服務器的確認包以後,可以使得本地應用找到對應的操做並執行確認處理。
- 數據的惟一標識
在找到具體操做的時候執行確認邏輯的處理時,都會涉及到對象自己的處理,更新也好刪除也好,都要在本地數據庫有所體現。因此這個標識就是用於找到對應數據的。
- 操做的類型
操做的類型就是Delete
,Update
,Insert
,對應不一樣的操做類型,對本地數據庫執行的操做也會不同,因此用它來進行標識。
- 具體的數據
當更新的時候有Update
或者Insert
操做的時候,就須要有具體的數據參與了。這裏的數據有的時候不見得是單條的數據內容,有的時候也會是批量的數據。好比把全部10月1日以前的任務都標記爲已完成狀態。所以這裏具體的數據如何表達,也須要定一個協議,何時做爲單條數據的內容去執行插入或更新操做,何時做爲批量的更新去操做,這個本身根據實際業務需求去定義就行。
- 操做的依賴標識
跟前面提到的依賴標識同樣,是爲了防止先發的包後到後發的包先到這種極端狀況。
- 用戶執行這項操做的時間戳
因爲跨設備,又由於舊數據也會被更新,所以在必定程度上就會出現衝突的可能。操做數據在從服務器同步下來以後,會存放在一個新的表中,這個表就是待操做
數據表,在具體執行這些操做的同時會跟待同步
的數據表中的操做數據作比對。若是是針對同一條數據的操做,且這兩個操做存在衝突,那麼就以時間戳來決定如何執行。還有一種作法就是直接提交到界面告知用戶,讓用戶作決定。
前面已經部分提到這一點了。從服務器拉下來的同步操做列表,咱們存在待執行
數據表中,操做完畢以後若是有告知服務器的需求,那就等因而走單向同步方案告知服務器。在執行過程當中,這些操做也要跟待同步
數據表進行匹配,看有沒有衝突,沒有衝突就繼續執行,有衝突的話要麼按照時間戳執行,要麼就告知用戶讓用戶作決定。在拉取待執行
操做列表的時候,也要把最後一次操做的identifier丟給服務器,這樣服務器才能返回相應數據。
待同步
數據表的做用其實也跟單向同步方案時候的做用相似,就是防止在發送請求的時候用戶有操做,同時也是爲解決衝突提供方便。在發起同步請求以前,咱們都應該先去查詢有沒有待執行
的列表,當待執行
的操做列表同步完成以後,就能夠刪除裏面的記錄了,而後再把本地待同步
的數據交給服務器。同步完成以後就能夠把這些數據刪掉了。所以在正常狀況下,只有在待操做
和待執行
的操做間會存在衝突。有些從道理上講也算是衝突的事情,好比獲取待執行
的數據比較晚,但其中又和待同步
中的操做有衝突,像這種極端狀況咱們其實也無解,只能由他去,不過這種狀況也是屬於比較極端的狀況,發生概率不大。
待執行
操做推送過來待執行
,待同步
數據表記錄要執行的操做和要同步的操做我也見過有的方案是直接把SQL丟出去進行同步的,我不建議這麼作。最好仍是將操做和數據分開,而後細化,不然檢測衝突的時候你就得去分析SQL了。要是這種實現中有什麼bug,解這種bug的時候就要考慮先後兼容問題,機制重建成本等,由於貪圖一時偷懶,到最後其實得不償失。
這篇文章主要是基於CTPersistance講了一下如何設計持久層的設計方案,以及數據遷移方案和數據同步方案。
着重強調了一下各類持久層方案在設計時要考慮的隔離,以及提出了Virtual Record
這個設計思路,並對它作了一些解釋。而後在數據遷移方案設計時要考慮的一些點。在數據同步方案這一節,分開講了單向的數據同步方案和雙向的數據同步方案的設計,然而具體實現仍是要依照具體的業務需求來權衡。
但願你們以爲這些內容對各自工做中遇到的問題可以有所價值,若是有問題,歡迎在評論區討論。
另外,關於動態部署方案,其實直到今天在iOS領域也並無特別好的動態部署方案能夠拿出來,我以爲最靠譜的其實仍是H5和Native的Hybrid方案。React Native在我看來相比於Hybrid仍是有比較多的限制。關於Hybrid方案,我也提供了CTJSBridge這個庫去實現這方面的需求。在動態部署方案這邊其實成文已經好久,遲遲不發的緣由仍是由於以爲當時並無什麼銀彈能夠解決iOS App的動態部署,另外也有一些問題沒有考慮清楚。當初想到的那些問題如今我已經確認無解。當初寫的動態部署方案我一直認爲它沒法做爲一個單獨的文章發佈出來,因此我就把這篇文章也放在這裏,權當給各位參考。
這裏討論的動態部署方案,就是指經過不發版的方式,將新的內容、新的業務流程部署進已發佈的App。由於蘋果的審覈週期比較長,並且蘋果的限制比較多,業界在這裏也沒有特別多的手段來達到動態部署方案的目的。這篇文章主要的目的就是給你們列舉一下目前業界作動態部署的手段,以及其對應的優缺點。而後給出一套我比較傾向於使用的方案。
其實單純就動態部署方案來說,沒什麼太多花頭能夠說的,就是H五、Lua、JS、OC/Swift這幾門基本技術的各類組合排列。寫到後面以爲,動態部署方案實際上是很是好的用於講解某些架構模式的背景。通常咱們經驗總結下來的架構模式包括但不限於:
Layered Architecture
Event-Driven Architecture
Microkernel Architecture
Microservices Architecture
Space-Based Architecture
我在開篇裏面提到的MVC等方案跟這篇文章中要提到的架構模式並非屬於同一個維度的。比較容易混淆的就是容易把MVC這些方案跟Layered Architecture
混淆,這個我在開篇這篇文章裏面也作過了區分:MVC等方案比較側重於數據流動方向的控制和數據流的管理。Layered Architecture
更加側重於各分層之間的功能劃分和模塊協做。
另外,上述五種架構模式在Software Architecture Patterns這本書裏有很是詳細的介紹,整本書才45頁,個把小時就看完了,很是值得看和思考。本文後半篇涉及的架構模式是以上架構模式的其中兩種:Microkernel Architecture
和Microservices Architecture
。
最後,文末還給出了其餘一些關於架構模式的我以爲還不錯的PPT和論文,裏面對架構模式的分類和總結也比較多樣,跟Software Architecture Patterns
的總結也有些許不同的地方,能夠博採衆長。
其實所謂的web app,就是經過手機上的瀏覽器進行訪問的H5頁面。這個H5頁面是針對移動場景特別優化的,好比UI交互等。
web app通常是創業初期會重點考慮的方案,由於迭代很是快,並且創業初期的主要目標是須要驗證模式的正確性,並不在於提供很是好的用戶體驗,只須要完成閉環便可。早年facebook曾經嘗試過這種方案,最後由於用戶體驗的問題而宣佈放棄。因此這個方案只能做爲過渡方案,或者當App不可用時,做爲降級方案使用。
經過市面上各類Hybrid框架,來作H5和Native的混合應用,或者經過JS Bridge來作到H5和Native之間的數據互通。
Hybrid方案更加適合跟本地資源交互不是不少,而後主要之內容展現爲主的App。在天貓App中,大量地採用了JS Bridge的方式來讓H5跟Native作交互,由於天貓App是一個之內容展現爲主的App,且營銷活動多,週期短,比較適合Hybrid。
嚴格來講,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就顯得捉襟見肘。
因爲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更新功能
這樣的問題,幫助有限。
大衆點評的屠毅敏同窗在基於wax的基礎上寫了waxPatch,這個工具的主要原理是經過lua來針對objc的方法進行替換,因爲lua自己是解釋型語言,能夠經過動態下載獲得,所以具有了必定的動態部署能力。然而iOS系統原生並不提供lua的解釋庫,因此須要在打包時把lua的解釋庫編譯進app。
lua的解決方案在必定程度上解決了動態部署的問題。實際操做時,通常不使用它來作新功能的動態部署,主要仍是用於修復bug時代碼的動態部署。實際操做時須要注意的另一點是,真的很容易改錯,尤爲是你那個方法特別長的時候,因此改了以後要完全迴歸測試一次。
這個工做原理其實跟上面說的lua那套方案的工做原理同樣,只不過是用javascript實現。並且最近新出了一個JSPatch這個庫,至關好用。
在對app打補丁的方案中,目前我更傾向於使用JSPatch的方案,在可以完成Lua作到的全部事情的同時,還不用編一個JS解釋器進去,並且會javascript的人比會lua的人多,技術儲備比較好作。
其實這個方案的原理是這樣的:使用JSON來描述一個View應該有哪些元素,以及元素的位置,以及相關的屬性,好比背景色,圓角等等。而後本地有一個解釋器來把JSON描述的View生成出來。
這跟React-Native有點兒像,一個是JS轉Native,一個是JSON轉Native。可是一樣有的問題就是事件處理的問題,在事件處理上,React-Native作得相對更好。由於JSON不可以描述事件邏輯,因此JSON生成的View所須要的事件處理都必需要本地事先掛好。
其實JSON描述的View比React-Native的View有個好處就在於對於這個View而言,不須要本地也有一套對應的View,它能夠依據JSON的描述來本身生成。然而對於事件的處理是它的硬傷,因此JSON描述View的方案,通常比較適用於換膚,或者固定事件不一樣樣式的View,好比貼紙。
其實咱們要作到動態部署,至少要知足如下需求:
我更加傾向於H5和Native以JSBridge的方式鏈接的方案進行動態部署,在cocoapods裏面也有蠻多的JSBridge了。看了一圈以後,我仍是選擇寫了一個CTJSBridge,來知足動態部署和後續維護的需求。關於這個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提供的服務有兩點要求:
只要Native提供的服務符合上述兩個條件,HTML5在實現業務的時候,束縛就會很是少,也很是容易管理。
而後這種方案也會有必定的侷限性,就是若是Native沒有提供這樣的服務,那仍是必須得靠發版來解決。等於就是Native向HTML5提供API,這其實跟服務端向Native提供API的道理同樣。
但基於Native提供的服務的通用性這點來看,添加服務的需求不會特別頻繁,每個App都有屬於本身的業務領域,在同一個業務領域下,其實須要Native提供的服務是有限的。而後結合JSPatch提供的動態patch的能力,這樣的架構可以知足絕大部分動態部署的需求。
而後隨着App的不斷迭代,某些HTML5的實現實際上是能夠逐步沉澱爲Native實現的,這在必定程度上,下降了App早期的試錯成本。