OC之Blocks

2018-12-09  本文已影响0人  苏沫离

Blocks 是 C 语言的扩充:持有自动变量的匿名函数。

特别说明:本文代码如无说明,均在 ARC 下编译;在 MRC 下编译会做注释

1、Blocks 语法

1.1、Blocks 变量声明

Blocks 类型由返回值类型和参数类型列表构成。使用脱字符 ^ 声明 Blocks 类型的变量。

声明Blocks类型的语法.png

声明的一些简单Blocks 变量:

//变量 sumBlock 拥有两个 int 类型参数,返回值也是 int 类型
int (^sumBlock)(int,int);

//变量 logBlock 拥有一个 NSString 类型参数,无返回值 void
void (^logBlock)(NSString *);

//变量 simpleBlock 没有参数 void,无返回值void
void (^simpleBlock)(void);

既然可以将变量设为Blocks 类型,那么也可以将函数与方法的参数、类属性设为Blocks (能够定义变量的地方就能够定义Blocks ):

@interface ModelClass : NSObject

//requestBlcok 用作对象的属性,使用 copy 修饰,具有文件作用域,被分配在堆区
@property (nonatomic ,copy) void (^requestBlcok)(BOOL isSuccess);

//requestBlcok 用于方法的参数,具有块作用域,被分配在栈区
- (void)requestCompletion:(void(^)(BOOL isSuccess))requestBlcok;

@end

使用 typedefBlocks提供别名,可以简化 Blocks 类型变量名称:

//为 block 类型提供别名 RequestBlcok
typedef void(^RequestBlcok)(BOOL isSuccess);

@interface ModelClass : NSObject

//别名 RequestBlcok 用作对象的属性类型,使用 copy 修饰,具有文件作用域,被分配在堆区
@property (nonatomic ,copy) RequestBlcok requestBlcok;

//别名 RequestBlcok 用于方法的参数类型,具有块作用域,被分配在栈区
- (void)requestCompletion:(RequestBlcok)completion;

@end

疑问 1:为何使用 copy 修饰属性变量 Blocks,?能用strong 嘛?
答:当Blocks不持有自动变量时,使用 retaincopystrong 都会持有该变量;但当Blocks持有作用域之外的自动变量时,使用 retain 修饰,被持有的自动变量引用计数没有变化,可能随时释放,存在内存问题!
而使用 copystrong 修饰的 Blocks,会调用 _Block_object_assign() 函数将持有的自动变量引用计数 加 1,保证当使用时不会被释放!

1.2、Blocks 常量表达式

定义Blocks常量表达式:以脱字符^开头,后跟返回值类型、参数列表、Blocks 主体:

块常量表达式的语法.png

Blocks 常量表达式的简单定义:

//有返回类型的 Blocks 常量表达式
^int(int a ,int b){
    return a + b;
};

/* 没有设置返回类型的 Blocks 常量表达式
 * @note 返回值类型 int 可以不做定义,因为编译器会从 return 语句推断出返回值类型。
 * @note Blocks常量表达式被编译器存储在栈区
 */
^(int a,int b){
    return a + b;
};

Blocks 被用作方法参数,类属性时,定义Blocks常量表达式:

ModelClass *test = [[ModelClass alloc] init];
test.requestBlcok = ^(BOOL isSuccess) {
    printf("Block 主体,执行某些操作\n");
};

[test requestCompletion:^(BOOL isSuccess) {
    printf("Block 主体,执行某些操作\n");
}];

Blocks的声明与定义组合为一条语句:

/* @note Blocks常量表达式被编译器存储在栈区
 * @note 通过赋值操作将其赋给Blocks变量,此时被复制到堆上或者全局静态区。
 */
void (^logBlock)(NSString *) = ^(NSString *log){
    NSLog(@"log:%@",log);
};

//在Blocks定义中,没有参数可不使用void
void (^simpleBlock)(void) = ^{
    
};

如果在函数体外面声明并定义 Blocks 变量,那么它具有文件作用域,由编译器静态分配,存储在全局静态区的数据区:

static BOOL(^isEvenNumberBlock1)(int number) = ^BOOL(int number){return (number % 2) == 0 ? YES : NO;};

关于 C 的内存,可以阅读 C 内存管理 一文。

