『libextobjc』@weakify 与 @strongify 的实现

libextobjc 对 Objective-C 做了非常多的扩展,包括 protocol 默认实现、 @keypath()@weakify()@onExit() 等等。其中对 runtime 与宏定义的使用可谓出神入化,本文将讨论 @weakify() 的实现。


为避免循环引用,一般会有以下写法:

__weak typeof(self) self_weak_ = self;
self.block = ^{
  __strong typeof(self_weak_) strongSelf = self_weak_;
};

而如果使用 @weakify(),写法为:

@weakify(self);
self.block = ^{
  @strongify(self);
};

至此,有几个疑问:

  1. @strongify() 是如何匹配到 weak 定义的变量的?
  2. 为什么经过 @strongify() 修饰以后,使用 self 就不会再造成循环引用?
  3. @weakify() 是如何实现可以修饰多个变量的?
  4. @ 到底是什么?如何实现?

问题一:如何匹配到 weak 修饰的变量

@weakify() 的目标,就是在预编译时期将括号中的变量替换为用 __weak 修饰的变量。如果 @weakify() 括号中只有一个变量,那很简单(暂时不考虑 @ 符号的实现):

#define weakify(VAR) __weak typeof(VAR) var_weak_ = VAR

但是这样有一个问题,如果出现以下情况:

weakify(self);
weakify(_tableView);

// 替换之后变为:
__weak typeof(self) var_weak_ = self;
__weak typeof(_tableView) var_weak_ = _tableView;

因为 var_weak_ 是写死的,所以如果多次使用 weakify,就会出现变量重定义的问题。

如此,首要任务是「将不同变量赋值给对应的 weak 变量」。其中需要使用到宏定义的字符串化与拼接:

#define concat(A, B) A ## B

// concat(self, 3) -> self3

所以,宏定义可改为:

#define concat(A, B) A ## B
#define weakify(VAR) __weak typeof(VAR) concat(VAR, _weak_) = VAR

// 替换之后变为:
__weak typeof(self) self_weak_ = self;

那么,在 @strongify() 的时候,去匹配 concat(VAR, _weak_) 的变量就好了。

问题二:为什么 @strongify() 之后,self 就不会再循环引用

首先,@strongify() 肯定需要匹配上 @weakify() 生成的变量 var_weak_,然后再做替换,所以,@strongify() 的实现大致为:

#define strongify(VAR) __strong typeof(VAR) VAR = concat(VAR, _weak_)

// 替换之后变为:
__strong typeof(self) self = self_weak_

所以,在此 @strongify() 之后,self 即 self_weak_。由此还可推断出,如果没有在 block 中使用 @strongify(),还是会导致循环引用。

问题三:@weakify() 如何实现多个变量

其实这个问题可以归结为:「如何在预编译时期,获得可变参数个数」。linextobjc 中的实现非常取巧。首先,需要知道在宏中,__VA_ARGS__ 表示可变参数列表。

假设代码是这样的:

@weakify(self, _a, _b);

在预编译阶段,获得可变参数个数

libextobjc 中用 metamacro_argcount(...) 来获得参数个数:

#define metamacro_argcount(...) \
        metamacro_at(20, __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)

metamacro_argcount 中,直接调用了 metamacro_at 宏,可以将传入 metamacro_at 的参数看为三段:20__VA_ARGS__20, 19, 18, ..., 1。即:20self, _a, _b20, 19, 18, ..., 1

metamacro_at 的定义为:

#define metamacro_at(N, ...) \
        metamacro_concat(metamacro_at, N)(__VA_ARGS__)

metamacro_at 接收的参数列表为 (N, ...),根据传入的参数可知,N => 20。而 self, _a, _b, 20, 19, 18, ..., 1。在实现中,metamacro_concat(metamacro_at, N)metamacro_at20 拼接为了 metamacro_at20。所以,转化为:metamacro_at20(__VA_ARGS__)

metamacro_at20 的实现…,大家自己看吧:

#define metamacro_at0(...) metamacro_head(__VA_ARGS__)
#define metamacro_at1(_0, ...) metamacro_head(__VA_ARGS__)
#define metamacro_at2(_0, _1, ...) metamacro_head(__VA_ARGS__)
#define metamacro_at3(_0, _1, _2, ...) metamacro_head(__VA_ARGS__)
#define metamacro_at4(_0, _1, _2, _3, ...) metamacro_head(__VA_ARGS__)
.
.
.
#define metamacro_at20(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, ...) metamacro_head(__VA_ARGS__)

所以,有 metamacro_at 有 1~20 个实现,区别在于它们的固定参数个数。而前 20 个参数(self, _a, _b, 20, 19, ..., 5, 4)都被固定参数接收,而剩下的 3, 2, 1 被可变参数列表接收,所以,metamacro_head(__VA_ARGS__) 接收的参数就是 3, 2, 1

再来看看 metamacro_head 的实现:

#define metamacro_head(...) \
        metamacro_head_(__VA_ARGS__, 0)

#define metamacro_head_(FIRST, ...) FIRST

这一步就很清晰了,直接返回 FIRST 即 3,也就是参数的个数。

脑洞确实比较清奇,为此我画了一幅不那么清晰的图,如果前面的描述能看懂,就没必要看了。

遍历所有参数,实现 __weak

之前求参数的个数,就是为了在这里能知道需要遍历多少次。遍历的步骤,也是在预编译时期完成的。遍历的方式也和之前计算参数个数的方式异曲同工:

metamacro_foreach_cxt 与参数个数拼接,可知应该调用 metamacro_foreach_cxt3。然后递归调用,直到 metamacro_foreach_cxt0 替换为 __weak

#define metamacro_foreach_cxt0(MACRO, SEP, CONTEXT)
#define metamacro_foreach_cxt1(MACRO, SEP, CONTEXT, _0) MACRO(0, CONTEXT, _0)

#define metamacro_foreach_cxt2(MACRO, SEP, CONTEXT, _0, _1) \
    metamacro_foreach_cxt1(MACRO, SEP, CONTEXT, _0) \
    SEP \
    MACRO(1, CONTEXT, _1)

#define metamacro_foreach_cxt3(MACRO, SEP, CONTEXT, _0, _1, _2) \
    metamacro_foreach_cxt2(MACRO, SEP, CONTEXT, _0, _1) \
    SEP \
    MACRO(2, CONTEXT, _2)

问题四:@ 的实现

@ 前缀目的是为了显眼,对此,libextobjc 采用的是加 @autoreleasepool {}@try {} catch() {}

#if defined(DEBUG) && !defined(NDEBUG)
#define ext_keywordify autoreleasepool {}
#else
#define ext_keywordify try {} @catch (...) {}
#endif

在 debug 模式下,用的是 @autoreleasepool{},而在 release 模式下,是 @try {} @catch () {},原因是因为 try...catch 会影响 Xcode 的警告功能,导致返回值的检测出问题:

// 比如这个 block 是需要有返回值的,但是因为 try...catch 的存在,导致没有报错或警告
@weakify(self);
self.block = ^BOOL {
  @strongify(self);
  NSLog(@"123");
}

对于空的 try...catch,Xcode 会在编译时期优化掉,而 @autoreleasepool 不会被优化,所以在 release 中不采用。

参考资料

发表评论

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