美文网首页
ObjC 基于上下文的设计

ObjC 基于上下文的设计

作者: 笔凡 | 来源:发表于2019-03-26 09:50 被阅读0次

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

本篇文章主要介绍了关于上下文(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,那么这个栗子就举到这里吧!

接下来该做什么

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

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

相关文章

  • ObjC 基于上下文的设计

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

  • iOS基础之KVC与KVO

    1. 概述 ObjC主要基于Smalltalk进行设计, 因此它有很多类似Ruby,Python的动态特性, 例如...

  • iOS之KVC KVO

    KVC与KVO 由于ObjC主要基于Smalltalk进行设计,因此它有很多类似于Ruby、Python的动态特性...

  • 基础 (十三) : KVC/KVO

    由于ObjC主要基于Smalltalk进行设计,因此它有很多类似于Ruby、Python的动态特性,例如动态类型、...

  • 基础 (十三) : KVC/KVO

    由于ObjC主要基于Smalltalk进行设计,因此它有很多类似于Ruby、Python的动态特性,例如动态类型、...

  • KVC总结(转)

    概述由于ObjC主要基于Smalltalk进行设计,因此它有很多类似于Ruby、Python的动态特性,例如动态类...

  • 理解Context

    什么是Context Context: 字面理解为上下文,语境。Android应用模型是基于组件的应用设计模式,组...

  • Linux中的Context与同步

    Linux中的Context (上下文) 基于ARM64 Context 上下文: 上下文的概念:https://...

  • runtime源码中的类和对象

    本文基于objc4-709源码进行分析。关于源码编译:objc - 编译Runtime源码objc4-706 ob...

  • 谈谈 dart_objc 混合编程引擎的设计

    谈谈 dart_objc 混合编程引擎的设计谈谈 dart_objc 混合编程引擎的设计

网友评论

      本文标题:ObjC 基于上下文的设计

      本文链接:https://www.haomeiwen.com/subject/ydbwvqtx.html