深入理解oc中的block

苹果在Mac OS X10.6 和iOS 4之后引入了block语法。这一举动对于许多OC使用者的编码风格改变很大。就我本人而言,感觉block用起来还是很爽的,但一直以来,都是知其然,而不知所以然。这篇文章一共有两篇,其中基础篇讲解了block的基本的使用和创建,以及一些注意事项。在深入篇中,我将会对block的一些原理陈述出来,探讨block的内部。

深入篇


在这篇文章中,我主要记录一些block的原理性的知识。

  • 为什么说block是一个结构体,也是一个对象,同时还是携带数据的匿名函数
  • 全局block ,栈block以及堆区block的区别和他们之间的联系,探究block的内存管理
  • 为什么使用__block 就可以使得block可以修改外部变量
  • 引起强引用循环的原因是什么,我们解决它的方法和原理又是什么

一、为什么说block是一个结构体,也是一个对象,同时还是携带数据的匿名函数

使用Mac的终端,创建一个.m文件:

vim block.m    

在文件中写下以下内容:

#include<stdio.h>
int main(){
void (^block)(void) = ^{
printf("a block");};
block();
}

查看clang中间文件:

clang -rewrite-objc block.m

查看生成的.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;}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {printf("a block");}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(){
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

到这里,我们可以看到,在编译之前,block复原成以上四个结构体。 它们分别是:

__main_block_impl_0        
__block_impl               

__main_block_desc_0        
__main_block_func_0

我们暂时不管这些结构体代表的都是什么。至少知道一点是这里很好的表明了,block是结构的这个事实。仔细观察 __main_block_impl_0 的机构中,有isa指针一项(黄色标出)。看到这里理解为什么苹果强调block也是一个对象了。再看还有 __main_block_func_0 ,这里其实就是我们对block函数题的实现,它实际上是一个匿名函数,作为block的众多结构体的一部分。

所以说,block的实际含义并不像很多人所说的那么单纯,而是做一种包含了匿名函数的特殊结构体,同时它还是OC中的对象!


二、全局block ,栈block以及堆区block的区别和他们之间的联系,探究block的内存管理

在OC中,实际上有三种不同的block类型。它们分别是 全局block _NSConcreteGlobalBlock  ,栈block _NSConcreteStackBlock,以及堆block _NSConcreteMallocBlock。

(1)三者如何区分

全局block

假如我创建一个下面这样block

int GlobalInt = 0;
int (^getGlobalInt)(void) = ^{return GlobalInt;
};

通过clang 命令获得中间编译内容

int GlobalInt = 0;struct __getGlobalInt_block_impl_0 {struct __block_impl impl;struct __getGlobalInt_block_desc_0* Desc;__getGlobalInt_block_impl_0(void *fp, struct __getGlobalInt_block_desc_0 *desc, int flags=0) {impl.isa = &_NSConcreteGlobalBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;}
};
static int __getGlobalInt_block_func_0(struct __getGlobalInt_block_impl_0 *__cself) {return GlobalInt; }static struct __getGlobalInt_block_desc_0 {size_t reserved;size_t Block_size;
} __getGlobalInt_block_desc_0_DATA = { 0, sizeof(struct __getGlobalInt_block_impl_0)};
static __getGlobalInt_block_impl_0 __global_getGlobalInt_block_impl_0((void *)__getGlobalInt_block_func_0, &__getGlobalInt_block_desc_0_DATA);
int (*getGlobalInt)(void) = ((int (*)())&__global_getGlobalInt_block_impl_0);

蓝色阴影自体中,_NSConcreteGlobalBlock 代表了这是一个全局的block。 全局block和全局变量一样,可以在整个数据域中使用。 这里的其他的任何retain、copy对它是没有影响的。它存储在静态区域,基本可以理解,在APP运行期间,它是一直存在的。 但是普通情况,我们不会经常用到这种类型的Block,因为大部分情况下我们使用block的时候都会携带上下文的环境,便于随时捕获环境的中的变量,事实上,作为全局block,它的具有其余全局变量一样的特性---可以在随处访问。然而全局block不接受所有的 retain等操作,它作为的对像属性的特点不能全部发挥出来,跟定义一个方法没有区别。

还有一种方式,像下面这样。这种方式就比较直接。我们基本可以很明显的看到,这个block是作为一个全局变量的形式被创建出来的。 还有一种更加隐秘的方式,像下面这样。

   void (^block)(void) = ^{printf("a block");};block();NSLog(@"%@",block);

打印:

同样是一个全局的block。

所以全局block的生成有两种情况,一是直接将block创建成一个全局变量。这是苹果官方文档中的做法。另一种是,创建一个局部变量的block,在block的函数体内,不使用任何外部的局部变量。但是这个block和作为全局变量的block是有所不同的。   有何不同,我们将这个block的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;}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {printf("a block");}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(){
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

看红色字体,竟然是 _NSConcreteStackBlock! 也即是我们所说的栈block! 这是为什么呢? 我个人的理解是,对于CPP文件中的指示,它是告诉用户这个结构体的存储的位置。 而在nslog打印中显示不一样是因为因为没有包含局部变量,所以block本身不需要携带上下文环境,系统在编译的时候,默认Block是全局环境。 这才导致两种展示的方式不一样。

栈block

刚才说了,全局block的其中创建方式是作为一个不包含外部变量的局部变量block。 那如果这个block变量包含了外部变量那又会怎样呢。没错,当包含了外部变量的时候,它是一个栈Blobk。

在基础篇中,介绍了三种创建block的方式。其中在将block当成一个变量被创建的时候,它就是栈block。在上文中block.m 中,代码:

    int a = 10;void (^block)(void) = ^{printf("%d",a);};block();NSLog(@"%@",block);

这段代码中,我分别在MRC和ARC下打印:

MRC:

MRC中,是栈block。

ARC:

ARC下是堆0block。

clang查看代码生成的中间文件。 发现也是存在于栈中的。

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

疑惑点:为什么ARC和MRC中的打印结果不一样呢?

正常情况下,block的申明都是在栈中的,如果需要将之转移到堆中,需要行进 block_copy或者其他发送copy的消息,比如Usingblock等。如果在MRC没有进行copy的话,那么当处于栈中的block的环境被销毁的时候,block也等同被销毁了,后续的调用将出现Crash。 而在ARC中 ,因为系统会自动对block发送copy消息,所以我们打印的时候看到block是mallco类型,即位于堆上的。在没有进行copy 之前,栈上的Block使用retain 等操作都是没有实际作用的。

如果在MRC中,我们将上面的代码更改如下,添加一条copy消息。

    int a = 10;void (^block)(void) = ^{printf("%d",a);};[block copy];      //发送一条copy的消息NSLog(@"%@",block);

打印结果:

堆上的block  

刚才我们发现,block的申明的时候都是在栈上的,如果发送了copy消息,那么block才会被复制到堆上。

当复制到堆上之后,我们使用block就可以像使用普通的属性一样,可以进行retain等。注意,重复发送copy 消息,也只会在堆上保留一份blcok。在block所在的栈中的内容没有被销毁之前,这个栈中的block还依然存在的。但是它多了一条跟堆中block的联系。我们回过头看他的一个结构体:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {printf("a block");}

在这个block的结构体中,结构体本身的类型是__main_block_func_0 。 内部有一个参数指针指向了一个__main_block_impl_0,cself 结构体,这个指针实际上指向的是自己,如果block接受了copy 消息之后,那么这个指针将指向堆上的那份block,而堆上的那份的block的cself 还是指向堆上的blobk结构体,这就是为什么在复制到堆上之后,当栈上的内容被销毁时,block调用不会crash的原因了。


三、为什么使用__block 就可以使得block可以修改外部变量

在在讨论开始我们先看看当block堆外部变量使用__block修饰局部变量前后的clang吧。

使用之前

#include<stdio.h>
int main(){    int a = 10;void (^block)(void) = ^{printf("%d",a);};block();
}

clang:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {int a = __cself->a; // bound by copy
printf("%d",a);}

使用之后:

#include<stdio.h>
int main(){__block int a = 10;void (^block)(void) = ^{printf("%d",a);};block();
}

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {__Block_byref_a_0 *a = __cself->a; // bound by ref
printf("%d",(a->__forwarding->a));}

这里我们看到了区别。 在调用block的匿名函数的时候,如果没有使用__block是修饰的话,那么函数的参数传递方式时 将值传递,也即block内部指示获取到了变量的值而已,并没有直接获取到变量本身,那么自然也就不可能对变量进行更改了。而在使用__block修饰之后,我们发现是将变量的地址传进去了,这样在block函数体内的修改实际上就是对变量的修改。


四、引起强引用循环的原因是什么,我们解决它的方法和原理又是什么

首先看一个强引用循环的典型例子。

@interface ViewController ()@property(nonatomic,assign) NSInteger intProp;
/* 将block作为一个属性申明 */
@property(nonatomic,strong) TestBlock testBlock;@end
@implementation ViewController
- (void)viewDidLoad {[super viewDidLoad];self.testBlock = ^{[self presentViewController:[[TestViewC alloc] init] animated:true completion:^{}];};
}@end

ARC 模式下会报错:

原因分析: ARC模式下,我们的常听见引用计数,一个对象,当被某个其他的对象持有的时候,会使它的饮用计数+1,如果计数为零的话,那么这个对象就会被自动销毁。这就使ARC的执行方式。保证可    以自动释放不需要的对象。

上面的代码中,block作为slef的一个属性,表明,self时持有block的,相当说:如果需要释放blcok,那么至少要做的一件事情先把self对它的持有解放,而block是self的属性,要想让slef不再持有它,只有一中情况下:self已经被释放了。      同时,我们看到block内部调用了slef,ARC会直接复制一份block到堆上去,这样,等于block会复制一份self,也即是持有了self,那么这样的情况下,如果要释放self,至少要做的一件事情是让block不再持有self,显然,上面的这种情况,要不持有slef,只能等待block被销毁。  这时候,block和slef相互等待着对方先被释放,才能释放自己,一直矛盾着两个对象都得不到释放。

好比下面的场景。

所以,所谓的强引用循环久对象得不到释放。 我们解决的办法很简单,只需要将参与引用循环的某个对象的引用设置为不持有不久解决了嘛!

有几种其他的方式可以做到。

1.OC提供了__weak修饰符,替代了这一功能。一般来说,我们会重新申请一个 weak slef 对象来参与block的copy使用。就像下面这样:

    __weak ViewController *weakSelf = self;    //__weak __typeof(&*self)weakSelf =self;self.testBlock = ^{[weakSelf presentViewController:[[TestViewC alloc] init] animated:true completion:^{}];};

这样警报就消失了。 很完美的感觉。

2.还看到过一篇文章,意思说当使用 __weak修饰之后,如果外部的对象为空了,那么在block的内部对象也将为空,这样有时候并不是我们想要的。 AFNetWorking中使用了一个办法解决这一问题。就是在block内部继续使用__Strong来修饰带进来的weakself, 如下:

    __weak ViewController *weakSelf = self;self.testBlock = ^{__strong __typeof(&*weakSelf) strongSelf = weakSelf;[strongSelf presentViewController:[[TestViewC alloc] init] animated:true completion:^{}];};

这样做的好处是,避免了强引用循环,同时保证了对象在block中的持续存在,而不会因为block外的变量因为被释放到值block的内的变量也变空了。经过尝试,确实是没问题的。

3.也有说使用__block 来避免强引用循环。 让我有点懵逼。 不过看到一篇文章对这个做了一个分析。大概意思是,使用block修饰之后,在block内部可以访问修改外部对象,而在block使用结束的时候,通过这一功能,将外部对象销毁。 避免了强引用循环,但是这样的做法有很多隐患。  我自己并没有尝试这个方法,也就不说太多了。

 参考:

http://honglu.me/2015/01/06/weak与block区别/

block 中强引用与弱引用

http://blog.csdn.net/abc649395594/article/details/47086751

转载于:https://www.cnblogs.com/FBiOSBlog/p/6667435.html

iOS开发 - OC - block的详解 - 深入篇相关推荐

  1. iOS 开发之照片框架详解

    一. 概要 在 iOS 设备中,照片和视频是相当重要的一部分.最近刚好在制作一个自定义的 iOS 图片选择器,顺便整理一下 iOS 中对照片框架的使用方法.在 iOS 8 出现之前,开发者只能使用 A ...

  2. IOS开发学习笔记-----UILabel 详解

    IOS开发学习笔记-----UILabel 详解 01 //创建uilabel 02 UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectMa ...

  3. iOS开发 - OC - block的详解 - 基础篇

    深入理解oc中的block 苹果在Mac OS X10.6 和iOS 4之后引入了block语法.这一举动对于许多OC使用者的编码风格改变很大.就我本人而言,感觉block用起来还是很爽的,但一直以来 ...

  4. IOS开发中单例模式使用详解

    第一.基本概念 单例模式是一种常用的软件设计模式.在它的核心结构中只包含一个被称为单例类的特殊类.通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问. 第二.在IOS中使用单例模式的情 ...

  5. ios开发---URL Schemes 使用详解-app协议

    用原生 iOS 的人分两种,懂 URL Schemes 的和不懂的. 前者是「魔法师」,后者是「麻瓜」. URL Schemes 应用在 iOS 上已经很久了.对于使用者来说,在沙盒机制下的 iOS ...

  6. 【iOS开发必收藏】详解iOS应用程序内使用IAP/StoreKit付费、沙盒(SandBox)测试、创建测试账号流程!2012-6-25日更新iap恢复

    转载自:http://www.himigame.com/iphone-cocos2d/550.html 本站文章均为 李华明Himi 原创,转载务必在明显处注明: 转载自[黑米GameDev街区] 原 ...

  7. 【iOS开发必收藏】详解iOS应用程序内使用IAP/StoreKit付费、沙盒(SandBox)测试、创建测试账号流程!...

    为什么80%的码农都做不了架构师?>>>    Himi  原创, 欢迎转载,转载请在明显处注明! 谢谢. 原文地址:http://blog.csdn.net/xiaominghim ...

  8. iOS开发——frame和bounds详解

    在iOS的UI开发中,frame和bounds是两个非常容易搞混的概念,而很多开发者在实际项目中也很少去区分,因此会导致出现一些意想不到的问题.本篇博客以实际代码的方式来学习frame和bounds的 ...

  9. iOS开发——深拷贝与浅拷贝详解

    深拷贝和浅拷贝这个问题在面试中常常被问到,而在实际开发中,只要稍有不慎,就会在这里出现问题.尤其对于初学者来说,我们有必要来好好研究下这个概念.我会以实际代码来演示,相关示例代码上传至 这里 . 首先 ...

最新文章

  1. Redis初学:8(Hash类型)
  2. 【c语言】蓝桥杯算法提高 3-3求圆面积表面积体积
  3. 集合 判断是否为同一元素 总结
  4. 编译安装 redis 2.2.14
  5. 标准C++的类型转换符:static_cast、dynamic_cast、reinterpret_cast和const_cast(转载)
  6. JAVA socket编程实例
  7. SQL SERVER中的纵横查询
  8. Android MVP Presenter 中引发的空指针异常
  9. BZOJ4196 NOI2015 软件包管理器
  10. obs源码简析之推流
  11. 华为ensp模拟校园网/企业网实例(附完整设备配置命令和ensp项目)
  12. [因果推断] 什么是因果推断(一)
  13. 沃顿商学院自我管理课——完美融合
  14. 2019级第二次月赛暨软件计科联合新生赛题解
  15. 1228 序列求和 (伯努利数)
  16. 宏碁(Acer)蜂鸟Fun 2020新款 14英寸轻薄商务笔记本使用真实评测
  17. 优秀的计算机简历,计算机优秀简历范文
  18. poj求排列的逆序数
  19. 驱动加载错误:insmod: error inserting 'hello.ko': -1 Device or resource busy
  20. Stata:如何正确检验U型关系的存在

热门文章

  1. 继续昨日计划: 2022-2-16
  2. Kernel i2c gpio spi pinctrl platform 分析讲解 (未完待续)
  3. matlab nargout
  4. “Could not import PIL.Image. The use of array_to_img requires PIL.”错误的解决办法
  5. 分享五个你应该了解的宣言
  6. 轻松搞定Retrofit不同网络请求方式的请求参数配置,及常用注解使用
  7. centos7安装redis3.2.5
  8. Apache Commons Math3学习笔记(2) - 多项式曲线拟合(转)
  9. junit4X系列--Builder、Request与JUnitCore
  10. 老话题,不要在遍历容器中增删容器数据