内容简介:Block真的难,笔者静下心来读《Objective-C 高级编程 iOS与OS X多线程和内存管理》,读的时候顺便记录下来自己的心得,方便以后再翻回,也希望能带给大家一些帮助。本文将以一个菜dog的角度,从 Block 不截获变量、截获变量不修改、截获并修改变量 、 截获对象 四个层次 浅浅探究Block的实现。Block的语法就不回顾了,不好记Block语法可以翻这篇
Block真的难,笔者静下心来读《Objective-C 高级编程 iOS与OS X多线程和内存管理》,读的时候顺便记录下来自己的心得,方便以后再翻回,也希望能带给大家一些帮助。
本文将以一个菜dog的角度,从 Block 不截获变量、截获变量不修改、截获并修改变量 、 截获对象 四个层次 浅浅探究Block的实现。
Block的语法就不回顾了,不好记Block语法可以翻这篇 How Do I Declare A Block in Objective-C? 。
Block实现
转成C++ 的源代码学习,笔者加了适当的注释方便理解。
不截获自动变量值
int main() { void (^blk)(void) = ^{printf("Block\n");}; blk(); retrun 0; } 复制代码
将转为
// block中通用的成员变量 结构体 // 文章后面的代码不再给出,但都有用到 struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; // 代表Block 的结构体 struct __main_block_impl_0 { struct __block_impl impl;// block通用的成员变量 struct __main_block_desc_0* Desc;// block 的大小 // 构造函数 __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; } }; // 原本的代码块 转到一个C函数 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { printf("Block\n"); } // 计算block大小的结构体 // 声明的同时,初始化一个变量__main_block_desc_0_DATA static struct __main_block_desc_0 { unsigned long reserved; unsigned long Block_size; } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0) }; int main() { // 声明定义block // 用到了构造函数方法 void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA); /* 相当于以下 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; 栈上生成的结构体实例的指针,赋值给变量blk。 */ // 调用block // 第一个参数为 blk_>FuncPtr,即C函数 // 第二个参数为 blk本身 ((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk); /* 相当于以下 普通的C函数调用 (*blk->impl.FuncPtr)(blk); */ return 0; } 复制代码
即把原本的代码块,转到一个C函数中。并且创建一个 代表Block 的结构体,最后一个构造函数,Block对象把函数和成员绑定起来。
截获自动变量不修改的情况
和以上区别在于,Block结构体中的成员变量多了截获的自动变量,并且构造函数参数也是。
int main() { int dmy = 256; int val = 10; const char *fmt = "val = %d\n"; void (^blk)(void) = ^{printf(fmt, val);}; val = 2; fmt = "These values were changed.val = %d\n"; blk(); return 0; } 复制代码
将转为
// 跟上面一样 struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; // 代表Block 的结构体 struct __main_block_impl_0 { struct __block_impl impl;// block通用的成员变量 struct __main_block_desc_0* Desc;// block 的大小 // 截获的自动变量 // 结构体中有名字一样的成员变量 const char *fmt; int val; // 构造函数 // 参数多了截获的自动变量 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags = 0) : fmt(_fmt), val(_val) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; // 原本的代码块 转到一个C函数 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { const char *fmt = __cself->fmt; int val = __cself->val; printf(fmt, val); } // 计算block大小的结构体 // 声明的同时,初始化一个变量__main_block_desc_0_DATA static struct __main_block_desc_0 { unsigned long reserved; unsigned long Block_size; } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0) }; int main() { int dmy = 256; int val = 10; const char *fmt = "val = %d\n"; void (*blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, fmt, val); /* 结构体初始化如下: impl.isa = &_NSConcreteStackBlock; impl.Flags = 0; impl.FuncPtr = __main_block_func_0; Desc = &__main_block_desc_0_DATA; fmt = "val = %d\n"; val = 10; */ return 0; } 复制代码
根据以上,我们知道截获变量后,实质上是Block结构体中有一个成员变量存了起来。调用Block时,是访问取结构体成员变量,而不是外面的局部变量。
Block中修改值
Block不允许修改外部变量的值。Apple这样设计,应该是考虑到了block的特殊性,block也属于“函数”的范畴,变量进入block,实际就是已经改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。又比如我想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。于是栈区变成了红灯区,堆区变成了绿灯区。
iOS Block不能修改外部变量的值,指的是栈中指针的内存地址。下面举几个例子理解。
- 非OC对象,修改会编译错误。
int val = 0; void (^blk)(void) = ^{ val = 1; }; 复制代码
- OC对象,发送消息可以,但改指针内存地址不行。
以下没问题
id array = [[NSMutableArray alloc] init]; void (^blk)(void) = ^{ id obj = [[NSObject alloc] init]; [array addObject:obj]; }; 复制代码
以下编译报错
id array = [[NSMutableArray alloc] init]; void (^blk)(void) = ^{ array = [[NSMutableArray alloc] init]; }; 复制代码
- C 数组 截获自动变量的方法没有实现对 C语言 数组的截获。
以下编译错误
const char text[] = "hello"; void (^blk)(void) = ^{ printf("%c\n", text[2]); }; 复制代码
需改成指针
const char *text = "hello"; void (^blk)(void) = ^{ printf("%c\n", text[2]); }; 复制代码
那么Block 要怎么修改变量呢?
方法一:用到静态或全局变量
-
C 中有一个变量,允许Block改写值。
- 静态变量
- 静态全局变量
- 全局变量
-
例子
int global_val = 1;// 全局变量 static int static_global_val = 2;// 静态全局变量 int main() { static int static_val = 3;// 静态变量 void (^blk)(void) = ^{ global_val *= 1; static_global_val *= 2; static_val *= 3; } return 0; } 复制代码
转换后
int global_val = 1; static int static_global_val = 2; // 代表Block 的结构体 struct __main_block_impl_0 { struct __block_impl impl;// block通用的成员变量 struct __main_block_desc_0* Desc; // 成员变量只多了静态变量,原因在后面分析 int *static_val; // 构造函数 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_staitc_val, int flags=0) : static_val(_static_val) { impl.isa = &_NSConcreteStackblock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; // 原本的代码块 转到一个C函数 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int *static__val = __cself->static_val; global_val *= 1; static_global_val *= 2; (*static_val) *=3; } // 计算block大小的结构体 // 声明的同时,初始化一个变量__main_block_desc_0_DATA static struct __main_block_desc_0 { unsigned long reserved; unsigned long Block_size; } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0) }; int main() { static int static_val = 3; blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &static_val); return 0; } 复制代码
为什么成员变量只多了静态变量呢?
这就要先了解 iOS 内存区域。 iOS-MRC与ARC区别以及五大内存区
- 栈:
- 由系统管理分配和释放
- 存放函数参数值,局部变量值
- iPhone 的栈区只有512K
- 局部变量在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用结束后,变量就被撤销,其所占用的内存也被收回。
- 堆:
- 由程序猿管理
- 存放程序猿创建的对象
- C用malloc/calloc/relloc分配的区域
- 代码区:
- 存放函数的二进制代码
- 全局区(又称静态区):
- 存放全局变量和静态变量
- 程序运行时一直存在
- 由编译器管理(分配释放),程序结束后由系统释放
全局区又分为 BSS段 和 数据段(data)。
BSS段:BSS段(bss segment)通常是指用来存放程序中未初始化的或者初始值为0的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。
数据段:数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
但不同的是C++中,不区分有没有初始化,都放到一块去。
- 文字常量区
- 存放常量字符串
- 为了节省内存,C/C++/OC把常量字符串放到单独的一个内存区域。当几个指针赋值给相同的常量字符串时,它们实际上会指向相同的内存地址。
再回到刚刚的代码上,为什么block结构体中成员变量只多了静态变量呢?
int global_val = 1;// 全局变量 static int static_global_val = 2;// 静态全局变量 static int static_val = 3;// 静态变量 复制代码
关于它们的区别—— 全局变量/静态全局变量/局部变量/静态局部变量的异同点
静态局部变量虽然程序运行时一直存在,但只对定义自己的函数体始终可见。
编译后,调用block实质上是在 一个新定义的函数 中访问静态局部变量,不能直接访问,所以需要保存其指针。而全局变量可以访问到,所以没有加到成员变量中。
方法二:用到__block 说明符
int main() { __block int val = 10; void (^blk)(void) = ^{val = 1;}; return 0; } 复制代码
转换后
// 变量将会变成的结构体 // 即val不是int类型,变成此结构体实例 struct __Block_byref_val_0 { void *__isa;// __block变量转化后所属的类对象 __Block_byref_val_0 *__forwarding;//指向__block变量自身的指针,后面解释 int __flags;// 版本号 int __size;// 结构体大小 int val;// 原本的int数值 }; // 代表Block 的结构体 struct __main_block_impl_0 { struct __block_impl impl;// block通用的成员变量 struct __main_block_desc_0* Desc;// block 的大小 __Block_byref_val_0 *val;// val转成成员变量,类型为结构体 // 构造函数 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->_forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; // 原本的代码块 转到一个C函数 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_val_0 *val = __cself->val; // 这里通过__forwarding赋值?后面解释 (val->__forwarding->val) = 1; } // 当Block从栈复制到堆时 // 通过此函数把截获的__block变量移到堆或者引用数+1 static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { _Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF); } // 当Block从堆被废弃时 // 通过此函数把截获的__block变量引用数-1 // 相当于对象的delloc方法 static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF); } // 计算block大小的结构体 // 该结构体有两个函数 // copy 和 dispose // 声明的同时,初始化一个变量__main_block_desc_0_DATA static struct __main_block_desc_0 { unsigned long reserved; unsigned long 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() { /* val变成了__Block_byref_val_0结构体实例 */ __Block_byref_val_0 val = { 0,// isa指针 &val,//forwarding成员,指向自己 0,// 版本号 sizeof(__Block_byref_val_0), 10 //原来int val的值 }; blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000); return 0; } 复制代码
从main函数中,我们可以发现,Block转换成Block的结构体类型**__main_block_impl_0的自动变量,__block变量val转换为block变量的结构体类型__Block_byref_val_0**的自动变量。它们都在栈上。所以Block的isa指针指向NSConcreteStackBlock。
除了NSConcreteStackBlock,还有两种类型 NSConcreteGlobalBlock 和 NSConcreteMallocBlock。
类 | 设置对象的存储域 |
---|---|
NSConcreteStackBlock | 栈 |
NSConcreteGlobalBlock | 全局区 |
NSConcreteMallocBlock | 堆 |
- NSConcreteGlobalBlock 在全局变量的地方生成的Block为NSConcreteGlobalBlock,如下。在全局变量的地方不能使用自动变量,也就不存在截获的问题。
void (^blk)(void) = ^{printf("Global Block\n");}; int main { return 0; } 复制代码
另外只要没有截获自动变量,Block类型就是NSConcreteGlobalBlock。
- NSConcreteMallocBlock 栈上的Block,在出了作用域后会被摧毁,__block变量也是。那么如果我们要在别的地方调用Block,就需要把它们移到堆中,手动管理它们的生命周期。这种Block类型就是NSConcreteMallocBlock。
先来理解为什么有个forwarding指向自己。
试想,Block如果截获了自动变量,然后移到堆上,在别的作用域调用(很常见)。如果__block变量在栈上已经释放了,Block访问__block变量会失败。所以系统需要在Block变成NSConcreteMallocBlock时,截获的__block变量也复制到堆上。
Block什么时候会复制到堆上呢?
- 调用Block的copy方法
- 将Block作为函数返回值时
- 将Block赋值给__strong修饰的变量时
- 向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时
__block变量的配置存储域 | Block从栈赋值到堆时的影响 |
---|---|
栈 | 从栈复制到堆并被Block持有 |
堆 | 被Block持有 |
当Block从栈复制到堆时,__block变量的 forwarding 会重新指向其在堆中的内存地址。
这样,无论是在Block语法中、Block语法外使用__block变量,还是__block变量配置在栈上或对上,都可以顺利地访问同一个__block变量。
笔者在书上刚看到这句话时,有点晕,后来想了一段时间应该是以下意思,如果有误,欢迎大神批斗。
如下代码,有注释
__block int val = 0; void (^blk)(void) = [^{++val;} copy]; ++val;// 转换为++(val.__forwarding->val);即(栈上的val).__forwarding->val,最终指向堆上的val blk();// 转换为++(val.__forwarding->val);即(堆上的val).__forwarding->val,最终指向堆上的val NSLog(@"%d", val); 复制代码
截获对象
- __strong 修饰的对象
blk_t blk; { id array = [[NSMutablArray alloc] init]; blk = [^(id obj) { [array addObject:obj]; NSLog(@"array count = %ld", [array count]); } copy]; } blk([NSObject alloc] init]); blk([NSObject alloc] init]); blk([NSObject alloc] init]); 复制代码
还记得上面提到的截获变量不修改,转为C++,Block结构体中的成员变量多了截获的自动变量。
这里,变量作用域结束时,理论上array被废弃,但执行输出结果为数组count123。
这意味着array超出作用域而存在。
会不会也是Block结构体中的成员变量多了截获的自动变量呢?
转换为C++后
struct __main_block_impl_0 { struct __block_impl impl;// Block通用的成员变量 struct __main_block_desc_0* Desc;// Block的大小 // 指向数组的成员变量 id __strong array; // 构造函数 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,id __strong _array, int flags=0) : array(_array) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; // 原本的代码块 转到一个C函数 static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) { id __strong array = __cself->array; [array addObject:obj]; NSLog(@"array count = %ld", [array count]); } // 当Block从栈复制到堆时 // 通过此函数把截获的对象引用数+1 // 相当于retain static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) { _Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT); } // 当Block从堆被废弃时 // 通过此函数把截获的对象release引用数-1 // 相当于对象的delloc方法 static void __main_block_dispose_0(struct __main_block_impl_0 *src) { _Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT); } // 计算block大小的结构体 // 该结构体有两个函数 // copy 和 dispose // 声明的同时,初始化一个变量__main_block_desc_0_DATA static struct __main_block_desc_0 { unsigned long reserved; unsigned long 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转换如下。
blk_t blk; { id __strong array = [[NSMutableArray alloc] init]; // 构造函数 blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000); blk = [blk copy]; } // 调用,第一个参数为blk本身,第二个参数为id类型对象 (*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]); (*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]); (*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]); 复制代码
可以看到,和猜测一样,Block结构体中确实多了一个 id __strong array ;
我们知道,我们写的 C语言结构体不能带有__strong修饰符的变量 。原因是编译器不知道何时进行C语言结构体的初始化和废弃操作。
但是 OC运行时库 能 把握 Block从栈复制到堆以及堆上的Block被 废弃的时机 ,因此Block用结构体中可以 管理好 。
那么同时用__block 和 __strong 修饰的对象呢?
上面提到过__block int val,val将变为一个结构体,对象也一样。
__block id obj = [[NSObject alloc] init]; // 相当于__block id __strong obj = [[NSObject alloc] init]; 复制代码
转换为
// 对象将会变成的结构体 struct __Block_byref_obj_0 { void *__isa;// __block变量转化后所属的类对象 __Block_byref_val_0 *__forwarding;//指向对象自身的指针,后面解释 int __flags;// 版本号 int __size;// 结构体大小 void (*__Block_byref_id_object_copy)(void*, void*);// retain对象 void (*__Block_byref_id_object_dispose)(void*);// release对象 __strong id obj;//指向对象 }; static void __Block_byref_id_object_copy_131(void *dst, void *src) { _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131); } static void __Block_byref_id_object_dispose_131(void *src) { _Block_object_dispose(*(void * *) ((char*)src + 40), 131); } // 对象变成了__Block_byref_obj_0结构体实例 __Block_byref_obj_0 obj = { 0, &obj, 0x2000000, sizeof(__Block_byref_obj_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, [[NSObject alloc] init] }; 复制代码
- __weak 修饰的对象
blk_t blk; { id array = [[NSMutableArray alloc] init]; __block id __weak array2 = array; blk =[^(id obj) { [array2 addObject:obj]; NSLog(@"array count = %ld", [array2 count]); } copy]; } blk([[NSObject alloc] init]); blk([[NSObject alloc] init]); blk([[NSObject alloc] init]); 复制代码
输出结果为数组数目0。
这是由于array在作用域结束时被释放、废弃,nil被赋值在array2中。
结论:Block中持有weak声明的对象,对象引用数不会增加。
问题
- Block中是否需要对弱引用的对象强引用?
到底什么时候才需要在ObjC的Block中使用weakSelf/strongSelf
-
Block属性中内存语义用copy 还是strong?
在ARC下,这两种效果都会把Block 从栈上压到堆上。但事实上,copy更接近Block的本质。
block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。你也许会感觉我这种做法有些怪异,不需要写依然写。如果你这样想,其实是你“日用而不知”。
- 在这篇文章iOS-Block本质,看到许多关于Block理解的问题,对照着实现看挺有帮助。
参考
- [1] Kazuki Sakamoto,Tomohiko Furumoto.Objective-C高级编程 iOS与OS X多线程和内存管理[M].北京:人民邮电出版社,2013:79-136.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
零基础学Java Web开发
刘聪 编 / 机械工业出版社 / 2008-1 / 59.00元
《零基础学Java Web开发:JSP+Servlet+Sfruts+Spring+Hibernte》全面讲解Java Web应用开发的编程技术,并详细介绍Java Web开发中各种常用的技术,可作为Java Web开发技术的学习指南。 《零基础学Java Web开发:JSP+Servlet+Sfruts+Spring+Hibernte》共17章,分为3篇,其中第1~12章是基础篇,讲解了......一起来看看 《零基础学Java Web开发》 这本书的介绍吧!
CSS 压缩/解压工具
在线压缩/解压 CSS 代码
随机密码生成器
多种字符组合密码