美文网首页
Block原理探索

Block原理探索

作者: Breezes | 来源:发表于2020-09-09 16:40 被阅读0次

    Block定义

    闭包是一个函数(或指向函数的指针),再加上该函数执行的外部的上下文变量(有时候也称为自由变量)
    block实际上就是OC对于闭包的实现。
    block本质上也是一个OC对象,它内部也有个isa指针
    block是封装了函数调用以及函数调用环境的OC对象

    Block结构分析

    void blockTest()
    {
        void (^block)(void) = ^{
            NSLog(@"Hello World!");
        };
        block();
    }
    
    int main(int argc, char * argv[]) {
        @autoreleasepool {
            blockTest();
        }
    }
    

    编译后得到👇:

    struct __block_impl {
      void *isa;//isa指针,指向一个类对象,有三种类型:_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock,可以看出这里使用的是_NSConcreteStackBlock
      int Flags;//Block的负载信息(引用计数和类型信息),按位存储
      int Reserved;//保留变量
      void *FuncPtr;//一个指针,指向Block执行时的函数,也就是Block需要执行的代码块,在这里指向的是__blockTest_block_func_0函数
    };
    
    //通常包含两个成员变量:__block_impl、__blockTest_block_desc_0,和一个构造函数
    struct __blockTest_block_impl_0 {
      struct __block_impl impl;//
      struct __blockTest_block_desc_0* Desc;
      __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    //__blockTest_block_func_0就是Block执行调用的函数,参数是一个__blockTest_block_impl_0类型的指针
    static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_yz_sz8wz4q52_xf5dw0q_8nbgch0000gn_T_main_42229c_mi_0);
    }
    
    static struct __blockTest_block_desc_0 {
      size_t reserved;//Block版本升级所需预留区空间,这里为0
      size_t Block_size;//Block的大小, sizeof(struct __blockTest_block_impl_0)
    } __blockTest_block_desc_0_DATA = { 0, sizeof(struct __blockTest_block_impl_0)};//是__blockTest_block_desc_0的一个实例
    
    void blockTest()
    {
        /**
         1.block的定义:通过__blockTest_block_impl_0结构体生成一个实例,并用一个指针指向了当前实例,
         __blockTest_block_impl_0q在初始化时需要两个参数:
         __blockTest_block_func_0:Block块的函数指针
         __blockTest_block_desc_0_DATA:作为静态全局变量初始化__blockTest_block_desc_0_DATA结构体的实例指针
         */
        void (*block)(void) = (&__blockTest_block_impl_0(
                                                         __blockTest_block_func_0,
                                                         &__blockTest_block_desc_0_DATA)
                               );
        /**
         2.调用block:通过block)->FuncPtr找到__blockTest_block_func_0函数指针
         然后将block作为参数传递给这个函数
         */
        (block)->FuncPtr)(block);
    }
    
    int main(int argc, char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            blockTest();
        }
    }
    
    1. block的定义部分:
      block是一个结构体,该结构体需要两个参数
      __blockTest_block_func_0:Block块的函数指针;
      __blockTest_block_desc_0_DATA:作为静态全局变量初始化__blockTest_block_desc_0_DATA结构体的实例指针;
      通过__blockTest_block_impl_0结构体生成一个实例,并用一个指针指向了当前实例。
    2. block调用部分:
      通过block)->FuncPtr找到__blockTest_block_func_0函数指针,并将step1 block指针传递给该函数,

    __blockTest_block_func_0就是block执行时调用的函数,接收的参数是__blockTest_block_impl_0类型的指针,step1生成的就是__blockTest_block_impl_0结构体的实例

    Flags(Block_private.h)
    enum {
        BLOCK_DEALLOCATING =      (0x0001),  // 释放标记。一般常用 BLOCK_NEEDS_FREE 做 位与 操作,一同传入 Flags ,告知该 block 可释放。
        BLOCK_REFCOUNT_MASK =     (0xfffe),  // 一般参与判断引用计数,是一个可选用参数。
        BLOCK_NEEDS_FREE =        (1 << 24), // 通过设置该枚举位,来告知该 block 可释放。意在说明 block 是 heap block ,即我们常说的 _NSConcreteMallocBlock 。
        BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // 是否拥有拷贝辅助函数(a copy helper function)。
        BLOCK_HAS_CTOR =          (1 << 26), // 是否拥有 block 析构函数(dispose function)。
        BLOCK_IS_GC =             (1 << 27), // 是否启用 GC 机制(Garbage Collection)。
        BLOCK_IS_GLOBAL =         (1 << 28), // compiler
        BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
        BLOCK_HAS_SIGNATURE  =    (1 << 30)  // 与 BLOCK_USE_STRET 相对,判断是否当前 block 拥有一个签名。用于 runtime 时动态调用。
    };
    

    Block结构如图(网上借的):


    Block

    Block捕获变量

    捕获auto变量(局部变量)

    先看下面这段代码:

    void blockTest()
    {
        int num = 10;
        void (^block)(void) = ^{
            NSLog(@"%d",num);
        };
        num = 20;
        block();
    }
    
    int main(int argc, char * argv[]) {
        @autoreleasepool {
            blockTest();
        }
    }
    

    num应该输出什么?
    答案:应该输出10
    编译后的代码👇:

    struct __blockTest_block_impl_0 {
      struct __block_impl impl;
      struct __blockTest_block_desc_0* Desc;
      int num;
      __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    void blockTest()
    {
        int num = 10;
        void (*block)(void) = (&__blockTest_block_impl_0(
                                                         __blockTest_block_func_0,
                                                         &__blockTest_block_desc_0_DATA,
                                                         num)
                               );
        num = 20;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    

    __blockTest_block_impl_0结构体多了一个成员变量num
    构造函数__blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int _num, int flags=0)可以看到第三个参数num只是变量的值,这就解释了为什么num打印的是10,因为block捕获auto变量时,捕获的是其值。

    捕获static变量

    void blockTest()
    {
        static int num = 10;
        void (^block)(void) = ^{
            NSLog(@"%d",num);
            num = 30;
        };
        num = 20;
        block();
        NSLog(@"%d",num);
    }
    

    两次num分别输出什么?
    答案:block块内的num输出20,第二个num输出30
    编译后👇:

    struct __blockTest_block_impl_0 {
      struct __block_impl impl;
      struct __blockTest_block_desc_0* Desc;
      int *num;
      __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int *_num, int flags=0) : num(_num) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {
      int *num = __cself->num; // bound by copy
    
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_yz_sz8wz4q52_xf5dw0q_8nbgch0000gn_T_main_a383ea_mi_0,(*num));
            (*num) = 30;
    }
    
    void blockTest()
    {
        static int num = 10;
        void (*block)(void) = (&__blockTest_block_impl_0(
                                                         __blockTest_block_func_0,
                                                         &__blockTest_block_desc_0_DATA,
                                                         &num));
        num = 20;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_yz_sz8wz4q52_xf5dw0q_8nbgch0000gn_T_main_a383ea_mi_1,num);
    }
    

    构造函数__blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int *_num, int flags=0)第三个参数*num传入的是num的指针,所以可以在内部和外部修改变量的值。

    为什么auto变量就是传递的值,而static变量传递的是指针呢?

    auto变量保存在栈中,并且会随着当前作用域(blockTest)消失而销毁,有可能销毁时机会比block更早,所以block内访问销毁的变量时会产生问题,而static变量保存在全局存储区(静态存储区),不会出现这样的问题。

    全局变量

    int num = 10;
    
    void blockTest()
    {
        void (^block)(void) = ^{
            NSLog(@"%d",num);
            num = 30;
        };
        num = 20;
        block();
        NSLog(@"%d",num);
    }
    

    编译后👇:

    int num = 10;
    
    
    struct __blockTest_block_impl_0 {
      struct __block_impl impl;
      struct __blockTest_block_desc_0* Desc;
      __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {
    
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_yz_sz8wz4q52_xf5dw0q_8nbgch0000gn_T_main_141607_mi_0,num);
            num = 30;
        }
    
    static struct __blockTest_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __blockTest_block_desc_0_DATA = { 0, sizeof(struct __blockTest_block_impl_0)};
    void blockTest()
    {
        void (*block)(void) = (&__blockTest_block_impl_0(
                                                         __blockTest_block_func_0,
                                                         &__blockTest_block_desc_0_DATA)
                               );
        num = 20;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_yz_sz8wz4q52_xf5dw0q_8nbgch0000gn_T_main_141607_mi_1,num);
    }
    
    
    
    int main(int argc, char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            blockTest();
        }
    }
    

    可以看到这里构造函数并没有传入变量的值或者指针,因为全局变量是直接可以访问的。
    总结一下:

    变量类型 是否捕获到block内部 访问方式
    auto变量 值访问
    static变量 指针访问
    全局变量 直接访问

    __block修饰的变量

    1.在 block 内为什么不能修改 block 外部变量
    答案:block 本质上是一个对象,block 的花括号区域是对象内部的一个函数,变量进入 花括号,实际就是已经进入了另一个函数区域---改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。又比如我想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。详解
    2.除了使用static变量、全局变量外如何在block内改变变量的值?为什么?
    答案:
    使用__block;
    static变量: block 内部对外部static修饰的变量进行指针捕获;
    全局变量:block 内外可直接访问全局变量;
    __block变量:要想在block内部修改auto变量,需要两个条件:
    (1)从栈区拷贝到堆区(栈的内存是由系统管理,堆由我们管理,其实在ARC下所有进入block内的auto变量都会被拷贝到堆区见这里)
    (2)把auto变量包装成结构体(对象),_block 作用是将 auto 变量封装为结构体(对象),在结构体内部新建一个同名 auto 变量,block 内截获该结构体的指针,在 block 中使用自动变量时,使用指针指向的结构体中的自动变量。于是就可以达到修改外部变量的作用。

    总结一下就是如果想在block内修改变量:将 auto 从栈 copy 到堆;将 auto 变量封装为结构体(对象)

    3.这三种修改变量值的方式哪个最好的?
    这个问题请查看https://github.com/ChenYilong/iOSInterviewQuestions第38题,结论是__block是最优解。

    从源码层面论证
    void blockTest()
    {
        __block int num = 10;
        void (^block)(void) = ^{
            NSLog(@"%d",num);
            num = 30;
        };
        num = 20;
        block();
        NSLog(@"%d",num);
    }
    

    编译后👇:

    struct __Block_byref_num_0 {
      void *__isa;
    __Block_byref_num_0 *__forwarding;
     int __flags;
     int __size;
     int num;
    };
    
    struct __blockTest_block_impl_0 {
      struct __block_impl impl;
      struct __blockTest_block_desc_0* Desc;
      __Block_byref_num_0 *num; // by ref
      __blockTest_block_impl_0(void *fp, struct __blockTest_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __blockTest_block_func_0(struct __blockTest_block_impl_0 *__cself) {
      __Block_byref_num_0 *num = __cself->num; // bound by ref
    
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_yz_sz8wz4q52_xf5dw0q_8nbgch0000gn_T_main_a71778_mi_0,(num->__forwarding->num));
            (num->__forwarding->num) = 30;
        }
    static void __blockTest_block_copy_0(struct __blockTest_block_impl_0*dst, struct __blockTest_block_impl_0*src) {_Block_object_assign((void*)&dst->num, (void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __blockTest_block_dispose_0(struct __blockTest_block_impl_0*src) {_Block_object_dispose((void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static struct __blockTest_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __blockTest_block_impl_0*, struct __blockTest_block_impl_0*);
      void (*dispose)(struct __blockTest_block_impl_0*);
    } __blockTest_block_desc_0_DATA = { 0, sizeof(struct __blockTest_block_impl_0), __blockTest_block_copy_0, __blockTest_block_dispose_0};
    void blockTest()
    {
        __attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 10};
        void (*block)(void) = ((void (*)())&__blockTest_block_impl_0(
                                                                     __blockTest_block_func_0,
                                                                     &__blockTest_block_desc_0_DATA,
                                                                     (__Block_byref_num_0 *)&num,
                                                                     570425344));
        (num.__forwarding->num) = 20;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_yz_sz8wz4q52_xf5dw0q_8nbgch0000gn_T_main_a71778_mi_1,(num.__forwarding->num));
    }
    
    
    
    int main(int argc, char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            blockTest();
        }
    }
    
    __block修饰的变量的拷贝
    static void __blockTest_block_copy_0(struct __blockTest_block_impl_0*dst, struct __blockTest_block_impl_0*src) {_Block_object_assign((void*)&dst->num, (void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);}
    

    注意:__block变量的结构体__Block_byref_num_0内有一个__Block_byref_num_0类型的指针 __forwarding,而且获取__Block_byref_num_0结构体时候都会使用__forwarding获取,至于原因会在后面讲

    该方法有一个dst(接收拷贝完成的对象)指针和一个src对象(被拷贝的对象),并调用了方法_Block_object_assign方法, _Block_object_assign需要三个参数,分别是:dst->numsrc->num、和一个flags 8,前两个参数就是__Block_byref_num_0的对象,这里先看一下这个flags枚举(Block_private.h):

    // Runtime support functions used by compiler when generating copy/dispose helpers
    enum {
        // see function implementation for a more complete description of these fields and combinations
        BLOCK_FIELD_IS_OBJECT   =  3,  //OC对象类型
        BLOCK_FIELD_IS_BLOCK    =  7,  //一个block变量
        BLOCK_FIELD_IS_BYREF    =  8,  // 在栈上被__block修饰的变量
        BLOCK_FIELD_IS_WEAK     = 16,  // 被__weak修饰的变量,只在Block_byref管理内部对象内存时使用
        BLOCK_BYREF_CALLER      = 128, // 处理Block_byref内部对象内存的时候会加的一个额外标记(告诉内部实现不要进行retain或者copy)
    };
    

    这里使用的是BLOCK_FIELD_IS_BYREF

    _Block_object_assign(__block对象的copy)

    接着通过runtime源码查看该方法的实现(只截取了关键部分):

    void _Block_object_assign(void *destArg, const void *object, const int flags) {
        const void **dest = (const void **)destArg;
        switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
          case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
          case BLOCK_FIELD_IS_BYREF:
            /*******
    
             // copy the onstack __block container to the heap
             // Note this __weak is old GC-weak/MRC-unretained.
             // ARC-style __weak is handled by the copy helper directly.
             __block ... x;
             __weak __block ... x;
             [^{ x; } copy];
             ********/
    
            *dest = _Block_byref_copy(object);
            break;
    

    调用_Block_byref_copy方法把object(src)传入到该函数,并返回到一个新的对象赋值给destdest就是新得到从栈上拷贝到堆上的新值。

    _Block_object_dispose(__block对象的释放)
    // When Blocks or Block_byrefs hold objects their destroy helper routines call this entry point
    // to help dispose of the contents
    void _Block_object_dispose(const void *object, const int flags) {
        switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
          case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
          case BLOCK_FIELD_IS_BYREF:
            // get rid of the __block data structure held in a Block
            _Block_byref_release(object);
            break;
          case BLOCK_FIELD_IS_BLOCK:
            _Block_release(object);
            break;
          case BLOCK_FIELD_IS_OBJECT:
            _Block_release_object(object);
            break;
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
          case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
            break;
          default:
            break;
        }
    }
    

    当需要释放堆上的auto变量对象时,调用_Block_byref_release释放该对象

    __block修饰的变量的包装

    被__block修饰的auto变量会被包装成一个 __Block_byref_num_0的结构体,同样拥有isa,因此也是一个对象;

    Block的内存管理

    __block修饰的变量什么时候会被从栈拷贝到堆?

    看这个问题之前我们先看一下我们先了解下Block的内存管理
    Block一共有三种类型:

    NSGlobalBlock ( _NSConcreteGlobalBlock )
    NSStackBlock ( _NSConcreteStackBlock )
    NSMallocBlock ( _NSConcreteMallocBlock )

    他们都继承自NSBlock,NSBlock继承自NSObject
    三种类型对应的内存分配以及调用copy后的效果如下:

    Block类型 副本源的配置存储域 copy效果
    NSGlobalBlock 数据区 什么也不做
    NSStackBlock 从栈复制到堆
    NSMallocBlock 引用计数增加

    有以下情况会把BLock从栈拷贝到堆:
    1.调用Block的copy实例方法时
    2.Block作为函数返回值返回时
    3.在带有usingBlock的Cocoa方法或者GCD的API中传递Block时候
    4.将block赋给带有__strong修饰符的id类型或者Block类型时
    下面通过验证一下第4种情况看下是否准确(前三种可自行测试):
    无__strong修饰符的id类型或者Block类型,代码⬇️:

    int num = 10;
    NSLog(@"%@",[^{
            NSLog(@"%d",num);
     } class]);
    

    打印结果:

    __NSStackBlock__
    

    有__strong修饰符的id类型或者Block类型,代码⬇️:

    void (^block)(void) = ^{
            NSLog(@"%d",num);
        };
        NSLog(@"%@",[block class]);
    

    打印结果:

    __NSMallocBlock__
    

    Block的拷贝时机介绍完了,那么__block修饰的变量何时会从栈区拷贝到堆区呢?
    答案:当Block从栈拷贝到堆区的时候,__block变量也会跟着Block被拷贝到堆区。
    验证一下👇,
    先看一下不拷贝到堆区的情况:

        __block int num = 10;
        NSLog(@"block前:%p",&num);
        ^{
            num = 20;
            NSLog(@"block内:%p",&num);
        }();
        NSLog(@"block后:%p",&num);
    

    打印结果:

    block前:0x7ffeea3f2c98
    block内:0x7ffeea3f2c98
    block后:0x7ffeea3f2c98
    

    拷贝到堆区的情况:

        NSLog(@"block前:%p",&num);
        void (^block)(void) = ^{
            num = 20;
            NSLog(@"block内:%p",&num;
        };
        block();
        NSLog(@"block后:%p",&num);
    

    打印:

    block前:0x7ffee4af3c98
    block内:0x600003650738
    block后:0x600003650738
    

    从内存地址不难看出不拷贝堆区时,__block变量也不会进行拷贝,当Block从栈区拷贝到堆区,__block变量也会进行拷贝

    下面这段代码为什么MRC和ARC下打印不一样
        int num = 1;
        void (^block)(void) = ^{
            NSLog(@"%d",num);
        };
        NSLog(@"%@",[block class]);
    

    MRC打印:

    __NSStackBlock__
    

    ARC打印:

    __NSMallocBlock__
    

    原因:
    由于在ARC环境下,使用strong修饰的变量指向block,会持有这个block。因此临时变量block会从栈复制到堆上

    __forwarding指针存在的意义是什么?

    __forwarding指针是为了在__block变量从栈复制到堆上后,在Block外对__block变量的修改也可以同步到堆上实际存储__block变量的结构体上。

    __forwarding.png

    __forwarding确保不管是堆栈访问__block变量结构体时都能访问到同一个对象

    Block捕获对象

    NSStackBlock

    在栈上的Block不会对auto对象进行强引用;

    NSMallocBlock

    堆上的Block会对auto对象进行强引用,直到Block释放时,才解除对auto对象的强引用

    typedef void(^Block)(void);
    
    int main(int argc, char * argv[]) {
        Block block;
        {
            Person *person = [[Person alloc] init];
            person.name = @"toby";
    
            block = ^{
                NSLog(@"%@",person.name);
            };
            person.name = @"david";
            NSLog(@"即将退出person作用域");
        }
        NSLog(@"已经退出person作用域");
        block ();
    }
    

    打印结果:

    即将退出person作用域
    已经退出person作用域
    david
    -[Person dealloc]
    

    Block的循环引用

    什么情况下会造成循环引用?

    当一个对象person持有了了block对象,而block内又持有了person互相持有,这就造成了循环引用

    如何打破循环引用?

    1.使用__block修饰对象person
    2.使用__unsafe_unretained修饰对象person
    3.使用__weak修饰对象person

    __block

        __block Person *person = [[Person alloc] init];
        person.blockTest = ^{
            person.name = @"toby";
            person = nil;
        };
        person.blockTest();
    

    需要在block内指定person=nil,并且需要调用调用block函数。
    __unsafe_unretained

        Person *person = [[Person alloc] init];
        __unsafe_unretained typeof(person) weakPerson = person;
        weakPerson.blockTest = ^{
            weakPerson.name = @"toby";
        };
    

    使用__unsafe_unretained虽然能解除循环引用,但是不安全,当指向对象销毁时,指针存储地址不变,如果再次访问可能会造成悬垂指针⬇️:

    访问悬垂指针.png
    __weak
        Person *person = [[Person alloc] init];
        __weak typeof(person) weakPerson = person;
        weakPerson.blockTest = ^{
            weakPerson.name = @"toby";
        };
    

    查看编译后的源码👇:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      Person *__weak weakPerson;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    可以看到当前block内引用的是一个weak类型的person,对person的引用变成了弱引用,这就打破了双向持有的局面,至于该weak person的释放时机是由runtime维护的一个hash table决定的,当person对象dealloc时,会以person 地址当作键值在hash table中查找weak对象置为nil。runtime如何实现weak变量的自动置nil
    但是实际应用会发现如果此时Block回调晚一些(异步线程执行耗时任务),此时的person已经出作用域,在block内访问weakPerson的时候就nil了。

        Person *person = [[Person alloc] init];
        __weak typeof(person) weakPerson = person;
        person.blockTest = ^{
            dispatch_async(dispatch_queue_create(0,DISPATCH_QUEUE_CONCURRENT), ^{
                [NSThread sleepForTimeInterval:3];
                NSLog(@"person = %@",weakPerson);
            });
        };
        person.blockTest();
    

    此时的办法就是在block内使用__strong再修饰一下weakPerson,让person延迟释放,至于释放时机当然是block执行完成

        Person *person = [[Person alloc] init];
        __weak typeof(person) weakPerson = person;
        person.blockTest = ^{
            __strong typeof(person) strongPerson = weakPerson;
            dispatch_async(dispatch_queue_create(0,DISPATCH_QUEUE_CONCURRENT), ^{
                [NSThread sleepForTimeInterval:3];
                NSLog(@"strong person = %@",strongPerson);
            });
        };
        person.blockTest();
    }
    

    总结

    1.Block是一个对象
    2.Block捕获变量:
    (1)auto变量:捕获的是值
    (2)static变量:捕获指针
    (3)global变量:无需捕获,直接访问
    3.Block捕获__block修饰的auto变量时,会把该变量包装成一个对象,并会根据Block是否会被拷贝到堆区对auto变量进行拷贝,修改auto变量时需要满足两个条件:
    (1)将 auto 从栈 copy 到堆;
    (2)将 auto 变量封装为结构体(对象)
    4.Block有三种类型,他们都继承自NSBlock->NSObject
    NSGlobalBlock ( _NSConcreteGlobalBlock ) 数据区域
    NSStackBlock ( _NSConcreteStackBlock ) 栈区
    NSMallocBlock ( _NSConcreteMallocBlock ) 堆区
    5.Block会被从栈拷贝到堆的情况:
    (1)调用Block的copy实例方法时
    (2)Block作为函数返回值返回时
    (3)在带有usingBlock的Cocoa方法或者GCD的API中传递Block时候
    (4)将block赋给带有__strong修饰符的id类型或者Block类型时
    6. __forwarding指针是为了在__block变量从栈复制到堆上后,在Block外对__block变量的修改也可以同步到堆上实际存储__block变量的结构体上。
    7.在栈上的Block不会对auto对象进行强引用;堆上的Block会对auto对象进行强引用,直到Block释放时,才解除对auto对象的强引用
    8.解除Block的循环引用,最安全的方法是使用__weak修饰auto变量,并在block内部对auto变量进行__strong修饰

    相关文章

      网友评论

          本文标题:Block原理探索

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