转载

iOS开发之block终极篇

前言

一星一度的周末又走了,这周加班iOSTalk来晚了。有人会说Swift都已经横空出世了,Objective-C在排行榜都已经在持续下跌了,未来Swift将取代Objective-C成为苹果开发的主流语言,那么深入研究Obiective-C的意义何在?那要是这么说。人终有一死,那你今天还浪费那粮食干啥呢?

从自认为是持有长远眼光的人来看不再需要深入了解Objective-C是明智的选择。然而在我看来,现在深入了解OC还是很有意义的。就如Block来说吧,Block的源码转化之后是C语言结构体,进一步了解C你敢说它啥时候会没用吗?还有就是新兴语言Swift,打开Swift SDK头文件,结构体类型随处可见,连基本的数据类型Int都是源于结构体,所以说研究研究还是有点用处滴!好了,自我安慰完毕,开始枯燥的OC代码之旅。

有人也肯定会觉得微信毕竟是信手拈来的浅阅读,你写一大串的纯理论会有人耐心的看么,其实我也知道不会,我自己有的时候看到短短进度条的文章,也失去阅读的兴趣,虽然说知道作者写这么多也着实不易。所以我想到了一个办法提升文章被阅读的可能性,具体措施在文末揭晓。

前面两篇写了block的基本使用以及基本的Block的实现源码。这篇将揭晓Block的最后一幕,内容是block的几个使用技巧以及其底层源码解析,里面包含对和栈的知识,如果你还不了解你可以通过历史记录查看我之前写的一篇「内存管理之引用计数」。

__block说明符

前面讲到Block会捕获外部变量,但是当你试图在Block里面修改捕获的外部变量时。就是出现编译错误,解决的一种办法是将外部变量使用 __block 修饰符修饰。下面是添加 __block 修饰的外部变量代码:

#include <stdio.h>  int main(int argc, const char * argv[]) {      __block int val = 10;     void (^blk)(void) = ^{ val = 1; };      return 0; } 

该代码可进行编译。变换后如下:

struct __Block_byref_val_0 {   void *__isa; __Block_byref_val_0 *__forwarding;  int __flags;  int __size;  int val; }; // [1] struct __main_block_impl_0 {   struct __block_impl impl;   struct __main_block_desc_0* Desc;   __Block_byref_val_0 *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;   } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself)  {    __Block_byref_val_0 *val = __cself->val; // bound by ref   (val->__forwarding->val) = 1;  } // [2] 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, 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[])  {  __Block_byref_val_0 val = {     (void*)0,   (__Block_byref_val_0 *)&val,    0,     sizeof(__Block_byref_val_0),     10  };  blk = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344);  return 0; }  

我们可以发现加上 __block 说明符,源码量就急剧的增加了(已做简化)。

  • 我们可以发现含有 __block 修饰的变量变成了 __Block_byref_val_0 结构体。[1]
  • __Block_byref_val_0 结构体实例的成员变量__forwarding持有该实例自身的指针。
  • Block转化而来的的 __main_block_func_0 结构体实例持有指向 __block 变量的 __Block_byref_val_0 结构体实例的指针。 __Block_byref_val_0 结构体实例的成员变量 __forwarding 持有指向该实例自身的指针。通过成员变量 __forwarding 访问成员变量val的地址,从而可以修改自动变量。
  • 我们需要负责 Block_byref_i_0 结构体相关的内存管理,所以 main_block_desc_0 中增加了 copydispose 函数指针,对于在调用前后修改相应变量的引用计数。

Block存储域

__block 变量转换成了 __block 变量的结构体类型的自动变量。所谓结构体类型的自动变量,即栈上生成的该结构体的实例。如下表所示:

名称 实质
Block 栈上Block的结构体实例
__block变量 栈上__block变量的结构体实例

前面我们看到Block的类型说明_NSConcreteStackBlock.虽然该类没有出现已变换源代码中,但还有与之相识的类,如:

  • _NSConcreteStackBlock
  • _NSConcreteGlobalBlock
  • _NSConcreteMallocBlock

他们分别对应的存储区域如下所示:

设置对象的存储区域
_NSConcreteStackBlock
_NSConcreteGlobalBlock 程序的数据区域(.data区域)
_NSConcreteMallocBlock

定义Block时期内存区域分配在栈中,其Block类型为 __NSConcreteStackBlock 类对象.

那么 _NSConcreteMallocBlock 类型的对象由何而来?这里肯定有人还在疑惑为什么 __block 变量转化而来的结构体为生成指向自身的 __forwarding 指针变量。其目的是为了能然超出范围的Block也有效。有人会说设置个全局的Block不就可以搞定了么。

行不行先来段代码看看:

void (^blk)(); if (/* some condition */) {     blk = ^{ NSLog(@"Block A"); }; }else {     blk = ^{ NSLog(@"Block B"); }; } blk(); 

