深入理解 Block

作者: AnICoo1 | 来源:发表于2017-10-06 17:47 被阅读182次

    本文主要根据《Objective-C高级编程》这本书中的第二章来进行的一个总结,其中包含了查看其它文章后的总结和自己的一些理解,关于 block 的一些定义在这里就不说了,这里主要讲一下 block 中的截获自动变量和 __block 关键字的实现
    文章主要内容如下:

    • 一个普通的 block
    • 截获自动变量值
    • __block 修饰符
    • 循环引用

    一个普通的 block

    int main(int argc, char * argv[]) {
        void(^blk)() = ^{
            printf("Hello World");
        };
        blk();
        return 0;
    }
    

    一个最普通的 block,内部没有使用到任何变量,让我们来看一下它的内部结构,通过 clang -rewrite-objc 命令将上面的代码解析为一份 C++ 代码:

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    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 World");
        }
    
    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(int argc, char * argv[]) {
        void(*blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        return 0;
    }
    

    main 函数

    对 main 函数进行分解处理,这样可以更加直观的查看

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

    分解为

    struct __main_block_impl_0 tempBlock = __main_block_impl_0(__main_block_func_0, &_main_block_desc_0_DATA);
    struct __main_block_impl_0 *blk = &tempBlock;
    

    可以看出,在编译之后 blk 是一个指向一个 __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 函数可以看到,创建的就是这样一个类型的结构体,我们来看一下这个结构体的内部构造:

    • impl:__block_impl 类型,成员变量之一,内部含有函数指针
    • Desc:__main_block_desc_0 类型指针,成员变量之一,用来描述当前 block 的一些附加信息,例如:结构体的大小
    • __main_block_impl_0 :结构体初始化函数,在结构体中进行成员变量的初始化

    接下来我们看一下 impl 所属结构体的主要构造

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    • isa:非常常见的指向类的指针
    • FuncPtr:函数指针,在 main 函数的初始化过程中,可以观察到是将一个静态函数 __main_block_func_0 对其赋值

    接下来看一下 Desc 指针的结构

    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)};
    
    • reserved:今后升级版本所需要区域
    • Block_size:block 的大小

    调用结构体初始化函数

    从源码可以看出,在 main 函数中通过调用结构体的初始化函数来创建一个结构体

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

    我们可以看出,isa 指针保持所属类的结构体的实例的指针,这也说明了 block 其实也就是一个 OC 对象,因为其包含了 isa 指针

    截获自动变量值

    先来看一段代码:

    int a = 10;
    void(^blk)() = ^{
        printf("%d\n", a);
    };
    a = 20;
    blk();//输出结果:10
    

    我们可以发现,当我们定义一个变量之后,又定义一个 block,并且在 block 内部引用了这个变量,然后在外面修改变量,然后调用 block,得到的变量还是修改前的变量,这是为什么呢?好像是我们定义 block 的时候将使用的变量 copy 了一份然后自己存起来,当调用 block 的时候,使用的变量是我们 copy 的那一份,所以不管外面对变量如何进行修改,都不会影响我们保存的那一份,当然这只是猜想,带着问题走进源码看一看.

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    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;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int a = __cself->a; // bound by copy
    
            printf("%d\n", a);
        }
    
    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(int argc, char * argv[]) {
        int a = 10;
        void(*blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        a = 20;
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        return 0;
    }
    

    对比前面那个普通的 block,我们发现几个不同点

    • __main_block_impl_0 结构体中多了一个成员变量 a, 并且在初始化函数中多了一个参数 a,这个 a 就是 block 截获的自动变量值
    • 在静态函数 __main_block_func_0 使用截获的自动变量的时候,用到了一个指针 _cself, 这是什么呢?在 main 可以发现这个参数是在进行 block 调用的时候传入的,传入的就是 block 本身,所以说在静态函数中操作的变量也就是在 block 创建的时候的那个成员变量,而不是操作的外部变量,也就是说,block 只是截获的自动变量的值,而并不是截获了其地址,所以外部变量在 block 创建之后进行修改在这里也没什么效果,前面的猜想得证.

    这里需要注意,block 只会截获使用到的变量,而对于没有使用到的变量是不会进行截获的.

    那是不是所有的变量都是如此呢?我们做测试的是局部变量,但是我们知道,在 C 语言中变量还有全局变量、全局静态变量和局部静态变量,我们使用这些来尝试一下,以下使用局部静态变量做测试,至于全局变量和全局静态变量留给读者自己探索,后面也会进行解释:

    static int a = 10;
    void(^blk)() = ^{
        printf("%d\n", a);
    };
    a = 20;
    blk();//输出20
    

    我们发现输出变了,怎么回事呢?还是看源码

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    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;
      }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int *a = __cself->a; // bound by copy
    
            printf("%d\n", (*a));
        }
    
    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(int argc, char * argv[]) {
        static int a = 10;
        void(*blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a));
        a = 20;
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        return 0;
    }
    
    

    和上面的局部变量作对比,可以发现,在 __main_block_impl_0 结构体中,变成了存储变量 a 的指针,那么我们访问的时候访问的是指针 a 指向的变量,那么就可以解释输出为 20 的原因.

    __block 修饰符

    当我们修改截获的变量的时候,示例如下:

    int a = 10;
    void(^blk)() = ^{
        a = 20;//报错:Variable is not assignable(missing,__block type specifier)
        printf("%d\n", a);
    };
    blk();
    

    因为在底层实现中,即使在 block 内部对对象进行改变,外部的对象没有任何改变,原因也就是前面的解释的自动变量的截获,所以可能苹果就在编译阶段就告诉开发者这样是无效的,也就是直接编译报错。
    那么,现在可以得出一个结论:被截获的自动变量值不能直接进行修改,但是,有两种方法可以解决这个问题:

    • 改变存储于特殊存储区域的变量
    • 使用 __block 修饰符

    我们先来说一下第一种,在 C 语言中变量一般分为五种,分别是:

    • 自动变量(局部变量)
    • 函数参数
    • 静态变量(局部静态变量)
    • 静态全局变量
    • 全局变量

    这里需要去除函数参数这一项,那么写一个测试代码,因为前面已经做过自动变量和静态变量的测试,所以这里只进行全局变量和静态全局变量的测试,如下:

    int globalA = 10;
    static int intGlobalA = 20;
    int main(int argc, char * argv[]) {
        void(^blk)() = ^{
            globalA += 10;
            intGlobalA += 20;
            printf("%d %d\n", globalA, intGlobalA);//输出:20 40
        };
        blk();
        return 0;
    }
    

    查看输出,发现可以进行改变,我们来看一下源码:

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    int globalA = 10;
    static int intGlobalA = 20;
    
    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) {
    
            globalA += 10;
            intGlobalA += 20;
            printf("%d %d\n", globalA, intGlobalA);
        }
    
    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(int argc, char * argv[]) {
        void(*blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        return 0;
    }
    

    通过源码我们发现,block 并未对两个变量进行截获,而进行访问和修改的时候,并未经过 block,也就是并未使用到 __cself 指针,而是直接进行访问和修改,如下:

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    
            globalA += 10;
            intGlobalA += 20;
            printf("%d %d\n", globalA, intGlobalA);
        }
    

    至此,可以得出一个结论:

    • 全局变量:可以直接进行访问,修改
    • 静态全局变量:可以直接进行访问,修改
    • 静态变量:指针引用,可以进行访问,修改
    • 自动变量:值引用,不可进行修改

    然后,我们再来看一下第二种,使用 __block 修饰符,使用 __block 修饰符修饰的变量称为 block 变量

    测试代码如下:

    int main(int argc, char * argv[]) {
        __block int a = 10;
        void(^blk)() = ^{
            a = 20;
            printf("%d\n", a);//输出:20
        };
        blk();
        printf("%d\n", a);//输出:20
        return 0;
    }
    

    可以发现,在使用 __block 修饰符之后 block 内可以修改自动变量了,同样的,我们来查看一下源码的实现:

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    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\n", (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(int argc, char * argv[]) {
        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
        void(*blk)() = ((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 *)blk)->FuncPtr)((__block_impl *)blk);
        printf("%d\n", (a.__forwarding->a));
        return 0;
    }
    
    

    可以发现,在使用 __block 修饰符之后,源码中多了很多东西,我们来一个个进行解析

    首先,还是从 __main_block_impl_0 这个结构体开始,可以发现,此时的 block 并不是将截获的 a 直接作为成员变量,而是使用一个结构体 __Block_byref_a_0 类型的同名变量来代替,我们来看一下这个结构体:

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

    可以看到,这个结构体中含有一个成员变量 a,即是截获的自动变量,也就是 __block 修饰的自动变量,这里可以看出,当一个自动变量使用 __block 修饰之后,原来的自动变量被生成一个结构体,而结构体中的一个成员变量持有原来的自动变量
    在这个结构体中,可以看到一个 __forwarding 指针,这个指针是指向自己的,它的作用就是无论当前 block 是处于栈中,还是堆中,都能够准确的进行访问,所以后面就是通过 __forwarding 指针来访问成员变量 a 的,这里引用《Objective-C 高级编程》中的一张图片来解释一下:

    取自《Objective-C 高级编程》
    • 最初 block 在栈上,那么 forwarding 指针指向自身的 __block 变量结构体
    • 在 block 被复制到堆上时,会将 forwarding 的值替换为堆上的目标 block 变量用结构体实例的地址,而在堆上的目标 block 变量自己的 forwarding 的值就指向它自己

    而在使用变量的时候,如下:

    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\n", (a->__forwarding->a));
        }
    

    可以看出,在调用 block 之后,会生成一个 __Block_byref_a_0 类型的指针指向 block 结 构体中的封装截获变量的结构体成员变量,然后通过 __Block_byref_a_0 结构体中的 __forwarding 指向目标 block 变量进行修改

    从源码中我们看到多了两个函数:

    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*/);}
    

    在 OC 中,C 语言结构体不能含有附有 __strong 修饰符的变量,因为编译器不知道应该什么时候进行 C 语言结构体的初始化和废弃操作,不能很好的管理内存,但是 OC 中的运行时库能够准确把握 block 从栈复制到堆以及堆上的 block 被废弃的时机,实际上就是使用了 __main_block_copy_0__main_block_dispose_0 这两个函数.
    其中,_Block_object_assign 相当于 retain 操作,将对象赋值在对象类型的结构体成员变量中。
    _Block_object_dispose相当于 release 操作。

    调用 copy 函数和 dispose 函数的时机:

    函数 调用时机
    copy 栈上的 block 复制到堆上时
    dispose 堆上的 block 被废弃时

    栈上的 block 复制到堆上的情况

    • 调用 block 的 copy 函数时
      在调用 block 的 copy 实例方法时,如果此时 block 在栈上,那么会被复制到堆上
    • Block 作为函数返回值返回时
      调用 _Block_copy 函数
    • 将 Block 赋值给附有 __strong 修饰符 id 类型的类或者 Block 类型成员变量时
      调用 _Block_copy 函数
    • 方法中含有 usingBlock 的 Cocoa 框架方法或者 GCD 的 API 中传递 Block 时
      调用 _Block_copy 函数或者 block 的 copy 实例方法

    堆上的 block 被废弃的情况

    • 堆上的 Block 被释放后,谁都不再持有Block时调用 dispose 函数

    block 的存储域

    前面说到,block 可以看做是一个 OC 对象,从源码中,我们可以看到 block 中的 isa 指针指向的是 &_NSConcreteStackBlock, 那么就可以说,此时 block 的类为 _NSConcreteStackBlock,和这个类相似的还有:

    • _NSConcreteStackBlock(栈区)
    • _NSConcreteMallocBlock(堆区)
    • _NSConcreteGlobalBlock(数据区)

    NSConcreteGlobalBlock

    在两种情况下,block 会存在于数据区,即isa 指针指向_NSConcreteGlobalBlock:

    • 记述全局变量的地方有 block 语法时
      测试代码:
    void(^globalBlk)() = ^{};
    int main(int argc, char * argv[]) {
        return 0;
    }
    
    • block 的语法表达式中不使用应该截获的自动变量时

    NSConcreteStackBlock

    除了以上两种情况外生成的 block 语法生成的 block 都是 NSConcreteStackBlock 类对象

    NSConcreteMallocBlock

    配置在全局变量上的 block,从变量作用域外也可以通过指针安全的使用,但是设置在栈上的 block,如果其所属的变量作用域结束,该 block 就会被废弃,由于 __block 变量也是在栈上的,同理,如果其所属的变量作用域结束,该变量就会被废弃.

    为了解决这个问题,通过将 block 和 __block 变量复制到堆上来解决,这样即使其所属的变量作用域结束,堆上的 block 依然存在,复制到堆上的 block 的 isa 变为 _NSConcreteMallocBlock

    三种类型 block 被复制之后:

    类型 存储位置 复制之后
    NSConcreteGlobalBlock 数据区 什么也不做
    NSConcreteStackBlock 栈上 从栈上复制到堆上
    NSConcreteMallocBlock 堆上 引用计数增加

    在 block 中使用对象类型自动变量时,除以下几种情况外,推荐手动调用 block 的 copy 方法:

    • block 作为函数值返回的时候
    • 将 Block 赋值给附有 __strong 修饰符 id 类型的类或者 Block 类型成员变量时
    • 向方法中含有 usingBlock 的 Cocoa 框架方法或者 GCD 的 API 中传递 Block 时

    当一个 block 被复制到堆上的时候,__block 变量也会被复制到堆上,并且被这个 block 所持有,如果又有 block 被复制到堆上,那么 __block 变量的引用计数会增加,然后如果 block 被废弃,那么就不再持有 __block 变量.

    block 循环引用

    如果在 block 中使用附有 __strong 修饰符的对象类型自动变量,那么当 block 从栈上复制到堆上的时候,该对象被 block 所持有,这样就容易发生循环引用

    例如下面这份代码:

    typedef void(^blk)(void);
    
    interface BlockTest : NSObject
    {
        blk blk_;
    }
    
    @implementation BlockTest
    
    - (instancetype)init
    {
        self = [super init];
        blk_ = ^{
            NSLog(@"self----------------%@",self);
        };
        return self;
    }
    
    @end
    

    上面这份代码中就出现了循环引用,即BlockTest 类对象持有 block,而在 block 中使用持有 __strong 修饰符的 id 类型变量 self,二者互相持有,导致都无法释放,这就是循环引用
    如下图所示:

    block 循环引用.png

    在编译器进行代码编译的时候,可以检测出这种循环引用,并且发出警告,但是并不是任何情况下都能有警告,这里需要注意

    为了避免循环引用,这里有几种方法,我们一个一个看

    __weak 修饰符

    即如下代码:

    - (instancetype)init
    {
        self = [super init];
        id __weak weakSelf = self;
        blk_ = ^{
            NSLog(@"self---------------%@",weakSelf);
        };
        return self;
    }
    

    加上 __weak 修饰符之后,block 不再是持有 self,而是持有对对象的弱引用,也就是 self 强引用 block,而 block 弱引用 self,这样就不会发生循环引用

    __block 修饰符

    先看一下如下代码:

    - (instancetype)init
    {
        self = [super init];
        __block id tempSelf = self;
        blk_ = ^{
            NSLog(@"self = %@",tempSelf);
            tempSelf = nil;
        };
        return self;
    }
    
    - (void)dellocBlk
    {
        blk_();
    }
    

    在上面的代码中,乍一看二者还是互相引用,但是在 block 内部执行完毕之后有了一个将 tempSelf 置为 nil 的操作,这样就是将 self 对 block 的强引用切断,那么循环引用被破坏,但是这样有一个缺点,那就是如果不执行 blk_ 这个 block,这个循环引用将会永远存在,只有执行了这个 block,tempSelf 才会被置为 nil,循环引用才会被破坏,但是这种方案并非没有优点,很明显的一个优点就是我们可以依靠这个 block 来控制对象的持有时间,这样可以控制对象的释放时间.

    Strong-Weak Dance

    在解决 block 的循环引用时,除了使用 __weak 修饰符外,还有一个被苹果称为:“Strong-Weak Dance” 的方法,那这个是什么呢?先看一下代码:

    - (instancetype)init
    {
        self = [super init];
        id __weak weakSelf = self;
        _blk = ^{
            id __strong strongSelf = weakSelf;
            if (strongSelf) {
                NSLog(@"self---------------%@", strongSelf);
            }
        };
        return self;
    }
    

    是不是很奇怪,我们好不容易使用 __weak 消除的强引用,为什么在里面又进行了强引用呢?也就是强引用->弱引用->强引用这个过程的作用是什么?我们先来看一下普通的 __weak 有什么问题

    __weak BlockTest *weakSelf = self;
    self.completionHandler = ^(NSInteger result) {
        [weakSelf.property removeObserver: weakSelf forKeyPath:@"pathName"];
    };
    

    在上面的代码中,__weak 确实解决了循环引用的问题,但是假设当前 block 在子线程中执行,但是在执行的时候 self 被释放了,这时候 weakSelf 也被置为 nil,但是我们使用 KVO 来进行移除操作,这时候就会发生崩溃
    这时候就需要 Strong-Weak Dance 登场了,还是先来看代码:

    - (instancetype)init
    {
        self = [super init];
        id __weak weakSelf = self;
        _blk = ^{
            id __strong strongSelf = weakSelf;
            NSLog(@"self---------------%@", strongSelf);
        };
        return self;
    }
    

    在 block 内部对 self 又进行一次强引用之后,self 所指对象的引用计数变为 2 ,即使主线程中的 self 被释放,那么对象的引用计数变为1,此时对象并没有销毁

    这时候,可能会有人有问题,在里面对 self 的对象进行了强引用,这样岂不是又变成了互相持有,循环引用?
    这个时候需要注意,block 只有截获外部变量时,才会引用它。如果是内部新建一个,则没有任何问题。

    但是这样真的能够解决多线程下,weakSelf 指向的对象在 block 执行之前被释放的问题吗?如果在执行 id __strong strongSelf = weakSelf 之前 weakSelf 指向的对象就已经被释放了,那么这个语句就没有任何意义了,在开始的那个 KVO 的例子中,崩溃还是依然会发生的,所以此时的 Strong-Weak Dance 没有任何作用,所以为了保证安全性,我们可以在 block 内部进行 strongSelf 的 nil 检测,即:

    - (instancetype)init
    {
        self = [super init];
        id __weak weakSelf = self;
        _blk = ^{
            id __strong strongSelf = weakSelf;
            if (strongSelf) {
                NSLog(@"self---------------%@", strongSelf);
            }
        };
        return self;
    }
    

    参考资料

    相关文章

      网友评论

        本文标题:深入理解 Block

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