Block的本质

当需要执行异步操作,或同步多个操作时,块(Block)会非常有用。这一篇文章将介绍 Block 的本质。如果你对 block 还不了解,推荐先查看Block的用法

1. Block的本质

Block 是封装了函数调用及函数调用环境的 Objective-C 对象,内部也有一个 isa 指针。即 Block 本质上也是一个 Objective-C 对象。

下面写一个简单的 block:

        int age = 10;
        void(^myblock)(void) = ^{
            NSLog(@"age: %d", age);
        };
        myblock();

使用 clang 命令将上述代码转化为 C++,方便查看 block 内部结构:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

转化后如下:

        int age = 10;
        // 定义block变量
        void(*myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
        // 调用block
        ((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);

1.1 声明 block

BlockDeclare.png
1.1.1 __main_block_impl_0

通过转化的 C++ 代码可以看到,block定义中调用了__main_block_impl_0函数,并将其地址赋值给myblock。进一步查看__main_block_impl_0函数:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  
  // 该构造函数最终返回__main_block_impl_0。会将传入的_age赋值给成员age。
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__main_block_impl_0结构体内的构造函数对变量进行赋值,最终返回__main_block_impl_0结构体,也就是最终返回给myblock变量的是__main_block_impl_0结构体。

1.1.2 __main_block_func_0

__main_block_impl_0函数的第一个参数是__main_block_func_0,其定义如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int age = __cself->age; // bound by copy
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_63c9df_mi_0, age);
}

__main_block_func_0函数内存储着 block 内代码。其函数内部先取出局部变量 age,后面调用NSLog

也就是将 block 内的代码封装到__main_block_func_0函数,将__main_block_func_0函数地址传递给__main_block_impl_0

1.1.3 __main_block_desc_0

__main_block_impl_0函数的第二个参数是__main_block_desc_0,其定义如下:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

__main_block_desc_0中存储着两个成员,reservedBlock_size。并且为reserved赋值0,为Block_size赋值sizeof(struct __main_block_impl_0),即block的大小。

最终,将__main_block_desc_0结构体传给__main_block_impl_0中,赋值给desc。

1.1.4 age

__main_block_impl_0函数的第三个参数是age,即定义的局部变量。

如果在 block 中使用了局部变量,block 声明的时候会将 age 作为参数传入,即 block 会捕获(capture)age。如果 block 中没有使用 age,则只会给__main_block_impl_0函数传入__main_block_func_0__main_block_desc_0_DATA参数。

由于 block 在声明时捕获了局部变量,在声明后、调用前修改局部变量值,不会影响 block 内捕获到的局部变量值。如下所示:

        int age = 10;
        void(^myblock)(void) = ^{
            NSLog(@"age: %d", age);
        };
        age = 11;
        myblock();

执行后,控制台打印如下:

age: 10

block 在定义之后已将局部变量age值存储在__main_block_impl_0结构体,调用时直接从结构体中取出。声明后修改局部变量的值不会影响__main_block_impl_0结构体捕获的值。

1.1.5 __block_impl

__main_block_impl_0结构体第一个成员是__block_impl结构体,__block_impl结构体如下:

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

isa指针指向类对象,FuncPtr指针存储着__main_block_func_0函数地址,即 block 内代码地址。

__block_impl结构体第一个成员就是 isa 指针。Objective-C对象本质上也是结构体,第一个成员也是 isa 指针。因此,block 本质上也是一个 OC 对象。__main_block_impl_0函数的构造函数将传入 block 的值存储到__main_block_impl_0结构体中,最终将__main_block_impl_0结构体地址赋值给myblock。

分析__main_block_impl_0构造函数,特点如下:

  • __main_block_func_0封装了函数地址,其中先取出局部变量,再调用 block 内代码。
  • __main_block_desc_0_DATA封装 block 大小。
  • age是 block 捕获的局部变量。
  • __main_block_impl_0结构体中的__block_impl结构体包含了isa指针、FuncPtr。

