Date Notes Notes
2019-07-27 首次提交 -

0

Preface

Obj-C 中的 Block,即闭包,其相关知识点非常多,那么本文就来仔细谈谈其中的概念与原理。

⚠️

文中代码块中如明确标示 // MRC 即表明该处代码块运行环境是 MRC,需要关闭 ARC(Automatic Reference Counting,自动引用计数)。ARC 的开关可以参照下图设置(No: MRC, Yes: ARC):

3

本质

// main.m
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ^{
            NSLog(@"Hello, World!");
        }();
    }
    return 0;
}

// OUTPUT:
// Hello, World!

main 函数中声明一个最简单的 Block,其中只有一句打印 Hello, World! 的语句。为了看清 Block 的结构,我们尝试使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp 命令将「main.m」翻译为 C/C++ 代码来分析:

struct __block_impl {
  void *isa;     // isa 指针
  int Flags;     // 标记,默认会被初始化为 0
  int Reserved;  // 保留域
  void *FuncPtr; // Block 代码块的函数指针
};

static struct __main_block_desc_0 {
  size_t reserved;   // 保留域,默认 0
  size_t Block_size; // Block 大小,sizeof 整个 Block 结构体
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

struct __main_block_impl_0 {
  struct __block_impl impl;         // 实现,注意非指针
  struct __main_block_desc_0* Desc; // 描述信息的引用
  // 构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// 静态函数封装 Block 内代码块
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_ps_0m9gnvtj0893vpf1cr595djh0000gn_T_main_429af3_mi_0);
        }

// 声明 block 变量(_指向 _main_block_impl_0 的指针)
// void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

// 执行 Block 内部代码
// 在 __main_block_impl_0 结构体中,impl 是第一个变量,因此其与结构体本身的首地址一致,因此可以强转
// block->FuncPtr(block);
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

当我们声明 Block 后,其中的代码会被封装到 __main_block_func_0 静态函数中(后缀 0 代表序号),用来和 __main_block_desc_0_DATA 构造 __main_block_impl_0,并在该构造函数中将函数名(即函数指针)赋值给 FuncPtr;最终在 Block 真正调用时,通过 block->FuncPtr(block) 执行 Block 代码块。

1

将如上结构图形化,Obj-C 的 Block 的本质是 __main_block_impl_0 结构体。该结构体中又直接包含了 __block_impl 结构体,以及指向 __main_block_desc_0 结构体的指针。在 __block_impl 结构体中,isa 暗示了其本质是 Obj-C 对象的事实,而 FuncPtr 函数指针则指向封装了 Block 中要执行的代码块的静态函数。所以总的来说 Block 本质即封装了函数调用以及函数调用环境的 Obj-C 对象

变量捕获

局部变量

自动变量

自动变量(Automatic Variable)即局部作用域变量,具体指在代码块中声明的变量(离开作用域会自动销毁),也可以使用 auto 关键字来显式声明。

// main.m
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // auto int a = 1;
        int a = 1;

        void (^block)(void) = ^() {
            NSLog(@"a == %d", a);
        };

        block(); // a == 10
    }
    return 0;
}

对于自动变量,由于其生命周期可能小于 Block 本身,因此 Block 会将自动变量捕获到结构体内部(即值传递),因此即使后续更改原有的变量值也不会影响已经被捕获的变量值。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a; // ⚠️ 捕获的变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  // a:取值
  int a = __cself->a; // bound by copy

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_ps_0m9gnvtj0893vpf1cr595djh0000gn_T_main_3c9d7c_mi_0, a);
}

int a = 1;
// 传入 a 本身作为参数
// 去掉类型转换:void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a));
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
// 去掉类型转换:block->FuncPtr(block);
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

刚才我们分析了 Block 对于不同类型的局部变量捕获区别,那么 self 呢?

#import "A.h"

@implementation A

- (void)objectFunc {
    void (^block)(void) = ^() {
        NSLog(@"%@", self);
    };

    block();
}

@end

Obj-C 中的方法默认都会带有两个参数,即当前对象 self 和当前方法 _cmd。函数参数的作用域又在函数体内,属于自动变量,因此 Block 也会对其本身直接进行捕获:

// - (void)objectFunc
static void _I_A_objectFunc(A * self, SEL _cmd) {
    void (*block)(void) = ((void (*)())&__A__objectFunc_block_impl_0((void *)__A__objectFunc_block_func_0, &__A__objectFunc_block_desc_0_DATA, self, 570425344));

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

struct __A__block_block_impl_0 {
  struct __block_impl impl;
  struct __A__block_block_desc_0* Desc;
  A *self; // ⚠️ 捕获的 self
  __A__block_block_impl_0(void *fp, struct __A__block_block_desc_0 *desc, A *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __A__block_block_func_0(struct __A__block_block_impl_0 *__cself) {
  A *self = __cself->self; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_ps_0m9gnvtj0893vpf1cr595djh0000gn_T_A_2e834a_mi_0, self);
    }

那么成员变量呢?

#import "A.h"

@implementation A {
    NSString *_memVar;
}

- (void)objectFunc {
    void (^block)(void) = ^() {
        NSLog(@"%@", _memVar);
    };

    block();
}

@end

同理如果 Block 中引入了成员变量,本质其实是通过 self 进行访问,也会对 self 本身进行捕获:

struct __A__objectFunc_block_impl_0 {
  struct __block_impl impl;
  struct __A__objectFunc_block_desc_0* Desc;
  A *self; // ⚠️ 捕获的 self
  __A__objectFunc_block_impl_0(void *fp, struct __A__objectFunc_block_desc_0 *desc, A *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __A__objectFunc_block_func_0(struct __A__objectFunc_block_impl_0 *__cself) {
  A *self = __cself->self; // bound by copy

        // self + OBJC_IVAR_$_A$_memVar:成员变量是根据 self 做的偏移
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_ps_0m9gnvtj0893vpf1cr595djh0000gn_T_A_f4faca_mi_0, (*(NSString **)((char *)self + OBJC_IVAR_$_A$_memVar)));
    }

局部静态变量

局部静态变量指定义在函数体(代码块)内的静态变量,其作用域在函数体内,但并不会随函数返回而销毁。

// main.m
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        static int a = 1;

        void (^block)(void) = ^() {
            NSLog(@"a == %d", a);
        };
        a = 10;

        block();
    }
    return 0;
}

// OUTOUT:
// a == 10

由于静态局部变量的生命周期随程序退出而结束,Block 只需要将局部静态变量的地址进行捕获(即引用传递),这样即使超过函数体的作用域 Block 中仍然可以访问到。而正因为捕获的是地址,因此当外界在 Block 执行前改变了局部静态变量的值,那么执行时也将获取到最新的值。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *a; // ⚠️ 捕获的变量地址
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  // *a:取地址中存储的值
  int *a = __cself->a; // bound by copy

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_ps_0m9gnvtj0893vpf1cr595djh0000gn_T_main_adf509_mi_0, (*a));
}

static int a = 1;
// 传入 &a 即 a 的地址作为参数
// 去掉类型转换:void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &a);
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a));

// a 的值被改变
a = 10;

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

全局变量

全局变量即定义在所有函数体外的变量,其生命周期结束语程序退出:

// main.m
// 全局变量:
int a = 1;
static int b = 2;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block)(void) = ^() {
            NSLog(@"a == %d, b == %d", a, b);
        };

        a = 10;
        b = 20;

        block();
    }
}

// OUTOUT:
// a == 10, b == 20

因此对于全局变量,Block 并不会去捕获,在使用的时候直接进行读取即可:

int a = 1;
static int b = 2;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_ps_0m9gnvtj0893vpf1cr595djh0000gn_T_main_437bca_mi_0, a, b);
        }

类型

Obj-C 中的 Block 根据其存储在不同的内存区域被分为三种:__NSGlobalBlock____NSStackBlock____NSMallocBlock__,它们又各自继承自 __NSGlobalBlock__NSMallocBlock__NSStackBlock,这些父类又都继承自 NSBlockNSBlock 又继承自 NSObject

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block1)(void) = ^() {
            NSLog(@"Hello, world!");
        };

        int a = 1;
        void (^block2)(void) = ^() {
            NSLog(@"%d", a);
        };

        NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{
            NSLog(@"%d", a);
        } class]);

        NSLog(@"%@ %@ %@", [block1 superclass], [block2 superclass],[^{
            NSLog(@"%d", a);
        } superclass]);

        NSLog(@"%@ %@ %@", [[block1 superclass] superclass], [[block2 superclass] superclass],[[^{
            NSLog(@"%d", a);
        } superclass] superclass]);

        NSLog(@"%@ %@ %@", [[[block1 superclass] superclass] superclass], [[[block2 superclass] superclass] superclass],[[[^{
            NSLog(@"%d", a);
        } superclass] superclass] superclass]);
    }
    return 0;
}

// OUTPUT:
// __NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__
// __NSGlobalBlock __NSMallocBlock __NSStackBlock
// NSBlock NSBlock NSBlock
// NSObject NSObject NSObject

⚠️

在上一节中,我们可以从翻译的 C++ 代码中看到 Block 内部的 isa 指针指向了 &_NSConcreteStackBlock,但其实在 Xcode 中并不存在「翻译」的步骤,因此这里我们最好使用运行时的方法来真正确定 Block 的类型。