看这段代码我声明了全局的Block变量blk,然后在if语句中定义。如果你不理解block那么就很容易写出这样的代码,其实这段代码是很危险的。因为全局的blk变量是分配在栈上的。在if和else语句中定义的blk内容,编译器会给每个块分配好栈内存,然后等离开了相应的范围之后,编译器有可能把分配给块的内存覆写了。如果编译器未覆写这块栈内存则程序照常运行,如果这块内容被覆写那么程序就会崩溃。

解决上面问题的方法就是使用 copy 方法,将block拷贝到堆中。拷贝完之后就是接下来要将的 _NSConcreteMallocBlock 类型。该类型是带有引用计数的对象,如果在ARC下,只要引用计数不为0,可以随意的访问,后继的内存管理就交给编译器来完成了。

还有一种类型是 _NSConcreteGlobalBlock 类型,这类Block不会捕捉任何状态的外部变量。块所使用的整个内存区域,在编译器已经完全的确定了,不需要每次调用时在栈中创建,如下就是一个全局快:

void (^blk)() = {  NSLog("This is a global block"); } void main() { }  

__block变量存储域

如果Block配置在栈中,则在Block中使用的 __block 变量也分配在栈中。当Block被复制到堆中时, __block 变量也一并被复制在堆中,并被Block所持有。如果非配在堆中的Block被废弃,那么它所使用的 __block 变量也就被释放了。下面来看堆和栈上 __block 混用的例子

__block int val = 0;  void (^blk) (void) = [^{val++;} copy];  ++val;  blk();  NSLog(@"val:%d",val); 

执行结果为:

val: 2 

在Block中和在Block外修改 __block 变量完全等效,它是怎么实现的呢?

是因为执行 copy 方法之后,Block被复制到堆中,其内部捕获的 __block 变量也一并被复制。而此时分配在栈上的val任然存在的,栈上的 __block 变量val会将原本指向自身的 __forwarding 指针指向复制到堆中的 __block 变量val的地址。这样堆中的 __block 变量被修改之后就等同于栈上的block被修改。

通过该功能,无论是在Block的语法中、Block语法外使用 __block 变量,还是 __block 变量配置在栈上还是堆上,都可以顺利地访问一个 __block 变量。

截获对象

先来看一段Blcok截获可变数组对象的例子:

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

执行该段代码的结果为

arrayCount = 1; arrayCount = 2; arrayCount = 3; 

从表面上看是没什么问题,运行的结果也是正确的。而实际上如果我们大量的调用block向可变数组中添加对象元素程序会强制结束。原因是block截获的NSMutableArray对象是分配在栈上的,随着当可变数组元素增加到一定程度会造成栈溢出。

解决方法是调用 copy 方法,形式如下:

blk = [^(id obj){    [array addObject:obj];     NSLog(@"arrayCount = %lu",(unsigned long)array.count); } copy]; 

Block循环引用

在实际项目中对于Block最常见的问题应该是循环引用。如果Block中使用了 __strong 修饰符的对象,那么当block从栈复制到堆时,该对象为Block所持有。这样容易造成循环引用,比较明显的我想大家肯定遇到过,我们来看一个比较隐蔽的,源代码如下:

typedef void (^blk_t)(void); @interface MyObject : NSObject @property (nonatomic, strong) id obj; @property (nonatomic, strong) blk_t blk; @end @implementation MyObject - (id)init {  self = [super init];  _blk = ^{NSLog(@"obj = %@",_obj);};  return self; } @end  

通过编译器我们可以看到造成了循环引用,即Block语法内部使用了 _obj 变量,是因为 _obj 变量实际上截获了self。对编译器来说, _obj 变量只不过是对象的成员变量罢了。

解决的方法便是便是通过 __weak 修饰符来修饰会被Block捕获的变量:

id __weak obj = _obj;  _blk = ^{NSLog(@"obj = %@",obj);}; 

还有一种定义设置weak变量的方式,可以用于宏定义。代码如下:

#define WS(weakSelf)  __weak __typeof(&*self)weakSelf = self; 

调用 WS(ws) 之后 ws 就变成为了 __weak 修饰符修饰的 self 了。

如果你觉得我写的东西对你有点价值的话,希望你能为我增加一个关注量。我的公众号「iOSTalk」。

对于微信的长文不适合作为浅阅读,以及个人公众号受微信团队“歧视”的问题。我也想过搭建自己的技术博客,之前使用零碎的时间折腾Octopress一个星期才让自己网址可以访问。兴奋之余就没有继续折腾页面的部署了。后来有幸获成了《程序员头条》的管理员。编辑文章之余,在上面发表了我的第一遍文章「iOS面试题集锦」为我带来了不少关注量,非常感谢iOS开发: iOSDevTip 博主后继我还会将我觉得不易浅阅读的好文发表到《程序员头条》。欢迎大伙关注。点击「阅读原文」直达《程序员头条》

本文由程序员头条管理员蒋小飞原创文章,转载务必注明出处。

蒋小飞微信公众号iOSTalk: JoneTalk

我叫小飞,iOS开发者,现就职于智能家居领域,坐标浙江杭州.iOSTalk用于记录iOS开发技巧,以及发表个人成长之路的一些观点.

iOS开发之block终极篇

正文到此结束
Loading...