iOS程序员iOS:block

iOS Block存储域及循环引用

2019-01-05  本文已影响176人  1江春水

系列文章:
iOS Block概念、语法及基本使用
iOS Block实现原理
iOS Block __block说明符

本文将讲解以下几点:

根据上几篇文章Block语法编译后的源代码我们看到,__block_impl结构体内部有一个成员变量:isa指针,__main_block_impl_0结构体初始化的时候,isa指针初始化为 impl.isa = &_NSConcreteStackBlock,因为Block也是OC对象,我们说该isa指针指向该Block实例所属的Block类。

Block种类

Block有以下几种:

Block 类 Block存储域
_NSConcreteStackBlock
_NSConcreteGlobalBlock 程序的数据区域(.data 区)
_NSConcreteMallocBlock

顺便说一下程序的内存分配情况:

区域 存放的东东
栈区(stack) 由编译器自动分配释放 ,存放函数的参数值,局部变量的值
堆区(heap) 程序员分配(alloc/new/copy/mutableCopy)
全局区(静态区)static 全局变量和静态变量
常量区 常量字符串等
数据区(代码区) 存放函数体的二进制代码

到目前位置看到的Block全都是_NSConcreteStackBlock,其实不是这样的,在记述全局变量的地方使用Block语法时,生成的Block为 _NSConcreteGlobalBlock,举个例子看下:

@implementation ViewController

void (^block)(void) = ^{
    NSLog(@"haha");
};

@end

编译后__block_block_impl_0结构体:

struct __block_block_impl_0 {
  struct __block_impl impl;
  struct __block_block_desc_0* Desc;
  __block_block_impl_0(void *fp, struct __block_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;//global
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

该Block的类为_NSConcreteGlobalBlock类,即存放在数据区也就是代码区,因为使用全局变量的地方不能使用自动变量,所以不存在对自动变量的截获。由此Block结构体实例的内容不依赖于执行的状态,所以整个程序中只需一个实例,因此把该结构体实例放在数据区。

在以下情况下生成的Block结构体实例属于 _NSConcreteGlobalBlock类:

  1. 记述全局变量的地方有Block语法时;
  2. Block语法的表达式中不使用截获的自动变量时;

除以上两种情况外,都会生成 _NSConcreteStackBlock类,且保存在栈区域

一、Block变量存储域

配置在全局变量上的Block,从变量作用域外也可以通过指针安全地使用,但是设置在栈的Block,如果其所属的变量作用域结束,该block也就被废弃。由于__block变量也配置在栈上,同样其所属的变量作用域结束,则该__block变量也同样被废弃。
Block提供了将Block从栈区copy到堆区的方法。如下图:


复制到堆上.jpg

复制到堆上的Block将_NSConcreteMallocBlock类对象写入Block结构体实例的成员变量isa:

impl.isa = &_NSConcreteMallocBlock;

还记得上一节说到的__block变量结构体实例的 __forwarding 指针指向__block变量结构体自己吧,也就是说无论Block结构体实例配置在栈上还是堆上,都能够访问__block变量。

那么什么时候Block从栈上复制到堆上呢,其实大多数情况下,编译器会恰当的进行判断,自动生成将Block从栈上复制到堆上。

以下情况需要程序员自己通过copy方法将Block从栈区复制到堆区:

  1. 向方法或函数的参数中传递Block时;

不需要手动复制的情况:

  1. Cocoa框架的方法且方法名中含有usingBlock等时;
  2. GCD的API

下图是按Block的存储域,使用copy后,Block有什么变化

Block的类 Block原区域 复制后
_NSConcreteStackBlock 从栈复制到堆
_NSConcreteGlobalBlock 数据区 什么也不做
_NSConcreteMallocBlock 引用计数增加

从从上边可以看出,不管Block配置在何处,用copy方法复制都不会出现任何问题。在不确定时调用copy方法即可。
此处有一个例子:

blk = [[[[blk copy] copy] copy] copy];

该代码解释如下:

{
    //将配置在堆上的Block复制给变量tmp,变量tmp持有强引用的Block;
    blk_t tmp = [blk copy];
    //将Block变量tmp赋值给blk变量,大括号走完后,tmp释放,blk继续持有Block;
    blk = tmp;
}
//以此类推...
{
    blk_t tmp = [blk copy];
    blk = tmp;
}
{
    blk_t tmp = [blk copy];
    blk = tmp;
}
{
    blk_t tmp = [blk copy];
    blk = tmp;
}

由此可见,ARC下使用copy完全没问题。

二、__block变量存储域

Block从栈上复制到堆上,那么在Block中使用的__block变量是怎么处理的呢,看下表:

__block变量配置区域 Block从栈复制到堆时的影响
从栈复制到堆并被Block持有
被Block持有

说明:若一个Block中使用了__block变量,当Block变量从栈复制到堆上时,那么__block变量也会被复制到堆上。


__block变量复制到堆上.jpg

多个Block变量使用__block变量时,因为最先会将所有的Block配置在栈上,所以__block变量也会配置在栈上。在任何一个Block变量被赋值到堆上时,__block变量一并被赋值到堆上,当其他的Block变量复制到堆上时,其使用的__block变量引用计数增加:


__block变量被复制到堆区.jpg

配置在堆上的Block被废弃时,__block变量也被废弃:


__block变量废弃.jpg

到这里我们看到,Block变量和OC对象的内存管理机制是一样的,都是使用引用计数,所以也验证了那句话:Block是OC对象。

三、截获对象

先来看一个例子:

typedef void (^block)(id obj);

block blk;//全局变量Block

- (void)viewDidLoad {
    [super viewDidLoad];
 
    id array = [NSMutableArray array];
    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]);
}