__NSGlobalBlock__

Block 内没有访问外界自动变量的 Block 均属于 __NSGlobalBlock__,其存储在内存的数据区(Data 段),该区域通常也会存放全局变量。

// main.m
int a = 1;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 没有访问任何外界变量
        void (^gloablBlock)(void) = ^{
            NSLog(@"This is a __NSGlobalBlock__.");
        };
        gloablBlock();

        NSLog(@"%@", [gloablBlock class]);

        static int b = 1;

        // 访问了全局变量或局部静态变量
        gloablBlock = ^{
            NSLog(@"a == %d, b == %d.", a, b);
        };
        gloablBlock();

        NSLog(@"%@", [gloablBlock class]);
    }
}

// OUTPUT:
//  This is a __NSGlobalBlock__.
// __NSGlobalBlock__
// a == 1, b == 1.
// __NSGlobalBlock__

__NSStackBlock__

__NSStackBlock__ 类型的 Block 存储在内存的栈区,而栈区内存是不需要开发者来管理的,超过作用域的栈区内存将会被自动回收:

// MRC

// main.m
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int c = 1;

        void (^stackBlock)(void) = ^{
            NSLog(@"c == %d.", c);
        };
        stackBlock();

        NSLog(@"%@", [stackBlock class]);
    }
}

// OUTPUT:
// c == 1.
//  __NSStackBlock__

内部访问了外界自动变量的 Block 在 MRC 下属于 __NSStackBlock__(自动变量被分配在了栈区),所以即使 Block 内部捕获了自动变量,但这个 Block 本身和捕获的变量也仍然在栈区,会随着其作用域而释放:

// MRC

// main.m
void (^stackBlock)(void);

void initBlock() {
    int c = 10;
    stackBlock = ^{
        NSLog(@"c == %d.", c);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 首先在 initBlock 中初始化 stackBlock
        initBlock();
        // 执行 stackBlock Block
        stackBlock();

        // EXC_BAD_ACCESS
        // NSLog(@"stackBlock is a %@.", [stackBlock class]);
    }
}

// OUTPUT:
// c == -272632648.

此时 c 变成了一个脏数据,同理访问 Block 也出现了 EXC_BAD_ACCESS。为了避免这种问题,我们需要将其分配在堆上(即 __NSMallocBlock__),这是因为堆区的内存是开发者自己来管理的,也就可以避免被自动回收。

__NSMallocBlock__

__NSMallocBlock__ 类型的 Block 存储在内存的堆区,堆区通常用作动态分配(Malloc)的内存。在 MRC 下,对于 __NSStackBlock__ 类型的 Block 只要再对其发送 copy 消息即可将栈上的内存拷贝到堆上:

// MRC

// main.m
void (^mallocBlock)(void);

void initBlock() {
    int c = 10;
    mallocBlock = [^{
        NSLog(@"c == %d.", c);
    } copy];
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 首先在 initBlock 中初始化 mallocBlock
        initBlock();
        // 执行 mallocBlock Block
        mallocBlock();

        NSLog(@"mallocBlock is a %@.", [mallocBlock class]);
    }
}

// OUTPUT:
// c == 10.
// mallocBlock is a __NSMallocBlock__.

⚠️

  1. 对于 [__NSGlobalBlock__ copy],仍将返回 __NSGlobalBlock__
  2. 对于 [__NSStackBlock__ copy],将从栈拷贝到堆区,返回 __NSMallocBlock__
  3. 对于 [__NSMallocBlock__ copy],引用计数会增加(相当于 ratain)。(TODO??如何证明??)
  • 在 ARC 下,编译器会根据情况将栈上的 Block(__NSStackBlock__)拷贝(copy)到堆上:
    • Block 作为函数返回值时;
    • 将 Block 赋值给 __strong 指针时;
    • Block 作为 Cocoa API 中方法名含有 usingBlock 的参数时(eg. - (void)enumerateObjectsUsingBlock:(void (NS_NOESCAPE ^)(ObjectType obj, NSUInteger idx, BOOL *stop))block);
    • Block 作为 GCD API 时。
typedef void(^StackBlock)(void);

