美文网首页iOS程序员iOS:block
iOS Block存储域及循环引用

iOS Block存储域及循环引用

作者: 1江春水 | 来源:发表于2019-01-05 20:07 被阅读176次

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

    本文将讲解以下几点:

    • Block种类
    • Block变量存储域
    • __block变量存储域
    • 截获对象
    • __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会从栈上复制到堆上,现在总结如下:

    • Block调用copy方法时
    • Block作为函数返回值返回时
    • 将Block赋值给赋有__strong修饰符id类型的类或Block类型成员变量时
    • 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递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,莫见怪,哈哈哈...

    相关文章

      网友评论

        本文标题:iOS Block存储域及循环引用

        本文链接:https://www.haomeiwen.com/subject/elsrrqtx.html