【iOS】 Blocks

【iOS】 Blocks

Blocks概要

Blocks是C语言的扩充功能,也就是带有局部变量的匿名函数。它提供了类似由OC和C++类生成实例或对象来保持变量值的方法。另外,"带有自动变量值的匿名函数"这一概念并不仅指Blocks,也存在于其他语言中。

  • ""匿名函数":通过Blocks模式来理解"匿名函数"。

    • Block语法:
    1. 返回值类型:同C语言函数的返回值类型
    2. 参数列表:同C语言函数的参数列表
    3. 表达式:同C语言函数中允许使用的表达式
  • Block类型变量:

在Block语法中,可将Block语法赋给声明为Block类型的变量中。Block既指源代码中的Block语法,也指由Block语法所生成的值。

我们可以使用typedef来解决记述方法复杂的问题:

  • "带有自动变量值":"带有自动变量值"在Blocks中表现为"截获自动变量值"。
  1. 截获变量值:
    Blocks表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值。因为Block表达式保存了自动变量的值,所以在执行Block语法后,即使改写Block中使用的自动变量的值也不会影响Block执行时自动变量的值。
  2. 截获OC对象:

用C语言说就是截获类对象用的结构体实例指针。赋值给截获的自动变量会产生编译错误,但使用截获的值却不会有问题

该情况下,同样是附加__block说明符。

objc 复制代码
__block id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
    array = [NSMutableArray array];
};

Blocks的实现

Block的实质

Block实质上是作为极普通的C语言源代码来处理的。通过支持Block的编译器,含有Block语法的源代码转换为一般C语言编译器能够处理的源代码,并作为极为普通的C语言源代码被编译。

查看源代码:

bash 复制代码
clang -rewrite-objc 源代码文件名
objc 复制代码
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^blk)(void) = ^{
            printf("123");
        };
        blk();
    }
    return 0;
}

通过clang编译查看源代码:

我们很清晰地看到我们写在Block中的内容:

我们可以看见_cself是一个block_impl的指针,指向两个结构体:

cpp 复制代码
struct __main_block_impl_0 { // block对象本体
  struct __block_impl impl; // block基类
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock; // 说明block本质也是OC对象
    impl.Flags = flags; // 标记block信息(是否copy、是否需要释放等)
    impl.FuncPtr = fp; // block真正执行的函数地址,函数指针调用
    Desc = desc; // block描述信息(block大小等)
  }
};

Block的"说明书结构":

cpp 复制代码
static struct __main_block_desc_0 {
  size_t reserved; // 保留字段
  size_t Block_size; // 这个Block占用的内存大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

总结一下,Block在底层就是一个结构体(__main_block_impl_0),其中包含函数指针(__block_impl impl)和描述信息指针(__main_block_desc_0* Desc)。Desc指向一个结构体实例(__main_block_desc_0_DATA),用于记录Block的元数据。运行时在对Block进行拷贝和内存管理时会依赖这些描述信息,即当Block捕获外部变量时,描述信息中会包含拷贝和释放函数指针,用于管理捕获变量的内存)

手动调用函数指针的过程是:

cpp 复制代码
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

实则就是在把Block当函数来执行:

  • 先构造Block对象:本质上是创建一个Block对象,取该Block地址,然后强制转化成函数指针类型,伪装成一个函数指针。

    cpp 复制代码
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
  • 再执行Block:本质上是把函数指针blk又强制转回Block结构体,取出里面的FuncPtr,也就是真正执行的函数地址,再把这个地址转化成函数指针,把Block自己当参数传进去执行。

    cpp 复制代码
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

去掉上面所说的转化过程,把编译器为了调用方便做的强制类型伪装还原回真实结构,其实就是:

  • 构造Block对象:
cpp 复制代码
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;

这段对应代码:

objc 复制代码
void (^blk)(void) = ^{
  printf("123");
};

这其实就是初始化一个局部变量,然后把他赋值给blk。

初始化局部变量:

  • 参数1:函数指针,本质是Block真正要执行的代码。
cpp 复制代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  printf("123");
}
  • 参数2:描述信息,本质是告诉系统该Block对象多大,怎么管理内存,作为静态全局变量初始化的__main_block_desc_0结构实例指针。