StackBlock returnStackBlock() {
    auto int autoVar = 10;

    NSLog(@"%@", [^{
        NSLog(@"%d", autoVar);
    } class]);

    // Block 作为函数返回值 -> copy(ARC)
    // 此处代码若在 MRC 下会编译报错「Returning block that lives on the local stack」
    // 即编译器已经发现返回的 Block 是在栈上,一旦函数体走完,Block 就会被销毁,因此在 MRC 下需要手动 copy:
    // return [^{ NSLog(@"%d", autoVar); } copy]; // MRC
    return ^{
        NSLog(@"%d", autoVar);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        StackBlock mallocBlock = returnStackBlock();
        mallocBlock();

        NSLog(@"mallocBlock is a %@.", [mallocBlock class]);

        auto int autoVar = 10;

        // block 强指针指向了 StackBlock,因此 ARC 下编译器自动为其 copy 为 __NSMallocBlock__
        StackBlock block = ^{
            NSLog(@"%d", autoVar);
        };

        NSLog(@"block is a %@.", [block class]);

        // 弱引用则不会 copy,即仍然是 __NSStackBlock__
        __weak StackBlock block2 = ^{
            NSLog(@"%d", autoVar);
        };

        NSLog(@"block2 is a %@.", [block2 class]);
    }
}

// OUTPUT:
//  __NSStackBlock__
// 10
// mallocBlock is a __NSMallocBlock__.
// block is a __NSMallocBlock__.
// block2 is a __NSStackBlock__.

因此,在 Block 作为属性时应当使用 copystrong 修饰:

// MRC
@property (nonatomic, copy) void (^block1)(void);

// ARC
@property (nonatomic, copy) void (^block2)(void);
@property (nonatomic, strong) void (^block3)(void);

对象类型的自动变量

自定义一个类型 Person 和 Block 的类型 SomeBlock,便于下面使用:

@interface Person : NSObject
@property (nonatomic, assign) NSUInteger age;
@end

@implementation Person
- (void)dealloc
{
    // [super dealloc]; // MRC 下需显式调用
    NSLog(@"dealloc");
}
@end

typedef void(^SomeBlock)(void);

__NSStackBlock__

上节提到,栈区的 __NSStackBlock__ 在超出作用域时会被自动销毁。ARC 下其捕获的自动变量无论被强弱指针指向,其栈区空间仍会随 Block 销毁而销毁:

{
    Person *p = [[Person alloc] init];
    p.age = 20;

    NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)(p)));

    __weak Person *weakP = p;

    NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)(p)));

    // __NSStackBlock__
    ^{
        NSLog(@"%lu", (unsigned long)p.age);
        NSLog(@"%lu", (unsigned long)weakP.age);
    };

    NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)(p)));
};

NSLog(@"---");

// OUTPUT:
// 1
// 1
// 2
// dealloc
// ---

MRC 下,__NSStackBlock__ 不会对捕获的变量本身进行强引用或持有(Retain)操作,因此超出 p 本身的作用域,即使 Block 本身没有被销毁,其捕获的对象也会被销毁。

// MRC
SomeBlock block;
{
    Person *p = [[Person alloc] init];
    p.age = 20;

    NSLog(@"%ld", [p retainCount]);

    // __NSStackBlock__
    block = ^{
        NSLog(@"%lu", (unsigned long)p.age);
    };

    NSLog(@"%ld", [p retainCount]);

    [p release];
};
NSLog(@"---");

// OUTPUT:
// 1
// 1
// dealloc
// ---

__NSMallocBlock__

ARC 下 __NSMallocBlock__ 捕获进去的 p 将被 Block 强引用还是弱引用呢?

SomeBlock block;
{
    Person *p = [[Person alloc] init];
    p.age = 20;

    // __NSMallocBlock__
    block = ^{
        NSLog(@"%lu", (unsigned long)p.age);
    };
};
NSLog(@"---");

// OUTPUT:
// ---
// dealloc

我们可以为翻译 Obj-C 代码的命令添加 -fobjc-arc -fobjc-runtime=ios-8.0.0 参数强制使用 ARC 并指定运行时平台和版本,这样即可输出带有 ARC 运行时的 C/C++ 代码:

struct __main_block_impl_0 {
  // ...
  Person *__strong p;
  // ...
};

如上,此时外界强引用的 p 被捕获后也仍是强引用,因此在超出指针本身作用域时并不会被释放,只有当 block 也被销毁时才会销毁。

MRC 下对 __NSStackBlock__ 进行 copy 将把 Block 拷贝到堆上,变为 __NSMallocBlock__。此时 block 将对 p 指向的对象持有,引用计数加一,因此在超出 p 作用域时也不会将对象销毁。

// MRC

SomeBlock block;
{
    Person *p = [[Person alloc] init];
    p.age = 20;

    // __NSMallocBlock__
    block = [^{
        NSLog(@"%lu", (unsigned long)p.age);
    } copy];

    [p release];
};
NSLog(@"---");

[block release];

// OUTPUT:
// ---
// dealloc

ARC 下,外界使用 __weak 声明的指针在 Block 中也不会被强引用,引用计数不变,此时超出指针的作用域后对象即被销毁:

