block的实质

1.什么是block

block是将函数及其执行上下文封装起来的对象,是一段代码块,是一个结构体,里面有isa指针指向自己的类(global malloc stack),有desc结构体描述block的信息,__forwarding指向自己或堆上自己的地址,如果block对象截获变量,这些变量也会出现在block结构体中。最重要的block结构体有一个函数指针,指向block代码块。block结构体的构造函数的参数,包括函数指针,描述block的结构体,自动截获的变量(全局变量不用截获),引用到的__block变量。(__block对象也会转变成结构体)

block代码块在编译的时候会生成一个函数,函数第一个参数是前面说到的block对象结构体指针。执行block,相当于执行block里面__forwarding里面的函数指针。

2.什么是block调用

block调用即是函数的调用

3.__block修饰符

一般情况下,对被截获变量进行赋值操作需添加__block修饰符
__block不能修饰全局变量、静态变量(static)

{
   NSMutableArray *array = nil;
   void(^Block)(void) = ^{
           array = [NSMutableArray array];
   }
   Block();
}
是否存在问题?
需要在array声明处添加__block修饰符
__block int multiplier = 6;
    int(^Block)(int) = ^int(int num)
    {
        return num * multiplier;
    };
    multiplier = 4;
    NSLog(@"result is %d", Block(2));

   结果为8
__block修饰的变量变成了对象
  • _ _block 这个修饰符做了什么操作呢?就是让block内部可以访问自动变量
    __weak将int类型的数据转换成了一个__Block_byref_i_0的结构体类型
struct __Block_byref_i_0 {
  void *__isa;
__Block_byref_i_0 *__forwarding;
 int __flags;
 int __size;
 int i;
};

从赋值上看,isa为0,既然有isa指针,那么说明这个结构体也是一个对象,__forwarding存储的是__Block_byref_i_0的地址值,flags为0,size为Block_byref_i_0的内存大小,i是真正存储变量值的地方,是通过__Block_byref_i_0结构体的指针__forwarding读取和修改的变量i.

  • 为什么要通过__forwarding转一下呢,而不是直接读取i
    这是因为当我们调用block的时候,block可能存在于栈中可能存在于堆中


__block修饰后的底层实现:

1.__block将int i进行包装,包装成一个__Block_byref_i_0结构体对象,结构体中的i是存储i的int值的;
2.当我们在block内修改或访问该对象时,是通过该对象的__forwarding去找对应的结构体再找对应的属性值,这是因为__forwarding在不同情况下指向不同的地址,防止只根据单一的一个内存地址出现变量提前释放无法访问的情况。
那么我们就明白为什么可以修改__block修饰的自动变量了,__block修饰下的i不再是int类型而变成一个对象(对象p),我们block内部访问和修改的是这个对象内部的一个属性,并不是这个对象,所以是可以修改访问的。只不过这个转化为对象的内部过程封装起来不让开发者看到,所以就给人的感觉是可以修改auto变量也就是修改时是int i。

4.block的内存管理

//全局block
_NSConcreteGlobalBlock
//栈block
_NSConcreteStackBlock
//堆block
_NSConcreteMallocBlock
说明.jpeg



  • 为什么捕获局部变量而不捕获全局变量?
    全局变量:整个项目都可以访问,block调用的时候可以直接拿到访问,不用担心变量被释放的情况;
    局部变量:则不同,局部变量是有作用域的,如果blcok调用的时候blcok已经被释放了,就会出现严重的问题,所以为了避免这个问题block需要捕获需要的局部变量。(比如我们局部变量和block都卸载了viewDidLoad方法,但是我在touchesBegan方法中调用block,这个时候局部变量早就释放了,所以block要捕获局部变量)

  • 为什么auto变量是捕获的值,而静态变量是捕获的地址呢?
    自动变量和静态变量存储的区域不同,两者释放时间也不同。
    自动变量:存放在栈中的,创建与释放是由系统设置的,随时可能释放掉。
    静态变量:存储在全局存储区的,生命周期和app是一样的,不会被销毁。
    所以对于随时销毁的自动变量肯定是把值拿进来保存了,如果保存自动变量的地址,那么等自动变量释放后我们根据地址去寻值肯定会发生怀内存访问的情况,而静态变量因为项目运行中永远不会被释放,所以保存它的地址值就完全可以了,等需要用的时候直接根据地址去寻值,就能找到。

  • 为什么静态变量和全局变量同样不会被销毁,为什么一个被捕获地址一个则不会被捕获呢?
    静态变量和全局变量因为两者访问方式不同造成的
    全局变量:整个项目都可以拿来访问,所以某个全局变量在全局而言是唯一的(也就是全局变量不能出现同名的情况,即使类型不同也不行,否则系统不知道你具体访问的是哪一个)
    静态变量:则不是,全局存储区可能存储着若干个名为type的静态变量。
    所以这就导致了访问方式的不同,比如说有个block,内部有一个静态变量和一个全局变量,那么在调用的时候系统可以直接根据全局变量名去全局存储区查找就可以找到,名称是惟一的,所以不用捕获任何信息即可访问。而静态变量而不行,全局存储区可能存储着若干个名为type的静态变量,所以blcok只能根据内存地址去区分调用自己需要的那个

  • block的copy操作


  • 栈上的block copy之后,MRC环境下是否会引起内存泄漏?
    是的,copy操作之后,堆上的block没有额外的成员变量指向它,正如我们和alloc对象后,没有进行relese,造成内存泄漏

