0%

ObjC 基于上下文的设计

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

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

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

什么是上下文

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

要想解释清楚这样一种抽象的概念,还是比较困难的,不过在我们现实的开发中,其实也已或多或少用到过上下文。这些上下文通常都是以XXXContext来命名,并且通常都有明确的区间分割,比如下面使用UIKit进行绘图的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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_)的一般实现里,也会有上下文,用于记录解析过程的中间状态。类似的例子还有很多,这里就不一一列出了。

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

嵌套上下文

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

1
2
3
4
5
6
7
BeginXXContext();
// 区间A
BeginXXContext();
// 区间B
EndXXContext();
// 区间A
EndXXContext();

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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

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

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

线程安全问题

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

1
2
3
4
5
6
7
8
9
10
11
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中的这段代码肯定是在一个线程里,或者说,上下文是线程相关的,一个上下文针对一个线程。所以下面这段代码是不对的(_或者说是不允许的_):

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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(_这里用到了上下文_)
  • 事件订阅者通过弱引用自动回收

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

1
2
3
4
5
6
7
8
9
10
11
12
13
// 发布事件
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,那么这个栗子就举到这里吧!

接下来该做什么

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

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