SomeBlock block;
{
    Person *p = [[Person alloc] init];
    p.age = 20;

    __weak Person *weakP = p;

    block = ^{
        NSLog(@"%lu", (unsigned long)weakP.age);
    };
};
NSLog(@"---");

// OUTPUT:
// dealloc
// ---

捕获的 weakP

struct __main_block_impl_0 {
  // ...
  Person *__weak weakP;
  // ...
};

__NSStackBlock__ 拷贝到堆上的细节

上节提到,ARC 下当 __NSStackBlock__ 被强指针指向时会被拷贝到堆上,那么在「拷贝」时会发生什么呢?

SomeBlock block;
{
    Person *p = [[Person alloc] init];
    p.age = 20;

    __weak Person *weakP = p;

    // __NSMallocBlock__
    block = ^{
        NSLog(@"%lu", (unsigned long)p.age);
        NSLog(@"%lu", (unsigned long)weakP.age);
    };
};
NSLog(@"---");

// OUTPUT:
// ---
// dealloc

我们将以上代码使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -fobjc-arc -fobjc-runtime=ios-8.0.0 -o main.cpp 翻译为 C/C++:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // 默认强引用
  Person *__strong p;
  // 显式弱引用
  Person *__weak weakP;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__strong _p, Person *__weak _weakP, int flags=0) : p(_p), weakP(_weakP) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  Person *__strong p = __cself->p; // bound by copy
  Person *__weak weakP = __cself->weakP; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_ps_0m9gnvtj0893vpf1cr595djh0000gn_T_main_38be7e_mi_1, (unsigned long)((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("age")));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_ps_0m9gnvtj0893vpf1cr595djh0000gn_T_main_38be7e_mi_2, (unsigned long)((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)weakP, sel_registerName("age")));
    }

// 栈上的 Block 拷贝到堆时,会调用 __main_block_copy_0 方法
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    // _Block_object_assign 函数会根据自动变量的修饰符(如上 Person *__strong p;)作出相应的操作,形成强引用或弱引用(类似 Retain):
    // 对外界 p 强引用,对外界 weakP 弱引用
    _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
    _Block_object_assign((void*)&dst->weakP, (void*)src->weakP, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }

// 栈上的 Block 销毁时
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    // _Block_object_dispose 函数使得自动变量的引用计数减一(类似 Release)
    _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
    _Block_object_dispose((void*)src->weakP, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  // 当在 Block 中访问对象类型时会增加额外的域来管理内存
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

__block

本质

上面的 Demo 中,Block 只是访问了外界的变量,但没有进行过修改,接下来我们尝试下:

static int staticGlobalVar = 0;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int autoVar = 0;
        static int staticVar = 0;

        ^{
            // Error: Variable is not assignable (missing __block type specifier)
            // autoVar = 10;

            staticVar = 10;
            staticGlobalVar = 10;
        }();
    }
}

Block 中按引用捕获静态局部变量,以及不捕获全局静态变量,可以直接修改这些变量的值。而对于自动变量,由于其是值传递,内部的 autoVar 与外界已无关系,所以此时不能在内部修改,编译器将提示 Variable is not assignable (missing __block type specifier),那么这里的 __block 是什么呢?

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int autoVar = 0;

        ^{
            autoVar = 10;
        }();
    }
}

我们尝试将以上代码翻译为 C++:

// 将自动变量包装的结构体(Obj-C 对象)
struct __Block_byref_autoVar_0 {
  void *__isa;
__Block_byref_autoVar_0 *__forwarding;
 int __flags;
 int __size;
 // 自动变量
 int autoVar;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // 原本为 int autoVar;
  __Block_byref_autoVar_0 *autoVar; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_autoVar_0 *_autoVar, int flags=0) : autoVar(_autoVar->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  // autoVar 为结构体指针
  __Block_byref_autoVar_0 *autoVar = __cself->autoVar; // bound by ref

            // 通过指向自身的指针改变了 autoVar 的值
            (autoVar->__forwarding->autoVar) = 10;
        }

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        // 初始化 __Block_byref_autoVar_0 结构体,将 autoVar 的地址赋值给结构体中 __forwarding 指针,值本身赋值给结构体中的 autoVar
        // __Block_byref_autoVar_0 autoVar = {0, &autoVar, 0, sizeof(__Block_byref_autoVar_0), 0}
        __attribute__((__blocks__(byref))) __Block_byref_autoVar_0 autoVar = {(void*)0,(__Block_byref_autoVar_0 *)&autoVar, 0, sizeof(__Block_byref_autoVar_0), 0};

        ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_autoVar_0 *)&autoVar, 570425344))();
    }
}

