GCD源码分析之base.h

本文属于原创文章。转载请联系作者。
本文首发地址为:http://www.jianshu.com/p/225c209085f9

2016年09月20日 添加 sentinel 的特殊用法,__attribute__ ((sentinel(0, 1)))

GCD & base

前言

GCD 是一个跨平台的库,它使用了大量的系统底层知识以及编译器优化内容。
本系列文章将主要分析该库的一些值得学习的地方。取其精华,去其糟粕。

base

base.h 主要声明了库中常用的宏。通过它,GCD 实现了对多平台的兼容。

attribute

__attribute__GCC 的一大特色1clang 实现大部分的描述符并添加了一些扩展2

通过它,开发者可以告诉编译器更多的信息以达到优化程序的目的。本文的主要内容即为分析 base.h 文件内的 __attribute__

noreturn

#define DISPATCH_NORETURN __attribute__((__noreturn__)) 宏将 __attribute__((__noreturn__)) 定义为 DISPATCH_NORETURN

noreturn 用于告诉编译器,该函数没有返回(既不需要 return ... 语句)。

noreturn 有如下实际作用:

  • 优化代码

函数的调用者在调用函数返回后可能需要做一些清理工作,比如调整桟指针。
当一个函数被标记为 __noreturn__ 时,那么编译器就可以知道这些清理工作能够被省略掉,这样可以达到少生成一些无用的代码的目的。

  • 抑制编译器的某些警告

在公共头文件中,只有 DISPATCH_EXPORT DISPATCH_NOTHROW DISPATCH_NORETURN void dispatch_main(void); 方法使用了该宏。

因为,我们在正常的应用开发中不会使用到该方法。所以,下面通过其它的示例代码对其进行说明。

DISPATCH_NORETURN extern void objc_terminate(void);
int sun_terminate() {
   objc_terminate();
}

void objc_terminate(void)objc 库的一个退出方法。

在这里,通过 extern 将其重新声明并添加 DISPATCH_NORETURN 宏。

int sun_terminater() 是我自定义的一个终止函数,它不接收参数,并返回 int 类型的结果。

在 clang 中,当函数声明了返回值,却没有提供时,编译器报出Control reaches end of non-void function的❗️错误提示。如下图所示。

2016-09-06-16-33-58.jpg

但是,这里因为有被 noreturn 描述的函数 objc_terminate(); 的存在,原来的错误提示会被编译器丢失,即开发者看不到原先的❗️错误提示。

一种更复杂的情况是,当函数有分支处理时,需要所有的分支都包含有被noreturn 描述的任意函数。否则,系统仍然会提示错误。如下图所示。

2016-09-06-16-39-08.jpg

nothrow

#define DISPATCH_NOTHROW __attribute__((__nothrow__)) 宏将 __attribute__((__nothrow__)) 定义为 DISPATCH_NOTHROW

该描述符的含义是,函数不会抛出异常。因为 iOS 中使用异常处理的情况比较少,所以这里就不进行展开讲解。

nonnull

#define DISPATCH_NONNULL1 __attribute__((__nonnull__(1)))
#define DISPATCH_NONNULL2 __attribute__((__nonnull__(2)))
#define DISPATCH_NONNULL3 __attribute__((__nonnull__(3)))
#define DISPATCH_NONNULL4 __attribute__((__nonnull__(4)))
#define DISPATCH_NONNULL5 __attribute__((__nonnull__(5)))
#define DISPATCH_NONNULL6 __attribute__((__nonnull__(6)))
#define DISPATCH_NONNULL7 __attribute__((__nonnull__(7)))
#if __clang__ && __clang_major__ < 3
// rdar://problem/6857843
#define DISPATCH_NONNULL_ALL
#else
#define DISPATCH_NONNULL_ALL __attribute__((__nonnull__))
#endif

__nonnull__用于指定函数或方法的入参类型不为空指针。

