block

作者: 小冰山口 | 来源:发表于2024-03-10 20:45 被阅读0次

    要了解什么是block, 我们先写一个block

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            void (^myBlock)(void) = ^{
                NSLog(@"this is a block");
            };
            myBlock();
        }
        return 0;
    }
    

    现在我写了一个简单的block
    利用

    
    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
    
    

    命令行生成编译完的C++代码, 发现block被编译后的样子:
    这是block的声明:

    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    

    这是block的调用:

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

    去掉一些无用的类型转换:

    void(*block)(void) = &__main_block_impl_0(
        __main_block_func_0, 
        &__main_block_desc_0_DATA
    );
    

    我们发现block实际上就是一个__main_block_impl_0函数的返回值的地址, 将地址赋值给一个名叫block的函数指针
    那么__main_block_impl_0是什么呢?

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

    我们发现__main_block_impl_0是一个结构体,

    要注意的是, 还有一个__main_block_impl_0的同名函数, 这是C++里定义的一个结构体的构造函数, 也就是说, 这个构造函数返回的是一个结构体struct __main_block_impl_0, 我们在上面看到的这个就是利用这个构造函数产生的一个struct __main_block_impl_0结构体
    void(*block)(void) = &__main_block_impl_0(
        __main_block_func_0, 
        &__main_block_desc_0_DATA
    );
    
    • 第一个参数__main_block_func_0,

      image.png
      通过这个NSLog就可以看出, 这是block这里面的代码实现.
    • 第二个参数&__main_block_desc_0_DATA
      这相当于是一个block的描述, 也是一个block,
      第一个成员变量是保留字段, 现在传的是0
      第二个成员变量是Block_size, 就是block的大小. 传的就是struct __main_block_impl_0的大小(size of)

      image.png

    参数传进去了之后就是给这个结构体赋值, 让我们再来看看这个结构体

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

    第一个成员变量impl, 类型是struct __block_impl是这样的:

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

    相当于block的内存布局是这样的:

    struct __main_block_impl_0 {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
      struct __main_block_desc_0* Desc;
    };
    
    我们发现block也是有isa指针的, 所以从本质上说, block也是OC对象
    image.png

    __main_block_func_0就是函数指针, 当做参数传进去构造函数, 然后在构造函数里传给结构体里的变量FuncPtr

    那我们来看一下block的调用:

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

    去掉一些类型转换

    myBlock->FuncPtr(myBlock)
    
    image.png

    实际上就是找到myBlock中保存的FuncPtr函数指针, 然后直接调用就好了

    我们再来看一下复杂一点的block

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int a = 10;
            void (^myBlock)(void) = ^{
                NSLog(@"this is a block--%d", a);
            };
            a = 20;
            myBlock();
        }
        return 0;
    }
    

    然后再编译成C++文件, 看看发生了什么:

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

    可以很清楚的看到, 这个block包含了一个新的成员变量int a, 这个block的内存布局就是:

    struct __main_block_impl_0 {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
      struct __main_block_desc_0* Desc;
      int a;
    };
    

    这个结构体的最后一个成员变量就是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;
      }
    

    这个构造函数多了一个参数, 就是int _a

    image.png

    而创建这个block的时候, 传进去的参数就是我定义的int a = 10, 传进去了之后, 就把_a的值, 赋给了结构体内部的成员变量a,

    : a(_a)
    

    这就是C++的语法, 将_a赋值给a, 所以外面的a和里面的a, 不是同一个东西, 外面的a, 是我定义的变量a, 里面的ablock在创建的时候, 由编译器生成的成员变量a

    相当于, 结构体在创建的时候, 捕获了这个外部的变量a
    image.png
    这就造成, 即使在block调用之前, 修改变量的值为20, 也不会改变block调用时获取的变量值, 因为block调用的值, 是block的成员变量的那个值.
    请记住一个关键的词- Capture捕获

    那么什么情况下会捕获呢?
    记住以下原则:

    • 局部变量会捕获
    • 全局变量不会捕获
      请问, 什么是局部变量, 什么是全局变量呢?
      简单来说, 声明在函数内部的变量是局部变量, 声明在函数外部的变量称之为全局变量

    block真的不会捕获全局变量吗?

    image.png

    好, 记住了两条大的原则:

    • 局部变量会捕获
    • 全局变量不会捕获
      还有, 局部变量又分为auto变量和static变量
      像这种声明之后存在于栈上的变量称之为auto变量, auto这个关键字是可以省略的
      image.png

    那么, 被static修饰了的变量和auto变量有什么不同呢?

    image.png

    放在常量区的变量有一个特点是生命周期延长了, 他的生命周期跟程序的运行周期是一致的, 只要程序没有终止, 那么常量区的数据是一直存在的. 这和栈区的数据不同, 栈区存放的数据的特点是, 只要作用域结束, 那栈区的内存就会被回收. 那我们来看看是不是这样的:

    图1 图2

    如图2的所示, 当用static关键字修饰变量时, 当作用域结束时, 变量是不会销毁的, 当时离开作用域, 是访问不到变量的, 意思是, 虽然变量存在数据段, 但离开作用域, 无法访问变量. 这相当于变量的作用域不变, 变量的生命周期延长了.

    由于这个情况, 局部变量中, auto变量和static变量被block时, 处理情况是不同的:

    • 值传递(auto变量)
      因为auto变量在作用域结束之后, 变量就会回收, 它的生命周期是很短的, 所以block在捕获时, 会把auto变量的值赋值给block内部的同名成员变量, 这个成员变量是一个新的内存空间存储这个值

    • 指针传递(static变量)
      static变量就不一样了, 它存储在数据段(常量区), 它的生命周期是跟程序的生命周期一致的, 也就是说, 在block需要访问这个变量的时候, 我访问的仍然是这个变量本身, 那么这时候, 我捕获的变量, 就是这个变量的指针(存储这个变量的地址), block把变量的指针赋值给block的同名成员变量

    image.png

    可以很清楚地看到, 是将变量a的地址值传到了block的构造函数中

    image.png

    最终, 赋值给了block内部的指针变量a, 所以block的成员变量的类型是int *.

    image.png image.png

    因此, 在调用myBlock之前, 修改了static变量a的值, 在调用myBlock时, a的值已经改了

    image.png

    上面是基本数据类型的情况, 那么如果是OC对象, block又将如何捕获呢?
    通过编译, 我们发现, 在struct __main_block_desc_0结构体中, 多出来两个成员变量

    image.png
    • copy
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
        _Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }
    

    栈 -> 堆

    • dispose
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
        _Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }
    

    那么这个有什么用呢? 还得从ARC的自动copy说起
    ARC模式下,

    • block作为函数返回值时
    • block赋值给__strong指针时(ARC环境下, 一般默认就是__strong指针, 除非用__weak或者__unsafe_unretained修饰的指针),
    • 或者GCD,
    • 或者函数名中有usingBlock的,
      都会对block进行一次copy操作. 那么这个copy操作有什么用呢?
      我们先来看看block的种类:
    image.png

    从上图可以看出, 我们把编译环境改成MRC(这是因为ARC环境编译器帮我们做了很多事情, 不方便我们探究本质), 然后打印这三个block的类型, 我们发现block是分为三种的:

    • __NSGlobalBlock__这个block是存在数据段的, 暂时不探究
    • __NSStackBlock__这个是存在栈区, 也叫栈block
    • __NSMallocBlock__这个是在堆区, 所以叫堆block

    说回来copy, 当我们对一个block执行copy操作时

    • __NSGlobalBlock__还是__NSGlobalBlock__
    • __NSStackBlock__会升级为__NSMallocBlock__(这一点要尤其注意)
    • __NSMallocBlock__还是__NSMallocBlock__

    当栈block升级为堆block时, 这时候堆中的数据就依靠程序员来管理了, 而不是像栈block一样, 栈空间自动回收之后, 保存在栈中的block就没有了. 在MRC环境下, 如果是栈block的话, 如下图所示

    image.png

    block内部的person指针只是指向person存储的那片内存空间, 并不会对person对象引用计数+1, 那么当person对象被回收后[person release], 再去访问栈block中的person指针指向的那片内存空间, 就很危险了, 就会造成野指针访问.

    但如果是堆block呢? 这时候就会对person对象进行引用计数+1, 那这时候再去访问堆block中的person指针指向的那片内存空间, 是没有问题的

    image.png

    但如果使用__unsafe_unretained关键字修饰person对象时, 会发生什么呢?

    image.png

    可以看到的是, 坏内存访问. 这是因为, 因为我们用__unsafe_unretained关键字修饰了person对象, 所以, 即使block被拷贝到堆区, block内部也不会对person对象引用计数+1 , 那么当我向person对象发送release消息后, person对象引用计数-1, 这时候是会被销毁的, 此时再去访问block内部的person指针指向的那片内存空间, 就会造成野指针访问

    ARC模式下,

    • block作为函数返回值时
    • block赋值给__strong指针时(ARC环境下, 一般默认就是__strong指针, 除非用__weak或者__unsafe_unretained修饰的指针),
    • 或者GCD,
    • 或者函数名中有usingBlock的,
      ARC在以上四种情况下, 会自动对block进行copy操作, 也就是说, 这个栈block会升级成堆block, 升级成堆block后, 堆block中的person指针会对person对象强引用, 那么这样一来, 即使block外面的person指针被回收了, person对象依然不会销毁, 它会随着block的生命周期结束而销毁.
    上面提到的例子中, 如果是局部的auto变量, 我们其实是无法修改变量的值. 因为auto变量的地址没有变, 假设我们要修改定义的局部变量的值, 我们需要做一件事, 就是加上__block关键字, 那么__block的作用是什么呢?

    还是先看看编译情况:

            __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 18};;
    

    简化下来就是:

     __Block_byref_a_0 a = {
      0,
      &a, 
      0, 
      sizeof(__Block_byref_a_0), 
      18
    };
    

    __Block_byref_a_0这个又是什么东西呢?

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

    可以看到的是, __block修饰的变量, 在编译时期, 自动包装成了一个结构体(这个结构体的名字跟修饰的变量同名), 这个结构体的内存布局如上图所示.

    • 0赋值给了_isa指针
    • &a(结构体a的地址)赋值给了__forwarding指针
    • 0赋值给__flags
    • sizeof(__Block_byref_a_0)赋值给了__size
    • 最后18这个变量赋值给了结构体的最后一个变量a

    所以原本的int a, 包装成了一个结构体, 然后这个结构体的地址作为参数传给了block的构造函数. block内部有一个成员变量__Block_byref_a_0 *相当于捕获了这个变量

    但是很明显, 在MRC模式下, 包装过后的这个结构体也存在于栈区

    image.png , block此时也是一个栈block, 那栈的内存空间是随着作用域的结束而回收的!
    事实上, 我理解的是, 例如下段代码:
    image.png

    在31行代码的时候, age作为一个存在于栈的变量, 它已经被系统回收了, 所以31行block去访问age这片内存空间的时候, 其实是很危险的. 虽然这里成功打印了age的值, 但这么做是不合理的.

    image.png
    这幅图应该这么画. 也就是说, 在MRC情况下, 实际上并没有强弱引用的概念. 指针只是指向这这片存储空间, 并没有强指针, 弱指针的概念. 当作用域结束, 栈空间被系统回收, 指针再指向被回收的栈空间, 是一件很危险的事, 可能取到的值不正确.

    即使是指针指向的是堆空间的对象, 也没有强弱指针的概念, 这就是为什么在MRC环境下, 需要手动给引用计数+1.

    image.png

    那么在MRC环境下, 需要手动将block拷贝到堆区. 或者, 在属性修饰时, 使用copy修饰
    没有用copy修饰

    image.png
    打印出来就是栈block
    image.png
    使用copy修饰
    image.png
    打印出来就是堆block
    image.png

    当使用了copy关键字时, block已经升级成了堆block, 同时, __block修饰的变量也在堆区, 何以见得? 请看下图:

    image.png
    很明显, age这个变量已经到了堆区. 那么问题来了, block内部会对这个__block修饰的变量有一个retain操作吗? 我觉得应该有一个类似retain的操作, 理由如下:
    __block修饰的变量在堆区, 堆区的变量回收是程序员来决定的, 而__block修饰的变量的生命周期是和block一致的, 其实就相当于block持有了__block修饰的变量

    我猜测内部的原理是这样的:
    堆区的block会调用__main_block_copy_0方法

    image.png

    __main_block_copy_0方法内部又调用了_Block_object_assign

    image.png

    你也可以理解为将__block修饰的变量(此时被包装成了一个对象), 然后堆block会持有这个对象(也就是引用计数+1).

    在这一点上ARCMRC是一致的, 那么不同点是什么呢?
    MRC环境下, __block修饰的变量(包装后的对象), 这个对象内部的指针并不会持有外面的对象, 举个例子:

    image.png
    这里出现了坏内存访问的错误, 尽管block持有了 __block修饰的变量(包装后的对象), 这个对象内部的指针并不会持有外面的对象, 也就是图中的person对象并没有被__block修饰的变量(包装后的对象)持有, 在MRC环境下: image.png

    MRC环境下, 给person对象发送release消息, 引用计数-1, 对象直接销毁, 坏内存访问, 说明person1并没有持有person对象, 画图表示就是

    image.png

    但是, 在ARC环境下, __block修饰的变量的person指针是会通过__Block_byref_id_object_copy方法, 对person对象强引用的(引用计数+1)

    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_copy_131(void *dst, void *src) {
     _Block_object_assign(dst + 40, src + 40, 131);
    }
    

    这句代码实际上就是调用_Block_object_assign, 内部对person对象的引用计数+1, 这样就形成了强引用.画图表示就是:

    image.png

    通过这幅图, 可以看出来的是, block内部的内存管理就是:
    MRC环境下, 执行copy操作. 在ARC环境下, 系统默认执行copy操作, 此时, block内部的person指针会指向__block修饰的结构体, 通过
    _Block_object_assign方法, 对__block修饰的结构体进行一次copy操作, 也就是引用计数+1, 在block执行完毕, 需要被销毁时, 执行_Block_object_dispose方法, 对__block修饰的结构体进行一次release操作, 也就是引用计数-1.此时, __block修饰的结构体和block一起被销毁.
    不同的是, 在MRC环境下, __block修饰的结构体内部的_Block_object_assign不会再对person对象引用计数+1, _Block_object_dispose方法也不会对person对象引用计数-1,
    , 在ARC环境下, __block修饰的结构体内部的_Block_object_assign会对person对象引用计数+1, _Block_object_dispose方法也会对person对象引用计数-1.

    以上就是block内部的内存管理.
    还有一点需要提到的是, __block修饰的变量转化成的结构体中, __forwarding指针是干嘛用的? 在我们之前的代码中, 看到是将结构体自己的地址传给了__forwarding指针. 那么这个指针的值就是自己这个结构体的地址, 也就是说__forwarding指针指向了自己, 那么为什么不直接从结构体中取值, 而是要通过一个__forwarding指针呢?

    当__block修饰的这个变量包装成的结构体还存在于堆区的时候, 现在这个地址是指向栈区的地址的, 但这本身并没有什么意义.
    但当这个包装结构体被拷贝到了堆区, 此时再去访问这个变量的时候, 就会指向堆区的那个包装结构体. 也就是说, a->__forwarding->a的这个过程就是访问堆区数据的过程.

    image.png image.png

    那么假设, 我想和MRC一样, 对person对象不进行强引用呢?
    这时候就需要用到__weak__unsafe_unretain关键字了. 事实上, 默认状态下, 都是相当于使用了__strong关键字, 相当于__block修饰的变量结构体里的那根person指针默认就是强指针. 当使用__weak修饰时, 被__block修饰的变量就变成了:

    image.png
    编译器还爆出了警告, 让我不要这么做:
    image.png

    将持有的对象赋值给一个弱指针, 对象将在赋值完成后立即释放

    使用__unsafe_unretained也是差不多的效果:

    image.png

    __weak__unsafe_unretained这两个关键字是用来解决循环引用的时候用到的. 那么什么是循环引用呢? 如下图所以:
    ARC环境下, 被强指针指向的对象引用计数+1, 此时person对象创建时引用计数+1, 被强指针指向时, 引用计数+1, 此时引用计数是2, block对象创建时引用计数+1, 被person对象内部的强指针指向时, 引用计数+1, 此时引用计数是2. 那么当他们引用计数都是2时. 它们两个就都无法销毁. 此时, 必须打破这个循环

    image.png

    一般打破循环的方式, 就是让其中一根指针变成弱指针, 一般就是将block内部指向对象的指针变成弱指针

    image.png
    一旦这跟指针变成弱指针后, person对象销毁后, person对象内部的那根指针被回收, 回收后, block对象释放.

    其实被__block修饰的变量也是同理, 它是这样形成:

    image.png

    而我们用__weak修饰变量后, 形成的闭环其实是将__block修饰的结构体里的person变成弱指针:

    image.png

    那么__weak__unsafe_unretained有什么区别呢?
    __weak修饰的变量, 一旦内存空间被回收, __weak修饰的指针变量就会置为nil, 后面再访问就会直接return, 因此它是安全的
    __unsafe_unretained修饰的变量, 一旦内存空间被回收,__unsafe_unretained修饰的指针变量不会置为nil, 后面再访问, 是非常危险的. 有可能会造成野指针访问.

    最后再探讨一个问题:

    iOS block内部为什么要加__strong?

    这是为了防止当程序执行block时, block内部的指针指向的那块地址突然为空. 举个例子:

    image.png
    假设上图中, 在程序执行到第22行时, self突然为空, 如果不写__strong typeof(self)strongSelf = weakSelf;这行代码时, 那后面访问weakSelf指向的地址空间时, 就可能为空. 但是当我写了__strong typeof(self)strongSelf = weakSelf;时, 此时, 我用一个栈区的局部变量强引用了self对象. 那self此时的引用计数+1, 它不会被置为空. 我后面的代码就可以继续访问. 等到作用域结束, 局部变量栈空间回收, self对象的引用计数-1. 这样是不会形成循环引用的, 如下图所示
    image.png

    iOS开发中在block中为什么要__weak和__strong配合使用
    上文中举出了一个__weak__strong配合使用的例子, 仅供参考

    相关文章

      网友评论

          本文标题:block

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