1.2 调用 block

        // 调用block
        ((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);

将上述代码中的强制转换移除后,变为下面的代码:

        (myblock->FuncPtr)(myblock);

调用myblock就是通过myblock找到FuncPtr指针,然后进行调用。

myblock是指向__main_block_impl_0结构体的指针,内部并没有FuncPtr指针,为什么这里可以直接访问?这是因为__main_block_impl_0结构体第一个成员是__block_impl,而__block_impl也是一个结构体,即__main_block_impl_0可以改为以下内容:

struct __main_block_impl_0 {
  // 使用__block_impl直接替换
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
  
  struct __main_block_desc_0* Desc;
  int age;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

另一方面,__main_block_impl_0结构体第一个成员是__block_impl__main_block_impl_0结构体地址就是__block_impl的地址,这样也可以查找到FuncPtr指针。

block 底层的数据结构也可以使用下面图片表示:

BlockLayout.png

2. 变量捕获

除了包含可执行代码,块还具有捕获块以外值的能力。如果在一个方法内声明了一个块,该块可以获取方法内任何变量,也就是可以捕获局部变量。

BlockCapture.png

2.1 局部变量

2.1.1 auto变量

局部变量默认是 automatic variable 类型,简写为auto,一般省略不写。当程序进入、离开局部变量作用域时,会自动分配、释放内存。

auto会自动捕获到 block 内,__main_block_impl_0结构体内增加了存储局部变量的成员。block 内访问auto变量的方式是值传递,即直接将auto变量传递给__main_block_impl_0函数。

2.1.2 static变量

static变量会一直存储在内存中。block 会捕获static修饰的局部变量,访问时使用指针访问。

下面分别添加使用autostatic修饰的局部变量:

        auto int age = 10;
        static int weight = 125;
        void(^myblock)(void) = ^{
            NSLog(@"age: %d, weight: %d", age, weight);
        };
        myblock();

生成C++后如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // 捕获了age、weight。
  int age;
  int *weight;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_weight, int flags=0) : age(_age), weight(_weight) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
  int *weight = __cself->weight; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_e2f202_mi_0, age, (*weight));
        }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        auto int age = 10;
        static int weight = 125;
        // age直接传递值,weight传递指针。
        void(*myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &weight));

        ((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);
    }
    return 0;
}

可以看到,__main_block_impl_0捕获了age、weight,并且给__main_block_impl_0函数传递age时直接传递值,传递weight时传递的是指针。

2.2 全局变量

block 是否会捕获全局变量?以及如何使用?

添加以下全局变量:

int height = 170;
static int number = 11;

生成C++代码如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  int *weight;
  // 并没有捕获全局变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_weight, int flags=0) : age(_age), weight(_weight) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
  int *weight = __cself->weight; // bound by copy

            // 直接使用height、number
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_964e22_mi_0, age, (*weight), height, number);
        }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        auto int age = 10;
        static int weight = 125;
        void(*myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &weight));

        ((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);
    }
    return 0;
}

可以看到__main_block_func_0并没有添加任何全局变量,而是直接使用。这是因为全局变量会一直存放在内存中,全局都可以使用。

3. Block 的类型

既然 block 也是 OC 对象,那么 block 是什么类型呢?

声明一个 block,并打印其父类,如下所示:

        void(^myblock)(void) = ^{
            NSLog(@"github.com/pro648");
        };
        NSLog(@"%@", [myblock class]);
        NSLog(@"%@", [[myblock class] superclass]);
        NSLog(@"%@", [[[myblock class] superclass] superclass]);
        NSLog(@"%@", [[[[myblock class] superclass] superclass] superclass]);

输出如下:

__NSGlobalBlock__
NSBlock
NSObject
(null)

即 block 的继承关系是:__NSGlobalBlock__NSBlockNSObject。进一步证实了 block 本质上也是一个 OC 对象。

