美文网首页
iOS-Blocks

iOS-Blocks

作者: doudo | 来源:发表于2017-09-04 16:11 被阅读10次

    花了一段时间对Block深入的研究了一下,以下是我边研究边写的笔记记录,其中大部分内容都是从多线程和内存管理那本书中而来,并加入了自己的说明与总结,对Block有了比较深入的理解。如果哪里有问题,还望留言指出。

    正文

    什么是blocks,blocks是C语言的扩展功能。概括为:带有(截获)自动变量(局部变量)的匿名函数。

    扩展
    1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
    2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由os回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
    3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放。
    4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放。
    5、程序代码区—存放函数体的二进制代码。

    一、 数据结构

    为了研究编译器是如何实现 block 的,我们需要使用 clang。clang 提供一个命令,可以将 Objetive-C 的源码改写成 c 语言的,借此可以研究 block 具体的源码实现方式。该命令是
    clang -rewrite-objc block.c

    block的数据结构定义如下:
    第一种:

    struct __block_impl{
        void *isa;
        int Flags;
        int Reserved;
        void *FuncPtr;
    };
    struct __main_block_desc_0{
        unsigned long reserved;
        unsigned long Block_size;
    };
    struct __main_block_impl_0{
        struct __block_impl impl;
        struct __main_block_desc_0 *desc;
    };
    

    第二种:

    struct __funcName_block_impl_index{
        void *isa;
        int Flags;
        int Reserved;
        void *FuncPtr;
        struct __funcName_block_desc_index *Desc;
        /* Imported variables. */
    }
    

    实际上,代码转化后是第一种的结构体的形式,但是为了便于理解,我们可以直接理解为第二种形式,不过仅是结构体的嵌套方式不一样,而且他们在内存上是完全一样的。解释:如下 2 个结构体 SampleA 和 SampleB 在内存上是完全一样的,原因是结构体本身并不带有任何额外的附加信息。(此处解释,来源于唐巧一篇文章中对该结构的解释)

    struct SampleA {
        int a;
        int b;
        int c;
    };
    struct SampleB {
        int a;
        struct Part1 {
            int b;
        };
        struct Part2 {
            int c;
        };
    };
    

    结构体的理解:

    1. isa 指针,所有对象都有该指针,用于实现对象相关的功能。指向 _NSConcreteStackBlock、_NSConcreteMallocBlock或_NSConcreteGlobalBlock类。
    2. flags,用于按 bit 位表示一些 block 的附加信息,本文后面介绍 block copy 的实现代码可以看到对该变量的使用。
    3. reserved,保留变量。
    4. FuncPtr,函数指针,指向具体的 block 实现的函数调用地址。Block使用的匿名函数部分,实际上被转化为简单的c语言函数来处理。
    5. descriptor, 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针。
    6. variables,capture 过来的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。

    二、 实质

    一个Block实际是个结构体实例(机构体的结构如上所示),block被复制给一个Block变量,该变量实际是个结构体指针,指向该结构体。这与oc对象的本质就一样了,oc对象都是个结构体指针,所以block其实也是个oc对象。

    扩展

    objc/runtime.h中objc_class和objc_object结构体的定义如下:

    struct objc_class {
        Class isa  OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
        Class super_class                       OBJC2_UNAVAILABLE;  // 父类
        const char *name                        OBJC2_UNAVAILABLE;  // 类名
        long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
        long info                               OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识
        long instance_size                      OBJC2_UNAVAILABLE;  // 该类的实例变量大小
        struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 该类的成员变量链表
        struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定义的链表
        struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法缓存
        struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 协议链表
    #endif
    } OBJC2_UNAVAILABLE;
    
    ```objc
    typedef struct objc_class *Class;
    
    struct objc_object {
        Class isa  OBJC_ISA_AVAILABILITY;
    };
    

    当创建一个特定类的实例对象时,分配的内存包含一个objc_object数据结构,然后是类的实例变量的数据。

    typedef struct objc_object *id;,id是一个objc_object结构类型的指针。
    可以看出block和id类型很相似。

    三、 截获自动变量值

    1. 自动变量(局部变量)

    Block语法表达式中使用自动变量被当做成员变量追加到了__main_block_impl_0结构体中。

    struct __main_block_impl_0{
        struct __block_impl impl;
        struct __main_block_desc_0 *Desc;
        //下边两个即为
        int val;
        const char fmt;
    }
    
    1. Block语法表达式中没有使用的自动变量不会被追加,Blocks的自动变量截获只针对Block中使用的自动变量。
    2. 结构体内声明的成员变量类型与自动变量类型完全相同。(如:const那个仍为const)。

    2. 可在block内修改的情况

    1.c语言中的变量类型:静态局部变量、全局变量(包括静态全局变量)
    1. 静态局部变量,静态变量的指针被追加到结构体中:
    int val;
    
    struct __main_block_impl_0{
        struct __block_impl impl;
        struct __main_block_desc_0 *Desc;
        //下边两个即为
        int *val;
    }
    
    1. 全局变量,不会被添加到结构体中,因为在全局区,使用的时候直接使用,与转化前完全相同,直接修改。
    2. 使用__block存储域类说明符,__block说明符类似于static、auto说明符。

    在自动变量前加__block :

    __block int val = 10;
        void (^blk)(void) = ^{
            val = 3;
        };
    

    转化后的代码为:

    struct __Block_byref_val_0{
        void *isa;
        __Block_byref_val_0 *__forwarding;
        int __flags;
        int __size;
        int val;
    };
    
    struct __main_block_impl_0{
        struct __block_impl impl;
        struct __main_block_desc_0 *Desc;
        
        __Block_byref_val_0 *val;
    };
    

    我们可以看到,加上__block的自动变量竟然变成了结构体实例,这个结构体实例包含原val(val是该实例自身持有的变量,它相当于原自动变量),而block结构体实例中持有__block变量生成的结构体实例指针。所以block结构体中对__block变量的修改实际是对其指针的操作

    3. Block存储域

    通过之前可知,Block也是oc对象。将Block当做oc对象来看时,该Block的类为_NSConcreteStackBlock/_NSConcreteMallocBlock/_NSConcreteGlobalBlock。

    • _NSConcreteStackBlock该类的实例对象Block设置在栈上。
    • _NSConcreteMallocBlock类的实例对象设置在堆上。
    • _NSConcreteGlobalBlock类的实例对象和全局变量一样,设置在全局区(静态区)。

    以下情况,Block为_NSConcreteGlobalBlock类的对象:

    • 全局变量的地方有Block语法。
    • Block语法的表达式中不使用截获的自动变量时。

    实际上当ARC有效时,大多数情形下编译器会恰当地进行判断,自动生成“将Block从栈上复制到堆上”的代码。

    1.以下情况,编译器会自动将Block从栈上复制到堆上:

    • Block作为函数返回值时。
    • 向方法或函数的参数传入Block时,并且在方法或函数中适当的复制了Block。
    • Cocoa框架的方法名中含有usingBlock等时。
    • GCD的API。

    2.以下情况,编译器判断不了,不会自动从栈复制到堆:

    • Block作为函数参数时,并且在方法中没有复制Block。

    例子:NSArray在使用enumerateObjectsUsingBlock方法时不用手动复制,相反的,在NSArray类的initWithObjects实例方法上传递Block时需要手动复制,因为此时编译器不能判断是否需要复制。

    - (id)getBlockArray
    {
        NSArray *array = [[NSArray alloc] initWithObjects:
                          [^{NSLog(@"block");} copy],
                          [^{NSLog(@"block");} copy],
                          nil];
        return array;
    }
    

    3.下面看一下,对这三种Block分别执行copy的复制结果:

    • 栈上Block,copy,结果:从栈上复制到堆上。
    • 堆上Block,copy,结果:引用计数增加。
    • 全局Block,copy,结果:什么也不做。

    不管在何处,用copy方法复制Block都不会产生任何问题。所以在不确定的时候调用copy即可。

    4.随意copy可以吗?

    但是,将Block从栈复制到堆上是相当消耗cpu的,所以还是搞清楚什么栈上的才需要copy,避免cpu资源浪费。

    那么堆上的Block多次调用copy会是什么结果呢?
    如下代码:

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

    该代码可解释如下:

    {
            blk_t tmp = [blk copy];
            blk = tmp;
        }
        {
            blk_t tmp = [blk copy];
            blk = tmp;
        }
        {
            blk_t tmp = [blk copy];
            blk = tmp;
        }
    

    由此可以看出,ARC有效时完全没有问题。

    4. __block变量存储域

    1. 使用__block变量的Block从栈上复制到堆上时,__block变量也会受到影响:
    • Block从栈复制到堆时,__block变量从栈复制到堆,此时Block持有(强引用)__block变量。
    • Block已经在堆上时,复制Block,对__block变量不会产生任何影响。
    1. 当一个__block变量,在多个Block中使用,这些Block从栈复制到堆上时,第一个Block被复制到堆上时,__block变量也会一并被复制到堆上,并被该Block持有(强引用),剩下的Block从栈复制到堆时,被复制的Block持有该__block变量,增加__block变量的引用计数。

    2. __block变量与oc引用计数式内存管理完全相同。使用__block变量的Block持有__block变量。如果Block被废弃,它所持有的__block变量也就被释放。

    3. 前边提到过使用__block修饰的变量,会被转化为一个结构体实例,该结构体结构如下:

    struct __Block_byref_val_0{
        void *isa;
        __Block_byref_val_0 *__forwarding;
        int __flags;
        int __size;
        int val;
    };
    

    那么,如果__block变量跟随Block一起被复制到堆上了,如果在Block外修改__block变量他们修改的是同一个值吗?是怎么回事呢?原来栈上的__block变量结构体呢?
    上代码:

    __block int val = 0;
        void (^blk)(void) = [^{val++;} copy];
        val++;
        blk();
    

    一步一步,分析,首先__block修饰的变量,会在栈上生成个结构体实例,这个结构体实例包含了原来的val变量。然后,Block执行copy的时候,Block结构体实例和__block结构体实例一起又被复制到了堆上,Block语法表达式中使用的val是堆上的结构体实例,Block语法外使用的val是栈上生成的那个结构体实例。(注意关键点来了,__block变量的结构体实例中有个__forwarding,正常情况下一直都是指向该结构体自己的)当__block变量的结构体实例被复制到堆上的时候,之前栈上的那个__block变量结构体实例的__forwarding指针会指向堆中新生成的__block变量的结构体实例,堆中的指向它自己。
    所以Block表达式中的val++ 和Block外val++,都是调用的

    (val.__forwarding->val)++;
    

    即堆中的那个__block变量生成的结构体实例。

    所以,无论是在Block语法中、语法外使用__block变量,还是__block配置在栈上或堆上,都可以顺利访问到同一个__block变量。

    四、 截获对象

    前边谈的都是Block对普通C语言自动变量的截获,下面来看看Block中使用oc对象时,是如何实现的。

        Model *model = [[Model alloc] init];
        model.name = @"lili";
        void(^blk)(void) = ^{
            NSLog(@"%@",model.name);
        };
        blk();
    

    转化后的代码为:

    struct __test1__test_block_impl_0 {
      struct __block_impl impl;
      struct __test1__test_block_desc_0* Desc;
      Model *model;
      __test1__test_block_impl_0(void *fp, struct __test1__test_block_desc_0 *desc, Model *_model, int flags=0) : model(_model) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    static struct __test1__test_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __test1__test_block_impl_0*, struct __test1__test_block_impl_0*);
      void (*dispose)(struct __test1__test_block_impl_0*);
    } __test1__test_block_desc_0_DATA = { 0, sizeof(struct __test1__test_block_impl_0), __test1__test_block_copy_0, __test1__test_block_dispose_0};
    
    static void __test1__test_block_copy_0(struct __test1__test_block_impl_0*dst, struct __test1__test_block_impl_0*src) {_Block_object_assign((void*)&dst->model, (void*)src->model, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    static void __test1__test_block_dispose_0(struct __test1__test_block_impl_0*src) {_Block_object_dispose((void*)src->model, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    static void _I_test1_test(test1 * self, SEL _cmd) {
        Model *model = ((Model *(*)(id, SEL))(void *)objc_msgSend)((id)((Model *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Model"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)model, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_6j_k3qjqfy94gl_jnc3llgm1nvr0000gn_T_test1_58f22c_mi_0);
        void(*blk)(void) = ((void (*)())&__test1__test_block_impl_0((void *)__test1__test_block_func_0, &__test1__test_block_desc_0_DATA, model, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    
    1. 我们会发现,Block生成的结构体实例中多了个成员变量,为对象即指针,它与Block引用的原对象类型完全一致(包括所有权修饰符),指向对象在堆中生成的内容。(我们在ARC的时候提到过,结构体中不能含有oc对象,但是此处oc的运行时能够准确的把握Block从栈复制到堆上以及堆上Block被废弃的时机。我的理解是我们写进去的,编译器不知道怎么处理,此处是编译器自己写进去的,它完全知道)
    2. copy函数和dispose函数
    • copy函数中的_Block_object_assign函数相当于retain实例方法,将对象赋值在结构体成员变量中的对象类型。
    • dispose函数中的_Block_object_dispose函数相当于release实例方法,释放赋值在结构体成员变量中的对象类型。
    1. copy函数和dispose函数调用的时机
    • copy函数:当栈上的Block被复制到堆时,会生成新的Block结构体实例,此时会调用copy函数,将原对象赋值在结构体成员变量中的对象类型。
    • dispose函数:当堆上的Block结构体被废弃时,会调用dispose函数,释放赋值在结构体成员变量中的对象类型。

    另外,堆上的Block结构体都是通过从栈上复制过来的,也就是有堆上的结构体之前一定先有栈上的结构体。我们可以打印一下引用计数:

    Model *model = [[Model alloc] init];
        NSLog(@"retainCount:%d",_objc_rootRetainCount(model));
        model.name = @"lili";
        void(^blk)(void) = ^{
            NSLog(@"%@",model.name);
        };
        NSLog(@"retainCount:%d",_objc_rootRetainCount(model));
        blk();
    

    打印出来结果为:

    2017-09-06 14:07:54.002 BlockTest[42344:3847518] retainCount:1
    2017-09-06 14:07:54.002 BlockTest[42344:3847518] retainCount:3
    

    可以发现经过Block之后,mode对象的引用计数增加了两次,应该栈上的Block结构体引用一次,复制到堆上的时候堆上的结构体又引用一次。栈上的结构体引用时直接通过model对象指针赋值给Block结构体成员变量中的对象类型。堆上的Block结构体是通过copy方法来完成了成员变量的对象引用。
    (最后这里只是我的个人理解,如果理解有问题,望留言指正。)

    前边说了,copy函数是栈上Block复制到堆上时执行,那么问题来了。

    1. 什么时候栈上的Block会复制到堆呢?(划重点)
    • 调用Block的copy实例方法时
    • Block作为函数返回值时
    • 将Block赋值给__strong修饰符的id类型或Block类型的成员变量时
    • 向方法名中含有usingBlock的Cocoa方法或GCD的API中传递Block时

    所以,在Block中使用对象类型自动变量时,除了以上情形外,推荐调用Block的copy实例方法。

    五、__block变量和对象

    __block说明符可以指定任何类型的自动变量。下面就看一下指定oc对象类型。
    看代码:

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

    通过clang转化如下:

    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;
    };
    
    static void _I_test11_test(test11 * self, SEL _cmd) {
        __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 *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))};
    }
    

    可以看到,
    1.通过__block修饰的对象类型也变成了一个结构体实例(这与__block修饰的普通C语言自动变量一样),原对象实例(直接想成指针更好理解)作为一个成员变量被包含于该结构体实例中。
    2.当Block被复制到堆时,该__block结构体实例也会被复制到堆中,堆上新建的__block变量的结构体实例,通过copy函数(_Block_object_assign函数)持有赋值给它的结构体实例中的对象。当堆上的__block变量被废弃的时候,使用_Block_object_dispose释放赋值给__block变量的对象。

    • 所以可以看出,在使用"对象所指向的内容"时与不加__block修饰符时相同,即加不加__block效果都一样(下边有补充的更通俗的说法),加了以后只不过多了一层结构体的包装。
    • __weak和__unsafe_unretained时,加不加__block也一样。
    • __autorelease 和__block一起时,会编译错误。

    补充,__block修饰oc对象的时候和__block修饰普通C语言自动变量的时候还有个区别,就是对象本身其实是指针,它其实包含两个元素:
    比如:

    NSMutableArray *array = [[NSMutableArray alloc] init];
    
    1. 指针本身,即array。
    2. 还有一个就是指针指向的值,即生成的数组空间。
      所以,对于oc对象来说,
    • __block修饰之前,指针在栈上,值在堆上,所以指针指向不能修改,值可以修改,比如addObject增加内容。
    • __block修饰之后,指针指向和值内容,都在堆上,都可以修改了。

    六、循环引用

    如果Block中使用了__strong修饰符的对象类型自动变量,那么当Block从栈复制到堆的时候,该对象被Block所持有。这样容易引起循环引用。
    可以使用__weak修饰对象,这样Block不会持有对象。
    也可以使用__block修饰符,比如:

    __block Model *tmp = self;
        _myBlock = ^{
            NSLog(@"name:%@",tmp.name);
            tmp = nil;
        };
        _myBlock();
    

    只要_myBlock()执行以后,Block不再持有self,则不会有问题了。

    Block与使用__weak的比较:

    优点:

    • 使用__block可以控制对象的持有期间。只要Block持有该对象,则该对象就不会释放。不想使用的时候再释放,可保证使用的时候该对象一定存在。
    • 在不想使用__weak的时候,不用不得已的去使用__unsafe_unretained修饰符(不必担心悬垂指针,野指针),直接用__block就行。

    缺点:

    • 必须执行Block才能避免循环引用。

    七、ARC无效时

    1. ARC无效时,一般需要手动将Block从栈复制到堆,由于ARC无效,所以肯定要释放。这里我们使用copy实例方法来复制,使用release实例方法来释放。
    2. Blocks是C语言的扩展,所以在C语言中使用Block语法,此时使用Block_copy和Block_release函数来代替实例方法。
    3. __block可以在ARC无效时避免循环引用
      这是由于ARC无效时,当Block从栈复制到堆时,若Block使用的变量为附有__block说明符的id类型或对象类型的自动变量,不会被retain;若Block使用的变量为没有__block说明符的id类型或对象类型的自动变量,则被retain。
      代码如下:
    __block Model *tmp = self;
        _myBlock = ^{
            NSLog(@"name:%@",tmp.name);
        };
    

    可以看出,ARC下通过在Block中置空对象来实现避免循环引用;非ARC下仅加个修饰符即可。

    注意:ARC有效和无效时,__block的作用差别很大,所以一定要弄清楚。

    相关文章

      网友评论

          本文标题:iOS-Blocks

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