『Apple API』__attribute__

__attribute__ 命令用于修饰 C, C++,Objective-C 语言的代码声明。给代码添加合适的命令,可使编译器进行相关优化,提高代码语义表达,编写时及时给出警告等。由此来说,这对编译器与开发者都是有益的事。

环境信息:
macOS 10.12.3
Xcode 8.2.1
iOS 10.2


constructor 与 destructor

使用 constructor 属性修饰的函数能在 main() 函数之前执行,而使用 destructor 属性修饰的函数,在 main() 函数结束或 exit() 函数调用后执行。

该属性不能用于修饰 Objective-C 的方法

// 任意类的 .m
__attribute__((constructor))
void testRunBeforeMain() {
    NSLog(@"run before main");
}

+ (void)load {
    NSLog(@"load called");
}

// main.m
int main(int argc, char * argv[]) {
    NSLog(@"main called");
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

// 分别输出
// load called
// run before main
// main called

从输出顺序可以看出,使用 constructor 修饰的 testRunBeforeMain() 虽然在 main() 函数之前调用,但是依然后于 + load 方法,所以还是需要注意各个方法调用时机的。

deprecated

标明方法、变量、类型已废弃。主要用于标明将来会废弃的方法,方便调用者及时查看。

- (void)testDeprecated __attribute__((deprecated));

调用时,首先会发现代码提示中,该方法已经划掉,若强行调用,则警告:testDeprecated is deprecated。

但是这样还不够明确,相应的,若调用同样废弃的方法,NSObjectfinalize 方法,却有着明确的弃用描述:

NSObject *obj = [NSObject new];
[obj finalize];

// Warning: Objective-C garbage collection is no longer supported

来看看 finalize 方法的声明:

- (void)finalize OBJC_DEPRECATED("Objective-C garbage collection is no longer supported");


// 其中 OBJC_DEPRECATED 宏在 objc-api.h 中的定义为

/* OBJC_DEPRECATED: deprecated, with a message where supported */
#if !defined(OBJC_DEPRECATED)
#   if __has_extension(attribute_deprecated_with_message)
#       define OBJC_DEPRECATED(_msg) __attribute__((deprecated(_msg)))
#   else
#       define OBJC_DEPRECATED(_msg) __attribute__((deprecated))
#   endif
#endif

其实这个宏,也是使用的 __attribute__((deprecated)),不过带了 _msg 参数而已。但这也是废弃方法、属性等操作更为常用的一种方式。

枚举

相比起方法的废弃,枚举值的废弃在日常迭代中,更为常见。对此,系统也提供了多种废弃方式,不过说到底,还是使用的 __attribute__((deprecated))

typedef NS_ENUM(NSInteger, TestEnum) {
    TestEnum1,
    TestEnum2 __deprecated_enum_msg("v5.0"),
    TestEnum3
};

format

要求编译器进行类型检测。

这个函数间接用得多,直接用得少。NSLogNSStringstringWithFormat: 等方法中,在书写 format 中对应的参数时,对类型进行检测。来看看 NSLog 后接的 NS_FORMAT_FUNCTION

NSLog(@"第一个参数 %d", 1);

// Marks APIs which format strings by taking a format string and optional varargs as arguments
#if !defined(NS_FORMAT_FUNCTION)
    #if (__GNUC__*10+__GNUC_MINOR__ >= 42) && (TARGET_OS_MAC || TARGET_OS_EMBEDDED)
	#define NS_FORMAT_FUNCTION(F,A) __attribute__((format(__NSString__, F, A)))
    #else
	#define NS_FORMAT_FUNCTION(F,A)
    #endif
#endif

实质就是用到的 format 属性,三个参数分别传入的 __NSString__, F, A,表示:接收的原型,format 的起始,需要检查的参数起始。

nonnull

用于修饰方法参数不可空,与 nullability 中修饰符类似。

// 以下两种定义等价,均为参数不可空

- (void)testNonnull:(id)avg1 avg2:(id)avg2 __attribute__((nonnull));

- (void)testNonnull:(nonnull id)avg1 avg2:(nonnull id)avg2;

虽然 __attribute__((nonnull)) 可以有参数,用于控制连续几个参数不为空,但是相比之下,对于单个参数给出 nonnull 显然更灵活。除了 nonnull 修饰符以外,还有 nullable 等,具体可查看:『Apple API』Nullability

noreturn

告知编译器,该方法不会返回(会直接结束)。编译器可忽略方法调用完成的处理,从而进行相应优化。多用于强制程序退出、死循环等情况。如系统提供的 abortexit,又或是我司的切换服务器业务,在切换后,直接退出程序:

- (void)switchServer:(NSURL *)server __attribute__((noreturn));

- (void)switchServer:(NSURL *)server {
    [[NSUserDefaults standardUserDefaults] setObject:server forKey:@"server"];
    exit(0);
}

该属性在 AFNetworking 中也用到了:

// 确保专门用于维护网络请求的线程,在程序生命周期中一直执行
+ (void) __attribute__((noreturn)) networkRequestThreadEntryPoint:(id)__unused object {
    do {
        @autoreleasepool {
            [[NSRunLoop currentRunLoop] run];
        }
    } while (YES);
}

pure 与 const

用于函数的返回值仅与入参有关,并且函数状态单一。pure 除了与入参有关外,还与全局变量有关。

这两个函数均属于函数式编程,且 const 要比 pure 更为严格一些。

对于这两个属性,twitter blog 给出的建议是:

  • 虽然对于 runtime 来说,加不加 constpure 关系不大,但是这对提高接口可读性帮助非常大。
  • 建议给不需要传入任何参数的方法加上该属性。正因为不需要入参,所以无论何时返回值都是相同的,那么完全可以对返回值进行缓存,之后调用时,直接返回缓存的结果即可。比如,单例的初始方法(#1)。
  • 如果 Objective-C 某方法用 pureconst 修饰了,并且调用非常频繁,那么应该考虑将其设计为 C 的接口,可优化函数开销(#2)。
  • 即便 pureconst 这么好用,但是一旦用错,会造成 bug 不易发现(详见文末介绍)。

除此之外,系统还在 usr/include/os/base.h 头文件中提供了两个宏:OS_PUREOS_CONST

// #1
+ (instancetype)shareInstance __attribute__((const)); 

// #2
const char *StringWithErrorNumber(int errorNumber) __attribute__((const));

sentinel

哨兵,用于确保函数调用时的参数为 NULL。在程序中,多见于 NSArrayarrayWithObjectsNSDictionarydictionaryWithObjectsAndKeys: 等方法。

然而在 Apple API 中,更常见到的是 NS_REQUIRES_NIL_TERMINATION 宏。详细介绍可查看:『Apple API』从 NSDictitonary 中学接口定义

warn_unused_result

警告返回值未使用。系统提供宏:OS_WARN_RESULT

const 使用事项

前文提到 constpure 的作用,当方法的返回值仅与入参(全局变量)有关时,可将方法修饰为 constpure,如果该方法会频繁调用,建议将方法改为函数,这有助于编译优化。那么,使用 const 有什么风险呢?下面来看 Twitter 文中提到的一个例子:

// 定义枚举 TestEnum
typedef NS_ENUM(NSInteger, TestEnum) {
    TestEnum1,
    TestEnum2,
    TestEnum3
};

// 函数声明
FOUNDATION_EXTERN NSString * EnumToString(TestEnum testEnum);

// 函数实现
NSString * EnumToString(TestEnum testEnum) {
    switch (testEnum) {
        case TestEnum1:
            return @"TestEnum1";
        case TestEnum2:
            return @"TestEnum2";
        case TestEnum3:
            return @"TestEnum3";
        default:
            return @"";
    }
}

可以发现,以上将 enum 转为 string 的函数返回值,仅与传入的 enum 有关,所以,完全可以给该函数加上 const:

FOUNDATION_EXTERN NSString * EnumToString(TestEnum testEnum) OS_CONST;

OK,没毛病。那如果改一下实现呢?

NSString * EnumToString(TestEnum testEnum) {
    return [NSString stringWithFormat:@"TestEnum%ld", testEnum + 1];
}

这样写…就会出问题了,而且很难复现,即使复现也很难找到原因。

仅在高度优化时,才会导致 crash,原因:访问了一个已释放的对象。让我们再来看看添加 constpure 时,编译器会做的事情:

当使用 constpure 时,编译器会将 NSString 缓存到持久内存中,此时引用计数为无穷大。

知道这一步处理,问题就很明确了。在新的实现中,将硬编码的返回值改为了 stringWithFormat: 动态创建,那就意味着,该方法中的返回值地址,每次都在变。这种创建方式有正常的引用计数,那么就会走正常的对象释放流程。所以,在编译器返回缓存的地址的时候,地址对应的对象,很有可能已经释放了,从而导致 crash。

对于这个问题,记住一条:如果方法或函数使用 const,那么返回值一定要是常量,若返回值为引用,则引用也要是常量(地址不变)。

参考资料

发表评论

电子邮件地址不会被公开。