定义三个不同的 block,分别打印其类型:

        // 没有调用外部变量的block
        void(^myblock1)(void) = ^{
            NSLog(@"github.com/pro648");
        };
        
        // 访问auto变量
        int age = 10;
        void(^myblock2)(void) = ^{
            NSLog(@"age: %d", age);
        };
        
        // 直接调用block的 class
        NSLog(@"%@ %@ %@", [myblock1 class], [myblock2 class], [^{
            NSLog(@"%d", age);
        } class]);

打印如下:

__NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__

将上述代码转换为C++,可以看到三个 block 类型都是_NSConcreteStackBlock类型。这可能是 runtime 运行时进行了某种转换,使用 clang 生成的C++代码仅供参考,不能保证和运行时完全一致。

三种类型的block在内存中的位置如下:

BlockMemory.png

__NSGlobalBlock____NSStackBlock____NSMallocBlock__三种类型的block是按照以下规则产生的:

block 类型 环境
__NSGlobalBlock__ 没有访问auto变量
__NSStackBlock__ 访问了auto变量
__NSMallocBlock__ __NSStackBlock__调用了copy方法

3.1 __NSGlobalBlock__

当 block 内没有访问auto变量时,block 为__NSGlobalBlock__类型,__NSGlobalBlock__存在数据段中,程序结束才会回收内存。但因为其与普通函数没有区别,很少使用__NSGlobalBlock__类型的 block。

3.2 __NSStackBlock__

在 block 内访问了auto变量为__NSStackBlock__类型。

__NSStackBlock__类型的 block 存放在栈中。栈的内存由系统自动分配和释放,超出变量作用域后自动释放。由于栈中代码超出作用域之后,内存就会被销毁,而有可能内存销毁之后才去调用它,此时就会出现问题。

ARC 自动管理内存时会帮助我们做很多事情,为了方便理解其本质,先关闭 ARC 使用 MRC 管理内存。进入TARGETS > Build Settings > Objective-C Automatic Reference Counting,修改其值为 NO。

关闭 ARC 后,使用以下代码验证问题:

void (^myblock)(void);

void test() {
    // __NSStackBlock__
    int age = 10;
    myblock = ^{
        NSLog(@"age: %d", age);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        myblock();
    }
    return 0;
}

执行后控制台输出如下:

age: -272632840

这是因为myblock是在栈中的,即__NSStackBlock__类型的。当test函数执行完毕后,栈内存中 block 已经被系统回收。

3.3 __NSMallocBlock__

为了避免函数执行完毕栈内存立即被回收,可以将__NSStackBlock__block copy 到堆中。以下是修改后的代码:

void (^myblock)(void);

void test() {
    // __NSMallocBlock__
    int age = 10;
    // 将 block 从栈中复制到堆中。
    myblock = [^{
        NSLog(@"age: %d", age);
    } copy];
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        myblock();
    }
    return 0;
}

执行后控制台输出如下:

age: 10

block 调用 copy 后,类型改变如下所示:

block类型 内存区域 调用copy的效果
__NSGlobalBlock__ 数据段 什么都不做,类型不变。
__NSStackBlock__ 从栈复制到堆,类型变为__NSMallocBlock__
__NSMallocBlock__ 引用计数加一,类型不变。

使用 MRC 管理内存时,经常需要使用 copy 保存 block,将栈上的 block 复制到堆上,超出作用域时 block 不会被释放,后续需调用 release 销毁 block。ARC 环境下,系统会自动调用 copy 操作,使 block 不被销毁;不再使用时,自动调用 release 引用计数减一。

4. ARC 在某些情况下会对 block 自动进行一次 copy 操作,将其从栈区移动到堆区

出现以下情况时,ARC 会自动对 block 执行一次 copy 操作,将其从栈区移动到堆区:

  1. 当 block 作为函数返回值时。
  2. 当 block 被强指针引用时。
  3. 当 Cocoa API 方法名包含usingBlock,且 block 作为参数时,或 block 作为 GCD API 方法参数。

