zoukankan      html  css  js  c++  java
  • iOS底层原理探索Block本质(二)

    面试题:

    问:在24行打断点,person对象是否被释放?

    在这里插入图片描述

    按说,person的作用域是其附近的两个{},过了两个{}后,person对象应该被释放,而实际上,在24行断点处,person对象并没有消失。

    问:为什么呢?

    首先我们将程序运行,可以看到其运行过程:

    24行打印block学习[2478:134123] ---------
    25行打印block学习[2478:134123] 调用了block---10
    26行结束打印block学习[2478:134123] YZPerson消失了
    

    将main.m转化为底层代码后,我们进行分析,可以看到block的构成

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      YZPerson *person;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, YZPerson *_person, int flags=0) : person(_person) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    可以看到,block内部捕获了局部变量YZPerson的值,相当于block内部有一个指针对person对象进行了强引用,从而保证了person对象没有消失。 在26行过后,block对象消失,因此,person对象也消失。

    由于在ARC环境下,系统会帮我们做很多事情,我们需要具体看一下里面的一些细节,因此,我们切换到MRC环境下。

    在MRC环境下:

    在这里插入图片描述

    在这里插入图片描述

    我们发现一个有趣的现象:
    在MRC环境下:
    没有对block进行copy操作,person会被释放。
    对block进行copy操作,person不会被释放。

    首先,上述例子中,block是局部变量,而我们知道:局部变量是存储在栈区

    ^{
        NSLog(@"调用了block---%d", person.age);
    };
    

    由于访问量auto变量person,因此,其实存储类型是NSStackBlock类型。

    [^{
        NSLog(@"调用了block---%d", person.age);
    } copy];
    

    由于访问量auto变量person,其实存储类型是NSStackBlock类型,又因为调用了copy,最终其存储类型是NSMallocBlock类型

    当block是NSStackBlock类型时,不能拥有其内部的变量。
    这是因为,其本身就是存储在栈区,是不稳定的。
    而当执行copy操作后,其存储在堆区,可以拥有其内部的变量。

    在ARC下,由于block指向对象是有强指针引用的,因此会默认对其进行copy操作,将block指向的对象存放在堆区,因此是可以拥有其内部的变量person。

    在ARC环境下:

    在这里插入图片描述

    注意,23行调用的是person.age,而不是weakPerson.age

    在这里插入图片描述

    注意,23行调用的是weakPerson.age

    一个现象:在block内部引用使用__weak修饰的auto变量weakPerson,在26行YZPerson消失了。这又是为什么呢?
    当block进行了copy操作,其内部又经历了哪些方法和操作呢?

    我们再次探究源码:

    #import "YZPerson.h"
    typedef void(^YZBlock)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            YZBlock block;
            {
                YZPerson *person = [[YZPerson alloc] init];
                person.age = 10;
                block = ^{
                           NSLog(@"调用了block---%d", person.age);
                };
            }
            NSLog(@"---------");
            block();
        }
        return 0;
    }
    

    使用命令行指令

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-12.0.0 main.m 
    
    其中:
    -fobjc-arc表明是arc环境
    -fobjc-runtime=ios-12.0.0需要用到运行时,版本12.0.0
    

    转换为底层代码后,block里面的内容为

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      YZPerson *__strong person;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, YZPerson *__strong _person, int flags=0) : person(_person) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    可以看出person是__strong修饰的。

    #import "YZPerson.h"
    typedef void(^YZBlock)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            YZBlock block;
            {
                YZPerson *person = [[YZPerson alloc] init];
                person.age = 10;
                __weak YZPerson *weakPerson = person;
                block = ^{
                           NSLog(@"调用了block---%d", weakPerson.age);
                };
            }
            NSLog(@"---------");
            block();
        }
        return 0;
    }
    

    查看底层代码:

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

    可以看出person是__weak修饰的。

    当block内部没有引用外部局部变量的时候

    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内部引用外部局部变量的时候

    block = ^{
    	NSLog(@"调用了block---%d", weakPerson.age);
    };
                
    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
    };
    

    两个函数的实现:

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    	_Block_object_assign((void*)&dst->weakPerson, (void*)src->weakPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    	_Block_object_dispose((void*)src->weakPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }
    

    可以发现,相比block内部没有引用外部局部变量,__main_block_desc_0里面多了两个函数指针,copy和dispose

    当block从栈拷贝到堆里面的时候,会自动调用 __main_block_copy_0 函数,在里面实现_Block_object_assign,在这个里面有调用外部引用的weakPerson,该调用是强指针或者弱指针是根据block定义里面的weakPerson类型做判断,追溯到上面,其实是代码中__weak YZPerson *weakPerson = person;__weak修饰起的作用。在我们的例子这,该调用是一个__weak弱指针调用。

    同样,有创建就有消除,当堆上的block将移除的时候,会自动调用__main_block_dispose_0函数,在里面实现_Block_object_dispose,在这个里面同样会调用外部引用的weakPerson。

    如果block是在栈上是NSStackBlock类型时,将不会对auto变量产生强引用

    参考:当block是NSStackBlock类型时,不能拥有其内部的变量,因为,其本身就是存储在栈区,是不稳定的。而当执行copy操作后,其存储在堆区,可以拥有其内部的变量
    当block内部访问了对象类型为auto的变量时候

    如果block被拷贝到堆上
    会调用block内部的copy函数
    copy函数内部会调用_Block_object_assign函数
    _Block_object_assign函数会根据auto变量的修饰符( __strong、 __weak、__unsafe_unretain )作出相应的操作,形成强引用或者弱引用。

    如果block从堆上移除
    会调用block内部的dispose函数
    dispose函数内部会调用_Block_object_dispose函数
    _Block_object_dispose函数会自动释放引用的auto变量
    在这里插入图片描述

    blcok内部引用__weak修饰的auto局部变量,在26行结束后,YZPerson被销毁的原因是因为,block内部对其进行的是__weak弱引用。

    需要注意的是,如果block内部访问的局部变量为非对象类型,是不会生成copy和dispose函数的。

    几个测试题b

    在这里插入图片描述

    block作为GCD参数的时候,会将block复制到堆上,而里面又引用了person局部变量的对象,因此会对block里面的person对象变量进行类似强引用功能,从而保证person在{}消失的时候不会消失。在3秒过后,GCD释放,从而person对象也释放。

    在这里插入图片描述

    block作为GCD参数的时候,会将block复制到堆上,而里面又引用了person局部变量的对象,但是,前面是__weak修饰的,因此会对block里面的person对象变量进行类似弱引用功能。因此,在{}执行完毕后,person就被销毁。

    在这里插入图片描述

    block作为GCD参数的时候,会将block复制到堆上,第二次使用block做GCD参数,会将block的引用做类似+1操作。第二个block里面引用了person局部变量的对象,因此会对第二个block里面的person对象变量进行类似强引用功能。因此,在第二个block执行完毕后,person才被销毁。

    在这里插入图片描述

    block作为GCD参数的时候,会将block复制到堆上,第二次使用block做GCD参数,会将block的引用做类似+1操作。第二个block里面引用了person局部变量的对象,但是,前面是__weak修饰的,因此会对block里面的person对象变量进行类似弱引用功能。因此,在{}执行完毕后,person就被销毁。

    在这里插入图片描述

    block作为GCD参数的时候,会将block复制到堆上。block里面引用了person局部变量的对象,因此会对第一个block里面的person对象变量进行类似强引用功能。
    第二次使用block做GCD参数,会将block的引用做类似+1操作。第二个block里面引用了person局部变量的对象,但是是__weak类型的,因此会对第二个block里面的person对象变量进行类似弱引用功能。
    因此,在第一个block执行完毕后,person就被销毁。
    在这里插入图片描述

    block作为GCD参数的时候,会将block复制到堆上。block里面引用了person局部变量的对象,但是是__weak类型的,因此会对第一个block里面的person对象变量进行类似弱引用功能。
    第二次使用block做GCD参数,会将block的引用做类似+1操作。第二个block里面引用了person局部变量的对象,因此会对第二个block里面的person对象变量进行类似强引用功能。
    因此,在第二个block执行完毕后,person才销毁。一个简单的栗子:

    问:下面的打印结果是什么?

    int age = 10;
    YZBlock block = ^{
        NSLog(@"---%d---", age);
    };
    age = 20;
    block();
    
    打印结果是:---10---
    

    原因很简单,是因为block将变量age捕获到block内部,并且由于是auto变量,捕获的是值。
    虽然age=20,但是在编译的时候,block已经将age=10的值捕获进去。因此,打印的是10。

    在这里插入图片描述

    问:为何不能直接在block内部修改外部局部变量呢?

    int age是在main函数里面创建的;

    block内部的age是定义在__main_block_func_0里面的;
    不能通过修改__main_block_func_0里面的age从而去反向改变main里面的age值。

    另外,捕获其实是新建一个同名变量,因此,block里面的age是一个新建的age,其值是10。

    从下面的例子可以看出,block内部的变量跟block外部的变量,不是同一个变量。
    在这里插入图片描述

    可以看出:
    16行、22行的age地址相同,也就是block外部的age是同一个
    19行、20行的age地址相同,也就是block内部的age是同一个
    而19-20行的age地址跟16、22行的age地址不同,说明block内部的age变量与block外部的age变量不是同一个。

    既然block内外age变量不是同一个,就不能通过修改block内部的age变量,去修改block外部的age变量。
    至于为什么内部的age变量也不能修改,是因为 block内部的捕获新建是隐式的,在外部看来并没有新建一个age,block内外的age就是同一个age。为了避免用户想去通过修改block内部的age而去修改外部的age值,苹果直接将block内部的age做了限制,只能使用,禁止赋值。

    类似的有局部变量NSMuttableArray *array,在block内部只能使用,不能赋值。
    也就是,只能做[array addObject:];等操作
    不能做,array = nil;或者 array = [NSMuttableArray array];等操作
    在这里插入图片描述

    在这里插入图片描述

    使用static修饰的局部变量就可以进行修改。
    这是因为,使用static修饰的局部变量,block内部捕获的是指针,因此,可以通过指针修改外部age的值,这我们前面讲过。

    当然,全局变量是可以修改的,这个就不用说了。

    现在我们还可以通过另外一种方法,进行修改外面布局变量的值,这就是__block。
    在这里插入图片描述

    问:为什么使用__block修饰局部变量就可以修改age的值呢?

    __block只能修饰auto局部变量,不能修饰 全局变量 和 static修饰的静态变量。

    通过底层源码可以看到:

    在这里插入图片描述

    首先,我们看下__block int age = 10;转换为:

    __attribute__((__blocks__(byref))) __Block_byref_age_0 age = 
    {
    (void*)0,
    (__Block_byref_age_0 *)&age, 
    0, 
    sizeof(__Block_byref_age_0), 
    10
    };
    

    简化后

    __Block_byref_age_0 age = {
    0,
    &age, 
    0, 
    sizeof(__Block_byref_age_0), 
    10
    };
    

    可以看出age最后被转换为__Block_byref_age_0类型的age结构体。那么__Block_byref_age_0这是个什么类型的东西呢?
    从源码可以看出__Block_byref_age_0的定义是

    struct __Block_byref_age_0 {
      void *__isa;//isa指针,代表该类型是一个对象
      __Block_byref_age_0 *__forwarding;//接收自己的地址
     int __flags;
     int __size;//改类型值的大小
     int age;//__block修饰的变量age10
    };
    
    那么两个结合到一起,可以看到那些值分别代表的意义。
    
    YZBlock block = ^{
                age = 30;
                NSLog(@"---%d---", age);
            };
    被转换为:
    
    YZBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
    
    简化后
    YZBlock block = &__main_block_impl_0(
    __main_block_func_0, 
    &__main_block_desc_0_DATA, 
    &age, 
    570425344);
    

    __main_block_impl_0的定义是

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

    可以看出,block里面并没有直接捕获age值,而是新创建了一个__Block_byref_age_0类型的age对象。

    通过其构造函数可以看到其赋值过程

    __main_block_impl_0(
    __main_block_func_0, 
    &__main_block_desc_0_DATA, 
    &age, 
    570425344);
    
    __main_block_impl_0(
    void *fp, 
    struct __main_block_desc_0 *desc, 
    __Block_byref_age_0 *_age, 
    int flags=0)
    

    在这里插入图片描述

    那,里面的age=30;是如何修改的呢?
    首先,block会将里面的内容封装到__main_block_func_0函数里面,然后通过(age.__forwarding->age) = 30;根据__Block_byref_age_0类型的age里面自己的__forwarding指针,获取里面的age,修改为30;
    这里,由于block定义里面是拿的类型为__Block_byref_age_0变量名为age的指针,由于是指针,因此,我们可以拿到里面的值,也可以修改里面的值。
    在这里插入图片描述

    问:下面的代码为何会报错?

    在这里插入图片描述

    static修饰变量的时候报错,错误提示是:
    Initializer element is not a compile-time constant

    这是因为,被static修饰的局部变量,会延长生命周期,使得周期与整个应用程序有关; 只会分配一次内存;程序一运行,就会分配内存,而不是运行到那才分配。

    而[[YZPerson alloc] init];是在运行到此处的时候才会分配内存。会有冲突,因此,不能这么写。
    在这里插入图片描述

    在这里插入图片描述

    可以看到,使用static修饰的局部变量,捕获的是指针YZPerson **person;

    在这里插入图片描述

    在这里插入图片描述

    上图表明,person、array等指针类型,由于auto类型,捕获进去的是值,因此,在block里面捕获的是指针。
    指针指向的内容可以修改,也就是person.age, [array addObject:@“4”];都可以修改。
    但是,person和array指针本身是不可以修改的。因此,person = nil; array = nil;是不可以执行的。

    这个试验引出了一个常问的面试题:

    Block与数组的关系

    从上面的结果来看:
    auto类型的array,在block内部可以进行添加删除元素操作,但不可以进行array = nil;操作
    static类型的array,捕获的是指针,也可以进行添加删除元素操作,可以进行array = nil;操作
    __block,也是对指针操作,也可以进行添加删除元素操作,可以进行array = nil;操作

  • 相关阅读:
    PAT 05-树7 File Transfer
    PAT 05-树6 Path in a Heap
    PAT 10-2 删除字符串中的子串
    PAT 10-1 在字符串中查找指定字符
    PAT 10-0 说反话
    PAT 08-2 求矩阵的局部最大值
    PAT 07-3 求素数
    PAT 07-2 A+B和C
    PAT 07-0 写出这个数
    PAT 06-3 单词长度
  • 原文地址:https://www.cnblogs.com/r360/p/15775395.html
Copyright © 2011-2022 走看看