cpp 复制代码
__main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

这就是__main_block_desc_0结构体实例的初始化代码,来分配内存大小。

  • 执行Block:
cpp 复制代码
(*blk->impl.FuncPtr)(blk);

这就是使用函数指针调用函数。也就是说Block不是直接执行代码,而是通过调用函数指针,而FuncPtr中存的则是__main_block_func_0 ,也就是Block里的代码,这也同时说明了__main_block_func_0函数的参数*__cself指向Block值。在调用该函数时Block正是作为参数进行了传递。

这样,我们就理清了Block的实质。

下面解释一下源码中的_NSConcreteStackBlock到底是什么:将Block指针赋给Block的结构体成员变量isa,用来说明Block其实就是一个对象。

cpp 复制代码
impl.isa = &_NSConcreteStackBlock; 

这里重新学习一下isa:

id为objc_object结构体的指针类型,Class为objc_class结构体的指针类型。这两个结构体归根结底都是在各个对象和类的实现中使用的最基本的结构体。

objc 复制代码
@interface MyObject : NSObject {
	int val0;
  int val1;
}
@end

基于objc_object结构体,该类对象的结构体如下:

objc 复制代码
struct MyObject {
  Class isa;
  int val0;
  int vall;
}

MyObject类的实例变量val0和val1被直接声明为对象的结构体成员。"Objective-C 中由类生成对象"意味着像该结构体这样"生成由该类生成的对象的结构体实例"。生成的各个对象,即由该类生成的对象的各个结构体实例,通过成员变量isa 保持该类的结构体实例指针

各类的结构体就是基于objc_class结构体的class_t结构体。在OC中,比如NSObject的class_t结构体实例以及NSMutableArray的class_t结构体实例等,均生成并保持各类的class_t结构体实例。该实例持有声明的成员变量、方法的名称、方法的实现(即函数指针)、属性以及父类的指针,并被OC运行时库所使用。

回到Block的结构体,此 __main_block_impl_0结构体相当于基于objc_object结构体的OC类对象的结构体,另外对其中的成员变量isa进行初始化。即_NSConcreteStackBlock相当于class_t结构体实例。

截获自动变量值

  • 普通变量捕获
objc 复制代码
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int val = 10;
        void (^blk)(void) = ^{
            printf("%d", val);
        };
        blk();
    }
    return 0;
}

同样先通过clang重新编译查看一下源码:

cpp 复制代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int val = __cself->val; // bound by copy

            printf("%d", val);
        }

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; 
        int val = 10;
        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

这里我们可以清楚的看到已经把自动变量val添加到block的结构体中充当一个成员变量了:

cpp 复制代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

注意我们的Block函数部分:我们发现Block内部访问的外部自动变量,实际上是访问Block结构体中保存的拷贝值,而不是原变量本身

cpp 复制代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int val = __cself->val; // bound by copy
  printf("%d", val);
}

这些自动变量在Block语法表达式之前被声明定义,编译器偷偷帮我们做了int val = __cself->val;,所以我们写val实际变成__cself->val,因此原来的源代码表达式无需改动便可使用截获的自动变量值执行。总结来说,Block在底层会将自动变量存入结构体此成员中,并在Block函数内部生成同名局部变量来引用这些成员,从而保证源代码中的变量访问方式无需改变

  • __block变量
objc 复制代码
__block int val = 10;
void (^blk)(void) = ^{
  val = 20;
};
blk();

重新编译后完全变了:变量不会被按值拷贝,而是会被封装成一个__Block_byref_val_0结构体。其中包含一个forwarding指针,用于指向当前变量的有效地址。

cpp 复制代码
struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

整个的核心机制是当Block从栈拷贝到堆时,_ _block变量也会从栈复制到堆,此时系统会更新相关结构体的forwarding指针,使其统一指向堆上的那一份变量。因此,Block内部访问__block变量时,实际上是通过forwarding指针间接访问,从而保证无论变量被复制多少次 ,访问的始终是同一份数据

__block说明符

若想在Block内修改变量,有两种方法:

  1. 用静态变量、静态全局变量、全局变量来修改。
objc 复制代码
#import <Foundation/Foundation.h>