它有两种写法,__attribute__((__nonnull__)) 或者 __attribute__((__nonnull__(1,2))

第一种要求所有的入参类型不为空指针,第二种要求指定位置的参数不为空指针。

比如下面的方法声明中,要求第二个参数 block 不能为空指针。当 block 为空时,根本没有必要调用该方法,所以这里使用 DISPATCH_NONNULL2 对其进行限制。

DISPATCH_EXPORT DISPATCH_NONNULL2 DISPATCH_RETURNS_RETAINED_BLOCK
DISPATCH_WARN_RESULT DISPATCH_NOTHROW
dispatch_block_t
dispatch_block_create(dispatch_block_flags_t flags, dispatch_block_t block);

sentinel

#define DISPATCH_SENTINEL __attribute__((__sentinel__)) 宏将 __attribute__((__sentinel__)) 定义为 DISPATCH_SENTINEL

__sentinel__ 的中文意思是哨兵。

当接收可变参数时,可以通过它指定NULL所在的位置。

默认情况下,NULL所在的位置为从后往前数第0个为NULL。
常见于数组或者字典等集合类型接受多个参数时指定最后一个为NULL。
通过它,函数内部可以知道何时终止参数的处理。

示例如下代码如下:

修饰符指定了从后往前数第三个为NULL,如果不满足这个规则,编译器会产生警告⚠️。

一个特殊的使用方法为: void f1(void *a, ...) __attribute__ ((sentinel(0, 1)));

sentinel(n, 0) 和 sentinel(n, 1) 之间的区别是处理变长参数前面最后一个被命名参数的方式不同。比如,下面示例中的 void* a。sentinel(0, 0) 意味着该参数 不被包含到空值中断列表中。sentinel(0,1) 意味着该参数被包含到空值中断列表中。

在下面的示例中,第二个函数使用 sentinel(0, 1) 进行修饰,用容易理解的读法就是,可变列表中,最后一个 NULL 后面有 0 个变量,该 NULL 值可以出现在 void *a 的位置也可以

而第一个函数使用 sentinel(0, 0) 进行修饰,用容易理解的读法就是,** void *a 后面的可变列表中,最后一个 NULL 后面有 0 个变量**。因为调用函数时,只传了一个 NULL,参数,该参数只满足了对 void *a 的赋值,却不满足从后往前数第0个为 NULL,所以会产生可变参数不够的警告⚠️。

Paste_Image.png

pure

#define DISPATCH_PURE __attribute__((__pure__)) 宏将 __attribute__((__pure__)) 定义为 DISPATCH_PURE

__pure__ 的中文意思是纯净的。它只依赖于入参和全局变量进行处理。并且不会对外部造成影响。

pure 修饰的函数不具有可重入性,但是可以通过关中断、信号量(即P、V操作)等手段对全局变量加以保护以实现。

当被 pure 修饰的函数的返回结果没有被使用时,编译器会直接丢弃调用被 pure 修饰的函数的相关代码。

下面以 Implications of pure and constant functions中的例子进行说明。

int someimpurefunction(int a, int b);
int somepurefunction(int a, int b)
__attribute__((pure));

int testfunction(int a, int b, int c) {
    int res1 = someimpurefunction(c, b);
    int res2 = somepurefunction(b, c);
    int res3 = a + b - c;

    return a;
}

代码分析:

我们很容易发现,testfunction 函数内的部分代码是可以被忽略的。

比如 int res2 = somepurefunction(b, c);,只有返回值 res2 是由该函数生成的,在它的内部不会影响全局变量,所以该行代码可以全部忽略。

代码行:int res2 = somepurefunction(b, c); 在执行过程中可能影响全局变量,所以无法忽略,但是其返回值没有用处,所以,返回值可以忽略返回值。

代码行: int res3 = a + b - c; 很明显,因为返回结果 res3 没有使用,该行代码可以忽略掉。

所以,编译优化后代码为:

    int someimpurefunction(int a, int b);

    int testfunction(int a, int b, int c) {
      someimpurefunction(c, b);

      return a;
    }

const

DISPATCH_CONST __attribute__((__const__)) 宏将 __attribute__((__const__)) 定义为 DISPATCH_CONST

__const____pure__ 类似,但是 __const 不能依赖于全局变量。也就意味着,const 修饰的函数是幂等的,具有可重入性。

下面仍然通过改造 Implications of pure and constant functions 中的例子进行说明。

const struct {
    const char *str;
    int val;
} strings[] = {
    { "foo", 31 },
    { "bar", 34 },
    { "baz", -24 }
};

const char*lookup(int val) __attribute__ ((const));

int count = 0;

const char*lookup(int val) {
    printf("lookup被调用\n");
    int i;
    for(i = 0; i < sizeof(strings)/sizeof(*strings); i++)
        if(strings[i].val == val)
            return strings[i].str;

    return NULL;
}

void testfunction(int val, const char * *str, unsigned long *len) {
    printf("testfunction被调用\n");
    if(lookup(val)) {
        *str = lookup(val);
        *len = strlen(lookup(val));
    }
}

输出日志:

testfunction被调用
lookup被调用

编译器发现,lookup(val)被调用了三次,且在该过程中,val的值没有发生变化。所以编译器只调用了一次该函数,并缓存了其结果。

特殊情况1

请注意,虽然 const 修饰的函数是幂等的,但是当被 const 修饰的函数有入参时,我没有发现能够合理优化如下代码的编译器。

在下面的代码中, square 函数的入参并没有发生变化,所以,合理的优化应该是只调用一次 square 函数,并缓存结果以提供给后续的代码使用。但实际上,square 函数被调用了1000次。

int square (int i) __attribute__ ((const));
int count = 0;
int square (int i){
    printf("square被调用%d次\n", count++);
    return i;
}

void test(){
    for(int i = 0; i < 1000; i++) {
        int result = square(31415);
        printf("square(31415)=%d\n", result);
    }
}

特殊情况2

当被 const 修饰的函数有没有入参时,编译结果却发生了非常大的变化。编译器会尽可能的在编译期间执行里面的代码,并将结果直接赋值给调用方。
示例代码:

int retainConst () __attribute__ ((const));
int count = 0;

int retainConst(){
    int i = 100;
    printf("retainConst被调用%d次\n", count++);
    return i*i;
}

void test1(){
    for(int i = 0; i < 1000; i++) {
        int result1 = retainConst();
        printf("retainConst()=%d\n", result1);
    }
}

优化结果:

void test1(){
    for(int i = 0; i < 1000; i++) {
        printf("retainConst()=%d\n", 1000);
    }
}

warn_unused_result

#define DISPATCH_WARN_RESULT __attribute__((__warn_unused_result__)) 宏将 __attribute__((__warn_unused_result__)) 定义为 DISPATCH_WARN_RESULT

当函数的调用方没有使用函数的返回结果时,会产生一个警告。

比如下面的代码会在第5行产生一个⚠️警告。Ignoring return value of function declared with warn_unused_result attribute

      int fn () __attribute__ ((warn_unused_result));
      int foo ()
      {
        if (fn () < 0) return -1;
        fn ();
        return 0;
      }

malloc

#define DISPATCH_MALLOC __attribute__((__malloc__)) 宏将 __attribute__((__malloc__)) 定义为 DISPATCH_MALLOC

当一个函数返回的指针是类似于调用 malloc 函数时,可以指定该修饰符。

它表示通过这个函数返回的非空指针肯定与当前程序中其它的有效指针不同,即其值肯定不同与当前任何有效指针。这样编译器就能够识别出该指针不存在别名,可以进行更好的优化。

always_inline

#define DISPATCH_ALWAYS_INLINE __attribute__((__always_inline__)) 宏将 __attribute__((__always_inline__)) 定义为 DISPATCH_ALWAYS_INLINE

内联(inline) 函数,会导致代码增大(同一份代码存在多个地方),但是运行速度会加快(不需要跨函数调用)。

一般情况下,只有开启优化选项后,部分函数才能够被编译器当做内联函数编译。但是 __always_inline__ 可以在没有开启优化选项时,依然强制某个函数当做内联函数使用。

unavailable

#define DISPATCH_UNAVAILABLE __attribute__((__unavailable__)) 宏将 __attribute__((__unavailable__)) 定义为 DISPATCH_UNAVAILABLE

__unavailable__用于声明某个函数不可用。

DISPATCH_EXPORT


#if TARGET_OS_WIN32 && defined(__DISPATCH_BUILDING_DISPATCH__) && \
        defined(__cplusplus)
#define DISPATCH_EXPORT extern "C" extern __declspec(dllexport)
#elif TARGET_OS_WIN32 && defined(__DISPATCH_BUILDING_DISPATCH__)
#define DISPATCH_EXPORT extern __declspec(dllexport)
#elif TARGET_OS_WIN32 && defined(__cplusplus)
#define DISPATCH_EXPORT extern "C" extern __declspec(dllimport)
#elif TARGET_OS_WIN32
#define DISPATCH_EXPORT extern __declspec(dllimport)
#elif __GNUC__
#define DISPATCH_EXPORT extern __attribute__((visibility("default")))
#else
#define DISPATCH_EXPORT extern
#endif

DISPATCH_EXPORT 可以在文件中声明一个全局变量或函数。

DISPATCH_INLINE

#if __GNUC__
#define DISPATCH_INLINE static __inline__
#else
#define DISPATCH_INLINE static inline
#endif

DISPATCH_INLINE 可以将函数声明为静态内联函数。

__builtin_expect((x), (v))

#define DISPATCH_EXPECT(x, v) __builtin_expect((x), (v)) 宏将 __builtin_expect((x), (v)) 定义为 DISPATCH_EXPECT(x, v)

#if __GNUC__
#define DISPATCH_EXPECT(x, v) __builtin_expect((x), (v))
#else
#define DISPATCH_EXPECT(x, v) (x)
#endif

__builtin_expect 可以指定某个值的预期值。这样可以让编译器对相关的分支代码进行优化。

下面部分需要有部分的处理器相关的知识才能理解该部分。

在现代的处理器设计中,因为 cpu 和内存的速度不匹配。所以现在的 cpu 会读取多条指令并执行(多流水线)。比如,遇到分支时,它会同时计算分支的条件,并选择第一个分支执行。

比如下面的代码,__builtin_expect(x, 5) 代码告诉编译器,x 的值具有很大的概率是5。

switch (__builtin_expect(x, 5)) {
default: break;
case 0:  // ...
case 3:  // ...
case 5:  // ...
}

如果没有任何优化,处理器会经常进入default分支执行,导致资源浪费。在这种情况下,编译后的代码会调整分支的顺序以达到提高资源效率的最优化。

switch (__builtin_expect(x, 5)) {
 case 5:  // ...
default: break;
case 0:  // ...
case 3:  // ...
}

ns_returns_retained

#ifndef DISPATCH_RETURNS_RETAINED_BLOCK
#if defined(__has_attribute)
#if __has_attribute(ns_returns_retained)
#define DISPATCH_RETURNS_RETAINED_BLOCK __attribute__((__ns_returns_retained__))
#else
#define DISPATCH_RETURNS_RETAINED_BLOCK
#endif
#else
#define DISPATCH_RETURNS_RETAINED_BLOCK
#endif
#endif

__ns_returns_retained__ 告诉编译器,该函数的返回值需要进行retain 操作。

因为,在 iOS 系统中,dispatch_block_t 是被当做对象管理的,而对象的生命周期是通过引用计数管理的。

所以,当一个对象初始化后,需要将其引用计数设置为1,防止其销毁。

但是在其它平台。它可以被当做结构体处理,所以,不需要引用计数设置为1。为了兼容这两种模式,通过 DISPATCH_RETURNS_RETAINED_BLOCK 可以检测平台并合理设置引用计数。

__OSX_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0)
DISPATCH_EXPORT DISPATCH_NONNULL2 DISPATCH_RETURNS_RETAINED_BLOCK
DISPATCH_WARN_RESULT DISPATCH_NOTHROW
dispatch_block_t
dispatch_block_create(dispatch_block_flags_t flags, dispatch_block_t block);