打印:

array count = 1
array count = 2
array count = 3

从源代码可以看出,array变量是临时变量,viewDidLoad方法走完就被废弃,但依然有打印,说明变量没有释放,从前几篇文章可以想象,打印的array变量被Block结构体实例持有了,下面来验证下,编译后的代码如下:

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

static struct __ViewController__viewDidLoad_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __ViewController__viewDidLoad_block_impl_0*, struct __ViewController__viewDidLoad_block_impl_0*);
  void (*dispose)(struct __ViewController__viewDidLoad_block_impl_0*);
} __ViewController__viewDidLoad_block_desc_0_DATA = {
    0,
    sizeof(struct __ViewController__viewDidLoad_block_impl_0),
    __ViewController__viewDidLoad_block_copy_0,
    __ViewController__viewDidLoad_block_dispose_0
};

//函数指针调用的函数
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself, id obj) {
  id array = __cself->array; // bound by copy
        ((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)array, sel_registerName("addObject:"), (id)obj);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_9k_z85dfkt91zd1j387gcxn8xkh0000gn_T_ViewController_503b9f_mi_0,((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
    }

//copy 和 dispose 函数
static void __ViewController__viewDidLoad_block_copy_0(struct __ViewController__viewDidLoad_block_impl_0*dst, struct __ViewController__viewDidLoad_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __ViewController__viewDidLoad_block_dispose_0(struct __ViewController__viewDidLoad_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}

//Block结构体
struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  id array;//持有array变量
  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//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"));

    id array = ((NSMutableArray * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"));
    blk = (block)((id (*)(id, SEL))(void *)objc_msgSend)((id)((void (*)(id))&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, array, 570425344)), sel_registerName("copy"));
    
    ((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
    
    ((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
    
    ((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
}

请注意,可以看到,id 类型的 array变量被Block结构体持有了。
在这里说明一点,其实我们创建的对象,默认会带上__strong所有权修饰符,比如:

id array = [NSMutableArray array];

上边代码等同于下边代码:

id __strong array = [NSMutableArray array];

在OC语言中,C语言的结构体不能含有附有__strong修饰符的变量,因为编译器不知道应何时进行C语言结构体的初始化和废弃操作,不能很好的管理内存。

前两节我们看到了copy dispose 函数,没有做详细解释,只是猜想了一下,接下来说说这两个函数。

OC运行时可以准确的把握block从栈上复制到堆上和Block废弃的时机,因此Block结构体内部使用带有__strong或__weak修饰符的变量,也可以在恰当的时刻初始化和废弃,为此需要在 __ViewController__viewDidLoad_block_desc_0 结构体内部加上 copy 和 dispose 成员变量,以及作为函数指针赋值给这两个变量的 __ViewController__viewDidLoad_block_copy_0 和 __ViewController__viewDidLoad_block_dispose_0函数

copy函数内部使用了_Block_object_assign函数将对象类型对象赋值给Block结构体内的成员变量并持有该对象。_Block_object_assign函数调用相当于retain实例方法的函数。

dispose函数内部使用_Block_object_dispose函数释放Block结构体内部的对象类型的成员变量。_Block_object_dispose函数调用相当于release实例方法的函数。

我们只看到了生成的copy和dispose函数,但是没看到调用啊,那到底啥时候调用这两个函数呢,这是系统自动发生的动作:

函数 调用时机
copy 栈上Block被复制到堆上时
dispose 堆上Block被废弃时

当Block从栈上复制到堆上时,会调用copy函数;当堆上的Block被废弃时,会调用dispose函数。

上一节提到了两点,什么时候block会从栈上复制到堆上,现在总结如下:

有了这种构造,通过使用__strong修饰符的变量,Block中截获的对象就能超出其变量作用域存在。

上一节我们研究__block变量的时候,看到过copy 和 dispose函数,现在Block截获对象的也出现了,而且转换后的代码基本相同,后边的注释不同:

类型 _Block_object_assign/dispose函数
Block截获对象 BLOCK_FIELD_IS_OBJECT
__block变量 BLOCK_FIELD_IS_BYREF

通过这两个OBJECT、BYREF来区分copy/dispose函数的对象类型是对象还是__block变量。与copy函数持有截获的对象,dispose释放持有的对象相同,copy函数持有Block所使用的__block变量,dispose函数释放__block变量。

有一点需要说明,这本书上的截获对象的例子,Block不调用copy方法,我本地测试的不会强制结束。可以解释为:blk变量为全局变量,生成的Block结构体实例也是全局变量,全局变量持有array变量,所以程序不会强制结束。如果这个解释有误的话,还请读者指正,谢!

四、__block变量和对象

__block说明符可指定任意类型的变量。下面看下__block修饰OC对象。

__block id obj = [[NSObject alloc] init];

clang转换如下:

__block结构体
struct __Block_byref_obj_0 {
  void *__isa;
__Block_byref_obj_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 id obj;
};

//声明部分
__attribute__((__blocks__(byref))) __Block_byref_obj_0 obj = {
    (void*)0,
    (__Block_byref_obj_0 *)&obj,
    33554432,
    sizeof(__Block_byref_obj_0),
    __Block_byref_id_object_copy_131,
    __Block_byref_id_object_dispose_131,
    [[NSObject alloc] init]
}

Block截获对象这一小节中,当Block从栈复制到堆上时,使用copy函数持有截获的对象,当Block被废弃时,使用dispose释放截获的对象。
在__block说明符修饰对象时,在__block变量结构体中看到了copy和dispose函数,那说明当__block变量从栈上复制到堆上时,使用copy函数持有赋值给__block变量的对象,当堆上的__block变量被废弃时,使用dispose函数释放赋值给__block变量的对象。

由此可知,只要堆上的__block结构体实例变量没有被释放,那么__block变量就不会被释放。

五、Block循环引用

原因:在Block内部使用对象类型的变量,该变量持有Block,当Block从栈上复制到堆上时,Block同时持有了对象类型变量,那么当对象类型释放时,由于变量和Block互相引用导致内存泄漏,举个例子:

typedef void (^block)(id obj);

@property (nonatomic, copy) block blk;

- (void)viewDidLoad {
    [super viewDidLoad];
    self.array = [NSMutableArray array];
    self.blk = ^(id obj){
        [self.array addObject:obj];
        NSLog(@"array count = %ld",[self.array count]);
    };
}

这样写如果这个VC被pop,那么这个VC是释放不了的,VC持有Block,Block内部持有VC。

循环引用.jpg

修改一下:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.array = [NSMutableArray array];
    ViewController * __weak temp = self;
    self.blk = ^(id obj){
        [temp.array addObject:obj];
        NSLog(@"array count = %ld",[temp.array count]);
    };
}

循环引用消失:


循环引用消失.jpg

在此根据自己的项目中使用到的Block场景,来总结下Block使用时的注意事项,说不定项目中真的有内存泄漏呢

1、UIView 的 animation动画块使用了Block,内部使用self不会循环引用,为什么呢

答:UIView 动画块是类方法,不被self持有,所以不会循环引用。

2、Monsary也使用了Block来设置控件的布局,Block内部使用self,为什么不会循环引用呢

答:看源码可以看出,Monsary使用的Block是当做参数传递的,即便block内部持有self,设置布局的view持有block,但是block不持有view,当block执行完后就释放了,self的引用计数-1,所以block也不会持有self,所以不会导致循环引用。

3、reactiveCocoa如果不使用@weakify @strongify,会循环引用,两个宏就等于下边代码:

__weak typeof(self) weakSelf = self;
__strong typeof(weakSelf) strongSelf = weakSelf;

六、总结

以上几篇文章基本就把Block(以及__block变量)的定义、语法、应用、原理介绍完了,主要的目的还是能更灵活的应用于项目。

欢迎提出宝贵意见,喜欢赞一下吧。

图有点low,莫见怪,哈哈哈...

上一篇下一篇

猜你喜欢

热点阅读