int global_val = 1;
static int static_global_val = 2;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        static int static_val = 3;
        void (^blk)(void) = ^{
            global_val *= 1;
            static_global_val *= 2;
            static_val *= 3;
        };
    }
    return 0;
}

使用clang重新编译:

cpp 复制代码
int global_val = 1;
static int static_global_val = 2;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_val = __cself->static_val; // bound by copy

            global_val *= 1;
            static_global_val *= 2;
            (*static_val) *= 3;
        }

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; 
        static int static_val = 3;
        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

由此可以看出:

  • 全局变量、静态全局变量:在Block里直接用,不捕获、不拷贝。这是因为它们的生命周期是整个程序,本来就在全局区。
  • 静态变量:通过静态变量的指针传递给构造函数并且保存,从而实现作用域访问。这是因为静态变量本质在全局区,但作用域在函数内。
  1. 需要在该自动变量上附加__ block说明符,该变量称为__ block变量。
objc 复制代码
__block int val = 10;
int fmt = 15;
void (^blk)(void) = ^ {
    val = 12;
    NSLog(@"%d %d",val, fmt);
};
fmt = 20;
val = 30;
blk();

重新编译一下:

cpp 复制代码
struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int fmt;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _fmt, __Block_byref_val_0 *_val, int flags=0) : fmt(_fmt), val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref
  int fmt = __cself->fmt; // bound by copy

            (val->__forwarding->val) = 12;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_qs_3b9g0pmd2kq8j41xrcbwbhr00000gn_T_main_0f96f5_mi_0,(val->__forwarding->val), fmt);
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
        int fmt = 15;
        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, (__Block_byref_val_0 *)&val, 570425344));
        fmt = 20;
        (val.__forwarding->val) = 30;
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    };
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

正如上面截获自动变量值里的block变量所说,__Block_byref _ val _0结构体的成员变量__forwarding持有该实例自身的指针。

值得注意的是:无论从多个Block中使用同一 个__block变量,还是反过来从一个Block中使用多个__block变量都是可以的。只要增加Block的结构体成员变量与构造函数的参数即可。

Block存储域

将Block当作OC对象来看时,Block类为_NSConcreteStackBlock。与此类似的类还有:

  • _ NSConcreteStackBlock:设置对象的存储域在栈上
  • _NSConcreteGlobalBlock:设置对象的存储域在程序的数据区的位置
  • _NSConcreteMallocBlock:设置对象的存储域在由malloc函数分配的堆上

接下来分别讨论:

  1. Block类为_NSConcreteGlobalBlock的情况有:
  • 在记述全局变量的地方使用Block语法时:
objc 复制代码
void (^blk)(void) = ^ {
  NSLog(@"Global Block");
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
    };
    return 0;
}

该Block用结构体实例设置在程序的数据区域中。因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量进行捕获 。由此Block用结构体实例的内容不依赖于执行时的状态,所以整个程序中只需一个实例。因此将Block用结构体实例设置在与全局变量相同的数据区域中即可。

内存分布:

cpp 复制代码
struct __blk_block_impl_0 {
  struct __block_impl impl;
  struct __blk_block_desc_0* Desc;
  __blk_block_impl_0(void *fp, struct __blk_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
  • Block语法的表达式中不使用应截获的自动变量时:

只在截获自动变量时,Block用结构体实例截获的值才会根据执行时的状态变化。例如以下代码中,虽然多次使用同一个 Block 语法,但每个 for 循环中截获的自动变量的值都不同。

需要截获自动变量:

objc 复制代码
typedef int (^blk_t)(int);
for (int rate = 0; rate < 10; ++rate) {
    blk_t blk = ^(int count) { 
      return rate * count; 
    };
}

不需要截获自动变量:

objc 复制代码
typedef int (^blk_t)(int);
for (int rate = 0; rate < 10; ++rate) {
    blk_t blk = ^(int count) { 
      return count; 
    };
}

综上,就是说如果不需要捕获自动变量的话,可以把Block用结构体实例放在程序的数据区域。

  1. _NSConcreteStackBlock:

默认在函数或方法里写的Block都是栈Block,只在作用域内有效,出了作用域就会被释放,可以捕获局部自动变量。

objc 复制代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^blk)(void) = ^{
            NSLog(@"Stack Block");
        };
    };
    return 0;
}
cpp 复制代码
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从变量作用域外也可以通过指针安全地使用。但设置在栈上的Block如果其所属的变量作用域结束,该Block就会被废弃。由于_ _block变量也配置在栈上,同样如果其所属的变量作用域结束,则该__block变量也会被废弃。

