__attribute__指令
__attribute__指令在C、C++和Objective-C中修饰一段代码,用处一是为编译器提供上下文,用处之二是为未来看到这段代码的人(包括自己)提供额外的信息。__attribute__指令对编译器优化有重要意义,并且对于开发者来说一定程度的额外信息是有积极意义的。在开发过程中可以发现苹果的SDK中已经普遍运用了__attribute__指令。
__attribute__指令格式如下:
可以看到__attribute__后跟随了两个圆括号,主要是为了与宏区分开来。其中的attribute-list由逗号,
隔开,每个attribute可为如下格式:
-
空值,将会被忽略;
-
一个词,例如:unused、const等;
-
一个词,跟随着括号,括号里为此attribute的参数,每个参数可为如下形式:
-
一个标识符,如:
__attribute__((mode))
-
一个标识符,后面跟随着逗号和非空的由逗号隔开的表达式列表,例如
__attribute__((format(printf, 1, 2)))
; -
一个由逗号隔开的表达式列表,如
__attribute__((format_arg(2)))
-
__attribute__根据不同attribute,可以拿来修饰函数、变量、类型声明等,放置的位置也有所不同。
什么时候该用__attribute__?
能用就用
在充分了解了所要使用的指令后,尽量用上。但考虑到使用__attribute__可能带来的风险,建议一定在了解其特性后使用,不然它在一些情况下也能造成一些难以调试的Bug。
警惕其可能带来的风险
毕竟,__attribute__指令主要是给编译器看的——优化不是错,编译器也不容易出错,但人却很容易出错。考虑下面一段代码:
在上述代码中,XPL802_11ProtocolToString
函数返回了一个字符串的指针常量,这些指针常量都存储在文字常量区,是不可变的且拥有无限大的引用计数。此时我们可以利用__attribute__来告诉编译器进行优化:
上面的代码里,我们向编译器打包票这个函数返回的是个常量,妥妥哒!而且这段代码的确运行得棒棒哒,完美收工!但万恶的产品经理偏偏不让你清闲,于是一段时候后你不得不把代码改为如下形式:
插上手机,运行,通过!妥妥哒,生活真美好!直到产品上线……突然收集到了N+1个崩溃,而且你在机器上不断运行不断调适,都无法重现,突然这个世界都不够美好了,到底怎么了?
原因在于大部分开发者在开发中都是Debug模式,此模式下很多优化都被禁用了,于是我们告诉编译器进行的工作编译器可能并没进行,而一旦发布出去,编译器把该做的优化都做了,问题也就随之出现了。
上面的问题在于原来我们告诉编译器返回的是个常量,毕竟是字符串常量嘛。但一旦我们用上[NSString stringWithFormat:@"802.11%c", protocol]
,事情就不一样了。编译器依旧呆萌呆萌地把这个指针当作常量。但是,字符串常量有着无限大的retain count,而后者只是普通的在堆上的字符串,它只有有限的引用计数。于是就可能出现还没用上返回值,它就被回收了,然后不小心就访问了个野指针。
所以,因为不了解或者不小心引起的问题是很难调试的,这就要求我们在使用时要多加小心。但容易出现问题并不是说我们不能用它,而是要小心谨慎,毕竟大部分指令还是很好用。
一些常用的attribute
availability
这个应该在iOS开发中相当常见,用于声明所修饰的对象的访问性。有如下参数可选:
- 平台,可为macosx, ios, tvos, watchos;
- 何时引入,introduced=版本;
- 何时弃用,deprecated=版本;
- 何时废弃,obsoleted=版本;
- 不可用,unavailable,标识此平台不可用;
- 额外信息,message=字符串,可用来额外说明,如提示新的可用的替代方法。
下面是一些例子:
当然,也有些稍微简单的用法:
不过直接使用__attribute__还是稍显啰嗦了些,不过Foundation为我们封装了以下的宏:
- NS_AVAILABLE(_mac, _ios)
- NS_AVAILABLE_MAC(_mac)
- NS_AVAILABLE_IOS(_ios)
- NS_DEPRECATED(_macIntro, _macDep, _iosIntro, _iosDep, …)
- NS_DEPRECATED_MAC(_macIntro, _macDep, …)
- NS_DEPRECATED_IOS(_iosIntro, _iosDep, …)
以下就是这些宏的简单用法,相比之下比上面的简洁许多。
format(archetype, string-index, first-to-check)
format允许编译器检查传入的参数的格式,可以为printf、scanf类型风格的标准格式化输入输出方法类型。
- archetype: 可以为printf、scanf类型的标准格式化输入输出方法类型,如,printf、scanf、strftime、strfmon,下面是一些例子:
- string-index:大部分情况为format格式位置,从1开始计算;
- first-to-check:大部分情况为format参数位置,从1开始计算,视情况不同可能为0等。
对于Objective-C的方法,也是同样的,虽然最终依然生成C函数,并且隐式地包含了两个参数self和_cmd,但在Objective-C中依然是从1开始。
如果觉得上面的格式稍显繁杂的话,还是有相应的宏简化代码:
- __printflike(fmtarg, firstvararg);
- __scanflike(fmtarg, firstvararg);
- __strftimelike(fmtarg);
- __strftimelike(fmtarg);
改写上面的代码如下:
nonnull(…)
nonnull运用范围也是相当广泛,用以指定某个参数不能为空。其参数可为空也可为index列表,分别用以单独指定某个参数和指定多个参数不能为空情况。具体如下:
returns_nonnull
用以标识一个返回值为指针的函数不能返回空。
noreturn
这个相对特殊,标识一个函数不会返回。什么情况下一个函数被调用后不回返回值呢?1、直接结束程序;2、无限循环。
其中1相对好理解,对于2,AFNetworking有下面的用法:
const / pure
const
表示函数/方法的结果严格依赖于输入的参数,并且不修改外部任何状态,也就是说对应相同的参数输入一定是相同的输出。
pure
与const
相似,也不会改变任何外部状态。但pure除了输入还能依赖外部状态,如全局、静态变量等,根据外部状态不同,同样的参数输入不一定是相同的输出。
尽管Objective-C是一门动态的语言,但这两个参数还是有其积极意义的:
- 他人阅读起来会更清晰;
- 推荐对所有单例方法使用const;
- 编译器还是会进行优化的,并且如果Objective-C的方法经常被使用,并且用到了const或者pure,考虑转换为C函数。
当然,const的不正确使用会带来怎样的影响已经在上面提到了,不再赘述。
unused
此属性可能有一些小歧义,其表示的意义为:所修饰的对象可能不会被用到,并且编译器不会产生相应的警告。
可以使用__unused
作为替代。
overloadable
Objective-C不能实现方法重载(Overloading),因为消息传递的机制决定了重载的不可行。但是Objective-C是兼容C和C++的。所以,可以在C或者C++中实现函数重载。当然C++毋需多说,C中通过引入__attribute__((overloadable))可实现重载。其函数名重整方式与C++类似。
在C中,由overloadable修饰的函数名会通过与C++相同的方式进行重整,例如上面的三个函数名将会分别被重整为_Z5tgsinf
、_Z5tgsind
和_Z5tgsine
。同样,已有宏封装了此指令:OS_OVERLOADABLE。
需要注意的是,由于重载不依赖于运行时,所以调用哪个函数在编译时就已经确定了。对于需要重载的函数,必需包含一个及以上的参数。
enable_if
enable_if可实现参数的静态检查。其包含两个参数,第一个参数是条件,第二个参数是额外信息提示。
- 确定某个条件成立情况下函数可用;
- 配合overloadable使用,可对特定条件成立情况下进行定义;
- 配合overloadable使用,可在不可用时使用另一个重载的函数。
对于1,假设有如下代码:
对于2,假设有如下代码
可以看到在参数c在超出范围时,提示了unavailable定义的信息”Deprecated”。有人可能会注意到上面的两个overload_func(int c)并未提示重复定义,这是因为enable_if修饰的话,也同时定义在函数签名中(这也是为什么需要使用overloadable的原因)。
对于3,假设有如下代码:
可以看到在参数c不满足条件时,虽然c看起来是个整数,但依然自动执行了float为参数的函数。
objc_boxable
结构体(struct)和联合体(union)可以通过此属性使用Objective-C中的装箱语法@(...)
。
假设我们有着么一个结构体,包括其两个变量st1(用以装箱), st2(用以拆箱):
如果没有@(...)
的话,我们想要将一个结构体装入NSValue和取出的话需要这么做:
而通过@(...)
,这一切就自然多了:
objc_requires_super
有些类要求子类在重写方法时调用super方法,此属性可用以标识方法需要调用super。Foundation中封装了宏NS_REQUIRES_SUPER
。
父类中使用了此属性,子类中重写时如果未调用[super method],则会提示Method possibly missing a [super method] call
。
objc_runtime_name
objc_runtime_name允许在编译时将interface
或者protocol
的名字指定为其他名字。
此属性只能放在@protocol或者@interface前面。因为此方法可以很容易地更改类名,所以可以用来做代码混淆。
objc_method_family
Objective-C有其自己的命名规则,一定的命名规则对应一定的行为。如果我们希望一定的命名对应不同的规则,或者无规则的命名能够适用某个规则的话该怎么办?objc_method_family
给了方法。其格式为:__attribute__((objc_method_family(X)))
,其中X
可能为none
、alloc
、copy
、init
、mutableCopy
、new
。此属性只能放在Objective-C方法名后.
cleanup
这是一个神奇的属性,它允许一个变量在超出作用域范围时运行某个函数。运行的函数必须传入此变量的指针作为参数。如果有返回值都会被忽略。
注意:在执行的函数中,不允许捕获异常,其仅仅为执行一个操作而已。函数如果不正常返回的话,其行为是未定义的。
因为Block也是一个变量,所以在Reactive Cocoa中有个有意思的用法:
上面的代码将首先输出”Start”,然后输出”Hey, this is the end”。值得注意的是,如果有多个cleanup,则函数执行的顺序为栈的方式,后入先出。
constructor / destructor
constructor允许在main()函数执行前运行相应的函数。而destructor则在main()退出后或者exit()被调用后进行,但App被杀死后不会调用此函数。constructor / destructor都为GCC扩展(也就是其并非标准)。两个属性只能修饰函数而不能是方法。
假设有下列的constructor方法:
其输出应该为”constructor1”、”main”。说明constructor函数在main函数之前执行了,如果有多个constructor函数,其执行顺序是比较不确定的(与其编译单元定义有关),但我们可以通过设置优先级来进行constructor (priority)
,其中priority为101到65535的数字,越小的数字拥有越高的优先级。
可以看到constructor函数与+load
方法很类似,二者有什么区别呢?以下为+load
的调用时机顺序:
- 项目链接的所有framework初始化构造器;
- 自己代码的所有
+load
方法; - 所有C++的静态初始化构造器和C/C++的
__attribute__((constructor))
函数; - 链接此项目的framework的初始化构造器。
可以看到constructor函数的调用时机是晚于+load
的。并且相对于+load
,constructor函数可以在除Class以外的其他文件中。并且在constructor函数被调用时,我们可以保证所有类已经加载完毕,即此时可以不用顾忌操作类。
objc_subclassing_restricted
使用此属性修饰的类不能被继承,试图创建其子类编译器会提示错误。Swift在其生成的Objective-C代码的Header中添加这个属性,带来的结果就是Swift的类不能在Objective-C中被继承。
写在最后
__attribute__看起来简单,但搜集资料过程中(不得不吐槽下资料挺分散的),发现它竟包罗万象,异常强大,仅仅其attribute就多达数百个。上面提到的一些仅仅是我觉得比较有意思的几个,也是比较常用的几个。活用__attribute__的话,能够实现不少神奇的功能(如上面提到的cleanup)。这些神奇的功能也在不少开源框架中被用到,了解这些用法有助于我们更好地阅读它们。