Block 的变量捕获

348 阅读16分钟

Block 的内部数据结构

不含变量的 Block

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^bananaBlock)(void) = ^{
            NSLog(@"Block with nothing");
        };
        bananaBlock();
    }
    return 0;
}

输出:

2021-01-06 22:00:29.960525+0800 Block[66168:3680187] Block with nothing

实现下面指令生成c++,查看里面的数据结构:

clang -rewrite-objc main.m -o main.cpp

可以看到此时 Block 的数据结构是这样的:

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_impl 的结构:

struct __block_impl {
  void *isa;       // 指向类的 isa 指针
  int Flags;
  int Reserved;
  void *FuncPtr;   // 函数的实现地址
};

__main_block_desc_0 描述信息:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size; // block 的内存大小
}

函数实现:


static __NSConstantStringImpl __NSConstantStringImpl__var_folders_7__prlb26657hdbrtk1kmg7m8nw0000gn_T_main_b8fa28_mi_0 __attribute__ ((section ("__DATA, __cfstring"))) = {__CFConstantStringClassReference,0x000007c8,"Block with nothing",18};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {           
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__prlb26657hdbrtk1kmg7m8nw0000gn_T_main_b8fa28_mi_0);    
}

main 函数里面的代码大致是这样一个东西:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ {
        // bananaBlock: 可以看成是一个对象
        // __main_block_func_0: 是函数实现地址
        // __main_block_desc_0_DATA: block描述信息
        bananaBlock = __main_block_impl_0(__main_block_func_0,
                                    &__main_block_desc_0_DATA);
        
        // FuncPtr 具体的函数调用
        bananaBlock->FuncPtr;
    }
    return 0;
}

从数据结构可以知道,block 其实就是一个 OC 对象,里面包含 isa 指针,函数调用地址,内存分配大小等信息(当然还可能包含捕获的变量,下面会演示)。

添加测试代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^bananaBlock)(void) = ^{
            NSLog(@"Block with nothing");
        };
        bananaBlock();
        
        NSLog(@"Block superclass type = %@", [[bananaBlock superclass] superclass]);
    }
    return 0;
}

输出:

2021-01-06 22:20:47.843630+0800 Block[66984:3696369] Block with nothing
2021-01-06 22:20:47.843886+0800 Block[66984:3696369] Block superclass type = NSObject

也是可以看出 Block 的最终父类是 NSObject

包含变量的 Block

测试代码:

// auto 全局变量
int orange = 6;

// static 全局变量
static int banana = 5;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 局部变量,自动类型:auto int milk = 7 (auto 默认不写)
        int milk = 7;
        
        // 局部变量, 静态类型
        static int egg = 8;
        
        void(^bananaBlock)(void) = ^{
            NSLog(@"All info: orange = %d, banana = %d, milk = %d, egg = %d", orange, banana, milk, egg);
        };
        bananaBlock();
    }
    return 0;
}

例子中包含了 4 个变量,orange, banana, milk, egg

输出:

2021-01-06 22:28:53.504792+0800 Block[67321:3702430] All info: orange = 6, banana = 5, milk = 7, egg = 8

转成 C++ 查看里面的数据结构

__main_block_impl_0 block 里面的数据结构是这样的:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int milk;   //  milk 是 auto int 类型的局部变量,它会直接拷贝到block的内存结构中,相当于值拷贝
  int *egg;   // egg 是 static int 静态类型局部变量,它会拷贝地址值到block的内存中,相当于引用拷贝(地址拷贝)
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _milk, int *_egg, int flags=0) : milk(_milk), egg(_egg) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

main 函数的实现:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ {
        int milk = 7;
        static int egg = 8;
        bananaBlock = __main_block_impl_0(__main_block_func_0,
                                    &__main_block_desc_0_DATA, 
                                    milk, 
                                    &egg);
        
        // FuncPtr 具体的函数调用
        bananaBlock->FuncPtr;
    }
    return 0;
}

关系图:

函数调用:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int milk = __cself->milk; // bound by copy ... 从 block 取出来保存的值
    int *egg = __cself->egg; // bound by copy ...  从block取出地址里面的值

    // orange,banana 这两个全局变量直接访问
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__prlb26657hdbrtk1kmg7m8nw0000gn_T_main_a3862d_mi_0, orange, banana, milk, (*egg));
}

