美文网首页
block 那点儿破事

block 那点儿破事

作者: seej | 来源:发表于2018-04-11 17:51 被阅读0次

    在日常开发中,我们常常会定义block将一段代码保存起来等待合适的时机调用来完成一系列的操作(hehe...出bug了吧)。我们知道block中无法修改引用的外部变量除非使用 __block 修饰外部变量,使用block时需要格外注意循环引用。在文章中我们将会详细探讨究竟是什么原因导致了上述问题。

    从hello word开始

    编写hello word是一位程序员必备的编程技能,为了提升我们的逼格与社会接轨,我们也来写一段hello word。

    int main() {
        void(^block)(void) = ^{
            printf("hello word");
        };
        block();
        return 0;
    }
    

    很简单是不是? 然而事情并没有那么简单,此hello word非彼hello word。
    接下来进入烧脑环节 ^_^ ,有条件的读者可以先喝罐一二三四五六七八个核桃...
    接下来我们借助clang编译器,将这段block转换为C++代码。在控制台输入 clang -rewrite-objc 文件路径,我们会得到一个.cpp文件,打开文件找到 main 函数。

    int main() {
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        return 0;
    }
    

    Excuse me? 这一坨是什么鬼?懵逼吗?懵逼就对了因为代码没贴全😊😊😊。

    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;
        }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        
        printf("hello word");
    }
    
    static struct __main_block_desc_0 {
        size_t reserved;
        size_t Block_size;
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
    int main() {
        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        return 0;
    }
    

    好了,贴全了,各位是不是有种恍然大悟茅塞顿开的感觉。
    路人甲乙丙丁戊己庚辛壬癸:你TM在逗我...
    哎哎哎,别急我话还没说完呢麻烦各位刀先收一收,大家都是文明人。
    那什么... 二营长,你他娘的意大利炮能不能先抬回去!
    #@#¥¥#...

    咳咳... 好了我们先切入正题。
    接下来我们逐行分析。

    void(^block)(void) = ^{
        printf("hello word");
    };
    //对应C++代码
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    

    void(*block)(void):定义一个名为block的函数指针指向类型为 void(*)(void) /无返回值无参 的函数。而从代码中可以看出实际上block指向的并非一个函数而是一个结构体对象。
    __main_block_impl_0:block结构体 __main_block_impl_0表示这个block是名为mian的函数中定义的第0个block。(第0个?emm... 有毛病?没毛病!)

    struct __main_block_impl_0 {
    //可以理解为block的基类,所有block结构体都包含__block_impl中定义的成员变量
    struct __block_impl impl;
    //block描述信息
    struct __main_block_desc_0* Desc;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        //block存储区域 _NSConcreteStackBlock栈  _NSConcretGlobalBlock全局 _NSConcretMallocBlock堆
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        //函数指针 指向代码块所对应的函数
        impl.FuncPtr = fp;
        //block描述信息
        Desc = desc;
    }
    

    __main_block_func_0:block对应的函数,函数的实现由block中包含的代码转换而来。这个函数接收__main_block_impl_0类型的结构体对象做为入参。

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        //__cself的作用在此我们先不做解释后文中讲解__block时在详细说明。
        //我们的block中输出hello word的代码
        printf("hello word");
    }
    

    __main_block_desc_0_DATA:包含block的描述信息。

    static struct __main_block_desc_0 {
        size_t reserved;
        //结构体大小
        size_t Block_size;
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
    

    block();
    //对应C++代码
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    

    上文中我们提到block指针指向的并非是一个函数,而是一个block结构体对象,所以在这里我们通过block指针找到结构体对象中的FuncPtr指针,FuncPtr指针指向的才是我们需要调用的函数,并将block结构体对象做为入参。(千万不要看着一堆圆括号就感觉头大,不妨将圆括号中的内容拆解一下可能会比较好理解)

    (
        //类型转换 将FuncPtr强转为void (*)(__block_impl *)类型的函数指针
        (void (*)(__block_impl *))
        //取FuncPtr
        ((__block_impl *)block)->FuncPtr
    )
    //将block转为__block_impl * 类型做为入参 调用FuncPtr
    ((__block_impl *)block);
    

    到这里大家是不是对block有了一些不一样的理解呢?
    如果没有请把上面的内容再认真阅读一遍。

    我们知道在block中是不能修改引用的外部变量的,如果想要修改那么我们需要使用__block来修饰外部变量,接下来我们就来看一看__block到底对我们的代码做了什么。

    咦? 二营长人呢?
    路人甲:他去看上面的内容了。
    emm...


    __block? 放开那行代码,让我来!

    在讲__block之前我们不妨先看看下面的代码

    int main () {
        int a = 10;
        void(^block)(void) = ^{
            printf("%d",a);
        };
        block();
        return 0;
    }    
    

    打开生成的C++文件我们可以发现__main_block_impl_0的成员变量中多了一个 int a;

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

    __main_block_impl_0结构体的构造函数中多了一个int类型的参数_a,_a最终会被赋值给结构体中的成员变量a。而结构体中的成员变量a、函数参数_a、外部变量a他们的内存地址都不相同。当执行过block的语法后,即使外部对a重新赋值也不会改变block结构体中a的值(修改的不是同一块内存)。
    讲道理我们是可以修改结构体中成员变量a的值的,但是当我们修改a的值时编译器会无情的给我们一个大大的报错。

    xCode:兄dei,这里的a和外面的a不是同一个a,你改这里的a外面的a不会变的,哈哈哈...
    程序员0:哦,__block。
    xCode:咳咳... 老夫纵横江湖这么多年,见过的程序员连起来能绕地球两圈,你是他们中最优秀的,没有之一!
    程序员0:哦。


    为了能够在block内部修改引用的外部变量的值,我们需要用__block修饰外部变量。

    int main () {
        __block int a = 10;
        void(^block)(void) = ^{
            a = 20;
            printf("%d",a);
        };
        block();
        return 0;
    }
    

    同样转为C++代码

    struct __Block_byref_a_0 {
        void *__isa;
        __Block_byref_a_0 *__forwarding;
        int __flags;
        int __size;
        int a;
    };
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_a_0 *a; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_a_0 *a = __cself->a; // bound by ref
    
                (a->__forwarding->a) = 20;
                printf("%d",(a->__forwarding->a));
    }
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t 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), __main_block_copy_0, __main_block_dispose_0};
    int main () {
            __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
            void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
            ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
            return 0;
    }
    

    说实话,确实有点辣眼睛... 不过别担心,同样的我们逐行分析。

    __block int a = 10;
    //您的__block请查收
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
    

    我们的int a变成了__Block_byref_a_0 a,what? __Block_byref_a_0又是什么鬼。

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

    __Block_byref_a_0结构体中包含一个本身类型的指针 __forwarding 和一个 int 类型的变量a 。当我们使用__block修饰变量a时变量被转换成了上述结构体对象a,而这个结构体中的__forwarding会被赋值为本身的地址,__forwarding的作用会在后续讲解block在内存中的存储区域时讲到,这里我们先不管他。我们对a赋值会转换为对结构体中的成员变量a赋值。

    void(^block)(void) = ^{
        a = 20;
        printf("%d",a);
    };
    //C++
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    

    block:函数指针,指向的实际是一个block结构体。
    __main_block_impl_0:我们的block被转换为结构体

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      //指向外部变量a
      __Block_byref_a_0 *a; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    __main_block_func_0:其中包含了block中的代码。

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        __Block_byref_a_0 *a = __cself->a; // bound by ref
        //通过__forwarding来修改结构体中a的值确保修改的正确性
        (a->__forwarding->a) = 20;
        printf("%d",(a->__forwarding->a));
    }
    

    前面我们提到过__forwarding指向的是自己的地址,为什么这里不直接对成员变量a赋值而是通过__forwarding找到自己然后在对自己的成员变量a赋值呢?可能大家会感觉这样做是多此一举,但是现实很残酷。我们后面会详细介绍。

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

    至于这一行我们就不讲了,不懂请翻前面的内容。
    好吧实际上到这里为止好像我们只知道__block把我们的 int a 转换成了 __Block_byref_a_0 a ,还挖了一堆坑。
    其实__block只做了一件事情,那就是把我们的变量转换为结构体,至于为什么这样做我们还需要先了解一下block的存储区域。


    block存储区域
    _NSConcreteStackBlock 保存在栈中的block,出栈时会被销毁
    
    _NSConcreteGlobalBlock 全局的静态block,不会访问任何外部变量
    
    _NSConcreteMallocBlock 保存在堆中的block,当引用计数为0时会被销毁
    

    当我们把block作为全局变量使用时,生成的block将被存储在全局区,对应的impl.isa会被设置为 &_NSConcreteGlobalBlock。除此之外block创建时内存分配在栈上impl.isa会被设置为 &_NSConcreteStackBlock,作用域结束block会在出栈时被销毁。那么问题来了,如果我们需要block脱离当前作用域的限制该怎么办呢?这时候_NSConcreteMallocBlock就粉墨登场了。
    将栈上的block复制到堆上,这样当栈上的block超过他的作用域时,堆上的block还可以继续存在,被复制到堆上的block结构体成员变量isa将变为 &_NSConcreteMallocBlock。

    impl.isa = &_NSConcreteMallocBlock;
    

    __forwarding

    当我们将一个栈上的block复制到堆上时,与其相对应的__block变量同样会被复制到堆上,此时我们拥有两份block以及__block变量,并且他们的内存地址不同,此时问题就出现了。
    栈上的block结构体存储的是栈上的__block变量而堆上的block结构体存储的是堆上的__block变量,当我们在其中一个block中修改__block变量时,另一个block结构体中的__block变量如何进行同步?
    此时我们的__forwarding终于派上了大用处,当我们将栈上的__block变量拷贝到堆上时,将栈上的变量的__forwarding指向堆上的变量,堆上的变量的__forwarding指向自己,这样无论是在栈上还是堆上__forwarding始终指向堆上的变量,当我们通过__forwarding修改变量时修改的都是堆上的变量。

    copy
    因此就有了我们上面见到的代码
    (a->__forwarding->a) = 20;
    

    为什么block属性需要使用copy修饰

    在ARC下,使用strong和copy都是一样的,因为在访问/修改外部变量的时候,block都是在堆区,苹果官方建议使用copy

    在MRC下,单纯的Block是存放在全局/常量区的,如果Block访问/修改外部变量后,block存放在了栈区,在栈区是不可以全局共享的,只有堆区的对象,变量才会被全局共享,所以使用copy拷贝一份Block到堆区中,这样Block才会全局共享


    关于循环引用

    程序员1:呦,哥们,撸代码呢。
    程序员0:嗯(self.xxx = xxx)。
    程序员1:哎哎哎,哥们,你这里循环引用了,block里面怎么能直接用self,快weak weak。
    程序员0:滚犊子!

    block中使用self就一定会循环引用吗? 呵呵...

    造成循环引用的条件是出现引用环,而解决的方案也很简单,打破引用环。
    我们先看看下面的代码

    @implementation RetainCircle {
        void(^_block)(void);
    }
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            void(^block)(void) = ^{
                NSLog(@"%@",self);
            };
            block();
            _block = ^{
                NSLog(@"%@",self);
            };
            _block();
        }
        return self;
    }
    
    - (void)dealloc {
        NSLog(@"GG思密达");
    }
    
    @end
    

    问:有循环引用吗?
    这两个block都引用了self,block和self的关系无非就是被self持有和不被self持有,上面的代码所有的组合都占了,那必须有循环引用啊,这还用问吗?
    程序员嘛,就这一点好,逻辑推理能力6的不要不要的,来来来,双击666,小礼物走一波~

    咳咳...其实我想问的是哪一个block有循环引用。

    上面两个block中都持有了self,而_block被self持有,这样就造成了_block持有self,self持有_block。

    _block:哥,要不你先挂?
    self:凭什么我先挂,我是你哥,要挂也轮不到我。
    _block:hehe...
    block:两位哥哥,小弟先走一步,勿念...

    所以当我们在block中使用self时并不一定会造成循环引用。
    如果造成了循环引用也不要担心,我们是有解决方案的,先平复一下心情,下面我们看一看解决方案。

    解决方案:

    略。

     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     

    啊,不好意思各位,翻错页码了,见谅见谅。

    MRC环境中:
    新建一个__block的局部变量,并把self赋值给它,而在block内部则使用这个局部变量来进行取值。因为__block标记的变量是不会被自动retain的。

    __block typeof(self) mSelf = self;
    _block = ^{
        NSLog(@"%@",mSelf);
    };
    

    ARC环境下:
    __block要换成__weak,因为ARC环境下自动释放池会自动做引用计数的增减。此处我们需要一个弱引用对象controller指向self对象,这样即便在block中使用了controller,由于它是一个弱引用,可以使用self的地址空间但是并不会造成引用计数加1

    __weak typeof(self) weakSelf = self;
    _block = ^{
        NSLog(@"%@",weakSelf);
    };
    

    补充 2018-5-18

    关于解决block的循环引用还需要了解的一件事情

    如果我们的block中引用了成员变量该怎么解决循环引用呢?


    image.png

    当然是weak啦


    image.png
    信度娘,得永生

    Excuse me?

    image.png

    那你让我strong可别怪我不客气了。


    image.png

    哎呦喂!好了。开心,撸斤小龙虾先。

    为什么在block里面需要使用strong
    是为了保证block执行完毕之前self不会被释放,执行完毕的时候再释放。这时候会发现为什么在block外边使用了__weak修饰self,里面使用__strong修饰weakSelf的时候不会发生循环引用?!
    __strong修饰的self只是为了保证在block内部执行的时候不会释放,但存在执行前self就已经被释放的情况,导致self=nil。注意判空处理。

    不会引起循环引用的原因
    因为block截获self之后self属于block结构体中的一个由__strong修饰的属性,会强引用self, 所以需要使用__weak修饰的weakSelf防止循环引用。
    block使用的__strong修饰的self是为了在block生命周期中self不会提前释放。self实质是一个局部变量(在block这个“函数”里面的局部变量),当block执行完毕就会释放自动变量self,不会对self进行一直进行强引用。

    参考

    iOS内存管理---block机制详解
    iOS 关于 __block 底层实现机制的疑问?
    blocksruntime
    iOS Block底层实现原理详解
    你真的理解__block修饰符的原理么?

    相关文章

      网友评论

          本文标题:block 那点儿破事

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