Blocks 变量声明 和 Blocks 常量表达式的语法区别
语法元素 Blocks变量声明 Blocks常量表达式
脱字符 ^ 标识Blocks变量声明的开始,脱字符位于Blocks变量名称之前,两者都被封装在圆括号中 标识Blocks常量表达式的开始
名称 Blocks变量的名称是必选项 Blocks 常量表达式没有名称
返回值类型 必选项,没有返回值时声明为void 可选项,因为编译器能够从Blocks 表达式推断出返回值类型。
参数 必选项,没有参数时声明为void 参数列表是可选项

2、Blocks 是一个闭包

Blocks 是一个实现的闭包,一个允许访问其常规作用域之外变量的函数。

在 C 函数中声明的变量具有块作用域、无链接;这意味着这些自动变量仅在该函数访问。
与 C 函数相比,Blocks 扩大了变量的作用域,可以访问 Blocks 主体之外的自动变量。

2.1、持有被访问的自动变量

Blocks 访问其作用域之外的变量

void func(void){
    NSString *log = @"打印日志";
    int a = 5;
    NSLog(@"log1:%@  : %p : %p",log,log,&log);
    NSLog(@"a  1:%d  : %p",a,&a);
    void (^simpleBlock)(void) = ^{//block 声明并赋值
        NSLog(@"log2:%@  : %p : %p",log,log,&log);
        NSLog(@"a  2:%d  : %p",a,&a);
    };
    log = @"hello word!";
    a = 6;
    NSLog(@"log3:%@  : %p : %p",log,log,&log);
    NSLog(@"a  3:%d  : %p",a,&a);
    simpleBlock();//调用 block 变量
}

/* 打印数据
log1:打印日志  : 0x1000021c0 : 0x7ffeefbff5c8
a  1:5  : 0x7ffeefbff5c4
log3:hello word!  : 0x100002260 : 0x7ffeefbff5c8
a  3:6  : 0x7ffeefbff5c4
log2:打印日志  : 0x1000021c0 : 0x100705250
a  2:5  : 0x100705258
 */

分析打印数据:Blocks 表达式中的变量与作用域之外的自动变量,内存地址相同,但不是同一个指针指向该内存;类似于 Blocks 表达式对作用域之外的变量进行浅拷贝

Blocks 表达式对通过浅拷贝 持有 作用域之外的自动变量。

疑问 2:Blocks变量为何要持有作用域之外的自动变量?如何持有?

2.2、存储类别说明符__block

默认情况下,Blocks 常量表达式对作用域之外的局部变量无权修改,否则编译器报错 Variable is not assignable (missing __block type specifier)
存储类别说明符 __block 用于指定将变量值设置到哪个存储区域,可以将这些变量切换为读写模式。

__block NSString *log = @"打印日志";
void (^simpleBlock)(void) = ^{
    log = @"hello word!";
};

__block 说明符不能与存储类别说明符 autoregisterstatic组合使用。

2.3、Blocks访问全局变量

Blocks常量表达式中可以直接修改全局变量,无需使用 __block 说明符。

int a = 3;//声明并赋值一个全局变量 a

int main(int argc, const char * argv[]) {
    void (^simpleBlock)(void) = ^{
        printf("a 1==== %d \n",a);
        a = 4;
        printf("a 2==== %d \n",a);
    };
    a = 5;
    simpleBlock();
    return 0;
}

/* 打印数据
a 1==== 5
a 2==== 4
*/

在 C 语言中,下述类型变量允许在 Blocks 常量表达式中直接修改:

同样的,在 OC 中对象的属性属于全局变量,也可以在 Blocks 常量表达式中直接修改。

疑问 3:Blocks表达式中的全局变量为什么可以修改?Blocks持有该全局变量了嘛?

3、Blocks 底层实现

Blocks 是持有自动变量的匿名函数,允许访问其作用域之外的变量。那么,Blocks 到底是什么呢?利用 clang 将 Objective-C 代码转为 C ++ 源码:

clang -rewrite-objc main.m

//支持 ARC 并指定 Runtime 版本
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 ModelClass.m

//指定 sdk 和真机
xcrun -sdk iphoneos clang -rewrite-objc main.m

//指定 sdk 和模拟器
xcrun -sdk iphonesimulator clang -rewrite-objc main.m