变量捕获类型

从上面捕获变量的例子中可以总结变量的捕获情况:

变量类型是否捕获到block内部访问方式
局部 auto 变量值传递
局部 static 变量地址传递
全局变量直接访问

为什么会这样?其实可以这样想:

  • orangebanana 这两全局变量始终会在内存的全局数据区里面,block 始终能在需要的时候访到它们,如果把它们捕获到自己的内存结构中还要做额外的操作,所以这个没必要。

  • int milk 是一个局部的自动变量,milk 是一个局部变量,执行完当前方法,生命周期就没了。如果 block 不捕获它的话,以后想访问它就不行了,所以需要捕获,自己有一个拷贝(int milk 是基本数据类型)。

  • static int egg 虽然是一个局部变量,但它是静态类型,它会一直呆在数据区,所以即使当前方法执行完毕,它还是会存在内存中,此时 block 只需要它的一个地址值就好了(毕竟 static int egg 以后可能还会修改)。

对 OC 对象的捕获情况

添加一个测试类 Cat

@interface Cat : NSObject

// 包含一个 name 属性
@property (nonatomic, copy) NSString *name;

@end

@implementation Cat

@end

测试代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Cat *cat = [[Cat alloc] init];
        cat.name = @"Tom";
        
        void(^bananaBlock)(void) = ^{
            NSLog(@"Hi %@", cat.name);
        };
        bananaBlock();
    }
    return 0;
}

输出:

2021-01-06 22:57:23.503163+0800 Block[68378:3721751] Hi Tom

查看里面 Block 定义的 __main_block_impl_0 的数据结构:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Cat *cat;  // cat 是一个局部变量,以指针形式捕获进来了
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Cat *_cat, int flags=0) : cat(_cat) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

描述信息 __main_block_desc_0

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*); // (*copy) 对象相关
  void (*dispose)(struct __main_block_impl_0*);  // (*dispose) 对象相关
}

可以看到在描述信息结构体中多了 (*copy)(*dispose) 这两个东西,由于 block 此时捕获的是一个 OC 对象,所以此时需要执行 (*copy) 看是否需要对捕获对象的引用计数+1, (*dispose) 查看是否引用计数-1。

关系图:

Block 类型

在开发中使用的 Block 有3种类型:

  • NSGlobalBlock // 在全局数据区

  • NSMallocBlock // 在堆上

  • NSStackBlock // 在栈上

下面查看一下不同类型的区别:

在 MRC 环境调试

关闭 ARC

添加测试代码:

typedef void (^BananaBlock)(void);
typedef void (^AppleBlock)(void);

// 全局变量
int treeHeight = 100;

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        // 局部变量 auto 变量。如果是 static int 那么它是一个静态类型变量,会存在全局数据区
        int count = 2;
        
        // 访问了局部变量 ... 为 __NSStackBlock__ 类型
        BananaBlock bananaBlock = ^{
            NSLog(@"bananaBlock count = %d, treeHeight = %d", count, treeHeight);
        };
        
        // 没访问局部变量 ... 为 __NSGlobalBlock__ 类型
        AppleBlock appleBlock = ^{
            NSLog(@"appleBlock treeHeight = %d", treeHeight);
        };

        // 访问了局部变量 ... 为 __NSStackBlock__ 类型
        void(^orangeBlock)(void) = ^{
            NSLog(@"orangeBlock count = %d", count);
        };
        
        // 访问了局部变量,并且添加了 copy 操作 ... 为 __NSMallocBlock__ 类型
        void(^cherryBlock)(void) = [^{
            NSLog(@"cherryBlock count = %d", count);
        } copy];
        
        // 什么变量都没访问 ... 为 __NSGlobalBlock__ 类型
        void(^normalBlock)(void) = ^{
            NSLog(@"Nothing");
        };
       
        NSLog(@"bananaBlock class = %@", [bananaBlock class]);
        NSLog(@"appleBlock class = %@", [appleBlock class]);
        NSLog(@"orangeBlock class = %@", [orangeBlock class]);
        NSLog(@"cherryBlock class = %@", [cherryBlock class]);
        NSLog(@"normalBlock class = %@", [normalBlock class]);
    }
    return 0;
}

