• iOS底层原理探索Block本质(三)


    首先,我们看两段代码:

    在这里插入图片描述

    从运行结果可以看出,如果是普通局部变量age,第17行和第22行的age地址是一样的,第20行的地址跟前面两个是不同的。
    这个原因我们在上节已经分析过。是因为:
    第17和第22行的age是age的地址
    第20行的age是捕获进去的age,其是一个在block内部新建的同名age,因此,地址不同。

    在这里插入图片描述

    转化为底层代码可以看到:

    在这里插入图片描述

    从底层代码可以看出,三者最后都是取的&(age.__forwarding->age),也就是被包装为__Block_byref_age_0类型后里面的age。可以解释2—和3—地址一样。

    按说1—的地址应该也是一样的,不知为何1—的地址不同

    上面两端代码都是在ARC环境下进行的,是不是ARC又为我们做了哪些操作,从而造成这样的结果呢?
    我们分别将两段代码运行在MRC上

    在这里插入图片描述

    这个结果跟在ARC上的结果是一致的,不再谈论

    在这里插入图片描述

    这个结果就跟我们跟在ARC上的结果是不同的,同样,我们看下其底层源码:

    在这里插入图片描述

    源码也一样,怎么结果就是不一样呢?到底ARC为我们做了什么?

    我们继续做实验

    在这里插入图片描述

    在ARC下,我们添加了几个打印,可以看出在ARC下,blcok的类型是NSMallocBlock,存储在堆上的。

    修改为MRC后:

    在这里插入图片描述

    不一样的地方出现了,在MRC下block的类型是NSStackBlock类型的,存储在栈上。

    按照我们之前的结论:
    访问auto变量的block是NSStackBlock类型的。
    将^{}block赋值给强指针YZBlockL类型的block,也就是 ^{}block有强指针引用,因此,在ARC下 ^{}block 会自动将栈上的block复制到堆上,所以,block的类型是NSMallocBlock。

    也就三个打印的age都是&(age.__forwarding->age)的地址。
    但是1—是在栈上的age。2—和3—都是访问的堆上面的age。

    上面的代码,也可以看出,使用__block修饰的age变量,底层的时候,也调用了copy和dispose相关函数,并在copy函数里面通过__Block_object_assign使用&dst->age,访问age,并对age进行强引用操作。而&dst就是__main_block_impl_0,其里面的age就是__Block_byref_age_0 *age。
    换句话说,就是使用_block修饰的变量,底层会进行copy操作,并在copy操作中对里面转换为__Block_byref_age_0类型的age进行相应的内存管理。

    __block的内存管理

    当block在栈上时,并不会对__block变量产生强引用

    当block被copy到堆上时
    会调用block内部的copy函数
    copy函数内部会调用__Block_object_assign函数

    __Block_object_assign函数会对__block修饰的变量进行内存管理
    在这里插入图片描述

    这个图表明,在进行copy操作后,block会从栈复制到堆上面。同时,会将block内部访问的使用__block修饰的变量也复制到堆上面,并且,堆上面的block还是会对堆上面的__block修饰的变量进行引用关系。

    如果,有两个block,同时引用一个__block修饰的变量呢?
    在这里插入图片描述

    两个block同时引用一个__block修饰的变量,会对__block进行相当于+2的操作

    在这里插入图片描述

    运行结果

    2020-04-28 16:55:09.859862+0800 block学习[5641:192209] 1----0x7ffeefbff4f8
    2020-04-28 16:55:09.860258+0800 block学习[5641:192209] 2----0x100703b48
    2020-04-28 16:55:09.860365+0800 block学习[5641:192209] ---30---
    2020-04-28 16:55:09.860406+0800 block学习[5641:192209] 3----0x100703b48
    2020-04-28 16:55:09.860437+0800 block学习[5641:192209] ---30---
    2020-04-28 16:55:09.860465+0800 block学习[5641:192209] 4----0x100703b48
    

    可以看到2、3、4的age都是同一个内存地址。

    在这里插入图片描述

    在这里插入图片描述

    上面两个图可以看到,一个是__main_block_impl_0类型,一个是__main_block_impe_1类型,但是从定义可以看出

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_age_0 *age; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    struct __main_block_impl_1 {
      struct __block_impl impl;
      struct __main_block_desc_1* Desc;
      __Block_byref_age_0 *age; // by ref
      __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    里面的age统一都是__Block_byref_age_0类型,因此,上图里面&dst->age访问的age都是同一个,是__Block_byref_age_0类型里面的age。

    当block从堆中移除的时
    会调用block内部的dispose函数
    dispose函数内部会调用__Block_object_dispose函数

    __Block_object_dispose函数会自动释放引用的__block修饰的变量
    在这里插入图片描述

    在这里插入图片描述

    __block的__forwarding指针

    在__block修饰变量的几次例子中,我们都看到,最后访问里面的age是使用age.__forwarding->age访问的方式,这是为什么呢?
    首先,我们知道__forwarding指向的就是他自己,那么, 为什么不直接访问里面的age变量,而是通过__forwarding指针访问age变量呢?

    这是因为,如果我们访问的age变量在栈上的时候,通过__forwarding找到它自己,然后访问里面的age,很容易找到age,这是行的通的,虽然复杂了一步。
    但是,如果block已经复制到堆上面,__forwarding的作用就突显出来了。__forwarding指针会指向堆上面的block,这样里面访问的__block修饰的变量,确保了是堆上面的。

    注:复制并不是剪切,栈上面是还有的。
    在这里插入图片描述

    前面我们讲的是__block修饰基础变量类型,如果用__block修饰对象类型的变量,将会是怎样的情形呢?

    __block修饰对象类型

    在这里插入图片描述

    代码通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-12.0.0 main.m指令,转换为底层代码
    部分代码为:

    在这里插入图片描述

    可以看出,使用__block修饰的对象类型,也会生成一个__Block_byref_person_0类型且变量名为person的变量。在__Block_byref_person_0定义里面,有一个YZPerson类型且变量名为person的变量。
    首先,我们看到这个person是__strong修饰的。
    那么,如果使用__weak修饰对象的话,会是怎样的情况呢?
    在这里插入图片描述

    首先,我们要明确一点,__weak是修饰person对象的,而跟__block无关。这也就解释了为何__block __weak int age = 10;会有一个警告。因为__weak只能修饰对象。
    转换为底层代码可以看到:

    在这里插入图片描述

    可以看到,__weak是修饰的YZPerson类型的person对象,而跟__Block_byref_weakPerson_0类型无关。
    也就是__block修饰的对象生成的__Block_byref_weakPerson_0类型,都是强引用。而__Block_byref_weakPerson_0里面的YZPerson类型的person对象是强还是弱,跟外部使用的强或弱有关。

    在这里插入图片描述

    继续研究发现

    在这里插入图片描述

    __Block_byref_weakPerson_0内部也有一个__Block_byref_id_object_copy和__Block_byref_id_object_dispose函数。这是使用__block修饰基本数据类型所没有的两个新东西。
    那么,这两个新东西是什么呢?

    我们知道,当block被复制到堆上时,__block修饰的变量也会被拷贝到堆上面去,当__block修饰的变量被拷贝到堆上时,内部就会调用上图两个函数,对其进行内存管理相关操作。

    在对__block修饰的变量进行转换的时候,我们可以看到:

    __block __weak YZPerson *weakPerson = person;
    转换为:
    
    __attribute__((__blocks__(byref))) __attribute__((objc_ownership(weak))) __Block_byref_weakPerson_0 weakPerson = {
    (void*)0,
    (__Block_byref_weakPerson_0 *)&weakPerson, 
    33554432, 
    sizeof(__Block_byref_weakPerson_0), 
    __Block_byref_id_object_copy_131, 
    __Block_byref_id_object_dispose_131, 
    person
    };
    
    对应
    struct __Block_byref_weakPerson_0 {
      void *__isa;
    __Block_byref_weakPerson_0 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*);
     void (*__Block_byref_id_object_dispose)(void*);
     YZPerson *__weak weakPerson;
    };
    

    可以看到__Block_byref_id_object_copy_131传给了 void (* __Block_byref_id_object_copy)(void, void);
    __Block_byref_id_object_dispose_131, 传给了 void (* __Block_byref_id_object_dispose)(void*);

    通过定义可以看到:
    在这里插入图片描述

    可以看到,其内部也有一个__Block_object_assign或__Block_object_dispose函数。并且里面有一个dst+40的操作,dst+40后是谁呢?

    在这里插入图片描述

    可以看出,dst+40正好指向的是YZPerson类型的weakPerson对象。这样就看出,__Block_byref_id_object_copy和__Block_byref_id_object_dispose是对YZPerson类型的weakPerson对象进行内存管理的。

    当__block变量在栈上时,不会对指向的对象产生强引用
    
    当__block变量被copy到堆时
    会调用__block变量内部的copy函数
    copy函数内部会调用__Block_object_assign函数
    __Block_object_assign函数会根据所指向对象的修饰符(__strong\__weak\__unsafe_unretain)做出相应的操作,形成强引用或弱引用(注意:ARC才会强或弱,MRC只会是弱)
    
    如果__block变量从堆上移除
    会调用__block变量内部的dispose函数
    dispose函数内部会调用__Block_object_dispose函数
    __Block_object_dispose函数会自动释放指向的对象
    

    关于上面注意里面的话,我们可以举一个在MRC下的例子:

    在这里插入图片描述

    在23行,block还没释放,person就已经被释放了,这说明:
    在MRC下,__block后面修饰的变量是强或者弱都没有关系,最后block对__block修饰的变量都是弱引用


    block的循环引用问题

    首先,看一段代码

    在这里插入图片描述

    可以看到,在19行结束后,也没有打印person消失的信息,说明person没有消失,被循环引用了。

    img

    大致就是,block内部其实是有一个强指针引用了YZPerson类型的person对象,指向着YZPerson。而YZPerson对象里面有一个_block的成员变量,指向着对应的block,这就产生了循环引用问题。

    左上角的block是指的16行等号右边的block
    右边的YZPerson(MJPerson)是指的14行等号右边的东西
    左下角的person是指的14行左边的内容

    在ARC下解决循环引用一般使用__weak或者__unsafe_unretained
    两者的区别是:
    __weak会在指向的对象消失的时候,将指针置为nil;

    __unsafe_unretained会在指向的对象消失的时候,不会将指针置为nil;

    当然,也可以借助__block来解决循环引用问题
    在这里插入图片描述

    如果没有person = nil的情况下,其各种关系是如下:

    在这里插入图片描述

    __block指的是:__block修饰的person
    对象指的是:(__block修饰的person)里面的YZPerson类型的person对象
    Block指的是:person.block(),是YZPerson类型的person对象拥有的成员变量
    Block持有__block,是指的16行代码,等号右边的内容

    加上一句置为nil的操作后,其关系为:
    在这里插入图片描述

    在MRC下,可以使用__unsafe_unretained解决循环引用。因为MRC没有__weak,也就不存在使用__weak解决循环引用的问题了。
    使用__unsafe_unretained可以解决的原因是,block内部引用对象的时候,不会对引用计数器+1。
    另外,我们也可以直接使用__block解决循环引用问题。原因的话,我们在之前有讲过,就是因为,在MRC下,block对__block修饰的变量都是相当于弱引用,不会对引用计数器+1。

  • 相关阅读:
    力扣(LeetCode)922. 按奇偶排序数组 II
    力扣(LeetCode)1002. 查找常用字符
    力扣(LeetCode)15. 三数之和
    Java == 和 equals 区别
    力扣(LeetCode)125. 验证回文串
    力扣(LeetCode) 905. 按奇偶排序数组
    力扣(LeetCode)832. 翻转图像
    力扣(LeetCode) 771. 宝石与石头
    Sticks
    荷马史诗
  • 原文地址:https://www.cnblogs.com/r360/p/15788194.html
Copyright © 2020-2023  润新知