iOS libdispatch浅析

前言

本文分析源码为libdispatch-1173.40.5,主要分析常用的dispatch API具体的实现原理;先讲解下常用的数据结构体便于后续分析使用:
dispatch_object_s

struct dispatch_object_s {
    const struct dispatch_object_vtable_s *do_vtable,
  int volatile do_ref_cnt,                                          //引用计数
  int volatile do_xref_cnt,                                         //外部引用计数,两者都为0时才会释放对象内存
  struct dispatch_object_s *volatile do_next;   //下一个do
    struct dispatch_queue_s *do_targetq;          //目标队列
    void *do_ctxt;                                //上下文
    void *do_finalizer;                           //销毁时调用函数
    unsigned int do_suspend_cnt;                  //suspend暂停计数
};

其中do_vtable包含了对象的类型及函数指针;

dispatch_object_t

dispatch_object_t是个union的联合体,可以用dispatch_object_t代表这个联合体里的所有数据结构。

typedef union {
    struct _os_object_s *_os_obj;
    struct dispatch_object_s *_do;             //object结构体
    struct dispatch_continuation_s *_dc;       //任务,dispatch_aync的block会封装成这个数据结构
    struct dispatch_queue_s *_dq;              //队列
    struct dispatch_queue_attr_s *_dqa;        //队列属性
    struct dispatch_group_s *_dg;              //群组操作
    struct dispatch_source_s *_ds;             //source结构体
    struct dispatch_mach_s *_dm;
    struct dispatch_mach_msg_s *_dmsg;
    struct dispatch_timer_aggregate_s *_dta;
    struct dispatch_source_attr_s *_dsa;       //source属性
    struct dispatch_semaphore_s *_dsema;       //信号量
    struct dispatch_data_s *_ddata;
    struct dispatch_io_s *_dchannel;
    struct dispatch_operation_s *_doperation;
    struct dispatch_disk_s *_ddisk;
} dispatch_object_t __attribute__((__transparent_union__));

dispatch_continuation_s

struct dispatch_continuation_s {
    struct dispatch_object_s *volatile do_next; // 下一个任务
    dispatch_function_t dc_func;                // 执行的方法
    void *dc_ctxt;                              // 方法上下文
    void *dc_data;                              // 相关数据
    void *dc_other                              // 其它信息
}

dispatch_continuation_s 是中的任务的结构体,被传入的 block会被变成这个结构体对象塞入队列;

dispatch_queue_s

struct dispatch_queue_s {
    struct dispatch_queue_s *do_targetq;               // 目标队列,这个最终会指向一个系统的默认队列
    struct dispatch_object_s *volatile dq_items_head;  // 队列头部
    struct dispatch_object_s *volatile dq_items_tail;  // 队列尾部
    unsigned long dq_serialnum;                        // 队列序号
    const char *dq_label;                              // 队列名
    dispatch_priority_t dq_priority;                   // 优先级
    dispatch_priority_t volatile dq_override;          // 是否被覆盖
    uint16_t dq_width;                                 // 可并发执行的任务数
    dispatch_queue_t dq_specific_q;                    // 特殊队列
    uint32_t dq_side_suspend_cnt;                      // 暂停的任务数
  
    const struct queue_vtable_s *do_vtable {           // 队列的一些函数指针
        unsigned long const do_type;               // 队列类型,例如:DISPATCH_QUEUE_CONCURRENT_TYPE、DISPATCH_QUEUE_SERIAL_TYPE、DISPATCH_QUEUE_GLOBAL_ROOT_TYPE ...
        const char *const do_kind;                 // 队列种类,例如:"serial-queue"、"concurrent-queue"、"global-queue"、"main-queue"、"runloop-queue""mgr-queue" ...
        void (*const do_dispose)(/*params*/);      // 销毁队列
        void (*const do_suspend)(/*params*/);      // 暂停队列
        void (*const do_resume)(/*params*/);       // 恢复队列
        void (*const do_invoke)(/*params*/);       // 开始处理队列
        void (*const do_wakeup)(/*params*/);       // 唤醒队列
        void (*const do_set_targetq)(/*params*/);  // 设置target queue
    };
}

dispatch_queue_s是队列的结构体,在它的 do_vtable 中有很多函数指针,对应队列的一些操作方法,对应有一些宏可以调用队列中的这些方法。比如, do_dispose方法对应有一个宏 dx_dispose