输出:

2021-01-07 14:43:19.457406+0800 Block[83300:4678850] bananaBlock class = __NSStackBlock__
2021-01-07 14:43:19.457701+0800 Block[83300:4678850] appleBlock class = __NSGlobalBlock__
2021-01-07 14:43:19.457732+0800 Block[83300:4678850] orangeBlock class = __NSStackBlock__
2021-01-07 14:43:19.457758+0800 Block[83300:4678850] cherryBlock class = __NSMallocBlock__
2021-01-07 14:43:19.457794+0800 Block[83300:4678850] normalBlock class = __NSGlobalBlock__

在 ARC 环境调试

打开 ARC 开关

编译运行刚刚在 MRC 环境下添加的测试代码,输出如下:

2021-01-07 14:48:47.773190+0800 Block[83501:4682590] bananaBlock class = __NSMallocBlock__
2021-01-07 14:48:47.773701+0800 Block[83501:4682590] appleBlock class = __NSGlobalBlock__
2021-01-07 14:48:47.773763+0800 Block[83501:4682590] orangeBlock class = __NSMallocBlock__
2021-01-07 14:48:47.773783+0800 Block[83501:4682590] cherryBlock class = __NSMallocBlock__
2021-01-07 14:48:47.773805+0800 Block[83501:4682590] normalBlock class = __NSGlobalBlock__

例子中的 bananaBlock 和 orangeBlock 从 MRC 环境的 __NSStackBlock__ 变成了 ARC 环境下的 __NSMallocBlock__

为什么会这样?主要是在 ARC 环境下,编译器自动为 bananaBlockorangeBlock 添加了 copy 操作,于是它们就从内存中的栈上拷贝到堆了。

总结

Block 类型成立情况
NSGlobalBlock没访问局部变量(auto 类型)
NSStackBlock访问了局部auto变量
NSMallocBlockNSStackBlock 执行 copy 操作

Blockcopy 操作:

Block 类型执行 copy 操作以后
NSGlobalBlock什么都不会发生
NSStackBlock从栈拷贝到堆
NSMallocBlock引用计数+1

对 OC 对象是 auto 变量的引用问题

如果 OC 对象是一个局部变量,那么有下面一些情况需要注意:

  • 如果 Block 为 NSStackBlock, 那么这个 Block 不会对它访问的 OC 对象进行强引用;

  • 如果 Block 为 NSMallocBlock,那么会根据传入的 OC 对象是的修饰符(__strong, __weak 或者 __unsafe_unretained )类型来进行是否强引用。

查看一下里面的捕获类型:

测试代码 1

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Cat *cat = [[Cat alloc] init];
        cat.name = @"Tom";
        
        void(^orangeBlock)(void) = ^{
            NSLog(@"Hi %@", cat.name);
        };
        
        orangeBlock();
    }
    return 0;
}

使用如下指令查看一下 Block 的内部结构

clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main.cpp (需要使用 runtime 特性,所以额外添加 -fobjc-arc -fobjc-runtime=ios-10.0.0,10.0.0 为随便指定的版本 )

Block 内部数据结构:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Cat *__strong cat;   // cat 为 __strong 类型,强引用
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Cat *__strong _cat, int flags=0) : cat(_cat) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

此时从内部数据结构可以看出 Cat *__strong cat; 捕获的 cat 为 __strong 类型,强引用。

测试代码 2

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Cat *cat = [[Cat alloc] init];
        cat.name = @"Tom";
        
        // 添加 __weak 修饰
        __weak Cat *weakCat = cat;
        void(^orangeBlock)(void) = ^{
            NSLog(@"Hi %@", weakCat.name);
        };
        
        orangeBlock();
    }
    return 0;
}

查看内部数据结构:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Cat *__weak weakCat; // cat 为 __weak 类型,弱引用
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Cat *__weak _weakCat, int flags=0) : weakCat(_weakCat) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看出 Cat *__weak weakCat; 捕获的 cat 为 __weak 类型,弱引用。