5.block的底层结构

通过clang命令将oc代码转换成c++代码(如果遇到_weak的报错是因为_weak是个运行时函数,所以我们需要在clang命令中指定运行时系统版本才能编译):

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main.cpp
-(void)viewDidLoad{
    [super viewDidLoad];
    int i = 1;
    void(^block)(void) = ^{
        NSLog(@"%d",i);
    };
    block();
}

转换成c++代码如下:

//block的真实结构体
struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  int i;
    //构造函数(相当于OC中的init方法 进行初始化操作) i(_i):将_i的值赋给i flags有默认值,可忽略
  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int _i, int flags=0) : i(_i) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//封存block代码的函数
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
  int i = __cself->i; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_3g_7t9fzjm91xxgdq_ysxxghy_80000gn_T_ViewController_c252e7_mi_0,i);
    }

//计算block需要多大的内存
static struct __ViewController__viewDidLoad_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)};

//viewDidLoad方法
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
    //定义的局部变量i
    int i = 1;
    //定义的blcok底部实现
    void(*block)(void) = &__ViewController__viewDidLoad_block_impl_0(
                                            __ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, i));
    //block的调用
    bloc->FuncPtr(block);
}

可看出,定义的block实际上就是一直指向结构体_ViewController_viewDidLoad_block_impl_0的指针(将一个_ViewController_viewDidLoad_block_impl_0结构体的地址赋值给了block变量)。

_ViewController_viewDidLoad_block_impl_0包含以下几个部分:

  • impl
  • Desc : 存储两个参数reserved和Block_size,并且reserved赋值为0而Block_size则存储着__ViewController__viewDidLoad_block_impl_0的占用空间大小。最终将desc结构体的地址传入__ViewController__viewDidLoad_block_impl_0中赋值给Desc。所以Desc的作用是记录Block结构体的内存大小。
  • 引用的局部变量
  • 构造方法

其中impl包含:

  • isa指针,存放结构体的内存地址,存储着&_NSConcreteStackBlock地址,可以暂时理解为其类对象地址,block就是_NSConcreteStackBlock类型的
  • Flags:这个用不到 有默认值
  • FuncPtr:block代码块地址,存储着viewDidLoad_block_func_0函数的地址,也就是block代码块的地址。所以当调用block的时候,bloc->FuncPtr(block);是直接调用的FuncPtr方法。


    简化图.png

6.循环引用问题

循环引用也是block中一个常见的问题,什么是循环引用呢?
从block捕获对象变量的过程中可看出,block在堆中的时候会根据变量自己的修饰符来进行强引用或者弱引用,假设block对person对象进行强引用,而person如果对block也进行强引用的话,那就形成了循环引用,person对象和block都有强指针指引着,使它们得不到释放。
解决方法:
__weak和__unsafe_unretained
相同点:表示的是对象的一种弱引用关系
不同点:__weak修饰的对象被释放后,指向对象的指针会置空,也就是指向nil,不会产生野指针
__unsafe_unretained修饰的对象被释放后,指针不会置空,而是变成一个野指针,那么此时如果访问这个对象的话,程序就会Crash,抛出BAD_ACCESS的异常。

block可以给NSMutableArray中添加元素吗,需不需要添加__block?

不需要,因为在block块中仅仅是使用了array的内存地址,往内存地址中添加内容,并没有修改arry的内存地址,因此array不需要使用__block修饰也可以正确编译。

blcok为什么能回调声明时的代码块呢?

因为oc调用”block()” 实际就是这句block->FuncPtr(block);,因为blcok->FuncPtr保存的就是__main_block_func_0函数。
总结block声明的时候保存了__main_block_impl_0地址,而__main_block_impl_0则保存了函数体,block的类型,和blcok的结构体大小,最后block回调的时候block->FuncPtr(block)就是调用了__main_block_impl_0中保存的函数__main_block_func_0.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容