春節(jié)長假歸來元媚,相信大多數(shù)人都犯了節(jié)后綜合征,那么就寫一篇博文來收收心弯予。沒有心思干活的同學們戚宦,可以看看我的這篇文章,權(quán)當是散散心锈嫩,找找感覺受楼。
本篇文章主要介紹了關(guān)于上下文(Context)的一些概念,并提出了在設(shè)計上下文時應該考慮到的問題呼寸,最后通過一個實例來演示如何用 Objective-C
實現(xiàn)一個上下文艳汽。相信通過閱讀本篇文章,大家能夠基本掌握軟件設(shè)計中上下文的使用对雪,并且河狐,我相信,想象力如此豐富的你們,會將此推演到更高的境界甚牲。
那么义郑,讓我們從一些比較輕松的環(huán)節(jié)開始吧!
什么是上下文
既然我們要說上下文(Context)丈钙,那么我們首先應該能夠比較清晰的理解非驮,什么是上下文,以及它適用于哪些場景雏赦。那么什么是上下文呢劫笙?上下文就是在某個特定的場景里,用于記錄該場景特定狀態(tài)的一種抽象星岗。
要想解釋清楚這樣一種抽象的概念填大,還是比較困難的,不過在我們現(xiàn)實的開發(fā)中俏橘,其實也已或多或少用到過上下文允华。這些上下文通常都是以 XXXContext
來命名,并且通常都有明確的區(qū)間分割寥掐,比如下面使用 UIKit
進行繪圖的代碼:
CGImageRef flip(CGImageRef im) {
CGSize sz = CGSizeMake(CGImageGetWidth(im), CGImageGetHeight(im));
UIGraphicsBeginImageContextWithOptions(sz, NO, 0); // 上下文開始
CGContextDrawImage(UIGraphicsGetCurrentContext(),
CGRectMake(0, 0, sz.width, sz.height), im);
CGImageRef result = [UIGraphicsGetImageFromCurrentImageContext() CGImage];
UIGraphicsEndImageContext(); // 上下文結(jié)束
return result;
}
上面的代碼中靴寂,ImageContext
便是一種上下文,它會記錄下在 Begin
和 End
區(qū)間中的一些信息召耘,并影響這其間其他方法的行為百炬。在廣為人知的 GoF 設(shè)計模式中,解析器模式(Interpreter)的一般實現(xiàn)里污它,也會有上下文剖踊,用于記錄解析過程的中間狀態(tài)。類似的例子還有很多衫贬,這里就不一一列出了德澈。
那么,接下來我們來看看固惯,如果要去實現(xiàn)一個上下文圃验,需要注意哪些問題。
嵌套上下文
首先我們需要注意的是缝呕,一個健全的上下文必須是需要支持嵌套的,比如這樣一段代碼片段:
BeginXXContext();
// 區(qū)間A
BeginXXContext();
// 區(qū)間B
EndXXContext();
// 區(qū)間A
EndXXContext();
理想的情況下斧散,我們在 區(qū)間A 里所設(shè)定的信息應該是不能影響到 區(qū)間B 的供常,因為 區(qū)間B 是一個獨立的上下文。這樣的設(shè)計比起上下文行為繼承鸡捐,我覺得會更加合理栈暇,如果 區(qū)間B 繼承 區(qū)間A 上下文的信息,會導致一些不可預料的后果箍镜。比如源祈,整個 區(qū)間B 是在另一個子函數(shù)里煎源,那么就無法確保這個子函數(shù)對外能有一個確定的行為表現(xiàn)了。
那么香缺,我們?nèi)绾蝸韺崿F(xiàn)這樣的需求呢手销?其實很簡單,我們確保在 區(qū)間A 里獲取到的上下文與在 區(qū)間B 里獲取到的上下文是兩個對象即可图张。這樣就需要我們在抽象時锋拖,考慮父子關(guān)系,下面是簡略的代碼實現(xiàn):
@implementation XXContext {
// 父 Context
XXContext *_parent;
}
static XXContext *sXXContext;
// 開始一個上下文
+ (void)begin {
XXContext *parent = sXXContext;
sXXContext = [XXContext new];
sXXContext->_parent = parent;
}
// 當前 Context
+ (instancetype)current {
return sXXContext;
}
// 結(jié)束一個上下文
+ (void)end {
sXXContext = sXXContext->_parent;
}
@end
上面的代碼還是非常簡陋的祸轮,未做任何異常處理兽埃,但這里只是提供出實現(xiàn)的思路,有興趣的朋友适袜,可以自己再細化下柄错。
好的,我們解決了嵌套的問題苦酱,那么接下來要談談線程安全了售貌。
線程安全問題
上下文的實現(xiàn)中,非常重要的一環(huán)就是要考慮上下文的線程安全躏啰〕梅考慮一下,上一節(jié)代碼實現(xiàn)的上下文给僵,在如下的代碼中毫捣,表現(xiàn)會是怎樣:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[XXContext begin];
// 區(qū)間A
[XXContext end];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[XXContext begin];
// 區(qū)間B
[XXContext end];
});
很明顯的可以看出來,如果是之前的實現(xiàn)帝际,在面對這種多線程并發(fā)操作的情況下蔓同,會有不可預料的結(jié)果。上面代碼里蹲诀,區(qū)間A 或 區(qū)間B 里獲取到的 [XXContext current]
都是不確定的斑粱,因為無法保證代碼的執(zhí)行順序。那么脯爪,我們?nèi)绻麃斫鉀Q這樣的問題呢则北?
換個角度來思考下,我們可以確保的是痕慢,begin
和 end
中的這段代碼肯定是在一個線程里尚揣,或者說,上下文是線程相關(guān)的掖举,一個上下文針對一個線程快骗。所以下面這段代碼是不對的(或者說是不允許的):
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[XXContext begin];
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
[XXContext current]; // 獲取不到
});
[XXContext end];
});
這樣分析下來,我們應該能夠很容易的想到一個概念:線程本地存儲,也就是所謂的 TLS(Thread Local Storage)方篮,顧名思義名秀,就是可以針對線程存儲一些信息,并且存儲的這些信息只有在該線程才可以訪問到藕溅,與其他線程是隔離的匕得。
在 Objective-C
中,TLS 的使用非常簡單蜈垮,NSThread
中有個 threadDictionary
屬性耗跛,用于存儲信息,所以攒发,我們可以將上面的實現(xiàn)改成如下這樣:
@implementation XXContext {
// 父 Context
XXContext *_parent;
}
// 開始一個上下文
+ (void)begin {
XXContext *ctx = [XXContext new];
ctx->_parent = [self current];
[NSThread currentThread].threadDictionary[@"xx-ctx"] = ctx;
}
// 當前 Context
+ (instancetype)current {
return [NSThread currentThread].threadDictionary[@"xx-ctx"];
}
// 結(jié)束一個上下文
+ (void)end {
XXContext *ctx = [self current];
ctx = ctx->_parent;
[NSThread currentThread].threadDictionary[@"xx-ctx"] = ctx;
}
@end
上面的改動其實很簡單调塌,也就是把原先的 sXXContext
靜態(tài)變量,替換成 [NSThread currentThread].threadDictionary[@"xx-ctx"]
這樣一種線程相關(guān)的存儲方式惠猿。經(jīng)過這樣的改造羔砾,我們可以輕松面對本節(jié)開頭的那段代碼了,所以偶妖,接下來我們可以做一些更有意義的事情姜凄。
實現(xiàn)舉例 - 事件總線
前先時間,在微信上看到一篇關(guān)于 蘑菇街組件化的文章趾访,里面講到了它們的MGJRouter态秧,用于模塊間的解耦。這個庫主要都是主動去取另一個模塊的數(shù)據(jù)扼鞋,但模塊間除了這種主動的行為申鱼,有時還會需要監(jiān)聽另一個模塊的特定事件,這種被動的行為云头,在 Objective-C
中有 Notification
可以使用捐友,但, Notification
太弱溃槐,類型太弱匣砖,需要太多的約定。
所以昏滴,我們有必要自己再造一個輪子猴鲫,事件總線(Event Bus),更進一步的將模塊解耦谣殊。這個庫我已經(jīng)放到了 GitHub:
https://github.com/prinsun/MKXEventBus
這個庫支持這樣一些特性:
- 強類型事件發(fā)布
- 事件支持合并配置拂共,在符合條件的情況下,多個事件會自動合并成一個事件發(fā)布
- 事件訂閱支持
block
也支持selector
- 事件訂閱支持指定回調(diào)的
dispatch_queue
(這里用到了上下文) - 事件訂閱者通過弱引用自動回收
具體實現(xiàn)可以看代碼蟹倾,也歡迎大家來發(fā)現(xiàn)問題,并貢獻代碼,下面是一般的使用示例:
// 發(fā)布事件
MKXLoginSuccessEvent *event = [MKXLoginSuccessEvent eventWithAccount:account];
[[MKXEventBus sharedBus] publish:event];
...
// 訂閱事件
[MKXEventBus beginSubscribe:self.dispatchQueue];
[[MKXEventBus sharedBus] subscribe:MKXLoginSuccessEvent.class for:self with:^(MKXLoginSuccessEvent *event) {
...
}];
[MKXEventBus endSubscribe];
上面代碼中的 beginSubscribe
和 endSubscribe
便是一個典型的上下文設(shè)計鲜棠,實現(xiàn)方式也與本文中所描述類似肌厨,感興趣的可以去瞅瞅代碼。
OK豁陆,那么這個栗子就舉到這里吧柑爸!
接下來該做什么
看到了這里,我覺得大家可以再深入的去思考下盒音,上下文除了這些簡短生命周期的實現(xiàn)外表鳍,其實還有很多生命周期是非常長的。比如應用上下文祥诽、服務上下文譬圣、賬戶上下文等,上下文的核心設(shè)計理念在于隔離存儲雄坪,這是一個非常有用厘熟,也非常有意思的東西。
所以维哈,接下來發(fā)揮你的想象绳姨,用上下文去創(chuàng)造奇跡吧!