ObjC 基于上下文的设计

春节长假归来,相信大多数人都犯了节后综合征,那么就写一篇博文来收收心。没有心思干活的同学们,可以看看我的这篇文章,权当是散散心,找找感觉。

本篇文章主要介绍了关于上下文(Context)的一些概念,并提出了在设计上下文时应该考虑到的问题,最后通过一个实例来演示如何用 Objective-C 实现一个上下文。相信通过阅读本篇文章,大家能够基本掌握软件设计中上下文的使用,并且,我相信,想象力如此丰富的你们,会将此推演到更高的境界。

那么,让我们从一些比较轻松的环节开始吧!

什么是上下文

既然我们要说上下文(Context),那么我们首先应该能够比较清晰的理解,什么是上下文,以及它适用于哪些场景。那么什么是上下文呢?上下文就是在某个特定的场景里,用于记录该场景特定状态的一种抽象。

要想解释清楚这样一种抽象的概念,还是比较困难的,不过在我们现实的开发中,其实也已或多或少用到过上下文。这些上下文通常都是以 XXXContext 来命名,并且通常都有明确的区间分割,比如下面使用 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(); // 上下文结束
 
    return result; 
} 

上面的代码中,ImageContext 便是一种上下文,它会记录下在 BeginEnd 区间中的一些信息,并影响这其间其他方法的行为。在广为人知的 GoF 设计模式中,解析器模式(Interpreter)的一般实现里,也会有上下文,用于记录解析过程的中间状态。类似的例子还有很多,这里就不一一列出了。

那么,接下来我们来看看,如果要去实现一个上下文,需要注意哪些问题。

嵌套上下文

首先我们需要注意的是,一个健全的上下文必须是需要支持嵌套的,比如这样一段代码片段:

BeginXXContext();
    // 区间A
    BeginXXContext();
        // 区间B
    EndXXContext();
    // 区间A
EndXXContext();

理想的情况下,我们在 区间A 里所设定的信息应该是不能影响到 区间B 的,因为 区间B 是一个独立的上下文。这样的设计比起上下文行为继承,我觉得会更加合理,如果 区间B 继承 区间A 上下文的信息,会导致一些不可预料的后果。比如,整个 区间B 是在另一个子函数里,那么就无法确保这个子函数对外能有一个确定的行为表现了。

那么,我们如何来实现这样的需求呢?其实很简单,我们确保在 区间A 里获取到的上下文与在 区间B 里获取到的上下文是两个对象即可。这样就需要我们在抽象时,考虑父子关系,下面是简略的代码实现:

@implementation XXContext {
    // 父 Context
    XXContext *_parent;
}

static XXContext *sXXContext;

// 开始一个上下文
+ (void)begin {
    XXContext *parent = sXXContext;
    sXXContext = [XXContext new];
    sXXContext->_parent = parent;
}

// 当前 Context
+ (instancetype)current {
    return sXXContext;
}

// 结束一个上下文
+ (void)end {
    sXXContext = sXXContext->_parent;
}

@end

上面的代码还是非常简陋的,未做任何异常处理,但这里只是提供出实现的思路,有兴趣的朋友,可以自己再细化下。

好的,我们解决了嵌套的问题,那么接下来要谈谈线程安全了。

线程安全问题

上下文的实现中,非常重要的一环就是要考虑上下文的线程安全。考虑一下,上一节代码实现的上下文,在如下的代码中,表现会是怎样:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [XXContext begin];
    // 区间A 
    [XXContext end];
});
        
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [XXContext begin];
    // 区间B
    [XXContext end];
});

很明显的可以看出来,如果是之前的实现,在面对这种多线程并发操作的情况下,会有不可预料的结果。上面代码里,区间A区间B 里获取到的 [XXContext current] 都是不确定的,因为无法保证代码的执行顺序。那么,我们如果来解决这样的问题呢?

换个角度来思考下,我们可以确保的是,beginend 中的这段代码肯定是在一个线程里,或者说,上下文是线程相关的,一个上下文针对一个线程。所以下面这段代码是不对的(或者说是不允许的):

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 属性,用于存储信息,所以,我们可以将上面的实现改成如下这样:

@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"];
}

// 结束一个上下文
+ (void)end {
    XXContext *ctx = [self current];
    ctx = ctx->_parent;
    [NSThread currentThread].threadDictionary[@"xx-ctx"] = ctx;
}

@end

上面的改动其实很简单,也就是把原先的 sXXContext 静态变量,替换成 [NSThread currentThread].threadDictionary[@"xx-ctx"] 这样一种线程相关的存储方式。经过这样的改造,我们可以轻松面对本节开头的那段代码了,所以,接下来我们可以做一些更有意义的事情。

实现举例 - 事件总线

前先时间,在微信上看到一篇关于 蘑菇街组件化的文章,里面讲到了它们的MGJRouter,用于模块间的解耦。这个库主要都是主动去取另一个模块的数据,但模块间除了这种主动的行为,有时还会需要监听另一个模块的特定事件,这种被动的行为,在 Objective-C 中有 Notification 可以使用,但, Notification 太弱,类型太弱,需要太多的约定。

所以,我们有必要自己再造一个轮子,事件总线(Event Bus),更进一步的将模块解耦。这个库我已经放到了 GitHub:

https://github.com/prinsun/MKXEventBus

这个库支持这样一些特性:

  • 强类型事件发布
  • 事件支持合并配置,在符合条件的情况下,多个事件会自动合并成一个事件发布
  • 事件订阅支持 block 也支持 selector
  • 事件订阅支持指定回调的 dispatch_queue这里用到了上下文
  • 事件订阅者通过弱引用自动回收

具体实现可以看代码,也欢迎大家来发现问题,并贡献代码,下面是一般的使用示例:

// 发布事件
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 便是一个典型的上下文设计,实现方式也与本文中所描述类似,感兴趣的可以去瞅瞅代码。

OK,那么这个栗子就举到这里吧!

接下来该做什么

看到了这里,我觉得大家可以再深入的去思考下,上下文除了这些简短生命周期的实现外,其实还有很多生命周期是非常长的。比如应用上下文服务上下文账户上下文等,上下文的核心设计理念在于隔离存储,这是一个非常有用,也非常有意思的东西。

所以,接下来发挥你的想象,用上下文去创造奇迹吧!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,794评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,050评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,587评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,861评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,901评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,898评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,832评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,617评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,077评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,349评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,483评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,199评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,824评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,442评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,632评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,474评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,393评论 2 352