__block 将原本要捕获的变量类型封装为 __Block_byref_autoVar_0 结构体,其中也含有 isa 指针,因此本质上是个 Obj-C 对象;__forwarding 指向了该结构体本身,在赋值时被赋值为声明的结构体的地址;__size 即该结构体的大小;autoVar 即捕获的变量本身。这样,在 Block 内部改变变量值时,其实是更改了引用的结构体指向自身的变量值,而非直接修改值原本传递捕获的变量值。捕获了 __block 变量的 Block 结构如下图所示:

4

__block Person *p = [[Person alloc] init];
p.age = 20;

block = ^{
    // Block 中想要改变自动变量的指针内容(即其存储的对象内存地址)也必须使用 __block
    p = [[Person alloc] init];
};

对于对象类型的自动变量 __Block_byref_xxx 结构体将有些变化:

struct __Block_byref_p_0 {
  void *__isa;
__Block_byref_p_0 *__forwarding;
 int __flags;
 int __size;
 // copy dispose 用作其内存管理
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 Person *__strong p;
};

细节

虽然 __block 将原本的自动变量封装到结构体中,但其实在使用时开发者几乎对此没有感知:

typedef void(^SomeBlock)(void);

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
//    void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
//    void (*dispose)(struct __main_block_impl_0*);
};

// 0x000000010050ab10
struct __Block_byref_autoVar_0 {
    void *__isa; // 8
    struct __Block_byref_autoVar_0 *__forwarding; // 8
    int __flags; // 4
    int __size;  // 4
    int autoVar; // 4 // 0x000000010050ab28 => 0x000000010050ab10 + 24
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    struct __Block_byref_autoVar_0 *autoVar; // by ref
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 0x00007ffeefbff4d8
        __block int autoVar = 0;
        // 0x00007ffeefbff4bc
        int autoVar2 = 0;

        SomeBlock block = ^{
            // block 中 __main_block_impl_0 结构体内部的 autoVar
            // 0x000000010050ab28
            autoVar = 10;
        };
        block();

        // 将 block 转换为 __main_block_impl_0 结构体
        struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block;

        // block 中 __main_block_impl_0 结构体内部的 autoVar
        // 0x000000010050ab28
        NSLog(@"%p", &autoVar); // 0x10050ab28
    }
}

// LLDB:
// (lldb) p &autoVar
// (int *) $0 = 0x00007ffeefbff4d8
// (lldb) p &autoVar
// (int *) $1 = 0x00000001007162b8
// (lldb) p &autoVar
// (int *) $2 = 0x000000010050ab28
// (lldb) p &(blockStruct->autoVar)
// (__Block_byref_autoVar_0 **) $3 = 0x000000010280f1d0
// (lldb) p &(blockStruct->autoVar->autoVar)
// (int *) $4 = 0x000000010050ab28
// (lldb) p 0x000000010050ab28 - 0x000000010050ab10
// (long) $5 = 24

内存管理

由于 __block 使得 Block 捕获的基础类型自动变量被封装到结构体(Obj-C 对象)中,那么内存管理便必不可少:

struct __main_block_impl_0 {
  // ...
  __Block_byref_foo_0 *foo; // by ref
  // ...
};

// Block 被 copy 到堆上时调用 __main_block_copy_0 将 foo 拷贝到堆上
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    // _Block_object_assign 将对 foo 形成强引用(Retain)
    _Block_object_assign((void*)&dst->foo, (void*)src->foo, 8/*BLOCK_FIELD_IS_BYREF*/);
}

// Block 从堆上移除时调用 __main_block_dispose_0 将 foo 从堆上移除
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    // _Block_object_dispose 将释放 foo(Release)
    _Block_object_dispose((void*)src->foo, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

我们可以发现这与 Block 中捕获对象类型的自动变量时的内存管理类似,不同的是 __block 默认即是强引用,因此 _Block_object_assign 中会进行强引用(Retain)的操作,且 _Block_object_assign & _Block_object_dispose 方法的最后一位参数的不同(对象:BLOCK_FIELD_IS_OBJECT__blockBLOCK_FIELD_IS_BYREF)。

__forwarding

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  // autoVar 为结构体指针
  __Block_byref_autoVar_0 *autoVar = __cself->autoVar; // bound by ref

            // 通过指向自身的指针改变了 autoVar 的值
            (autoVar->__forwarding->autoVar) = 10;
        }

__Block_byref_xxx 结构体仍在栈上时,foo->__forwarding->foo 就等同于直接 foo->foo 来访问,因为此时 __forwarding 就指向自己;而当复制到堆上时,__forwarding 将指向堆上的结构体,保证后续的数据变动是在堆上。

对象类型

那么当 __block 遇上对象类型的自动变量呢?