也就是说:

  • 如果传入block的是什么修饰符类型的 OC 对象,那么 block 就会捕获什么类型的修饰符对象。

  • 如果 Block 是强引用 OC 对象,那么 OC 对象的引用计数会 +1,弱引用则不修改引用计数。

  • Block 被销毁的时候,会执行自己 __main_block_desc_0 描述信息里面的

    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*);
    }
    

    void (*dispose) 方法,并且如果有强引用 OC 对象,那么释放该 OC 对象,对象引用计数 -1。

OC 类中 Block 属性为什么使用 copy 修饰

如下所示:

typedef void(^ShowName)(void);

@interface Cat : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) ShowName show; // Block 属性, 使用 copy

@end

ARC 中,编译器会自动在下面情况将 NSStackBlock 拷贝到堆,成为 NSMallocBlock 延长生命周期:

  • block 作为 函数返回值

  • block 赋值给 __stong 指针的时候

  • block 作为 Cocoa API 中方法名含有 usingBlock的方法参数时

  • block 作为 GCD API 的方法参数时

MRC 环境下,编译器在下面情况将 NSStackBlock 拷贝到堆,成为 NSMallocBlock

  • 主动为 block 执行 copy 操作

所以在 MRC 使用 block 作为 属性的时候需要如此声明:

@property (nonatomic, copy) MyBlock blockStuff; // 到时候 NSStackBlock -> NSMallocBlock

由于在 ARC 环境编译器会自动在满足条件的合适 NSStackBlock -> NSMallocBlock,所以 ARC 环境可以如下使用:

@property (nonatomic, copy) MyBlock blockStuff;
@property (nonatomic, strong) MyBlock blockStuff;

__block 修饰符

我们知道如果一个基本数据类型的自动变量被 block 访问了以后,它会使用值传递的形式传递进 block,然后block 有一个当前基本数据类型的值。所以如果该自动变量之后修改自己也是不会影响 block 里面捕获的值的,而且在 block 里面也不能修改自动变量的值。

如下代码编译器就会报错:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 5;
        void(^orangeBlock)(void) = ^{
            age = 6; // 编译不通过,报错
            NSLog(@"Tom's age is %d", age);
        };
        age = 7; //  这里修改的值不会影响 block 里面的 age 值
        orangeBlock();
    }
    return 0;
}

当然下面的例子就能对变量进行修改:

int height = 10; // 全局变量 block 不会捕获

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        static int age = 20; // static 类型的局部变量,存储在全局区中,拷贝到block的时候是地址拷贝
        
        void(^orangeBlock)(void) = ^{
            age = 30;
            height = 20;
            NSLog(@"age = %d, height = %d", age, height);
        };
        orangeBlock();
        
    }
    return 0;
}

但是这些全局变量或者静态变量它们也有一个缺陷,就是这些变量会一直存储在全局区中不会销毁,直到app结束。

使用 __block 修饰符修饰自动变量

使用 __block 修饰 age 变量

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 使用 __block 修饰
        __block int age = 20;
        void(^orangeBlock)(void) = ^{
            age = 30; // 能修改,没问题
            NSLog(@"age = %d", age);
        };
        orangeBlock();
    }
    return 0;
}

输出:

2021-01-07 17:02:04.422584+0800 Block[88456:4759058] age = 30

查看 __block 修饰符修饰自动变量后的数据结构

此时 __block int age 被封装成 __Block_byref_age_0 OC 对象:

struct __Block_byref_age_0 {
    void *__isa; // 有 isa 指针,这是一个对象
    struct __Block_byref_age_0 *__forwarding;
    int __flags;
    int __size;
    int age; // 具体的变量
};

里面有 isa 指针。它们的关系图是这样的

所以现在 Block 不是直接拷贝 age 自动变量了,而是包含了一个 __Block_byref_age_0 对象。 它们的指向关系为:

__main_block_impl_0 -> __Block_byref_age_0 -> age

查看 __block 修饰 OC 对象

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 使用 __block 修饰 cat
        __block Cat *cat = [[Cat alloc] init];
        cat.name = @"Tom";
        
        void(^orangeBlock)(void) = ^{
            NSLog(@"age = %@", cat.name);
        };
        orangeBlock();
    }
    return 0;
}