//指定具体某版本的模拟器
xcrun -sdk iphonesimulator9.3 clang -rewrite-objc main.m

//如果使用了第三SDK
xcrun -sdk iphonesimulator9.3 clang -rewrite-objc –F      /Users/goanywhere/Downloads/nbs-newlens-ios-2.3.6  main.m
3.1、分析Blocks 的本质

变量有地址,同样的,函数也有地址,指向函数的指针中存储着函数代码的起始处地址。

Objective-C 代码:

int main(int argc, const char * argv[]) {
    int (^addBlock)(int,int) = ^(int a ,int b){
        return a + b;
    };
    printf("1 + 2 = %d \n",addBlock(1,2));
    return 0;
}

转换后相关的 C++ 源码:

/* Block 结构模版
 * @param isa 指向所属类的指针,
 * @param FuncPtr 函数指针,指向Block的功能实现函数
 */
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

/* Block的描述信息
 * @note Block_size 赋值 sizeof(struct __main_block_impl_0)
 *           意味着Block可能就是结构 __main_block_impl_0
 */
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_impl 与 __main_block_desc_0
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    /* 结构体的构造函数
     * @param fp 函数指针,被赋值 __main_block_func_0()
     * @param desc 结构指针,指向结构 __main_block_desc_0
     */
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;//isa 指针,栈内存
        impl.Flags = flags;//标志位,0
        impl.FuncPtr = fp;//函数指针,指向 Block 的功能实现函数
        Desc = desc;//描述信息:block 内存
    }
};

/* 实现 OC 代码中Block的主要功能。
 * @param __cself 结构指针,类似于 OC 中的 self 指针;指向模板 (__block_impl *)addBlock;
 *                意味着 Block 主体是一个__main_block_impl_0的结构;
 * @param int a、int b : Block 表达式的参数列表
 * @return 返回 int 类型
 */
static int __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
    return a + b;
}

/* main() 函数的实现,分为 3 大部分:声明、实现、调用
 * 1、Block 声明:本质为函数指针 int(*addBlock)(int,int),指向构造函数 __main_block_impl_0() 的起始位置
 * 2、Block 常量表达式:获取函数地址 &__main_block_impl_0()
 *     2.1、强制类型转换 ((int (*)(int, int))
 *     2.2、构造函数 __main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))
 *     2.3、函数指针指向构造函数
 * 3、Block 的调用转为 ((int (*)(__block_impl *, int, int))((__block_impl *)addBlock)->FuncPtr)((__block_impl *)addBlock, 1, 2)
 *     3.1、函数 ((int (*)(__block_impl *, int, int))((__block_impl *)addBlock)->FuncPtr) 就是 __main_block_func_0()
 *     3.2、参数列表 ((__block_impl *)addBlock, 1, 2)
 *     3.3、__cself 指针指向模板类 (__block_impl *)addBlock
 *     3.4、调用 addBlock(1,2) 也就是调用函数指针 __block_impl.FuncPtr 指向的函数__main_block_func_0()
 */
int main(int argc, char * argv[]) {
    int (*addBlock)(int,int) = ((int (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    printf("1 + 2 = %d \n",((int (*)(__block_impl *, int, int))((__block_impl *)addBlock)->FuncPtr)((__block_impl *)addBlock, 1, 2));
    return 0;
}

疑问 4:在构造函数__main_block_impl_0()中,isa指针指向_NSConcreteStackBlock_NSConcreteStackBlock是什么,isa指针为何指向_NSConcreteStackBlock

小结:

3.2、Blocks如何持有其作用域之外的自动变量?

Blocks 与函数、方法类似,但除了是可执行代码外,Blocks 还持有与堆内存或栈内存绑定的变量

int main(int argc, const char * argv[]) {
    int a = 3;//基本数据类型的自动变量
    NSString *string = @"Hello word!!";//OC 类:自动变量
    __weak NSString *weakString = @"弱引用";
    void (^simpleBlock)(void) = ^{
        NSLog(@"string1 ==== %@ : %p",string,string);
        NSLog(@"weakString = %@ : %p",weakString,weakString);
        printf("a1 ========= %d : %p \n",a,&a);
    };
    a = 4;
    string = @"打印日志";
    simpleBlock();
    return 0;
}

转为 C++ 代码:

/* 将一个对象的值复制给另一个对象
 * @param dst 目的对象:接收值
 * @param src 源对象:
 * @note  BLOCK_FIELD_IS_OBJECT 代表Objective-C对象,此处相当于调用 -retain 方法
 */
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->string, (void*)src->string, BLOCK_FIELD_IS_OBJECT);
    _Block_object_assign((void*)&dst->weakString, (void*)src->weakString, BLOCK_FIELD_IS_OBJECT);
}

/* 销毁一个对象
*/
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->string, BLOCK_FIELD_IS_OBJECT);//相当于调用 -release 方法
    _Block_object_dispose((void*)src->weakString, BLOCK_FIELD_IS_OBJECT);
}

