美文网首页iOS
Objective-C Block篇(二) : Block捕获自

Objective-C Block篇(二) : Block捕获自

作者: Tenloy | 来源:发表于2018-09-24 20:16 被阅读30次

    目录知识点

    • 现象:打印栈上、堆上Block截获的自动变量在Block定义前中后的地址与引用计数变化
    • 带有(截获)自动变量(局部变量:对象与非对象)值机制及原理
    • __block存储域说明符的机制及原理
    • __block变量的存储域

    现象(先从现象入手)

    《Effective Objective-C 2.0编写高质量iOS与OS X代码的52个有效方法》中说道:
    块默认会把所有捕获的局部变量copy一份到该块所在的内存空间。捕获了多少个变量,就要占据多少内存空间。(注意:拷贝对象时,拷贝的并不是对象本身,而是指向这些对象的指针变量
    block作用域完成,销毁的时候,会把捕获的对象release一次

    实践:
    使用栈上Block、堆上Block分别捕获自动变量(对象类型、基本类型),打印了定义前、中、后该变量名(不是指向的内容地址)的地址变化,以及对象类型的引用计数变化

    栈上的block捕获局部变量时
        没有__block修饰时,block定义中&取地址,都变了,"前、后一致",不过都是在"栈上",对象的应用计数+1,block销毁时,对象引用计数-1
        用__block修饰之后,&取地址,"前、中、后一致不变,都在栈上"。对象的应用计数不变
    
    //-----------------------比较分析-----------------------
    
    栈上的block以上的现象与"函数参数传递"时现象比较:
        "现象与函数保留参数的现象一模一样,不过__block在函数(方法)参数传递中是无效的。"
        像block这种,新创建一份地址,存储着复制而来的数据传递过去的,我们通常叫值传递。
        通常我们所指的对象是地址传递,其实意思是堆上的地址传递过去。变量名在栈上的地址其实也是复制一份传过去的。
    
    因为block本质上就是函数,函数就是这样的。
    函数举例:
      C中,在A函数中,给B函数传过去一个对象名,B中对对象修改可以,A中能同步,但是直接对对象赋值,在A中是无效的,A中还是最初的值。
    
    "OC可能是基于这一点,在编译的层面就优化了,防止开发者可能犯的错误".因为默认没法在Block中改变外部变量的值,所以编译过程中就报编译错误。
    "__block是在函数的基础上,扩展的功能(因为block比函数灵活了很多,所以同时的要更注意灵活带来的程序开发中引起的问题)"
    
    //-----------------------比较分析-----------------------
    
    堆上的block(栈上的block copy到堆上)捕获局部变量时:
    
        没有__block修饰时,block定义中&取地址,都变了,"前、后一致,在栈上",block定义"中的对象名(对象指针)被从栈上拷贝到了堆上"
            不管调用几次copy方法,对象引用计数+1 ,销毁时,对象引用计数-1     
            如果是赋值给一个Block类型变量,不管赋值几次都 + 2,再copy无效,还是+2。销毁时,对象引用计数-2
        用__block修饰后,&取地址,"中、后一致(哪怕block定义马上调用并作用域完成,销毁),在堆上",与前不同,"前在栈上"(对象名、非对象本来就在栈上)。对象引用计数不变
        "栈上的__block变量,在__block变量从栈上复制到堆上的时候,会将成员变量__forwarding的值替换为复制到的目标堆上的__block变量的地址`。下面会详细解释"
    
    /* MRC下Block引用obj对象的引用计数变化,没深究
    
      MRC下block引用obj对象
        block copy时:
            没有__block修饰
                不管调用几次copy方法,obj对象引用计数+1    
                直接赋值给Block类型变量,引用计数不变           
            有__block修饰,obj对象引用计数不变
        block没有copy,即栈上的block引用obj对象时:
            不管有没有__block修饰,obj对象引用计数都是不变的
     */
    
    //捕获全局变量、静态全局变量、静态局部变量
    对于 static 变量,全局变量,在 block中是有读写权限的,因为此变量存于全局数据区(非栈区),不会随时释放掉,也不会新开辟内存创建变量。
    block 内可以直接使用指向这些变量的指针,可以修改原变量。
    "定义的前中后 打印这三种类型的变量地址,都是一致的"
    Block的引用没有对其造成任何影响。此时的Block的存储域在全局上
    
    

    综上所述:

    在block定义时
        没有__block修饰,只拷贝一份指针出来,所以引用计数增加。`会对捕获的对象有一份强引用`
        __block修饰时,相当于对定义前的局部变量重新赋值了一个栈上、堆上的新地址(存储内容不变,还是局部变量数据的指针地址),将原先的变量名(栈上)的给释放了,所以`定义中、后打印表现引用计数不变。`
            此时,"作用域不变。不过block对局部变量还是存在强引用,不过由于__block修饰,所以在block中可以将局部变量置为nil,可以手动控制断开循环引用"
    

    为什么__block修饰之后才能改?
    根据上面的现象推测:
    __block修饰之后,能保证在block之后用到的该变量跟block中的该变量是一个变量,block中的变量跟函数一样默认(没有__block修饰)是与前后不一样的。是单独拷贝出来的一份对象名(对象指针,也指向堆上对象,但不是拷贝对象),对这个变量名赋值是没有意义的。

    __block修饰的变量,在block定义后,还可以修改,之后的block调用时,是变量的最新赋值

    带有(捕获)自动变量值

    正确理解Block的定义:带有自动变量的匿名函数.

    关于"本质是匿名函数"在上一篇中已经讲过。正确理解"带有自动变量值",也还是需要从Block的底层实现是函数出发

    # 机制

    Block会自动截获定义语法中所使用到的自动变量(全局变量、静态自动变量、静态全局变量不用截获,因为作用域的原因,可直接使用)的值,即保存该自动变量的瞬间值

    • 定义后(保存后)修改,对调用时没有任何影响。
    • 定义中修改自动变量值编译器报错

    捕获非对象自动变量

    • 可以使用,但是不能赋值变量

    捕获对象自动变量

    • 可以使用对象的任何方法(比如:mutableArray的addObject都可以),但是不能赋值变量
    • 可以使用、赋值修改对象的属性

    注意:
    Blocks中,截获自动变量的方法并没有实现对C语言数组的截获

    const char text[] = "hello";
    在Block中使用text,获取元素是会编译报错的
    //可以使用指针
    const char * text = "hello";
    

    # 实现原理(跟C函数的值传递、地址传递结合理解)

    带有自动变量的值的原因是:Block需要保证定义中使用的自动变量在外部随时可能释放,所以Block需要保留该变量(全局、静态因为不会被释放,所以Block对此类变量没有操作)

    在上一篇中可以看到Block在没有拦截自动变量时的默认形式如下

    // Block类型变量对应的结构体
    struct __main_block_impl_0 {
        struct __block_impl imp1;   
        struct __main_block_desc_0* Desc;
        //默认构造函数,C++中的语法
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
            imp1.isa = &_NSConcreteStackBlock;
            imp1.Flags = flags  //不传默认=0,Reserved默认也是0
            imp1.FuncPtr = fp;
            Desc = desc:
        }
    };
    
    //该函数的参数__cself相当于C++实例方法中指向实例自身的变量this,或是Objective-C实例方法中指向对象自身的变量self
    //即参数__cself为`指向Block值的变量`
    static void __main_block_func_0(struct __main_block_impl_0* cself)
    {
        printf("Block\n");
    }
    
    //今后版本升级所需的区域和Block的大小
    static struct __main_block_desc_0{
        unsigned long reserved;
        unsigned long Block_size;
    } __main_block_desc_0_DATA = {
        0,
        sizeof(struct __main_block_impl_0)   //block对应结构体的实例大小
    };
    
        //定义
         void (*blk)(void) = (void(*)(void)) &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);  
        //调用
        ( (void (*)(struct __block_impl *)) ((struct __block_impl *)blk)->FuncPtr )((struct __block_impl *)blk);
    
    

    Block将语法表达式中使用到的自动变量作为成员变量追加到了Block类型变量对应的结构体(__main_block_impl_0)中

    ## Block拦截非OC对象自动变量

    反编译代码:

    // Block类型变量对应的结构体
    struct __main_block_impl_0 {
        struct __block_impl imp1;   
        struct __main_block_desc_0* Desc;
       "const char * fmt;
        int val;"
        //默认构造函数,C++中的语法
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, "const char * _fmt, int _val,"  int flags=0)" : fmt(_fmt), val(_val)"{
            imp1.isa = &_NSConcreteStackBlock;
            imp1.Flags = flags  //不传默认=0,Reserved默认也是0
            imp1.FuncPtr = fp;
            Desc = desc:
        }
    };
    
    //该函数的参数__cself相当于C++实例方法中指向实例自身的变量this,或是Objective-C实例方法中指向对象自身的变量self
    //即参数__cself为`指向Block值的变量`
    static void __main_block_func_0(struct __main_block_impl_0* cself)
    {
        "
          const char * fmt = __cself->fmt;
          int val = __cself->val;
          printf(fmt, val);
        "
    }
    
    //今后版本升级所需的区域和Block的大小
    static struct __main_block_desc_0{
        unsigned long reserved;
        unsigned long Block_size;
    } __main_block_desc_0_DATA = {
        0,
        sizeof(struct __main_block_impl_0)   //block对应结构体的实例大小
    };
    
        //定义
         void (*blk)(void) = (void(*)(void)) &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA ", fmt, val");  
        //调用
        ( (void (*)(struct __block_impl *)) ((struct __block_impl *)blk)->FuncPtr )((struct __block_impl *)blk);
    
    
    ## Block拦截OC对象自动变量

    现状:

    1. Block将语法表达式中使用到的自动变量作为成员变量追加到了Block类型变量对应的结构体(__main_block_impl_0)中,
    2. C语言结构体不能含有附有__strong修饰符的变量,因为编译器不知道何时进行C语言结构体的初始化和废弃操作,不能很好的管理内存。

    拦截NSArray对象自动变量反编译代码:

    "=========OC代码========="
    id array = [[NSMutableArray alloc] init];
    blk = [^(id obj) {
        [array addObject: obj];
        NSLog(@“array count = %ld”, [array count]);
    } copy];
    
    blk([NSObject new]);
    
    "========反编译代码========"
    // Block类型变量对应的结构体
    struct __main_block_impl_0 {
        struct __block_impl imp1;   
        struct __main_block_desc_0* Desc;
       "id __strong array;"
        //默认构造函数,C++中的语法
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, "id __strong _array,"  int flags=0)" : array(_array)"{
            imp1.isa = &_NSConcreteStackBlock;
            imp1.Flags = flags  //不传默认=0,Reserved默认也是0
            imp1.FuncPtr = fp;
            Desc = desc:
        }
    };
    
    //该函数的参数__cself相当于C++实例方法中指向实例自身的变量this,或是Objective-C实例方法中指向对象自身的变量self
    //即参数__cself为`指向Block值的变量`
    static void __main_block_func_0(struct __main_block_impl_0* cself ", id obj")
    {
        "
        id __strong array = __cself->array;
        [array addObject:obj];
        NSLog(@“array count = %ld”, [array count]);
        "
    }
    
    /*
    使用_Block_object_assign函数将 对象类型 对象赋值给Block结构体的成员变量array,并持有该对象
    _Block_object_assign函数调用相当于retain实例方法的函数
    */
    "static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 * src)
    {
      _Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
    }"
    
    /*
     Block_object_dispose函数调用相当于release实例方法的函数,释放Block结构体成员变量array中的对象
    */
    "static void __main_block_dispost_0(struct __main_block_impl_0 * src)
    {
      _Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
    }"
    
    //今后版本升级所需的区域和Block的大小
    static struct __main_block_desc_0{
        unsigned long reserved;
        unsigned long Block_size;
       "void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0 *);
        void(*dispose)(struct __main_block_impl_0 *);"
    } __main_block_desc_0_DATA = {
        0,
        sizeof(struct __main_block_impl_0),   //block对应结构体的实例大小
       "__main_block_copy_0,
        __main_block_dispose_0"
    };
    
        //定义
         void (*blk)(void) = (void(*)(void)) &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA ", fmt, val");  
        //调用
        ( (void (*)(struct __block_impl *)) ((struct __block_impl *)blk)->FuncPtr )((struct __block_impl *)blk);
    
    

    内部实现:
    __main_block_desc_0结构体中增加的成员变量copy和dispose , 以及作为指针赋值给该成员变量__main_block_copy_0函数和__main_block_dispose_0函数。

    • 用途:在上面代码段中有注明
    • 调用时机:在上面转换的源码中,没有发现调用。在Block从栈赋值到堆上(即_Block_copy函数)以及堆上的Block被废弃时才会调用这些函数
    • 在__block变量时,也是这两个方法,不过其中有些不同,对象是BLOCK_FIELD_IS_OBJECT, __block变量是BLOCK_FIELD_IS_BYREF,通过这两个参数区分两种类型。不过持有、释放时机与机制都是一样的。

    这是为什么堆上的Block捕获的对象与__block类型变量能超出变量作用域而存在

    __block存储域说明符

    # 机制

    为什么Block拦截的自动变量为什么默认不能修改?
    C函数对参数的处理机制是:对自动变量-非对象是值传递,对自动变量-对象,是复制栈上的指针变量(对象名)
    Block被定义为匿名函数,是借鉴了C函数的机制,其实是拿不到实质的自动变量,修改没有意义,对外部没有影响。
    为什么Block要如C一样对其进行复制?
    因为如果直接使用最初的自动变量,当变量作用域结束的同时,原来的自动变量也会被废弃。将不能通过指针(地址)访问原来的自动变量。Block如同C一样,不做额外操作,是可以直接访问,修改静态变量、静态全局变量、全局变量,不用担心作用域问题

    使用附有__block说明符的自动变量可在Block中修改、赋值,此类变量称为__block变量

    # 实现原理

    C语言有以下存储域类说明符:

    • typedef
    • extern
    • static 表示作为静态变量存储在数据区中
    • auto 表示作为自动变量存储在栈中
    • register

    __block说明符类似于static、auto和register,用于指定将变量值设置到哪个存储域中

    "block拦截__block变量与拦截全局、静态全局变量的代码分析"
    1. 拦截全局、静态全局变量
        没有形态改变,在源码中也是以全局变量、静态全局变量形态存在
        原处定义相同的代码,__main_block_func_0中直接使用变量
        "其他与默认的Block的源码一样"
    
    2. 拦截静态自动变量(与上面看到的拦截自动变量形式一样)
        //拦截静态自动变量:static int static_auto_val = 3;
        - 在原处定义static int static_auto_val = 3;
        - 在__main_block_impl_0中增加了成员变量 int * static_auto_val; 指向该静态变量的指针
        "其他没变化"
    
    3. 拦截__block变量
        拦截 
            __block int val = 10;
            __block id __strong(默认) obj = [[NSObject alloc] init];
    //源码:
        //__block变量如同Block一样变为了结构体__Block_byref_val_0实例、__Block_byref_obj_0实例
    struct __Block_byref_val_0 {
      void * _isa;
      __Block_byref_val_0 * __forwarding;  //指向自身,确保__block是配置在堆上还是在栈上,都可以通过这个指针正确访问栈上的__block变量和堆上的__block变量
      int __flags;
      int __size;
      int val;  //原先变量值,意味着该结构体持有相当于原自动变量的成员变量
    }
    
    static void __main_block_copy_0 (struct __main_block_impl_0*dst, struct __main_block_impl_0*src){
      __Block_object_assign(&dst->val, src-val, Block_FIELD_IS_BYREF);
    }
    static void __main_block_dispost_0(struct __main_block_impl_0 * src)
    {
      _Block_object_dispose(src->array, Block_FIELD_IS_BYREF);
    }
    
    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*);
      __strong id obj;
    }
    
    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_dispose_131(void* src)
    {
      _Block_object_dispose(*(void * *)   ((char *)src+40), 131);
    }
    
    __main_block_impl_0结构体新增成员变量__Block_byref_val_0 *val;
    static void_main_block_func_0(struct __main_block_impl_0 * __cself){
      __Block_byref_val_0 * val = __cself->val;
      (val->__forwarding->val);  //具体使用
    }
    
    //今后版本升级所需的区域和Block的大小
    static struct __main_block_desc_0{
        unsigned long reserved;
        unsigned long Block_size;
       "void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0 *);
        void(*dispose)(struct __main_block_impl_0 *);"
    } __main_block_desc_0_DATA = {
        0,
        sizeof(struct __main_block_impl_0),   //block对应结构体的实例大小
       "__main_block_copy_0,
        __main_block_dispose_0"
    };
    
    
        //定义
        __Block_byref_val_0 val = {
            0,
            &val,
            0,
            sizeof(__Block_byref_val_0),
            10  //原先变量值
        }
        __Block_byref_obj_0 obj = {
            0,
            &obj,
            0x2000000,
            sizeof(__Block_byref_obj_0),
            __Block_byref_id_object_copy_131,
            __Block_byref_id_object_dispose_131,
            [[NSObject alloc] init];
        }
         void (*blk)(void) = (void(*)(void)) &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA ", &val, 0x22000000");  
        //调用
        ( (void (*)(struct __block_impl *)) ((struct __block_impl *)blk)->FuncPtr )((struct __block_impl *)blk);
    
    

    由此可见,block对__block修饰的OC对象,与未用__block修饰的,在内存管理上几乎是一致的,_Block_object_assign持有,_Block_object_disposes释放. _block修饰的OC对象,只要_block变量还在堆上存在,就不会释放。只不过,__block修饰的OC对象,不增加引用计数,block不retain变量

    以上对__block作用域修饰符的底部实现,做了反编译查看,那么__block变量具体是怎么运作的呢?

    • 在block复制到堆上的时候,__block变量也会复制到堆上,在block已经被复制到堆上的时候,再复制,对__block变量没有任何影响。当block被废弃的时候,__block变量也会被释放。
    • 当多个block引用__block变量时,引用计数增加。与OC的引用计数内存管理完全相同。从编译的源码来看,与block拦截OC自动变量时除了类型Block_FIELD_IS_BYREF不一样,其他都一样。
    • __forwarding成员变量指向自身,确保__block是配置在堆上还是在栈上,都可以通过这个指针正确访问栈上的__block变量和堆上的__block变量
    • 栈上的__block变量,在__block变量从栈上复制到堆上的时候,会将成员变量__forwarding的值替换为复制到的目标堆上的__block变量的地址 这也能解释为什么__block修饰过的指针变量(对象名),在block定义后,再打印对象名的地址,发现变为了堆上的地址。
    • 栈上的__block变量,在block不发生复制,一直在栈上的时候,也不会发生复制,会仍然在栈上,不过此时__forwarding指向自身,会将自身传入block,不跟默认一样,复制指针变量传入。
      最后两条也是为什么:__block修饰的变量可以修改?的原因。可以保证__block变量在block中的修改,对外部真实有效

    # __block与其他所有权修饰符结合

    上面,讲的都是__block与默认的__strong修饰的现象。

    __weak与block的结合使用
    block不管赋值、copy怎么操作,对外部__weak修饰的对象,不持有,这是避免循环引用的机制之一。

    __block与__weak同时使用
    现在与__weak单独使用是一致的

    __block与__autoreleasing不能同时使用

    # __block避免循环引用

    根本机制是:

      1. __block修饰时,引用计数不会增加,block不retain变量
      1. __block修饰时,可以在block中修改,置为nil,手动解除循环引用,但是比__weak的缺点是必须要执行block,要不然不触发置nil行为,还是会循环引用

    相关文章

      网友评论

        本文标题:Objective-C Block篇(二) : Block捕获自

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