block

要了解什么是block, 我们先写一个block

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^myBlock)(void) = ^{
            NSLog(@"this is a block");
        };
        myBlock();
    }
    return 0;
}

现在我写了一个简单的block
利用


xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

命令行生成编译完的C++代码, 发现block被编译后的样子:
这是block的声明:

void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

这是block的调用:

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

去掉一些无用的类型转换:

void(*block)(void) = &__main_block_impl_0(
    __main_block_func_0, 
    &__main_block_desc_0_DATA
);

我们发现block实际上就是一个__main_block_impl_0函数的返回值的地址, 将地址赋值给一个名叫block的函数指针
那么__main_block_impl_0是什么呢?

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;
  }
};

我们发现__main_block_impl_0是一个结构体,

要注意的是, 还有一个__main_block_impl_0的同名函数, 这是C++里定义的一个结构体的构造函数, 也就是说, 这个构造函数返回的是一个结构体struct __main_block_impl_0, 我们在上面看到的这个就是利用这个构造函数产生的一个struct __main_block_impl_0结构体
void(*block)(void) = &__main_block_impl_0(
    __main_block_func_0, 
    &__main_block_desc_0_DATA
);
  • 第一个参数__main_block_func_0,

    image.png

    通过这个NSLog就可以看出, 这是block这里面的代码实现.

  • 第二个参数&__main_block_desc_0_DATA
    这相当于是一个block的描述, 也是一个block,
    第一个成员变量是保留字段, 现在传的是0
    第二个成员变量是Block_size, 就是block的大小. 传的就是struct __main_block_impl_0的大小(size of)

    image.png

参数传进去了之后就是给这个结构体赋值, 让我们再来看看这个结构体

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;
  }
};

第一个成员变量impl, 类型是struct __block_impl是这样的:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

相当于block的内存布局是这样的:

struct __main_block_impl_0 {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
  struct __main_block_desc_0* Desc;
};
我们发现block也是有isa指针的, 所以从本质上说, block也是OC对象
image.png

__main_block_func_0就是函数指针, 当做参数传进去构造函数, 然后在构造函数里传给结构体里的变量FuncPtr

那我们来看一下block的调用:


((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

去掉一些类型转换

myBlock->FuncPtr(myBlock)
image.png

实际上就是找到myBlock中保存的FuncPtr函数指针, 然后直接调用就好了

我们再来看一下复杂一点的block

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        void (^myBlock)(void) = ^{
            NSLog(@"this is a block--%d", a);
        };
        a = 20;
        myBlock();
    }
    return 0;
}

然后再编译成C++文件, 看看发生了什么:

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;
  }
};

可以很清楚的看到, 这个block包含了一个新的成员变量int a, 这个block的内存布局就是:

struct __main_block_impl_0 {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
  struct __main_block_desc_0* Desc;
  int a;
};

这个结构体的最后一个成员变量就是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;
  }

这个构造函数多了一个参数, 就是int _a

image.png

而创建这个block的时候, 传进去的参数就是我定义的int a = 10, 传进去了之后, 就把_a的值, 赋给了结构体内部的成员变量a,

: a(_a)

这就是C++的语法, 将_a赋值给a, 所以外面的a和里面的a, 不是同一个东西, 外面的a, 是我定义的变量a, 里面的ablock在创建的时候, 由编译器生成的成员变量a

相当于, 结构体在创建的时候, 捕获了这个外部的变量a

image.png

这就造成, 即使在block调用之前, 修改变量的值为20, 也不会改变block调用时获取的变量值, 因为block调用的值, 是block的成员变量的那个值.

请记住一个关键的词- Capture捕获

那么什么情况下会捕获呢?
记住以下原则:

  • 局部变量会捕获
  • 全局变量不会捕获
    请问, 什么是局部变量, 什么是全局变量呢?
    简单来说, 声明在函数内部的变量是局部变量, 声明在函数外部的变量称之为全局变量

block真的不会捕获全局变量吗?

image.png

好, 记住了两条大的原则:

  • 局部变量会捕获
  • 全局变量不会捕获
    还有, 局部变量又分为auto变量和static变量
    像这种声明之后存在于栈上的变量称之为auto变量, auto这个关键字是可以省略的
    image.png

那么, 被static修饰了的变量和auto变量有什么不同呢?

image.png

放在常量区的变量有一个特点是生命周期延长了, 他的生命周期跟程序的运行周期是一致的, 只要程序没有终止, 那么常量区的数据是一直存在的. 这和栈区的数据不同, 栈区存放的数据的特点是, 只要作用域结束, 那栈区的内存就会被回收. 那我们来看看是不是这样的:

图1
图2

如图2的所示, 当用static关键字修饰变量时, 当作用域结束时, 变量是不会销毁的, 当时离开作用域, 是访问不到变量的, 意思是, 虽然变量存在数据段, 但离开作用域, 无法访问变量. 这相当于变量的作用域不变, 变量的生命周期延长了.

由于这个情况, 局部变量中, auto变量和static变量被block时, 处理情况是不同的:

  • 值传递(auto变量)
    因为auto变量在作用域结束之后, 变量就会回收, 它的生命周期是很短的, 所以block在捕获时, 会把auto变量的值赋值给block内部的同名成员变量, 这个成员变量是一个新的内存空间存储这个值

  • 指针传递(static变量)
    static变量就不一样了, 它存储在数据段(常量区), 它的生命周期是跟程序的生命周期一致的, 也就是说, 在block需要访问这个变量的时候, 我访问的仍然是这个变量本身, 那么这时候, 我捕获的变量, 就是这个变量的指针(存储这个变量的地址), block把变量的指针赋值给block的同名成员变量

image.png

可以很清楚地看到, 是将变量a的地址值传到了block的构造函数中

image.png

最终, 赋值给了block内部的指针变量a, 所以block的成员变量的类型是int *.

image.png

image.png

因此, 在调用myBlock之前, 修改了static变量a的值, 在调用myBlock时, a的值已经改了

image.png

上面是基本数据类型的情况, 那么如果是OC对象, block又将如何捕获呢?
通过编译, 我们发现, 在struct __main_block_desc_0结构体中, 多出来两个成员变量

image.png

  • copy
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

栈 -> 堆

  • dispose
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

那么这个有什么用呢? 还得从ARC的自动copy说起
ARC模式下,

  • block作为函数返回值时
  • block赋值给__strong指针时(ARC环境下, 一般默认就是__strong指针, 除非用__weak或者__unsafe_unretained修饰的指针),
  • 或者GCD,
  • 或者函数名中有usingBlock的,
    都会对block进行一次copy操作. 那么这个copy操作有什么用呢?
    我们先来看看block的种类:
image.png

从上图可以看出, 我们把编译环境改成MRC(这是因为ARC环境编译器帮我们做了很多事情, 不方便我们探究本质), 然后打印这三个block的类型, 我们发现block是分为三种的:

  • __NSGlobalBlock__这个block是存在数据段的, 暂时不探究
  • __NSStackBlock__这个是存在栈区, 也叫栈block
  • __NSMallocBlock__这个是在堆区, 所以叫堆block

说回来copy, 当我们对一个block执行copy操作时

  • __NSGlobalBlock__还是__NSGlobalBlock__
  • __NSStackBlock__会升级为__NSMallocBlock__(这一点要尤其注意)
  • __NSMallocBlock__还是__NSMallocBlock__

当栈block升级为堆block时, 这时候堆中的数据就依靠程序员来管理了, 而不是像栈block一样, 栈空间自动回收之后, 保存在栈中的block就没有了. 在MRC环境下, 如果是栈block的话, 如下图所示

image.png

block内部的person指针只是指向person存储的那片内存空间, 并不会对person对象引用计数+1, 那么当person对象被回收后[person release], 再去访问栈block中的person指针指向的那片内存空间, 就很危险了, 就会造成野指针访问.

但如果是堆block呢? 这时候就会对person对象进行引用计数+1, 那这时候再去访问堆block中的person指针指向的那片内存空间, 是没有问题的

image.png

但如果使用__unsafe_unretained关键字修饰person对象时, 会发生什么呢?

image.png

可以看到的是, 坏内存访问. 这是因为, 因为我们用__unsafe_unretained关键字修饰了person对象, 所以, 即使block被拷贝到堆区, block内部也不会对person对象引用计数+1 , 那么当我向person对象发送release消息后, person对象引用计数-1, 这时候是会被销毁的, 此时再去访问block内部的person指针指向的那片内存空间, 就会造成野指针访问

ARC模式下,

  • block作为函数返回值时
  • block赋值给__strong指针时(ARC环境下, 一般默认就是__strong指针, 除非用__weak或者__unsafe_unretained修饰的指针),
  • 或者GCD,
  • 或者函数名中有usingBlock的,
    ARC在以上四种情况下, 会自动对block进行copy操作, 也就是说, 这个栈block会升级成堆block, 升级成堆block后, 堆block中的person指针会对person对象强引用, 那么这样一来, 即使block外面的person指针被回收了, person对象依然不会销毁, 它会随着block的生命周期结束而销毁.