4.1 当 block 作为函数返回值时

typedef void (^MyBlock)(void);

MyBlock test() {
    int age = 10;
    // myblock 作为函数返回值,ARC 会自动进行copy。
    MyBlock myblock = ^{
        NSLog(@"age: %d", age);
    };
    return myblock;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyBlock myblock = test();
        NSLog(@"%@", [myblock class]);
    }
    return 0;
}

在 ARC 环境下,参数返回值为 block 类型时,系统会对 ARC 自动执行一次 copy 操作,使其变为__NSMallocBlock__类型。在 MRC 环境下,超出作用域后 block 会被销毁,此时再调用会引起闪退。

4.2 当 block 被强指针引用时

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        MyBlock myblock = ^{
            NSLog(@"age: %d", age);
        };
        NSLog(@"%@",[myblock class]);
    }
    return 0;
}

由于 block 访问了auto变量,其是__NSStackBlock__类型。在 MRC 环境中,不会自动进行 copy 操作,输出是__NSStackBlock__;在 ARC 环境中,有强指针引用时会自动执行 copy 操作,将 block 从栈中移动到堆中。

修改上述代码如下,即取消强指针对 block 的引用:

        int age = 10;
        // 取消强指针的引用
        NSLog(@"%@",[^{
            NSLog(@"age: %d", age);
        } class]);

可以看到输出为:

__NSStackBlock__

手动调用 copy,如下所示:

        int age = 10;
        NSLog(@"%@", [[^{
            NSLog(@"age: %d", age);
        } copy] class]);

输出为:

__NSMallocBlock__

这也进一步证明了 ARC 环境下,有强指针引用 block 时会自动调用 copy 方法。

4.3 当 Cocoa API 方法名包含usingBlock,且 block 作为参数时,或 block 作为 GCD API 的方法参数

当 Cocoa API 方法名包含usingBlock,且 block 作为参数时,或 block 作为 GCD API 的方法参数。ARC 会根据情况自动将栈上的 block copy到堆上。

        // Cocoa API
        NSArray *arr = @[@1];
        [arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            // 这个 block 在堆上
        }];

        // GCD API
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            // 这个 block 在堆上
        });

block 作为属性时与其它属性类似,但 MRC 环境下,只能使用copy修饰。因为,block 访问auto变量时,block 是__NSStackBlock__类型,超出作用域 block 会被自动销毁。如果想要在外部继续访问、调用 block,就需要将 block 从栈中复制到堆中,因此需用copy修饰。

在 ARC 环境下,系统会在需要时自动进行 copy 操作。此时属性可以使用strong,但copy更能表明用意。

5. Block 内引用对象

之前 block 内只引用过基本数据类型,这一部分介绍 block 内引用对象类型。如下所示:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            ^{
                NSLog(@"person.age = %d", person.age);
            }();
        }
        NSLog(@"--------");
    }
    return 0;
}

执行后控制台输出如下:

person.age = 10
-[Person dealloc]
--------

可以看到在打印虚线前person已经释放。此时,block 是栈类型 block,即__NSStackBlock__。栈区 block 即便引用了对象,也会在超出作用域时一起释放。

更新上述代码如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyBlock myblock;
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            myblock = ^{
                NSLog(@"person.age = %d", person.age);
            };
            myblock();
        }
        NSLog(@"--------");
    }
    return 0;
}

执行后输出如下:

person.age = 10
--------
-[Person dealloc]

可以看到执行到虚线位置时,person对象并没有释放。这是因为 block 内部对person对象进行了强引用,block 又被 myblock 强指针引用,即 block 是堆类型。堆类型的 block 会对外部对象强引用。

使用以下命令生成 C++ 代码,查看其底层实现:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 main.m

查看 C++ 代码,block 定义如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__strong person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__strong _person, int flags=0) : person(_person) {
    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*);
  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 内引用基本数据类型相比,__main_block_desc_0内增加了copydispose两个参数,用于管理对象内存。