@interface Person : NSObject
@property (nonatomic, assign) NSUInteger age;
@end

@implementation Person
- (void)dealloc
{
    NSLog(@"dealloc");
}
@end

typedef void(^SomeBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block Person *p = [[Person alloc] init];
        p.age = 20;

        __block __weak Person *weakP = p;

        SomeBlock block = ^{
            NSLog(@"%lu", (unsigned long)weakP.age); // 20

            NSLog(@"%lu", (unsigned long)p.age); // 20
            p = [Person new];
            p.age = 18;
            NSLog(@"%lu", (unsigned long)p.age); // 18
        };

        block();
    }
}

我们尝试将以上代码翻译为 C/C++:

struct __Block_byref_p_0 {
  void *__isa; // 8
__Block_byref_p_0 *__forwarding; // 8
 int __flags; // 4
 int __size; // 4
 void (*__Block_byref_id_object_copy)(void*, void*); // 8
 void (*__Block_byref_id_object_dispose)(void*); // 8
 // __strong 根据外界强或弱
 Person *__strong p;
};

struct __Block_byref_weakP_1 {
 // ...
 Person *__weak weakP;
};

struct __main_block_impl_0 {
  // ...
  // __Block_byref 为强指针,不受外界改变
  __Block_byref_weakP_1 *weakP; // by ref
  __Block_byref_p_0 *p; // by ref
  // ...
};

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

// Block 被 copy 到堆上时调用 __main_block_copy_0
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    // 1⃣️ 将 __Block_byref_weakP_1 *weakP & __Block_byref_p_0 *p 强引用
    _Block_object_assign((void*)&dst->weakP, (void*)src->weakP, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign((void*)&dst->p, (void*)src->p, 8/*BLOCK_FIELD_IS_BYREF*/);
}

// Block 要从堆上移除时调用 __main_block_dispose_0
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    // 3⃣️ 将 __Block_byref_weakP_1 *weakP & __Block_byref_p_0 *p 释放
    _Block_object_dispose((void*)src->weakP, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_dispose((void*)src->p, 8/*BLOCK_FIELD_IS_BYREF*/);
}

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        __attribute__((__blocks__(byref))) __Block_byref_p_0 p = {(void*)0,(__Block_byref_p_0 *)&p, 33554432, sizeof(__Block_byref_p_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"))};
        ((void (*)(id, SEL, NSUInteger))(void *)objc_msgSend)((id)(p.__forwarding->p), sel_registerName("setAge:"), (NSUInteger)20);

        __attribute__((__blocks__(byref))) __attribute__((objc_ownership(weak))) __Block_byref_weakP_1 weakP = {(void*)0,(__Block_byref_weakP_1 *)&weakP, 33554432, sizeof(__Block_byref_weakP_1), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, (p.__forwarding->p)};

        SomeBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_weakP_1 *)&weakP, (__Block_byref_p_0 *)&p, 570425344));

        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
}

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 // 2⃣️ 根据外界对 p / weakP 的强弱引用产生强弱引用(+ 40 即 Person *__strong p; / Person *__weak weakP;)
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
 // 4⃣️ 释放
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

特殊情况

MRC 下 __Block_byref_xxx 对于变量将总是弱引用(即不会进行 Retain),这也可被用来破解循环引用:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block Person *p = [[Person alloc] init];
        p.age = 20;

        SomeBlock block = [^{
            NSLog(@"%lu", (unsigned long)p.age); // 20
            p = [Person new];
            p.age = 18;
            NSLog(@"%lu", (unsigned long)p.age); // 18
        } copy];

        // 正常来说 Block 如果对 p 进行了强引用,引用计数加一,即使调用一次 release 也不应当 dealloc
        [p release]; // dealloc

        block();

        [block release];
    }
}

循环引用

我们知道,Obj-C 是根据引用计数来管理对象的内存的,但其中的一个问题便是循环引用,即两个对象以强引用互相指向对方,引用计数无法减少,对象无法释放,很容易导致内存泄漏:

typedef void(^SomeBlock)(void);

@interface Person : NSObject
@property (nonatomic, assign) NSUInteger age;
@property (nonatomic, copy) SomeBlock block;
@end

@implementation Person

- (void)dealloc
{
    NSLog(@"dealloc");
}

- (void)foo {
    // ➡️ Block 捕获了 self,self 中强引用 Block,导致双方都无法释放
    self.block = ^{
        NSLog(@"%lu", (unsigned long)self.age);
        NSLog(@"%lu", (unsigned long)_age); // self._age
    };
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        p.age = 20;

        p.block = ^{
            // ➡️ Block 捕获了 p,p 中强引用 Block,导致双方都无法释放(如下图)
            // Capturing 'p' strongly in this block is likely to lead to a retain cycle
            NSLog(@"%lu", (unsigned long)p.age);
        };

        p.block();
        [p foo];

        // Never dealloc
    }
}