__block Cat *cat 里面的数据结构是这样的:

struct __Block_byref_cat_0 {
    void *__isa;
    struct __Block_byref_cat_0 *__forwarding;
    int __flags;
    int __size;
    void (*__Block_byref_id_object_copy)(void*, void*);
    void (*__Block_byref_id_object_dispose)(void*);
    Cat *__strong cat; // 强引用 cat,当然如果是这样声明 __block __weak Cat *cat = [[Cat alloc] init];, 那么就是 Cat *__weak cat
};

它们的关系图是这样的:

由于 cat 对象的声明是这样的: __block Cat,所以此时的引用关系为:

orangeBlock 强引用 __Block_byref_cat_0 强引用 cat 对象。

如果声明为:__block __weak Cat *cat = [[Cat alloc] init];,那么就是 __Block_byref_cat_0 弱引用 cat 对象,变成 Cat *__weak cat;

一个需要注意的事情:

  • ARC 情况下:__block Cat *cat = [[Cat alloc] init];__Block_byref_cat_0cat 对象是强引用

  • ARC 情况下:__block __weak Cat *cat = [[Cat alloc] init];__Block_byref_cat_0cat 对象是弱引用

  • MARC 情况下:不管是 __block Cat *cat, 还是 __block __weak Cat *cat__Block_byref_cat_0cat 对象都是弱引用

循环引用问题

形成强引用

添加测试 Cat 类:

typedef void(^ShowCatBlock)(void);

@interface Cat : NSObject

@property (nonatomic, copy) NSString *name;

@property (nonatomic, copy) ShowCatBlock catBlock;

@end

@implementation Cat

// 辅助打印,看 cat 对象是否销毁了
- (void)dealloc {
    NSLog(@"Cat dealloc");
}

@end

测试代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Cat *cat = [[Cat alloc] init];
        cat.name = @"Tom";
        
        // show block 会强引用 cat, cat 拥有 show block,形成引用循环
        cat.catBlock = ^{
            NSLog(@"Hi %@", cat.name);
        };
        
        cat.catBlock();
        NSLog(@"----- main func finish -----");
    }
    return 0;
}

输出:

2021-01-07 22:22:17.555665+0800 Block[86177:4092084] Hi Tom
2021-01-07 22:25:29.930663+0800 Block[86299:4094780] ----- main func finish -----

可以看到 main 函数执行完毕, cat 对象的 dealloc 方法也没有执行。为什么会这样?

是因为 cat 对象里面的 catBlock 数据结构是这样的:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Cat *__strong cat; // 对 cat 进行了强引用
};

catBlock 对 cat 对象进行了强引用。而由于 cat 对象本来就拥有 catBlock,所以也就对 catBlock 有强引用。

它们的关系从:

变成了这样:

导致形成引用循环,所以内存都得不到释放。

解决强引用

在 ARC 环境下:

  • 使用 __weak 修饰:
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Cat *cat = [[Cat alloc] init];
        cat.name = @"Tom";
        
        // __weak 修饰,不会造成强引用
        __weak typeof(cat) weakCat = cat;
        cat.catBlock = ^{
            NSLog(@"Hi %@", weakCat.name);
        };
        
        cat.catBlock();
        NSLog(@"----- main func finish -----");
    }
    return 0;
}

输出:

2021-01-07 22:42:21.210631+0800 Block[86956:4106602] Hi Tom
2021-01-07 22:42:21.210914+0800 Block[86956:4106602] ----- main func finish -----
2021-01-07 22:42:21.210951+0800 Block[86956:4106602] Cat dealloc

cat 执行了 dealloc 方法,被释放了,没有和 catBlock 形成强引用。

catBlock 内部数据结构:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Cat *__weak weakCat; //  __weak
};
  • 使用 __unsafe_unretained 修饰:
__unsafe_unretained typeof(cat) weakCat = cat;

测试的时候可以看到 cat 对象也是被释放了的。

catBlock 内部数据结构:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Cat *__unsafe_unretained weakCat; // __unsafe_unretained
};