Blocks提供了将Block和__block变量从栈上拷贝复制到堆上的方法来解决这个问题。即将配置在栈上的Block复制到堆上,这样即使Blcok语法记述的变量作用域结束,堆上的Block还可以继续存在。

  1. _NSConcreteMallocBlock:

通过copy生成或者ARC下将Block存储为strong属性关键字都会自动copy到堆。出了当前作用域也不会被释放,可以捕获局部自动变量。

这里我们主要认识一下不同的Blockcopy的一个差异:

不管Block配置在何处,用copy方法复制都不会引起任何问题。在不确定时调用copy即可。但是在ARC中不能显式地release,而多次调用copy方法进行复制是没有问题的。

值得注意的是:某些情况需要我们自己手动复制一下栈上的Block,否则这个Block就会被释放掉。只有以下几种不需要进行手动复制:

  • Cocoa 框架的方法且方法名中含有 usingBlock 等时
  • Grand Central Dispatch 的 API

__block变量存储域

使用__block变量的Block从栈复制到堆上时,__block变量也会收到影响。

  • 若1个Block中使用__block变量时:

则当该Block从栈复制到堆时,使用的所有__block变量也必定配置在栈上,这些__block变量也全部被从栈复制到堆。此时Block持有__block变量。即使在该Block已复制到堆的情形下,复制Block也对所使用的__block变量没有任何影响。

  • 若多个Block中使用__block变量时:

因为最先会将所有的Block配置在栈上,所以__block变量也会配置在栈上。在任何一个Block从栈复制到堆时,__block变量也会一并从栈复制到堆并被该Block所持有。当剩下的Block从栈复制到堆时,被复制的Block持有__block变量,并增加__block变量的引用计数。

如果配置在堆上的Block被废弃,那么它所使用的__block变量也就被释放。这里就如同引用计数内存管理一样,引用计数为0就自动释放了。

这样我们就理解了前面说到的forwarding指针是为了让_ _block变量的值可以一直被访问到,关键机制就是通过Block的复制,__block变量也会从栈复制到堆,然后修改forwarding指向堆,堆上的forwarding指向自己 。这样,无论是在Block语法中、Block语法外使用__block变量还是配置在栈上或堆上,都可以顺利访问同一个__block变量。

截获对象

objc 复制代码
NSMutableArray* ary = [NSMutableArray array];
void (^blk)(id object) = [^(id object) {
    [ary addObject:object];
    NSLog(@"%ld", ary.count);
} copy];
blk([[NSMutableArray alloc] init]);
blk([[NSMutableArray alloc] init]);
blk([[NSMutableArray alloc] init]);
return 0;

输出结果:

这一结果意味着变量作用域结束的同时,变量array被废弃,强引用实效,但赋值给变量array的NSMutableArray类的对象在该源码最后Block的执行部分超出其变量作用域而存在。

这段代码转换为源码:

cpp 复制代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSMutableArray *array;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_array, int flags=0) : array(_array) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id object) {
  NSMutableArray *array = __cself->array; // bound by copy

            ((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)array, sel_registerName("addObject:"), (id)object);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_qs_3b9g0pmd2kq8j41xrcbwbhr00000gn_T_main_2547ab_mi_0, ((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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

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的结构体中array变为成员变量:

cpp 复制代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSMutableArray *array;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_array, int flags=0) : array(_array) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

这里其实是采用了__strong修饰的成员变量。

这个array是Block结构体里的一个成员变量,它虽然表面没写__strong,但是作为OC对象指针类型,在ARC语义下默认是强引用。

在OC中,C语言结构体不能含有附有__strong修饰符的变量,因为编译器不知道应何时进行C语言结构体的初始化和废弃操作,不能很好地管理内存。但是Block不同,OC的运行时库能够准确把握Block从栈复制到堆以及堆上的Block被废弃的时机。因此可以通过copy和dispose函数在赋值时调用_Block_object_assign,在销毁时调用_Block_object_dispose,从而正确管理捕获对象的引用计数。

cpp 复制代码
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}

_Block_object_assign函数调用相当于retain实例方法的函数,将对象赋值在对象类型的结构体成员变量中。

cpp 复制代码
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}