/* Block的描述信息
 * @note 持有 复制函数 与 释放函数 的函数指针
 */
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};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    NSString *__strong string; // Blocks 持有作用域之外的自动变量
    NSString *__weak weakString;
    int a;
    
    /* 结构体的构造函数
     * @param fp 函数指针,被赋值 __main_block_func_0()
     * @param desc 结构指针,指向结构 __main_block_desc_0
     * @param 作用域之外的 _string、_weakString、_a 被赋值给该结构体成员
     */
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSString *__strong _string, NSString *__weak _weakString, int _a, int flags=0) : string(_string), weakString(_weakString), a(_a) {
        impl.isa = &_NSConcreteStackBlock;//isa 指针,栈内存
        impl.Flags = flags;//标志位,0
        impl.FuncPtr = fp;//函数指针,指向 Block 的功能实现函数
        Desc = desc;//描述信息:block 内存
    }
};

/* 实现 OC 代码中Block的主要功能
 * @note __cself->string 调用的是Block对象的成员 string ,并不是作用域之外的 string 变量
 */
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    NSString *__strong string = __cself->string; // 复制的副本
    NSString *__weak weakString = __cself->weakString; // 复制的副本
    int a = __cself->a; // 复制的副本
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_d8_yqx6_fxx1gzbwsghfv7n6r8w0000gn_T_main_490ccf_mi_2,string,string);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_d8_yqx6_fxx1gzbwsghfv7n6r8w0000gn_T_main_490ccf_mi_3,weakString,weakString);
    printf("a1 ========= %d : %p \n",a,&a);
}

int main(int argc, const char * argv[]) {
    int a = 3;
    NSString *string = (NSString *)&__NSConstantStringImpl__var_folders_d8_yqx6_fxx1gzbwsghfv7n6r8w0000gn_T_main_490ccf_mi_0;
    __attribute__((objc_ownership(weak))) NSString *weakString = (NSString *)&__NSConstantStringImpl__var_folders_d8_yqx6_fxx1gzbwsghfv7n6r8w0000gn_T_main_490ccf_mi_1;
    void (*simpleBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, string, weakString, a, 570425344));
    a = 4;
    string = (NSString *)&__NSConstantStringImpl__var_folders_d8_yqx6_fxx1gzbwsghfv7n6r8w0000gn_T_main_490ccf_mi_4;
   ((void (*)(__block_impl *))((__block_impl *)simpleBlock)->FuncPtr)((__block_impl *)simpleBlock);
    return 0;
}

Blocks 持有其作用域之外的自动变量的大致过程:

3.2.1、 函数__main_block_copy_0()__main_block_dispose_0()的调用时机

函数__main_block_copy_0()的调用时机:

函数 调用时机
__main_block_copy_0() 栈上的 Block 复制到堆上时,即 调用_Block_copy() 函数 或 -copy方法时
__main_block_dispose_0() 堆上的 Block 被废弃时

Block从栈复制到堆上时,Block实例通过_Block_object_assign()函数,将持有的自动变量的引用计数加 1!
Block从堆释放时,Block实例通过_Block_object_dispose()函数,释放持有自动变量的所有权!

3.3、__Block 发挥什么作用?

既然 Blocks 持有作用域之外的自动变量,为何使用__Block修饰之后,修改Blocks 持有的自动变量,会对作用域之外的变量产生影响呢?

int main(int argc, const char * argv[]) {
    __block int a = 3;
    void (^simpleBlock)(void) = ^{
        printf("a 1==== %d \n",a);
        a = 4;
        printf("a 2==== %d \n",a);
    };
    a = 5;
    simpleBlock();
    return 0;
}

