http://blog.cnbang.net/tech/3262/git
通常一個 iOS APP 作的事就是:請求數據->保存數據->展現數據,通常用 Sqlite 做爲持久存儲層,保存從網絡拉取的數據,下次讀取能夠直接從 Sqlite DB 讀取。咱們先忽略從網絡請求數據這一環節,假設數據已經保存在 DB 裏,那咱們要作的事就是,ViewController 從 DB 取數據,再傳給 view 渲染:github
這是最簡單的狀況,隨着程序變複雜,多個 ViewController 都要向 DB 取數據,ViewController自己也會由於數據變化從新去 DB 取數據,會有兩個問題:數據庫
對這裏作優化,天然會想到在 DB 和 VC 層之間再加一層 cache,把從 DB 讀取出來的數據 cache 在內存裏,下次來取一樣的數據就不須要再去磁盤讀取 DB 了。數組
幾乎全部的數據庫框架都作了這個事情,包括微信讀書開源的 GYDataCenter,CoreData,Realm 等。但這樣作會致使一個問題,就是數據的線程安全問題。緩存
按上面的設計,Cache層會有一個集合,持有從DB讀取的數據。安全
除了 VC 層,其餘層也會從cache取數據,例如網絡層。上層拿到的數據都是對 cache 層這裏數據的引用:微信
可能還會在網絡層子線程,或其餘一些用於預加載的子線程使用到,若是某個時候一條子線程對這個 Book1 對象的屬性進行修改,同時主線程在讀這個對象的屬性,就會 crash,由於通常咱們爲了性能會把對象屬性設爲nonatomic,是非線程安全的,多線程讀寫時會有問題:網絡
1
2
3
4
5
6
7
8
|
//Network
WRBook *book = [WRCache bookWithId:@「10000」];
book.fav =
YES
;
//子線程在寫
[book save];
//VC1
WRBook *book = [WRCache bookWithId:@「10000」];
self
.view.title = book.title;
//主線程在讀
|
能夠經過這個測試看到 crash 場景:多線程
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
@interface
TestMultiThread :
NSObject
@property
(
nonatomic
)
NSArray
*arr;
@end
@implementation
TestMultiThread
@end
TestMultiThread *obj = [[TestMultiThread alloc] init];
for
(
int
i = 0; i < 100000; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
id
a = obj.arr;
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
obj.arr = [
NSArray
arrayWithObject:
@"b"
];
});
}
|
對這種狀況,通常有三種解決方案:框架
既然這個對象的屬性是非線程安全的,那加鎖讓它變成線程安全就好了。能夠給每一個對象自定義一個鎖,也能夠直接用 OC 裏支持的屬性指示符 atomic:
1
|
@property
(atomic)
NSArray
*arr;
|
這樣就不用擔憂多線程同時讀寫的問題了。但在APP裏大規模使用鎖極可能會致使出現各類不可預測的問題,鎖競爭,優先級反轉,死鎖等,會讓整個APP複雜性增大,問題難以排查,並非一個好的解決方案。
另外一種方案是一條線程建立一個 cache,每條線程只對這條線程對應的 cache 進行讀寫,這樣就沒有線程安全問題了。CoreData 和 Realm 都是這種作法,但這個方案有兩個缺點:
CoreData 在不一樣線程要建立本身的 NSManagedObjectContext,這個 context 裏維護了本身的 cache,若是某條子線程沒有建立 NSManagedObjectContext,要讀取數據就須要經過 performBlockAndWait:
等接口跑到其餘線程去讀取。若是多個 context 須要同步 cache 數據,就要調用它的 merge 方法,或者經過 parent-children context 層級結構去作。這致使它多線程使用起來很麻煩,API 友好度極低。
Realm 作得好一點,會在線程 runloop 開始執行時自動去同步數據,但若是線程沒有 runloop 就須要手動去調 Realm.refresh()
同步。使用者仍是須要明確知道代碼在哪條線程執行,避免在多線程之間傳遞對象。
咱們的問題是多線程同時讀寫致使,那若是隻讀不寫,是否是就沒有問題了?數據不可變指的就是一個數據對象生成後,對象裏的屬性值不會再發生改變,不容許像上述例子那樣 book.fav = YES
直接設置,若一個對象屬性值變了,那就新建一個對象,直接整個替換掉這個舊的對象:
1
2
3
4
5
6
7
8
9
|
//WRCache
@implementation
WRCache
+(
void
) updateBookWithId:(
NSString
*)bookId params:(
NSDictionary
*)params
{
[WRDBCenter updateBookWithId:@「10000」 params:{@「fav」: @(
YES
)}];
//更新DB數據
WRBook *book = [WRDBCenter readBookWithId:bookId];
//從新從DB讀取,新對象
[
self
.cache setObject:book forKey:bookId];
//整個替換cache裏的對象
}
@end
|
1
2
3
4
|
self
.book = [WRCache bookWithId:@「10000」];
// book.fav = YES; //不這樣寫
[WRCache updateBookWithId:@「10000」 params:{@「fav」: @(
YES
)}];
//在cache裏整個更新
self
.book = [WRCache bookWithId:@「10000」];
//從新讀取對象
|
這樣就不會再有線程安全問題,一旦屬性有修改,就整個數據從新從DB讀取,這些對象的屬性都不會再有寫操做,而多線程同時讀是沒問題的。
但這種方案有個缺陷,就是數據修改後,會在 cache 層整個替換掉這個對象,但這時上層扔持有着舊的對象,並不會自動把對象更新過來:
因此怎樣讓上層更新數據呢?有兩種方式,push 和 pull。
push 的方式就是 cache 層把更新 push 給上層,cache對整個對象更新替換掉時,發送廣播通知上層,這裏發通知的粒度能夠按需求斟酌,上層監聽本身關心的通知,若是發現本身持有的對象更新了,就要更新本身的數據,但這裏的更新數據也是件挺麻煩的事。
舉個例子,讀書有一個想法列表WRReviewController,存着一個數組 reviews,保存着想法 review 數據對象,數組裏的每個 review 會持有這個這個想法對應的一本書,也就是 review.book 持有一個 WRBook 數據對象。而後這時 cache 層通知這個 WRReviewController,某個 book 對象有屬性變了,這時這個 WRReviewController 要怎樣處理呢?有兩個選擇:
第一種是精細化的作法,優勢是不影響性能,缺點是蛋疼,工做量增多,還容易漏更新,須要清楚知道當前模塊持有了哪些數據,有哪些須要更新。第二種是粗獷的作法,優勢是省事省心,所有大刷一遍就好了,缺點是在一些複雜頁面須要組裝數據,會對性能形成較大影響。
另外一種 pull 的方式是指上層在特定時機本身去判斷數據有沒有更新。
首先全部數據對象都會有一個屬性,暫時命名爲 dirty
,在 cache 層更新替換數據對象前,先把舊對象的 dirty 屬性設爲 YES
,表示這個舊對象已經從 cache 裏被拋棄了,屬於髒數據,須要更新。而後上層在合適的時候自行去判斷本身持有的對象的 dirty
屬性是否爲 YES
,如果則從新在 cache 裏取最新數據。
實際上這樣作發生了多線程讀寫 dirty
屬性,是有線程安全問題的,但由於 dirty
屬性讀取不頻繁,能夠直接給這個屬性的讀寫加鎖,不會像對全部屬性加鎖那樣引起各類問題,解決對這個 dirty
屬性讀寫的線程安全問題。
這裏主要的問題是上層應該在什麼時機去 pull 數據更新。能夠在每次界面顯示 -viewWillAppear
或用戶操做後去檢查,例如用戶點個贊,就能夠觸發一次檢查,去更新讚的數據,在這兩個地方作檢查已經能夠解決90%的問題,剩下的就是同個界面聯動的問題,例如 iPad 郵件左右兩欄兩個 controller,右邊詳情點個收藏,左邊列表收藏圖標也要高亮,這種狀況能夠作特殊處理,也能夠結合上面 push 的方式去作通知。
push 和 pull 兩種是能夠結合在一塊兒用的,pull 的方式彌補了 push 後數據所有從新讀取大刷致使的性能低下問題,push 彌補了 pull 更新時機的問題,實際使用中配合一些事先制定的規則或框架一塊兒使用效果更佳。
對於 APP 緩存數據線程安全問題,分線程 cache 和數據不可變是比較常見的解決方案,都有着不一樣的實現代價,分線程 cache 接口不友好,數據不可變須要配合單向數據流之類的規則或框架纔會變得好用,能夠按需選擇合適的方案。