ObjC 基于上下文的設(shè)計

春節(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 便是一種上下文,它會記錄下在 BeginEnd 區(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這樣的問題呢则北?

換個角度來思考下,我們可以確保的是痕慢,beginend 中的這段代碼肯定是在一個線程里尚揣,或者說,上下文是線程相關(guān)的掖举,一個上下文針對一個線程快骗。所以下面這段代碼是不對的(或者說是不允許的):

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [XXContext begin];
    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        [XXContext current]; // 獲取不到
    });
    [XXContext end];
});

這樣分析下來,我們應該能夠很容易的想到一個概念:線程本地存儲,也就是所謂的 TLSThread 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];

上面代碼中的 beginSubscribeendSubscribe 便是一個典型的上下文設(shè)計鲜棠,實現(xiàn)方式也與本文中所描述類似肌厨,感興趣的可以去瞅瞅代碼。

OK豁陆,那么這個栗子就舉到這里吧柑爸!

接下來該做什么

看到了這里,我覺得大家可以再深入的去思考下盒音,上下文除了這些簡短生命周期的實現(xiàn)外表鳍,其實還有很多生命周期是非常長的。比如應用上下文祥诽、服務上下文譬圣、賬戶上下文等,上下文的核心設(shè)計理念在于隔離存儲雄坪,這是一個非常有用厘熟,也非常有意思的東西。

所以维哈,接下來發(fā)揮你的想象绳姨,用上下文去創(chuàng)造奇跡吧!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末阔挠,一起剝皮案震驚了整個濱河市飘庄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌购撼,老刑警劉巖跪削,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異份招,居然都是意外死亡切揭,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門锁摔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來廓旬,“玉大人,你說我怎么就攤上這事谐腰≡斜” “怎么了?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵十气,是天一觀的道長励背。 經(jīng)常有香客問我,道長砸西,這世上最難降的妖魔是什么叶眉? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任址儒,我火速辦了婚禮,結(jié)果婚禮上衅疙,老公的妹妹穿的比我還像新娘莲趣。我一直安慰自己,他們只是感情好饱溢,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布喧伞。 她就那樣靜靜地躺著,像睡著了一般绩郎。 火紅的嫁衣襯著肌膚如雪潘鲫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天肋杖,我揣著相機與錄音溉仑,去河邊找鬼。 笑死兽愤,一個胖子當著我的面吹牛彼念,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播浅萧,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼逐沙,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了洼畅?” 一聲冷哼從身側(cè)響起吩案,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎帝簇,沒想到半個月后徘郭,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡丧肴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年残揉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芋浮。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡抱环,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出纸巷,到底是詐尸還是另有隱情镇草,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布瘤旨,位于F島的核電站梯啤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏存哲。R本人自食惡果不足惜因宇,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一七婴、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧察滑,春花似錦本姥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽氛赐。三九已至魂爪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間艰管,已是汗流浹背滓侍。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留牲芋,地道東北人撩笆。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像缸浦,于是被迫代替她去往敵國和親夕冲。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350