copy操作调用的是__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((void*)&dst->person,
                         (void*)src->person,
                         3/*BLOCK_FIELD_IS_OBJECT*/);
}

最终调用_Block_object_assign函数,_Block_object_assign会对引用的对象person进行引用计数操作。如果引用的对象是__strong修饰(默认是__strong,即忽略时就是__strong),则引用计数加一;如果使用的__weak修饰,则引用计数不变。

当 block 执行完毕,会调用dispose方法,dispose底层会调用以下方法:

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->person,
                          3/*BLOCK_FIELD_IS_OBJECT*/);
}

__main_block_dispose_0内部会调用_Block_object_dispose方法。如果之前 copy 时使用了强引用,此时引用计数减一;如果之前使用了弱引用,直接取消对原来对象的弱引用。

6. Block 内修改外部变量

如果外部变量是auto类型,block 通过值传递的方式捕获变量。由于是值传递的方式进行的,其不能修改外部变量。如果需要外部变量,可以通过以下两种方式:

  • 使用 static 修饰外部变量。
  • 使用__block修饰外部变量。

6.1 使用 static 修饰外部变量

使用 static 修饰的变量会一直存在内存中,程序结束前不会被释放。block 捕获时通过引用方式进行,即传递地址。因此,使用 static 修饰的外部变量可以直接修改值。

6.2 使用__block修饰外部变量

使用 static 修饰的变量会一直存放在内存中,直到程序结束,这不利于性能优化。

使用__block修饰外部变量,也可以达到在 block 内修改成员变量的目的,那__block底层是如何实现的呢?

__block不能修饰全局变量、静态变量。

下面代码使用__block修饰局部变量:

        __block int age = 10;
        MyBlock myblock = ^{
            age = 20;
            NSLog(@"age: %d", age);
        };
        myblock();

使用以下命令将其转换为 C++:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 main.m

转换后的 block 定义如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // age 被封装成了对象。
  __Block_byref_age_0 *age; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到使用__block修饰的外部变量被封装成了__Block_byref_age_0对象类型,__Block_byref_age_0声明如下:

struct __Block_byref_age_0 {
    // 也有isa指针,即也是对象类型。
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 // 值
 int age;
};

__Block_byref_age_0结构体也有isa指针,即也是对象类型。

使用__block修饰的age被转换为:

        __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {
            (void*)0,(__Block_byref_age_0 *)&age,
            0,
            sizeof(__Block_byref_age_0),
            10
        };

__main_block_func_0函数被转换为:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_age_0 *age = __cself->age; // bound by ref
    
    // 使用age的forwarding指向age。
    (age->__forwarding->age) = 20;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_d88942_mi_0, (age->__forwarding->age));
}

使用age__forwarding取出变量地址,这样即使 block 从栈移动到了堆上,也可以正确修改变量值。

7. 对象类型的auto变量、__block变量

在 block 内访问了使用auto__block修饰的对象类型的变量:

  • 如果 block 在栈上,将不会对变量产生强引用。

  • 如果 block 被拷贝到堆上

    • 会调用 block 内部的copy函数。

    • copy函数内部会调用_Block_object_assign函数。

    • _Block_object_assign函数会根据变量修饰符__strong__weak__unsafe_unretained做出相应操作,类似于 retain(形成强引用、弱引用)。

      使用__block修饰的变量只有在 ARC 环境中会根据__strong__weak__unsafe_unretained修饰符进行强引用,在 MRC 环境中不会进行强引用。

  • 如果 block 从堆上移除:

    • 会调用 block 内部的 dispose 函数。
    • dispose 函数内部会调用_Block_object_dispose函数。
    • _Block_object_dispose函数会自动释放引用的变量,类似于 release。

8. Block 的循环引用

使用 block 容易产生循环引用。如果类中定义了一个 block,在 block 内又访问了类的属性,就会导致循环引用。