DISPATCH_ENUM

#if defined(__has_feature) && defined(__has_extension)
#if __has_feature(objc_fixed_enum) || __has_extension(cxx_strong_enums)
#define DISPATCH_ENUM(name, type, ...) \
        typedef enum : type { __VA_ARGS__ } name##_t
#else
#define DISPATCH_ENUM(name, type, ...) \
        enum { __VA_ARGS__ }; typedef type name##_t
#endif

__has_feature__has_extension 是 clang 的语言特性。为了增强安全性,dispatch 希望能够通过 typedef enum { ... } tagname; 语法进行声明并定义枚举。但是为了兼容一些不支持该语法的平台版本,dispatch 通过宏实现对其的兼容处理。通过 clang 进行编译时,

这里以 block.h 文件中的 DISPATCH_ENUM(dispatch_block_flags, unsigned long,...) 为例进行说明。当通过 clang 预处理时,其会被转换为 typedef enum : unsigned long { __VA_ARGS__ } dispatch_block_flags_t;当通过其它平台进行处理时,会被转换为标准写法 enum { __VA_ARGS__ }; typedef unsigned long dispatch_block_flags_t

dispatch_function_t

typedef void (*dispatch_function_t)(void *);

该行代码是 base.h 文件中唯一的函数指针。其定义了一个名为 dispatch_function_t 的函数指针,它接收一个空指针,并且该函数返回为空(即无返回值)。

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

推荐阅读更多精彩内容