_Block_object_dispose函数调用相当于release实例方法的函数,释放赋值在对象类型的结构体成员变量中的对象。

cpp 复制代码
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};

虽然__main_block_copy_0函数和__main_block_dispose_0函数指针被赋值在成员变量copy和dispose中,但在转换后的源码中,这些函数包括使用指针全都没有被调用,这里是先登记给运行时,Block运行库时再真正调用,即在Block从栈复制到堆时以及堆上的Block被废弃时会调用。

然而以下时机栈上的Block会复制到堆:

  • 调用Block的copy实例方法时
  • Block作为函数返回值返回时
  • 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
  • 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时

usingBlock方法Cocoa框架中一类以Block作为参数的方法命名方式,通常用于遍历集合、排序或执行回调逻辑,例如 :

objc 复制代码
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
  NSLog(@"%ld", array.count);
}];

这两种情况栈上Block会复制到堆上的原因在于:usingBlock可能被会异步执行或延迟执行和GCD异步执行都可能会使得在当前作用域之外持有或延迟执行,如果不复制到堆上,可能会被释放掉,生命周期无法保证,会导致访问到野指针。

以上其实都是编译器自动地将对象的Block作为参数并调用_Block_copy函数,这与调用Block的copy实例方法效果相同。同样,在释放复制到堆上的Block后,谁都不持有Block而使其被废弃时调用dispose函数,这相当于对象的dealloc实例方法。

这样,通过使用__strong修饰符的自动变量,Block中截获的对象就能够超出其变量作用域而存在了。

值得注意的是:虽然截获变量的结构体大致相同但还是有区别的。

通过以上来区分捕获的是对象类型还是__block变量。然而无论哪种,如果不调用_Block_copy函数,即使截获了对象,也会随着变量作用域的结束而被废弃。只有copy到堆上,才会真正持有它们,继而可超出其变量作用域而存在。

因此,Block中使用对象类型自动变量时,除以下情形外,推荐调用Block的copy实例方法:

  • Block作为函数返回值返回时
  • 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
  • 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时

__block变量和对象

__block说明符可指定任何类型的自动变量。

objc 复制代码
__block id obj = [[NSObject alloc] init];
// 等效于__block id __strong obj = [[NSObject alloc] init];
// ARC有效时,id类型以及对象类型变量必定附加所有权修饰符,__strong省略

源码为:

cpp 复制代码
struct __Block_byref_obj_0 {
  void *__isa;
__Block_byref_obj_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 id obj;
};

与Block一样,在__block变量为附有__strong修饰符的id类型或对象类型自动变量的情形下会发生同样的过程.当__block变量从栈复制到堆时,使用_Block_object_assign函数,持有赋值给__block变量的对象。当堆上的__block变量被废弃时,使用_Block_object_dispose函数,释放赋值给__block变量的对象。同样,即使对象赋值复制到堆上的附有__strong修饰符的对象类型__block变量中,只要__block变量在堆上继续存在,那么该对象就会继续处于被持有的状态。

如果使用__weak修饰符,有以下两种情况:

  • 在Block中使用附有__weak修饰符的id类型变量:
objc 复制代码
typedef void (^blk_t)(id);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        blk_t blk;
        {
            id array = [[NSMutableArray alloc] init];
            id __weak array2 = array;

            blk = ^(id obj) {
                [array2 addObject:obj];
                NSLog(@"array2 count = %lu", (unsigned long)[array2 count]);
            };
        }
        blk([[NSObject alloc] init]);
        blk([[NSObject alloc] init]);
        blk([[NSObject alloc] init]);

        return 0;
    }
}

执行结果:

这是由于附有_ _strong修饰符的array在该变量作用域结束的同时被释放、废弃,nil被赋值在附有__weak修饰符的array2中。

  • 在Block中使用同时附有_ _block说明符和__weak修饰符:
objc 复制代码
typedef void (^blk_t)(id);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        blk_t blk;
        {
            id array = [[NSMutableArray alloc] init];
            __block id __weak array2 = array;

            blk = ^(id obj) {
                [array2 addObject:obj];
                NSLog(@"array2 count = %lu", (unsigned long)[array2 count]);
            };
        }
        blk([[NSObject alloc] init]);
        blk([[NSObject alloc] init]);
        blk([[NSObject alloc] init]);

        return 0;
    }
}

执行结果:

这是因为即使增加了_ _block说明符,附有__strong修饰符的array也会在该变量作用域结束的同时被释放废弃,nil被赋值给附有__weak修饰符的array2中。

除此之外,需要注意:

  1. 附有__unsafe_unretained修饰符的变量与指针相同,所以在Block中使用或是附加到__block变量中,都需要注意不要通过悬垂指针访问已被废弃的对象。
  2. __autoreleasing修饰符与__block说明符同时使用会产生编译错误。

Block循环引用

在Block中使用__strong容易引起循环引用。

objc 复制代码
typedef void(^blk_t)(void);

@interface MyObject : NSObject {
    blk_t blk;
}

@end
  
@implementation MyObject

- (instancetype)init {
    self = [super init];
    blk = ^{
        NSLog(@"%@", self);
    };
    return self;
}

@end

该类强引用blk,Block里又捕获了self,因此自动触发Block被copy到堆上,这样Block强引用self。这形成了循环引用。

编译器报出警告:

另外,Block内没有使用self也同样截获了self,也会引起循环引用。

这是因为对编译器来说,obj只不过是对象用结构体的成员变量,Block依然会强引用self。

为避免此,我们需要用__weak变量来修饰一下self。

objc 复制代码
id __weak weakSelf = self;
blk = ^{
  NSLog(@"%@", weakSelf);
};

这里我们也可以使用__block:

objc 复制代码
@implementation MyObject

- (instancetype)init {
    self = [super init];
    __block id weakSelf = self;
    blk = ^{
        NSLog(@"%@", weakSelf);
        weakSelf = nil;
    };
    return self;
}

- (void)execBlock {
    blk();
}

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

@end

id obj = [[MyObject alloc] init];
[obj execBlock];

这里面要注意的是execBlock方法:如果不调用该方法,即不执行blk的Block,便会引起循环引用并引起内存泄漏。

不调用execBlock方法,会形成self → blk → weakSelf(=self)的循环引用。调用的话,nil被赋值给weakSelf,强引用失效。
ARC下_ _block VS MRC下__block:
__block MyObject *obj = self;:MRC下__block修饰的对象不会被retain,也就不会形成循环引用闭环。因此使用MRC下使用__block修饰符可直接避免循环引用的问题。

相关推荐
ACGkaka_2 小时前
JDK 版本管理工具介绍:jenv与sdkman(Mac端)
java·macos·sdkman
报错小能手3 小时前
ios开发方向——swift并发进阶核心 Task、Actor、await 详解
开发语言·学习·ios·swift
一块小土坷垃3 小时前
Pearcleaner:一款功能强大的免费开源 macOS 应用清理工具
macos·开源软件
光影少年3 小时前
开发RN项目时,如何调试iOS真机、Android真机?常见调试问题排查?
android·前端·react native·react.js·ios
承渊政道3 小时前
【递归、搜索与回溯算法】(递归问题拆解与经典模型实战大秘笈)
数据结构·c++·学习·算法·macos·dfs·bfs
中国胖子风清扬4 小时前
基于GPUI框架构建现代化待办事项应用:从架构设计到业务落地
java·spring boot·macos·小程序·rust·uni-app·web app
yuanzhengme19 小时前
AI【应用 04】FunASR离线文件转写服务开发指南(实践篇)
人工智能·macos·xcode
x-cmd20 小时前
[260412] x-cmd v0.8.13:x free 新增进程内存显示,feishu、telegram REPL 机器人齐上线!
linux·macos·机器人·内存·x-cmd·telegram·feishu
疯狂的程序猴21 小时前
Flutter应用代码混淆完整指南:Android与iOS平台配置详解
后端·ios