上面提到的例子中, 如果是局部的auto变量, 我们其实是无法修改变量的值. 因为auto变量的地址没有变, 假设我们要修改定义的局部变量的值, 我们需要做一件事, 就是加上__block关键字, 那么__block的作用是什么呢?

还是先看看编译情况:

        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 18};;

简化下来就是:

 __Block_byref_a_0 a = {
  0,
  &a, 
  0, 
  sizeof(__Block_byref_a_0), 
  18
};

__Block_byref_a_0这个又是什么东西呢?

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

可以看到的是, __block修饰的变量, 在编译时期, 自动包装成了一个结构体(这个结构体的名字跟修饰的变量同名), 这个结构体的内存布局如上图所示.

  • 0赋值给了_isa指针
  • &a(结构体a的地址)赋值给了__forwarding指针
  • 0赋值给__flags
  • sizeof(__Block_byref_a_0)赋值给了__size
  • 最后18这个变量赋值给了结构体的最后一个变量a

所以原本的int a, 包装成了一个结构体, 然后这个结构体的地址作为参数传给了block的构造函数. block内部有一个成员变量__Block_byref_a_0 *相当于捕获了这个变量

但是很明显, 在MRC模式下, 包装过后的这个结构体也存在于栈区

image.png
, block此时也是一个栈block, 那栈的内存空间是随着作用域的结束而回收的!
事实上, 我理解的是, 例如下段代码:
image.png

在31行代码的时候, age作为一个存在于栈的变量, 它已经被系统回收了, 所以31行block去访问age这片内存空间的时候, 其实是很危险的. 虽然这里成功打印了age的值, 但这么做是不合理的.

image.png

这幅图应该这么画. 也就是说, 在MRC情况下, 实际上并没有强弱引用的概念. 指针只是指向这这片存储空间, 并没有强指针, 弱指针的概念. 当作用域结束, 栈空间被系统回收, 指针再指向被回收的栈空间, 是一件很危险的事, 可能取到的值不正确.

即使是指针指向的是堆空间的对象, 也没有强弱指针的概念, 这就是为什么在MRC环境下, 需要手动给引用计数+1.

image.png

那么在MRC环境下, 需要手动将block拷贝到堆区. 或者, 在属性修饰时, 使用copy修饰
没有用copy修饰

image.png

打印出来就是栈block
image.png

使用copy修饰
image.png

打印出来就是堆block
image.png

当使用了copy关键字时, block已经升级成了堆block, 同时, __block修饰的变量也在堆区, 何以见得? 请看下图:

image.png

很明显, age这个变量已经到了堆区. 那么问题来了, block内部会对这个__block修饰的变量有一个retain操作吗? 我觉得应该有一个类似retain的操作, 理由如下:
__block修饰的变量在堆区, 堆区的变量回收是程序员来决定的, 而__block修饰的变量的生命周期是和block一致的, 其实就相当于block持有了__block修饰的变量

我猜测内部的原理是这样的:
堆区的block会调用__main_block_copy_0方法

image.png

__main_block_copy_0方法内部又调用了_Block_object_assign

image.png

你也可以理解为将__block修饰的变量(此时被包装成了一个对象), 然后堆block会持有这个对象(也就是引用计数+1).

在这一点上ARCMRC是一致的, 那么不同点是什么呢?
MRC环境下, __block修饰的变量(包装后的对象), 这个对象内部的指针并不会持有外面的对象, 举个例子:

image.png

这里出现了坏内存访问的错误, 尽管block持有了 __block修饰的变量(包装后的对象), 这个对象内部的指针并不会持有外面的对象, 也就是图中的person对象并没有被__block修饰的变量(包装后的对象)持有, 在MRC环境下:

image.png

MRC环境下, 给person对象发送release消息, 引用计数-1, 对象直接销毁, 坏内存访问, 说明person1并没有持有person对象, 画图表示就是

image.png

但是, 在ARC环境下, __block修饰的变量的person指针是会通过__Block_byref_id_object_copy方法, 对person对象强引用的(引用计数+1)

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_copy_131(void *dst, void *src) {
 _Block_object_assign(dst + 40, src + 40, 131);
}

这句代码实际上就是调用_Block_object_assign, 内部对person对象的引用计数+1, 这样就形成了强引用.画图表示就是:

image.png