转为C++代码:

/* 将一个对象的值复制给另一个对象
* @param dst 目的对象:接收值
* @param src 源对象:
*/
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->a, (void*)src->a, BLOCK_FIELD_IS_BYREF);
}

/* 销毁一个对象
*/
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->a, BLOCK_FIELD_IS_BYREF);
}

/* Block的描述信息
 */
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 变量转为结构体
 * @note __forwarding 指针,指向外部变量指针的内存地址
 */
struct __Block_byref_a_0 {
    void *__isa;//指向所属的类的 isa 指针
    __Block_byref_a_0 *__forwarding;
    int __flags;
    int __size;//该结构的内存大小
    int a;//该结构持有相当于原自动变量
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_a_0 *a; // by ref
    
    /* 结构体的构造函数
     * @param fp 函数指针,被赋值 __main_block_func_0()
     * @param desc 结构指针,指向结构 __main_block_desc_0
     * @param 作用域之外的 _a 被拷贝给该结构体成员
     */
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;//isa 栈内存
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

/* 实现 OC 代码中Block的主要功能
 * @note __cself->a 调用的是Block对象的成员 a ,并不是作用域之外的变量 a
 *       a->__forwarding 是作用域之外的变量 a 的指针地址
 *       a->__forwarding->a 作用域之外的变量 a 的成员
 *       (a->__forwarding->a) = 4 本质上修改的还是作用域之外的变量值
 */
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_a_0 *a = __cself->a; // bound by ref
    printf("a 1==== %d \n",(a->__forwarding->a));
    (a->__forwarding->a) = 4;//修改的是作用域之外的变量值
    printf("a 2==== %d \n",(a->__forwarding->a));
}

/* mian() 函数
 * __block a 转为结构体 __Block_byref_a_0
 * a=3 被赋值给结构成员 __Block_byref_a_0.a
 * __forwarding 指针指向变量a的内存地址
 */
int main(int argc, const char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {
        (void*)0,// __isa 指针:指向空指针NULL
        (__Block_byref_a_0 *)&a,// __forwarding 指针
        0,
        sizeof(__Block_byref_a_0),//__size 内存大小
        3
    };
    void (*simpleBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    (a.__forwarding->a) = 5;
    ((void (*)(__block_impl *))((__block_impl *)simpleBlock)->FuncPtr)((__block_impl *)simpleBlock);
    return 0;
}

__block 发挥的作用:

3.4、Blocks 表达式访问的全局变量

Blocks对象可以持有作用域之外的自动变量;但并不会持有全局变量!

int a = 3;//全局变量
int main(int argc, const char * argv[]) {
    void (^simpleBlock)(void) = ^{
    static int b = 13;//静态变量
    void (^simpleBlock)(void) = ^{
        a = 4;
        b = 14;
    };
    a = 5;
    b = 15;
    simpleBlock();
    return 0;
}

转为 C++ 代码:

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int *b = __cself->b; // bound by copy
    a = 4;
    (*b) = 14;
}

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[]) {
    static int b = 13;
    void (*simpleBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &b));
    a = 5;
    b = 15;
    ((void (*)(__block_impl *))((__block_impl *)simpleBlock)->FuncPtr)((__block_impl *)simpleBlock);
    return 0;
}

执行函数__main_block_func_0() 在全局变量a 的作用域之内,可以直接使用全局变量a,不需要复制。
但是具有块作用域的静态变量b,在函数__main_block_func_0()的作用域之外,不能直接访问;将静态变量的地址保存在结构__main_block_impl_0,通过地址访问作用域之外的静态变量!

4、Blocks内存管理

Blocks常量表达式的isa指针指向 _NSConcreteStackBlock,也就是说 Blocks常量被编译器存储到栈区!与之类似的类有:_NSConcreteGlobalBlock_NSConcreteMallocBlock

Blocks所属的类 设置对象的存储域
_NSConcreteStackBlock 由编译器自动分配在栈区,并在合适时机释放
_NSConcreteMallocBlock _malloc()函数分配在堆区
_NSConcreteGlobalBlock 全局静态区,存放全局变量
内存分区.png
4.1、类实例_NSConcreteGlobalBlock