__weak__unsafe_unretained 修饰 cat 对象的区别:

  • 都能打破 cat 对象和 catBlock 属性的引用循环

  • __weak 修饰的 cat 对象,在cat对象被销毁的时候,会自动设置为 nil

  • __unsafe_unretained 修饰的 cat 对象,在cat对象被销毁的时候,不会自动设置为 nil,还会保存之前存在的cat对象地址,如果此时访问 cat 对象,会报错

在 MRC 环境下:

  • 使用 __unsafe_unretained 修饰(原理和ARC 环境下一样)
@implementation Cat

// 辅助打印,看 cat 对象是否销毁了
- (void)dealloc {
    [super dealloc]; // MRC 需要调用 super
    NSLog(@"Cat dealloc");
}

@end

测试代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Cat *cat = [[Cat alloc] init];
        cat.name = @"Tom";
        
        __unsafe_unretained Cat* weakSelf = cat;
        cat.catBlock = ^{
            NSLog(@"Hi %@", weakSelf.name);
        };
                
        cat.catBlock();
        [cat release]; // 需要 release cat
        NSLog(@"----- main func finish -----");
    }
    return 0;
}

输出:

2021-01-07 23:09:54.680481+0800 Block[88453:4142948] Hi Tom
2021-01-07 23:09:54.680754+0800 Block[88453:4142948] Cat dealloc
2021-01-07 23:09:54.680785+0800 Block[88453:4142948] ----- main func finish -----
  • 使用 __block 修饰

测试代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Cat *cat = [[Cat alloc] init];
        cat.name = @"Tom";
        
        __block Cat* weakSelf = cat;
        cat.catBlock = ^{
            NSLog(@"Hi %@", weakSelf.name);
        };
                
        cat.catBlock();
        [cat release]; // 需要 release cat
        NSLog(@"----- main func finish -----");
    }
    return 0;
}

输出:

2021-01-07 23:10:23.464006+0800 Block[88479:4143845] Hi Tom
2021-01-07 23:10:23.464288+0800 Block[88479:4143845] Cat dealloc
2021-01-07 23:10:23.464318+0800 Block[88479:4143845] ----- main func finish -----

在 MRC 环境使用 __block 修饰也能打破引用循环是因为:

catBlock 强引用 __block 包装的对象, 而 __block 包装的对象对传入的 cat 对象不管什么情况都是弱引用。

(MRC 环境下不支持 __weak 修饰符)

注意一下的例子

修改了拷贝地址

int main(int argc, const char * argv[]) {
    @autoreleasepool {
                
        NSString *name = @"Tom";
        void(^orangeBlock)(void) = ^{
            NSLog(@"Hi %@", name); // 拷贝了一个 name 的指针
        };
        name = @"Jerry"; // 修改了指向地址,所以对 block 没影响
        orangeBlock();
        
    }
    return 0;
}

输出:

2021-01-07 16:29:45.255123+0800 Block[87301:4741637] Hi Tom

如果此时添加 __block 修饰:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __block NSString *name = @"Tom";
        void(^orangeBlock)(void) = ^{
            NSLog(@"Hi %@", name); // 拷贝了一个 name 指针
        };
        name = @"Jerry"; // 多了一层引用,此时对 block 有影响
        orangeBlock();
        
    }
    return 0;
}

输出如下:

2021-01-07 16:32:40.248197+0800 Block[87420:4743661] Hi Jerry

拷贝地址不变,只是修改了里面的属性内容

int main(int argc, const char * argv[]) {
    @autoreleasepool {
                
        Cat *cat = [[Cat alloc] init];
        cat.name = @"Tom";
        
        void(^orangeBlock)(void) = ^{
            NSLog(@"Hi %@", cat.name); // 拷贝了一个 cat 的指针
        };
        cat.name = @"Jerry"; // 指向 cat 的地址还是一样
        orangeBlock();
        
    }
    return 0;
}

输出:

2021-01-07 16:28:18.972028+0800 Block[87236:4740171] Hi Jerry

MRC环境的一个情况:

        // 不是 auto 变量,是 static 类型, 输出: __NSGlobalBlock__
        static int age = 2;
        BananaBlock ageBlock = ^{
            NSLog(@"ageBlock age = %d", age);
        };
        NSLog(@"ageBlock class = %@", [ageBlock class]);