通过这幅图, 可以看出来的是, block内部的内存管理就是:
MRC环境下, 执行copy操作. 在ARC环境下, 系统默认执行copy操作, 此时, block内部的person指针会指向__block修饰的结构体, 通过
_Block_object_assign方法, 对__block修饰的结构体进行一次copy操作, 也就是引用计数+1, 在block执行完毕, 需要被销毁时, 执行_Block_object_dispose方法, 对__block修饰的结构体进行一次release操作, 也就是引用计数-1.此时, __block修饰的结构体和block一起被销毁.
不同的是, 在MRC环境下, __block修饰的结构体内部的_Block_object_assign不会再对person对象引用计数+1, _Block_object_dispose方法也不会对person对象引用计数-1,
, 在ARC环境下, __block修饰的结构体内部的_Block_object_assign会对person对象引用计数+1, _Block_object_dispose方法也会对person对象引用计数-1.

以上就是block内部的内存管理.
还有一点需要提到的是, __block修饰的变量转化成的结构体中, __forwarding指针是干嘛用的? 在我们之前的代码中, 看到是将结构体自己的地址传给了__forwarding指针. 那么这个指针的值就是自己这个结构体的地址, 也就是说__forwarding指针指向了自己, 那么为什么不直接从结构体中取值, 而是要通过一个__forwarding指针呢?

当__block修饰的这个变量包装成的结构体还存在于堆区的时候, 现在这个地址是指向栈区的地址的, 但这本身并没有什么意义.
但当这个包装结构体被拷贝到了堆区, 此时再去访问这个变量的时候, 就会指向堆区的那个包装结构体. 也就是说, a->__forwarding->a的这个过程就是访问堆区数据的过程.

image.png
image.png

那么假设, 我想和MRC一样, 对person对象不进行强引用呢?
这时候就需要用到__weak__unsafe_unretain关键字了. 事实上, 默认状态下, 都是相当于使用了__strong关键字, 相当于__block修饰的变量结构体里的那根person指针默认就是强指针. 当使用__weak修饰时, 被__block修饰的变量就变成了:

image.png

编译器还爆出了警告, 让我不要这么做:
image.png

将持有的对象赋值给一个弱指针, 对象将在赋值完成后立即释放

使用__unsafe_unretained也是差不多的效果:

image.png

__weak__unsafe_unretained这两个关键字是用来解决循环引用的时候用到的. 那么什么是循环引用呢? 如下图所以:
ARC环境下, 被强指针指向的对象引用计数+1, 此时person对象创建时引用计数+1, 被强指针指向时, 引用计数+1, 此时引用计数是2, block对象创建时引用计数+1, 被person对象内部的强指针指向时, 引用计数+1, 此时引用计数是2. 那么当他们引用计数都是2时. 它们两个就都无法销毁. 此时, 必须打破这个循环

image.png

一般打破循环的方式, 就是让其中一根指针变成弱指针, 一般就是将block内部指向对象的指针变成弱指针

image.png

一旦这跟指针变成弱指针后, person对象销毁后, person对象内部的那根指针被回收, 回收后, block对象释放.

其实被__block修饰的变量也是同理, 它是这样形成:

image.png

而我们用__weak修饰变量后, 形成的闭环其实是将__block修饰的结构体里的person变成弱指针:

image.png

那么__weak__unsafe_unretained有什么区别呢?
__weak修饰的变量, 一旦内存空间被回收, __weak修饰的指针变量就会置为nil, 后面再访问就会直接return, 因此它是安全的
__unsafe_unretained修饰的变量, 一旦内存空间被回收,__unsafe_unretained修饰的指针变量不会置为nil, 后面再访问, 是非常危险的. 有可能会造成野指针访问.

最后再探讨一个问题:

iOS block内部为什么要加__strong?

这是为了防止当程序执行block时, block内部的指针指向的那块地址突然为空. 举个例子:

image.png

假设上图中, 在程序执行到第22行时, self突然为空, 如果不写__strong typeof(self)strongSelf = weakSelf;这行代码时, 那后面访问weakSelf指向的地址空间时, 就可能为空. 但是当我写了__strong typeof(self)strongSelf = weakSelf;时, 此时, 我用一个栈区的局部变量强引用了self对象. 那self此时的引用计数+1, 它不会被置为空. 我后面的代码就可以继续访问. 等到作用域结束, 局部变量栈空间回收, self对象的引用计数-1. 这样是不会形成循环引用的, 如下图所示
image.png

iOS开发中在block中为什么要__weak和__strong配合使用
上文中举出了一个__weak__strong配合使用的例子, 仅供参考

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

推荐阅读更多精彩内容