Person类中声明了属性age和 myblock,main.m文件中为 block 赋值,如下所示:

typedef void(^MyBlock)(void);
@interface Person : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, copy) MyBlock myblock;
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.age = 10;
        person.myblock = ^{
            NSLog(@"age: %d", 20);
        };
    }
    NSLog(@"-------");
    return 0;
}

执行后输出如下:

-[Person dealloc]
-------

可以看到person先释放,后打印虚线。

更新myblock赋值语句如下:

        person.myblock = ^{
            NSLog(@"age: %d", person.age);
        };

再次执行后,控制台只输出了虚线,person类没有被释放。

因为person强引用了myblock,此时myblock在堆上;myblock内访问了person对象,堆上的 block 会对对象进行强引用。此时person强引用myblockmyblock强引用person,形成了循环引用。

不止访问person会产生循环引用,在person类里的 block 内访问成员变量也会产生循环引用,因为访问成员变量本质上是在调用self->instance,即仍然访问了self

此外,OC 方法转换为 C 语言方法后,默认带有两个参数。第一个是 id 类型的self,第二个参数是SEL类型的_cmd,因此,平常访问的self也是局部变量。

void test(id self, SEL _cmd) {
    
}

ARC 环境下有以下三种解决循环引用的方案:

  • 使用__weak修饰变量。
  • 使用__unsafe_unretained修饰变量。
  • 使用__block修饰变量,同时在 block 内将变量设置为nil,最后确保调用 block。

下面详细介绍解决循环引用的方案。

8.1 使用__weak修饰变量

使用__weak修饰变量,更新如下:

        __weak typeof(person) weakPerson = person;
        person.myblock = ^{
            NSLog(@"age: %d", weakPerson.age);
        };

将其转换为 C++ 代码,__main_block_impl_0函数如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // 对捕获的person进行弱引用。
  Person *__weak weakPerson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

此时执行后,超出person作用域,person就会释放。

8.2 使用__unsafe_unretained修饰变量

使用__unsafe_unretained修饰变量也可以解决循环引用问题。

__unsafe_unretained__weak区别在于:

  • __weak:不会产生强引用。指向的对象销毁时,会自动让指针置为nil。
  • __unsafe_unretained:不会产生强引用,但没有__weak安全。指向对象销毁时,指针存储地址不变,但内存已经被回收,再次访问时产生野指针错误。

8.3 使用__block修饰变量,同时在 block 内将变量设置为nil,最后确保调用 block

使用__block也可以解决循环引用问题:

        // 1.添加__block修饰符
        __block Person *person = [[Person alloc] init];
        person.age = 10;
        person.myblock = ^{
            NSLog(@"age: %d", person.age);
            // 2.置为nil
            person = nil;
        };
        // 3.调用block()
        person.myblock();

使用__block解决循环引用问题时,上述三步缺一不可。其缺点就是必须调用 block,如果没有调用 block,就无法在 block 执行完毕后将person置为nil,就无法解决循环引用问题。

在 MRC 环境中,有以下两种方案解决循环引用问题:

  • 使用__unsafe_unretained,MRC 不支持弱指针__weak
  • 直接使用__block。在 MRC 环境中,__block结构体不会对结构体内对象进行强引用,不会产生循环引用。

参考资料:

  1. Cocoa blocks as strong pointers vs copy
  2. A look inside blocks: Episode 3 (Block_copy)
  3. How blocks are implemented (and the consequences)
  4. Objective-C Blocks Ins And Outs

欢迎更多指正:https://github.com/pro648/tips

本文地址:https://github.com/pro648/tips/blob/master/sources/Block的本质.md

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,924评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,902评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,716评论 0 239
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,783评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,166评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,510评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,784评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,476评论 0 196
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,196评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,459评论 2 243
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,978评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,321评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,964评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,046评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,803评论 0 193
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,530评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,420评论 2 265

推荐阅读更多精彩内容