Blocks作为全局变量或者类的属性时,创建的Blocks对象属于_NSConcreteGlobalBlock

//具有文件作用域,由程序静态分配,存储在全局静态区的数据区
static BOOL(^isEvenNumberBlock2)(int number) = ^BOOL(int number){return (number % 2) == 0 ? YES : NO;};
int main(int argc, const char * argv[]) {
   return 0;
}

声明并定义全局变量isEvenNumberBlock2,转为 C++ 代码:

static struct __isEvenNumberBlock2_block_impl_0 {
  struct __block_impl impl;
  struct __isEvenNumberBlock2_block_desc_0* Desc;
  __isEvenNumberBlock2_block_impl_0(void *fp, struct __isEvenNumberBlock2_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;//isa 初始化为 _NSConcreteGlobalBlock
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

全局变量isEvenNumberBlock2实例属于_NSConcreteGlobalBlock类。

内存分析.png

即使在块作用域内创建Blocks变量,只要Blocks不持有作用于之外的自动变量,编译器就会将Blocks变量存储在全局静态区。

4.2、Blocks变量从栈复制到堆上

由于Blocks可以看做 Objective-C 对象,而在 ARC 模式下编译器默认使用__strong 修饰变量;因此simpleBlock变量持有Blocks实例的所有权,类似于MRC下的-retain !因此simpleBlock变量存储在堆区。

假如使用 __weak修饰Block变量,则该变量存储在栈区!

从栈复制到堆上.png
4.2.1、什么情况下栈上的Block复制到堆上?

调用函数_Block_copy()Block变量从栈复制到堆!

都在什么情况下,编译器会将栈上的Block变量复制到堆上呢?

4.2.2、为什么把栈上的Block复制到堆上?

栈上的Block变量,如果其作用域结束,该Block就会被释放;由于__Block变量也配置在栈上,其变量作用域结束,则该__Block变量也被释放。

例子:MRC 下Block变量作为函数返回值时
/* @note MRC 编译:Block常量表达式分配在栈内存,不被任何指针持有,当函数返回时,Block变量被释放;
*           因此编译错误: Returning block that lives on the local stack
*           调用 -copy 将Block变量从栈复制到堆上,解决该编译错误;
*           需要在适当时机调用 -release 释放所有权,否则引起内存泄露;
* @note ARC 编译,Block常量表达式作为函数返回值时会被编译成 autoreleased NSConcreteMallocBlock 类型,存储在堆内存
*           编译器会自动释放,无需我们操心
*/
typedef  void (^SimpleBlock)(void);
SimpleBlock getBlockFunc(){
   int b = 20;
   return ^{
       NSLog(@"b ====== %d",b);
   };
}

注意:将 Block 从栈复制到堆上需要消耗CPU资源。如果Block在栈上也能使用,则无需复制到堆上!

疑问6:思考一下,在此处可以使用 -autorelease 或者 -retain方法嘛?

4.2.3、持有自动变量

Block从栈上复制到堆上时,作用域之外的自动变量仍然存储在栈上,仅仅对应的Block结构成员被复制在堆上。

a、Block变量调用-copy
//MRC 下编译代码
int main(int argc, const char * argv[]) {
   //自动变量存储在栈区,地址都是高位地址
    NSString *log = @"hello word";//栈上,高地址
    int a = 5;//栈上,高地址
    
    NSLog(@"log1:%@  : %p : %p",log,log,&log);
    NSLog(@"a  1:%d  : %p",a,&a);
    
    SimpleBlock simpleBlock =  [^{
        NSLog(@"log2:%@  : %p : %p",log,log,&log);//堆上
        NSLog(@"a  2:%d  : %p",a,&a);//堆上
    } copy];
    
    log = @"打印日志";//栈上,高地址
    a = 6;//栈上,高地址
    NSLog(@"log3:%@  : %p : %p",log,log,&log);
    NSLog(@"a  3:%d  : %p",a,&a);
    
    simpleBlock();
    
    NSLog(@"log4:%@  : %p : %p",log,log,&log);//栈上,高地址
    NSLog(@"a  4:%d  : %p",a,&a);//栈上,高地址
    
    return 0;
}

/*打印数据
log1:hello word  : 0x1000021e0 : 0x7ffeefbff578
a  1:5  : 0x7ffeefbff574
log3:打印日志  : 0x100002280 : 0x7ffeefbff578
a  3:6  : 0x7ffeefbff574
log2:hello word  : 0x1000021e0 : 0x100502d10
a  2:5  : 0x100502d18
log4:打印日志  : 0x100002280 : 0x7ffeefbff578
a  4:6  : 0x7ffeefbff574
*/
持有的自动变量 Block 从栈copy到堆时的影响 Block从堆区release
基本数据类型如int 从栈复制到堆 从堆区释放掉
Objective-C 对象 该对象引用计数加1,堆上的Block持有该对象 该对象引用计数减1,堆上的Block释放该对象所有权
b、对Block使用-retain-autorelease操作

Block 能否调用使用-autorelease或者 -retain方法?

Block调用-copy从栈复制到堆时, 底层调用_Block_object_assign()函数持有被捕获的对象(自动变量);从堆区释放时调用_Block_object_dispose()释放被捕获对象的所有权!

使用-retain或者-autorelease 获取Block所有权,引用计数retainCount加1;但是并没有调用_Block_object_assign()函数与_Block_object_dispose(),被持有自动变量仍然在栈内存中:

我们通过retain或者autorelease操作获取的Block结构体实例的所有权,所以调用Block可以确保该对象存在!但是对被捕获的 Objective-C 类对象,Block结构体实例没有该变量的所有权,程序运行到此处,变量指针(Block结构体实例的结构成员)指向的变量可能已被释放,此时程序可能闪退!!

被捕获的自动变量 Block 在栈上retain或者autorelease
基本数据类型如int 没影响
Objective-C 对象 没影响

如果Block没有捕获自动变量,那么它被配置在全局静态区,此时又没有必要再次获取所有权了!

获取Block 所有权的操作最好使用copy操作。

被捕获的自动变量释放问题
4.2.3、__block变量

通过 copy操作,因为捕获的自动变量是指针,复制操作会使引用计数加 1!!那么 copyBlock 从栈复制到堆的时候,__block变量会产生什么影响呢??

__block变量的配置存储域 Block 从栈复制到堆时的影响
从栈复制到堆并被Block 持有
Block 持有
a、在一个Block中使用多个__block变量

若在一个Block中使用__block变量,则Block持有该变量,该__block也必定配置在栈上。将Block从栈复制到堆上,所捕获的__block也被从栈上配置到堆上。

在一个Block中使用__block变量.png

注意__block变量若不在Block中使用,则不会被复制到堆区!Block的复制操作,对不被捕获的__block变量没有任何影响!!

b、在多个Block中使用某个__block变量
在多个Block中使用__block变量.png
c、Block被废弃

就像Objective-C的引用计数管理一样:使用__block变量的Block持有__block。如果Block被废弃,它所持有的__block变量就被释放。

Block的废弃和__block变量的释放.png
d、__block变量的__forwarding指针

通过Block 的复制,__block变量也从栈上复制到堆上,此时可同时访问栈上和堆上的__block变量!那么栈上的__block变量的__forwarding指针指向何处??堆上的__block变量的__forwarding指针又指向何处??

将栈上的__block变量复制到堆上,栈上的__block变量__forwarding指针将指向被复制到堆上的__block变量; 堆上__block变量的__forwarding指针指向其自身!!

__block变量从栈复制到堆上.png

通过__block变量的__forwarding指针,无论在Block表达式之中、在Block表达式之外,无论在栈上、还是复制到堆上。都能保证程序始终访问同一个__block变量!

e 、_Block_object_assign()函数与_Block_object_dispose()函数

__block变量有__strong修饰时:当__block变量从栈复制到堆时,使用_Block_object_assign()函数持有赋值给__block变量的对象!当堆上的__block变量被废弃时,使用_Block_object_dispose()函数释放赋值给__block变量的对象。

被捕获的对象 BLOCK_FIELD_IS_OBJECT
__block变量 BLOCK_FIELD_IS_BYREF

5、 Blocks 面试题


参考文章/图书

C/C++语言的闭包
精通Objective-C
Objective-C高级编程 iOS与OS X多线程和内存管理

上一篇下一篇

猜你喜欢

热点阅读