#define dx_dispose(queue) &(queue)->do_vtable->_os_obj_vtable->do_dispose(queue)

理解队列与线程间的关系

image.png

只有主队列是绑定主线程,其他队列都是从线程池获取分配,并且线程的调度无需用户管理;

dispatch_once

源码如下:

void
dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
    dispatch_once_gate_t l = (dispatch_once_gate_t)val;

#if !DISPATCH_ONCE_INLINE_FASTPATH || DISPATCH_ONCE_USE_QUIESCENT_COUNTER
  //原子性获取l->dgo_once的值
    uintptr_t v = os_atomic_load(&l->dgo_once, acquire);
  //判定上面的值是否为DLOCK_ONCE_DONE(大概率是,表明已经被赋值执行func),是则直接返回
    if (likely(v == DLOCK_ONCE_DONE)) {
        return;
    }
#if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
  //不同的判定形式
    if (likely(DISPATCH_ONCE_IS_GEN(v))) {
        return _dispatch_once_mark_done_if_quiesced(l, v);
    }
#endif
#endif
  //原子性判断是否已赋值
    if (_dispatch_once_gate_tryenter(l)) {
        return _dispatch_once_callout(l, ctxt, func);
    }
  //线程阻塞等待dispatch_function_t func执行完成
    return _dispatch_once_wait(l);
}

static inline bool
_dispatch_once_gate_tryenter(dispatch_once_gate_t l)
{
  //原子性的判断l->dgo_once是否等于DLOCK_ONCE_UNLOCKED(表示未赋值),
  //若是则赋值为线程id
    return os_atomic_cmpxchg(&l->dgo_once, DLOCK_ONCE_UNLOCKED,
            (uintptr_t)_dispatch_lock_value_for_self(), relaxed);
}

static void
_dispatch_once_callout(dispatch_once_gate_t l, void *ctxt,
        dispatch_function_t func)
{
  //调用block中的func
    _dispatch_client_callout(ctxt, func);
  //广播唤醒所有等待的线程
    _dispatch_once_gate_broadcast(l);
}

大概流程如下(摘抄别人的图):


image.png

常用于单例及swizzeld method等功能;

深入浅出 GCD 之 dispatch_once

queue

queue队列主要使用的API如下:

dispatch_queue_main_t
dispatch_get_main_queue(void);
dispatch_queue_global_t
dispatch_get_global_queue(long identifier, unsigned long flags);
dispatch_queue_t
dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);

dispatch_queue_create

伪代码如下:

dispatch_queue_t
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
{
    return _dispatch_lane_create_with_target(label, attr,
                                             DISPATCH_TARGET_QUEUE_DEFAULT, true);
}

//_dispatch_lane_create_with_target核心函数伪代码如下:
static dispatch_queue_t
_dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa,
                                  dispatch_queue_t tq, bool legacy)
{
  //Step 1: Normalize arguments (qos, overcommit, tq)
  dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);//对于串行队列(dqa为NULL)返回空属性对象,对于并行队列会默认初始化值
  //初始化qos优先级,overcommit是否需要创建新线程,tq目标队列默认为DISPATCH_TARGET_QUEUE_DEFAULT
  if(!tq) {
    tq = _dispatch_root_queues[2 * (qos - 1) + overcommit];//如未指定目标队列则从root队列数组中获取
  }
  //Step 2: Initialize the queue 核心代码如下:
  //申请内存空间
  dispatch_lane_t dq = _dispatch_object_alloc(vtable,
                                                sizeof(struct dispatch_lane_s));
  //初始化序列号从17开启,其余如下:
  // skip zero
  // 1 - main_q     //主队列
  // 2 - mgr_q      //管理队列
  // 3 - mgr_root_q//管理队列的目标队列
  // 4,5,6,7,8,9,10,11,12,13,14,15 - global queues,全局队列已在root队列数组中初始化,分别根据qos优先级指定不同队列
  // 17 - workloop_fallback_q
  // we use 'xadd' on Intel, so the initial value == next assigned
    dq->dq_serialnum = os_atomic_inc_orig(&_dispatch_queue_serial_numbers, relaxed);
  //队列并发数,若串行队列则为1,并行队列为DISPATCH_QUEUE_WIDTH_MAX
    dq->dq_width = dqai.dqai_concurrent ? DISPATCH_QUEUE_WIDTH_MAX : 1;
    dq->dq_label = label;//指定名称
    dq->dq_priority = _dispatch_priority_make((dispatch_qos_t)dqai.dqai_qos,
                                              dqai.dqai_relpri);//指定优先级
    dq->do_targetq = tq;//指定目标队列
  
  return dq._dq;
}

