GCD之dispatch_source

概述

Dispatch SourceBSD系统内核惯有功能kqueue的包装,kqueue是在XNU内核中发生各种事件时,在应用程序编程方执行处理的技术。它的CPU负荷非常小,尽量不占用资源。当事件发生时,Dispatch Source会在指定的Dispatch Queue中执行事件的处理。

使用篇

dispatch_source最常见的用法就是用来实现定时器,代码如下:

dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(source, dispatch_time(DISPATCH_TIME_NOW, 0), 3 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(source, ^{
    //定时器触发时执行
   NSLog(@"timer响应了");
});
//启动timer
dispatch_resume(source);

Dispatch Source定时器的代码看似很简单,但其实是GCD中坑最多的API了,如果处理不好很容易引起Crash。关于Dispatch Source定时器需要注意的知识点请参考文章最后的总结篇。

原理篇

dispatch_source_create

dispatch_source_create函数用来创建dispatch_source_t对象,简化后的代码如下:

dispatch_source_t dispatch_source_create(dispatch_source_type_t type,
    uintptr_t handle,
    unsigned long mask,
    dispatch_queue_t q) {
    //申请内存空间
    ds = _dispatch_alloc(DISPATCH_VTABLE(source),
            sizeof(struct dispatch_source_s));
    //初始化ds
    _dispatch_queue_init((dispatch_queue_t)ds);
    ds->dq_label = "source";

    ds->do_ref_cnt++; // the reference the manager queue holds
    ds->do_ref_cnt++; // since source is created suspended
    //默认处于暂状态,需要手动调用resume
    ds->do_suspend_cnt = DISPATCH_OBJECT_SUSPEND_INTERVAL;
    ds->do_targetq = &_dispatch_mgr_q;
    // First item on the queue sets the user-specified target queue
    //设置事件回调的队列
    dispatch_set_target_queue(ds, q);
    _dispatch_object_debug(ds, "%s", __func__);
    return ds;
}

dispatch_source_set_timer

dispatch_source_set_timer实际上调用了_dispatch_source_set_timer,看一下代码:

static inline void _dispatch_source_set_timer(dispatch_source_t ds, dispatch_time_t start,
        uint64_t interval, uint64_t leeway, bool source_sync) {
    //首先屏蔽非timer类型的source
    if (slowpath(!ds->ds_is_timer) ||
            slowpath(ds_timer(ds->ds_refs).flags & DISPATCH_TIMER_INTERVAL)) {
        DISPATCH_CLIENT_CRASH("Attempt to set timer on a non-timer source");
    }
    //创建dispatch_set_timer_params结构体绑定source和timer参数
    struct dispatch_set_timer_params *params;
    params = _dispatch_source_timer_params(ds, start, interval, leeway);
    _dispatch_source_timer_telemetry(ds, params->ident, &params->values);
    dispatch_retain(ds);
    if (source_sync) {
       //将source当做队列使用,执行dispatch_barrier_async_f压入队列,
       //核心函数为_dispatch_source_set_timer2
        return _dispatch_barrier_trysync_f((dispatch_queue_t)ds, params,
                _dispatch_source_set_timer2);
    } else {
        return _dispatch_source_set_timer2(params);
    }
}

_dispatch_source_set_timer实际上是调用了_dispatch_source_set_timer2函数:

static void _dispatch_source_set_timer2(void *context) {
    // Called on the source queue
    struct dispatch_set_timer_params *params = context;
    //暂停队列,避免修改过程中定时器被触发了。
    dispatch_suspend(params->ds);
    //在_dispatch_mgr_q队列上执行_dispatch_source_set_timer3(params)
    dispatch_barrier_async_f(&_dispatch_mgr_q, params,
            _dispatch_source_set_timer3);
}

_dispatch_source_set_timer2函数的逻辑是在_dispatch_mgr_q队列执行_dispatch_source_set_timer3(params),接下来的逻辑如下:

static void _dispatch_source_set_timer3(void *context) {
    // Called on the _dispatch_mgr_q
    struct dispatch_set_timer_params *params = context;
    dispatch_source_t ds = params->ds;
    ds->ds_ident_hack = params->ident;
    ds_timer(ds->ds_refs) = params->values;
    ds->ds_pending_data = 0;
    (void)dispatch_atomic_or2o(ds, ds_atomic_flags, DSF_ARMED, release);
    //恢复队列,对应着_dispatch_source_set_timer2函数中的dispatch_suspend
    dispatch_resume(ds);
    // Must happen after resume to avoid getting disarmed due to suspension
    //根据下一次触发时间将timer进行排序
    _dispatch_timers_update(ds);
    dispatch_release(ds);
    if (params->values.flags & DISPATCH_TIMER_WALL_CLOCK) {
        _dispatch_mach_host_calendar_change_register();
    }
    free(params);
}

当执行提交到_dispatch_mgr_q队列的block时,会调用&_dispatch_mgr_q->do_invoke函数,即&_dispatch_mgr_q的vtable中定义的_dispatch_mgr_thread。接下来会走到_dispatch_mgr_invoke函数。在这个函数里用I/O多路复用功能的select来实现定时器功能:

r = select(FD_SETSIZE, &tmp_rfds, &tmp_wfds, NULL,
            poll ? (struct timeval*)&timeout_immediately : NULL);


当内层的 _dispatch_mgr_q 队列被唤醒后,还会进一步唤醒外层的队列(当初用户指定的那个),并在指定队列上执行 timer 触发时的 block。

dispatch_source_set_event_handler

void dispatch_source_set_event_handler(dispatch_source_t ds,
        dispatch_block_t handler) {
    //将block进行copy后压入到队列中
    handler = _dispatch_Block_copy(handler);
    _dispatch_barrier_trysync_f((dispatch_queue_t)ds, handler,
            _dispatch_source_set_event_handler2);
}
static void _dispatch_source_set_event_handler2(void *context) {
    dispatch_source_t ds = (dispatch_source_t)_dispatch_queue_get_current();
    dispatch_assert(dx_type(ds) == DISPATCH_SOURCE_KEVENT_TYPE);
    dispatch_source_refs_t dr = ds->ds_refs;

    if (ds->ds_handler_is_block && dr->ds_handler_ctxt) {
        Block_release(dr->ds_handler_ctxt);
    }
    //设置上下文,保存提交的block等信息
    dr->ds_handler_func = context ? _dispatch_Block_invoke(context) : NULL;
    dr->ds_handler_ctxt = context;
    ds->ds_handler_is_block = true;
}

dispatch_source_set_cancel_handler

dispatch_source_set_cancel_handlerdispatch_source_set_event_handler功能类似,保存一下取消事件处理的上下文信息。代码如下:

void dispatch_source_set_cancel_handler(dispatch_source_t ds,
    dispatch_block_t handler) {
    //将block进行copy后压入到队列中
    handler = _dispatch_Block_copy(handler);
    _dispatch_barrier_trysync_f((dispatch_queue_t)ds, handler,
            _dispatch_source_set_cancel_handler2);
}
static void _dispatch_source_set_cancel_handler2(void *context) {
    dispatch_source_t ds = (dispatch_source_t)_dispatch_queue_get_current();
    dispatch_assert(dx_type(ds) == DISPATCH_SOURCE_KEVENT_TYPE);
    dispatch_source_refs_t dr = ds->ds_refs;

    if (ds->ds_cancel_is_block && dr->ds_cancel_handler) {
        Block_release(dr->ds_cancel_handler);
    }
    //保存事件取消的信息
    dr->ds_cancel_handler = context;
    ds->ds_cancel_is_block = true;
}

dispatch_resume/dispatch_suspend

//恢复
void dispatch_resume(dispatch_object_t dou) {
    DISPATCH_OBJECT_TFB(_dispatch_objc_resume, dou);
    // Global objects cannot be suspended or resumed.
    if (slowpath(dou._do->do_ref_cnt == DISPATCH_OBJECT_GLOBAL_REFCNT) ||
            slowpath(dx_type(dou._do) == DISPATCH_QUEUE_ROOT_TYPE)) {
        return;
    }
    //将do_suspend_cnt原子性减二,并返回之前存储的值
    unsigned int suspend_cnt = dispatch_atomic_sub_orig2o(dou._do,
             do_suspend_cnt, DISPATCH_OBJECT_SUSPEND_INTERVAL, relaxed);
    if (fastpath(suspend_cnt > DISPATCH_OBJECT_SUSPEND_INTERVAL)) {
        return _dispatch_release(dou._do);
    }
    if (fastpath(suspend_cnt == DISPATCH_OBJECT_SUSPEND_INTERVAL)) {
        _dispatch_wakeup(dou._do);
     return _dispatch_release(dou._do);
    }
    DISPATCH_CLIENT_CRASH("Over-resume of an object");
}
//暂停
void dispatch_suspend(dispatch_object_t dou) {
    DISPATCH_OBJECT_TFB(_dispatch_objc_suspend, dou);
    if (slowpath(dou._do->do_ref_cnt == DISPATCH_OBJECT_GLOBAL_REFCNT) ||
            slowpath(dx_type(dou._do) == DISPATCH_QUEUE_ROOT_TYPE)) {
        return;
    }
    //将do_suspend_cnt原子性加二
    (void)dispatch_atomic_add2o(dou._do, do_suspend_cnt,
            DISPATCH_OBJECT_SUSPEND_INTERVAL, relaxed);
    _dispatch_retain(dou._do);
}

判断队列是否暂停的依据是do_suspend_cnt是否大于等于2,全局队列和主队列默认都是小于2的,即处于启动状态。
而dispatch_source_create方法中,do_suspend_cnt初始为DISPATCH_OBJECT_SUSPEND_INTERVAL,即默认处于暂停状态,需要手动调用resume开启。
代码定义如下:

#define DISPATCH_OBJECT_SUSPEND_LOCK        1u
#define DISPATCH_OBJECT_SUSPEND_INTERVAL    2u
#define DISPATCH_OBJECT_SUSPENDED(x) \
        ((x)->do_suspend_cnt >= DISPATCH_OBJECT_SUSPEND_INTERVAL)

dispatch_after

dispatch_after是基于Dispatch Source的定时器实现的,函数内部直接调用dispatch_after_f,代码如下:

void dispatch_after_f(dispatch_time_t when, dispatch_queue_t queue, void *ctxt,
        dispatch_function_t func) {
    uint64_t delta, leeway;
    dispatch_source_t ds;
    //屏蔽DISPATCH_TIME_FOREVER类型
    if (when == DISPATCH_TIME_FOREVER) {
#if DISPATCH_DEBUG
        DISPATCH_CLIENT_CRASH(
                "dispatch_after_f() called with 'when' == infinity");
#endif
        return;
    }
    delta = _dispatch_timeout(when);
    if (delta == 0) {
        return dispatch_async_f(queue, ctxt, func);
    }
    leeway = delta / 10; // <rdar://problem/13447496>
    if (leeway < NSEC_PER_MSEC) leeway = NSEC_PER_MSEC;
    if (leeway > 60 * NSEC_PER_SEC) leeway = 60 * NSEC_PER_SEC;

    // this function can and should be optimized to not use a dispatch source
    //创建dispatch_source
    ds = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    dispatch_assert(ds);

    dispatch_continuation_t dc = _dispatch_continuation_alloc();
    dc->do_vtable = (void *)(DISPATCH_OBJ_ASYNC_BIT | DISPATCH_OBJ_BARRIER_BIT);
    dc->dc_func = func;
    dc->dc_ctxt = ctxt;
    dc->dc_data = ds;
    //将dispatch_continuation_t存储到上下文中
    dispatch_set_context(ds, dc);
    //设置timer并启动
    dispatch_source_set_event_handler_f(ds, _dispatch_after_timer_callback);
    dispatch_source_set_timer(ds, when, DISPATCH_TIME_FOREVER, leeway);
    dispatch_resume(ds);
}

timer到时之后,会调用_dispatch_after_timer_callback函数,在这里取出上下文里的block并执行:

void _dispatch_after_timer_callback(void *ctxt) {
    dispatch_continuation_t dc = ctxt, dc1;
    dispatch_source_t ds = dc->dc_data;
    dc1 = _dispatch_continuation_free_cacheonly(dc);
    //执行任务的block并执行
    _dispatch_client_callout(dc->dc_ctxt, dc->dc_func);
    //清理数据
    dispatch_source_cancel(ds);
    dispatch_release(ds);
    if (slowpath(dc1)) {
        _dispatch_continuation_free_to_cache_limit(dc1);
    }
}

总结篇

Dispatch Source使用最多的就是用来实现定时器,source创建后默认是暂停状态,需要手动调用dispatch_resume启动定时器。dispatch_after只是封装调用了dispatch source定时器,然后在回调函数中执行定义的block。

Dispatch Source定时器使用时也有一些需要注意的地方,不然很可能会引起crash:

1、循环引用:因为dispatch_source_set_event_handler回调是个block,在添加到source的链表上时会执行copy并被source强引用,如果block里持有了self,self又持有了source的话,就会引起循环引用。正确的方法是使用weak+strong或者提前调用dispatch_source_cancel取消timer。

2、dispatch_resumedispatch_suspend调用次数需要平衡,如果重复调用dispatch_resume则会崩溃,因为重复调用会让dispatch_resume代码里if分支不成立,从而执行了DISPATCH_CLIENT_CRASH(“Over-resume of an object”)导致崩溃。

3、source在suspend状态下,如果直接设置source = nil或者重新创建source都会造成crash。正确的方式是在resume状态下调用dispatch_source_cancel(source)后再重新创建。

结语

通过阅读GCD的源码还是学到了很多知识,也加深了工作中对GCD的使用和理解,希望通过GCD源码解析的文章和大家一起探讨相互学习。

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