// OUTPUT:
// 20

5

ARC

__weak & __unsafe_unretained

想要打破循环引用,我们需要一个不增加引用计数的指向。那么到底更改哪个引用呢?我们仍以上图为例:

  • 1⃣️ 处的强引用是我们在初始化时 Person *p = [[Person alloc] init]; 所赋予的,栈上的 p 指针存储了堆上的 对象的内存地址;
  • 2⃣️ 处的强引用是 Block 结构体(__main_block_impl_0)对自身捕获到内部的对象的强引用,其引用是根据外界即 1⃣️ 处声明时的强弱来决定的;
  • 3⃣️ 处的强引用是根据我们在 Person 类中声明的属性修饰 @property (nonatomic, copy) SomeBlock block; 所决定的。

而我们需要 Block 应当随 Person 对象销毁而销毁,如果将 3⃣️ 处改为弱引用或 __unsafe_unretained,则可能出现 Block 的提前释放。因此综上,我们可以将 1⃣️ 的引用改为弱引用或 __unsafe_unretained

// Person.m
- (void)foo {
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        NSLog(@"%lu", (unsigned long)weakSelf.age);

        // 需要 __strong 避免编译器报错(也保证在下面使用时 self 没有被释放)
        __strong typeof(weakSelf) strongSelf = weakSelf;
        NSLog(@"%lu", (unsigned long)strongSelf->_age);
    };
}

// main.m
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        p.age = 20;

        // 也可使用 typeof() 简化类型声明
        // __weak typeof(p) weakP = p;
        __weak Person *weakP = p;

        // __unsafe_unretained Person *unsafeUnretainedP = p;
        __unsafe_unretained typeof(p) unsafeUnretainedP = p;

        p.block = ^{
            NSLog(@"%lu", (unsigned long)weakP.age);
            NSLog(@"%lu", (unsigned long)unsafeUnretainedP.age);
        };

        p.block();
        [p foo];
    }
}

// OUTPUT:
// 20
// dealloc

__weak & __unsafe_unretained 的区别在于前者在指向的对象销毁时,指针将自动置为 nil(Autoniling),而后者将保留指向的内存地址。

__block

__block 的变量由于可以在 Block 内修改,因此其也可以用来解除循环引用:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block Person *p = [[Person alloc] init];
        p.age = 20;

        p.block = ^{
            NSLog(@"%lu", (unsigned long)p.age);

            p = nil;
        };

        p.block();

        p.block = ^{
            NSLog(@"%lu", (unsigned long)p.age);

            // 将 __Block_byref_p_0 中的 p 置为 nil,打破循环
            p = nil;
        };

        // p.block();
    }
}

// OUTPUT:
// 20
// dealloc

6

但需要注意:

  1. Block 必须执行才可以置为 nil,破解循环引用;
  2. p = nil; 被首次执行时就会释放,因此如果后续再次执行 Block 将出现 EXC_BAD_ACCESS 错误。

MRC

MRC 下,我们需要手动将栈上的 Block 复制到堆上,并在结束使用时手动释放(Release),但循环引用出现时即使手动释放对象也无法销毁:

@interface Person : NSObject
@property (nonatomic, assign) NSUInteger age;
@property (nonatomic, copy) SomeBlock block;
@end

@implementation Person
- (void)dealloc
{
    [super dealloc];
    NSLog(@"dealloc");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        p.age = 20;

        p.block = [^{
            NSLog(@"%lu", (unsigned long)p.age);
        } copy];

        p.block();

        [p release];

        // Never dealloc
    }
}

// OUTPUT:
// 20

由于 MRC 没有强弱引用的概念,因从破解循环引用只能使用 __unsafe_unretained,其使得 Block 内部不会对捕获的对象持有(Retain):

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __unsafe_unretained Person *p = [[Person alloc] init];
        p.age = 20;

        p.block = [^{
            NSLog(@"%lu", (unsigned long)p.age);
        } copy];

        p.block();

        [p release];
    }
}

// OUTPUT:
// 20
// dealloc

当然,正如 __block 一节中所述,其也可以用来破解 MRC 下的循环引用,因为 __block 修饰的变量在 MRC 下,__Block_byref_xxx 将不会对捕获的变量持有(Retain)。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block Person *p = [[Person alloc] init];
        p.age = 20;

        p.block = [^{
            NSLog(@"%lu", (unsigned long)p.age);
        } copy];

        p.block();

        [p release];
    }
}

// OUTPUT:
// 20
// dealloc