//已分配的全局队列,指定了不同qos class的队列
struct dispatch_queue_global_s _dispatch_root_queues[] = {
    //初始化属性
  ...
    _DISPATCH_ROOT_QUEUE_ENTRY(MAINTENANCE, 0,
        .dq_label = "com.apple.root.maintenance-qos",
        .dq_serialnum = 4,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(MAINTENANCE, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
        .dq_label = "com.apple.root.maintenance-qos.overcommit",
        .dq_serialnum = 5,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(BACKGROUND, 0,
        .dq_label = "com.apple.root.background-qos",
        .dq_serialnum = 6,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(BACKGROUND, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
        .dq_label = "com.apple.root.background-qos.overcommit",
        .dq_serialnum = 7,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(UTILITY, 0,
        .dq_label = "com.apple.root.utility-qos",
        .dq_serialnum = 8,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(UTILITY, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
        .dq_label = "com.apple.root.utility-qos.overcommit",
        .dq_serialnum = 9,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(DEFAULT, DISPATCH_PRIORITY_FLAG_FALLBACK,
        .dq_label = "com.apple.root.default-qos",
        .dq_serialnum = 10,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(DEFAULT,
            DISPATCH_PRIORITY_FLAG_FALLBACK | DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
        .dq_label = "com.apple.root.default-qos.overcommit",
        .dq_serialnum = 11,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(USER_INITIATED, 0,
        .dq_label = "com.apple.root.user-initiated-qos",
        .dq_serialnum = 12,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(USER_INITIATED, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
        .dq_label = "com.apple.root.user-initiated-qos.overcommit",
        .dq_serialnum = 13,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(USER_INTERACTIVE, 0,
        .dq_label = "com.apple.root.user-interactive-qos",
        .dq_serialnum = 14,
    ),
    _DISPATCH_ROOT_QUEUE_ENTRY(USER_INTERACTIVE, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
        .dq_label = "com.apple.root.user-interactive-qos.overcommit",
        .dq_serialnum = 15,
    ),
};

dispatch_queue_create主要是创建队列并初始化其参数,包括名称、优先级、并发数、序列号及目标队列,由上面的"线程与队列关系图"可以看出,指定的目标队列都是指向线程池;

dispatch_get_global_queue

//核心函数
static inline dispatch_queue_global_t
_dispatch_get_root_queue(dispatch_qos_t qos, bool overcommit)
{
    if (unlikely(qos < DISPATCH_QOS_MIN || qos > DISPATCH_QOS_MAX)) {
        DISPATCH_CLIENT_CRASH(qos, "Corrupted priority");
    }
    return &_dispatch_root_queues[2 * (qos - 1) + overcommit];
}

主要是从_dispatch_root_queues已初始化的全局队列中根据qosovercommit获取指定队列;

dispatch_get_main_queue

//核心代码
struct dispatch_queue_static_s _dispatch_main_q = {
    DISPATCH_GLOBAL_OBJECT_HEADER(queue_main),
#if !DISPATCH_USE_RESOLVERS
    .do_targetq = _dispatch_get_default_queue(true),
#endif
    .dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(1) |
            DISPATCH_QUEUE_ROLE_BASE_ANON,
    .dq_label = "com.apple.main-thread",
    .dq_atomic_flags = DQF_THREAD_BOUND | DQF_WIDTH(1),
    .dq_serialnum = 1,
};

主队列对应的就是_dispatch_main_q队列,其队列名为com.apple.main-threadwidth为1表示串行队列,序列号为1;

async

void dispatch_async(dispatch_queue_t dq, dispatch_block_t work)
{
  //分配dispatch_continuation内存
    dispatch_continuation_t dc = _dispatch_continuation_alloc();
    uintptr_t dc_flags = DC_FLAG_CONSUME;
    dispatch_qos_t qos;
    //初始化,主要copy(work)避免block执行前被销毁;
  //指定dc->dc_ctxt=work;
  //dc->func=_dispatch_call_block_and_release;用于block执行完成后释放
    qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
  //_dispatch_continuation_async核心函数为dx_push即do_vtable中保存的函数指针
    _dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
}

static inline void
_dispatch_continuation_async(dispatch_queue_class_t dqu,
        dispatch_continuation_t dc, dispatch_qos_t qos, uintptr_t dc_flags)
{
#if DISPATCH_INTROSPECTION
    if (!(dc_flags & DC_FLAG_NO_INTROSPECTION)) {
        _dispatch_trace_item_push(dqu, dc);
    }
#else
    (void)dc_flags;
#endif
    return dx_push(dqu._dq, dc, qos);//核心就是do_vtable中保存的dq_push函数指针
}

dx_push函数指针是在init.c初始化时指定,具体如下:

//workloop class instance vtable
_dispatch_workloop_push 
//queue_serial,queue_runloop,source,channel,mach 串行/runloop/source/channel/mach
_dispatch_lane_push
//queue_concurrent并发队列
_dispatch_lane_concurrent_push
//queue_global,queue_pthread_root 全局队列及root线程
_dispatch_root_queue_push
//queue_mgr管理队列
_dispatch_mgr_queue_push
//queue_main主队列
_dispatch_main_queue_push

下面分析常见的主队列及全局队列:

void _dispatch_main_queue_push(dispatch_queue_main_t dq, dispatch_object_t dou,
                               dispatch_qos_t qos)
{
  //伪代码
  _disaptch_queue_push();//push到主队列
  dx_wakeup()//调用do_vtable中的dq_wakeup函数唤醒队列并执行
}               

mac平台(ios存在些差异)调用栈:

dx_wakeup
  _dispatch_main_queue_wakeup
    _dispatch_runloop_queue_wakeup
        _dispatch_runloop_queue_poke
            _dispatch_runloop_queue_handle_init//dispatch_once_f只执行一次用于初始化,仅支持mac,不支持iphone平台
                mach_port_construct//构建线程mach port
                _dispatch_runloop_queue_set_handle
                    dq->do_ctxt = (void *)(uintptr_t)handle
        _dispatch_send_wakeup_runloop_thread//通过上面投建的mach port唤醒runloop

主线程runloop调用堆栈如下:

image.png

结论:对于异步加入主队列的任务,首先push压入主队列,并通过mach port调用_dispatch_send_wakeup_runloop_thread发送mach port消息至主线程runloop,以唤醒主线程runloop调用libdispatch.dylib动态库中的_dispatch_main_queue_callback_4CF函数,进而触发block调用;

global queue调用栈:

_dispatch_root_queue_push
   _dispatch_root_queue_push_override
       _dispatch_root_queue_push_inline//os_mpsc_push_list原子性push到队列中
           _dispatch_root_queue_poke
               _dispatch_root_queue_poke_slow
                   _pthread_workqueue_addthreads//将任务加入工作队列处理

工作队列调用线程堆栈如下:

image.png

对于global quque全局队列异步并发执行任务,就是将任务push压入root queue,并触发工作队列线程执行pop任务出队列并执行;

其他队列类型可通过源码及调用栈跟踪分析,只要明白工作原理即可,源码需要处理各种情况的队列逻辑比较复杂;

Sync

dispatch_sync加入全局队列分析:

dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"global queue sync fired");
});

dispatch_sync调用栈如下:

dispatch_sync
    _dispatch_sync_f
        _dispatch_sync_f_inline
            _dispatch_barrier_sync_f//串行队列
            _dispatch_queue_try_acquire_barrier_sync//尝试加锁处理,若加锁失败,则表明当前队列正在被调度
                _dispatch_sync_f_slow
            _dispatch_sync_recurse//存在多个目标队列的嵌套调用则递归调用
            _dispatch_lane_barrier_sync_invoke_and_complete//不存在上述情况,队列立即触发调用
    _dispatch_sync_f_slow//并发队列
        _dispatch_sync_function_invoke
            _dispatch_client_callout
                __main_block_invoke

同步执行任务,若存在任务正在执行或者当前队列被调度,则通过加锁(信号量或者kevent形式)等待任务执行后再继续执行,否则直接执行,以保证同步执行;

小知识

__builtin_expect

gcc引入的用于优化编译器的指令,避免指令跳转,提升cpu的效率;

将流水线引入cpu,可以提高cpu的效率。更简单的说,让cpu可以预先取出下一条指令,可以提供cpu的效率。如下图所示:


可见,cpu流水钱可以减少cpu等待取指令的耗时,从而提高cpu的效率。 如果存在跳转指令,那么预先取出的指令就无用了。cpu在执行当前指令时,从内存中取出了当前指令的下一条指令。执行完当前指令后,cpu发现不是要执行下一条指令,而是执行offset偏移处的指令。cpu只能重新从内存中取出offset偏移处的指令。因此,跳转指令会降低流水线的效率,也就是降低cpu的效率。 综上,在写程序时应该尽量避免跳转语句。那么如何避免跳转语句呢?答案就是使用builtin_expect。 这个指令是gcc引入的,作用是"允许程序员将最有可能执行的分支告诉编译器"。这个指令的写法为:builtin_expect(EXP, N)。意思是:EXP==N的概率很大。一般的使用方法是将__builtin_expect指令封装为likely和unlikely宏,常用于内核编程,这两个宏的写法如下:

#define likely(x) __builtin_expect(!!(x), 1) //x很可能为真       
#define unlikely(x) __builtin_expect(!!(x), 0) //x很可能为假

不过要明确:

if(likely(value))  //等价于 if(value)
if(unlikely(value))  //也等价于 if(value)

但是,使用likely(),执行 if 后面的语句的机会更大,使用 unlikely(),执行 else 后面的语句的机会更大。通过这种方式,编译器在编译过程中,会将可能性更大的代码紧跟着起面的代码,从而减少指令跳转带来的性能上的下降。

__builtin_expect 说明

GCC __builtin_expect的作用

os_atomic_cmpxchg

直接上结论:可以理解为p变量相当于atomic_t类型的ptr指针用于获取当前内存访问制约规则m的值,用于对比旧值e,若相当就赋值新值v

#define os_atomic_cmpxchg(p, e, v, m) \
        ({ _os_atomic_basetypeof(p) _r = (e); \
        atomic_compare_exchange_strong_explicit(_os_atomic_c11_atomic(p), \
        &_r, v, memory_order_##m, memory_order_relaxed); })

typedef enum memory_order {
  memory_order_relaxed = __ATOMIC_RELAXED,
  memory_order_consume = __ATOMIC_CONSUME,
  memory_order_acquire = __ATOMIC_ACQUIRE,
  memory_order_release = __ATOMIC_RELEASE,
  memory_order_acq_rel = __ATOMIC_ACQ_REL,
  memory_order_seq_cst = __ATOMIC_SEQ_CST
} memory_order;

对比参考linux原子操作函数atomic_cmpxchg

static inline int atomic_cmpxchg(atomic_t *ptr, int old, int new)

atomic_cmpxchg用于对 atmoic 变量的值进行对比,如果与内存中的值 相等,那么就替换成新值。参数 ptr 指向一个 atomic_t 变量;参数 old 代表与内存中对比 的值;new 代表替换的值。



atomic_cmpxchg源码分析

Linux原子操作 atomic_cmpxchg()/Atomic_read()/Atomic_set()/Atomic_add()/Atomic_sub()/atomi

std::memory_order

lldb 调试

使用断点匹配及镜像匹配查找函数符号,可以查看所有链接库的函数符号并打断点,跟进函数调用栈;

breakpoint set -r -n _dispatch_ //端点匹配的libdispatch.dylib函数
image lookup -r -n _dispatch_ libdispatch.dylib //指定查找libdispatch.dylib动态库的匹配函数

并且lldb支持python脚本解析,可以导入python脚本进行高级调试;

Refenrence

Dispatch

iOS多线程总结

扒了扒libdispatch源码

GCD之线程原理

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

推荐阅读更多精彩内容

  • 一:base.h 二:block.h 1. dispatch_block_flags:DISPATCH_BLOCK...
    小暖风阅读 2,409评论 0 0
  • 多线程技术在移动端开发中应用广泛,GCD 让 iOS 开发者能轻易的使用多线程,然而这并不意味着代码就一定高效和可...
    大西几M阅读 262评论 0 0
  • 本文用来介绍 iOS 多线程中 GCD 的相关知识以及使用方法。这大概是史上最详细、清晰的关于 GCD 的详细讲...
    花花世界的孤独行者阅读 495评论 0 1
  • iOS中GCD的使用小结 作者dullgrass 2015.11.20 09:41*字数 4996阅读 20199...
    DanDanC阅读 810评论 0 0
  • 我现在在地铁上,忽然就想随便写点东西。简书这个APP真的就是可以让每个人都在思如泉涌时来寥寥记几笔。短的也好,长的...
    赵十八Catherine阅读 610评论 1 0