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


    首先,在学习之前,增加一些动力。经常在面试中,会被问及到这些问题:

    block的本质是什么?

    __block的作用是什么?原理是什么?有哪些使用注意点?

    我们知道block在使用的时候,一般用copy修饰,用copy修饰发生了什么?具体过程是怎样的?

    带着这些疑问,我们开始今天的学习。

    block的数据结构长什么样?

    首先,我们写一个简单的block,以及block的调用:

    int age = 10;
    void(^block)(int, int) = ^(int a, int b){
        NSLog(@"调用该block----%d", age);
    };
    block(100, 100);
    

    通过clang编译指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m将main.m文件转换为底层代码,通过查找可以看到上面代码转换为底层代码的相关代码:

    int age = 10;
    void(*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
    
    ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 100, 100);
    

    可以看到,block最后被转换为__main_block_impl_0类型

    __main_block_impl_0类型是个什么样的结构存在的呢?

    通过查看定义能够知道,__main_block_impl_0的定义为:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int age;//函数调用的外部参数
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    其中,__main_block_impl_0里面第一个类型__block_impl和第二个类型__main_block_desc_0的定义分别为:

    struct __block_impl {
      void *isa;//isa指针
      int Flags;
      int Reserved;
      void *FuncPtr;//函数地址
    };
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } 
    

    首先,我们看到__main_block_func_0是一个函数;

    main函数中__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)将参数__main_block_func_0传到了block里面,赋值给__main_block_impl_0里面的fp,然后做了 impl.FuncPtr = fp。

    最后在执行block的时候,是执行的block->FuncPtr,就调用到了impl.FuncPtr,也就是fp,也就是__main_block_func_0
    在这里插入图片描述

    block内部直观表示大致如下:

    在这里插入图片描述

    总结:

    1. block是一个具有isa指针的oc对象
    2. block是封装了函数以及函数调用环境的OC对象

    封装的函数是指block{}内部的代码,被转换成一个函数__main_block_func_0,并将函数地址封装在了__main_block_impl_0(block类型)内部的impl.FuncPtr
    函数调用环境是指,函数调用的时候需要的参数,从图中可以看出,函数需要的变量age,已经被封装在了__main_block_impl_0里面。
    接下来,我们分析下block转换为底层源码的代码

    int age = 10;
    void(block)(int, int) = ((void ()(int, int))&__main_block_impl_0((void )__main_block_func_0, &__main_block_desc_0_DATA, age));
    上句代码是Block的定义
    一般小括号为强制转换,为了方便观察,可以将小括号以及小括号里面的内容删掉。
    简化为:
    void(
    block)(int, int) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age));
    等号后面,是一个函数,函数名为__main_block_impl_0,函数有三个参数。并且获取函数地址后赋值给block对象。也就是block是一个指针变量。其内部存放的是__main_block_impl_0类型的地址。

    而查看__main_block_impl_0的定义

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int age;//函数调用的外部参数
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
        impl.isa = &_NSConcreteStackBlock;//block的类型是_NSConcreteStackBlock
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    __main_block_impl_0函数与结构体名一样,该函数没有写返回值,但其实是返回结构体本身,该函数称为构造函数。
    那么,block其实指向的是__main_block_impl_0结构体的地址。

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

    sizeof(struct __main_block_impl_0):__main_block_impl_0即block的大小

    ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 100, 100);
    上句代码为block的执行,简化后的结果为:
    (block->FuncPtr)(block, 100, 100);
    利用block找到FuncPtr函数,进行调用。

    看一下(block->FuncPtr)(block, 100, 100);
    block即__main_block_impl_0类型,里面并没有FuncPtr函数

    __main_block_impl_0里面的__block_impl类型里面才有FuncPtr函数,怎么block直接就调用了FuncPtr函数呢?

    这是因为,里面有个强制转换操作,将block强制转换为__block_impl *类型,这样,就可以直接访问__block_impl里面的FuncPtr函数,即__block_impl->FuncPtr。
    那,为什么这个可以将__main_block_impl_0强制转换为__block_impl类型呢?
    这是因为,结构体__block_impl类型是__main_block_impl_0类型的第一个成员,那么__main_block_impl_0类型的内存地址跟__block_impl类型的内存地址是一样的,因此,可以强制转换。

    从另一个角度去分析,__main_block_impl_0里面的__block_impl是一个结构体,而不是指针,相当于直接把__block_impl类型的内容放入__main_block_impl_0之中,也就相当于可以直接进行__main_block_impl_0->FuncPtr访问。

    block捕获机制

    block内部访问局部变量

    来段简单的代码:

    int age = 10;
    void(^block)(void) = ^{
        NSLog(@"调用该block----%d", age);
    };
    age = 20;
    block();
    

    很容易,我们知道最后的运行结果是:

    调用该block----10

    那么,是怎样一个原理呢?

    同样,我们通过clang命令,将代码转换为底层代码:

    int age = 10;
    
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
    
    age = 20;
    
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    

    可以看到,block将age作为参数,传到__main_block_impl_0里面。

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

    在__main_block_impl_0里面,block自己定义了一个同名变量age。
    并通过age(_age)将_age的值赋值给age,即
    age(_age) 等价于 age = _age;

    执行
    age = 20;
    只是将int age = 10变为int age = 20,并没有改变block里面age的值

    执行
    block();
    调用block实现,就调用了__main_block_impl_0里面的FuncPtr函数,而FuncPtr函数里面已经封装了age

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) 
    {
      int age = __cself->age; // bound by copy
      NSLog((NSString *)&__NSConstantStringImpl__var_folders_91_sht6gqgs1xj8b0_gczwc0ygc0000gn_T_main_65a3fe_mi_0, age);
    }
    

    该age是__cself->age,即block内部的age。而block内部的age=10。
    因此,打印出来的age=10;也就是常说的值传递。

    为什么值传递的值不可以赋值或者修改呢?

    这个我们留到下一小节进行讲述

    block内部访问static修饰的局部变量

    如果用static修饰,会是怎么样呢?

    int age = 10;
    static int height = 170;
    void(^block)(void) = ^{
        NSLog(@"调用该block----%d, %d", age, height);
    };
    age = 20;
    height = 180;
    block();
    
    结果:调用该block----10, 180
    
    int age = 10;
    static int height = 170;
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
    age = 20;
    height = 180;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    

    从上面可以看出,age传的是值,height是传的指针

    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0* Desc;
        int age;//新建同名变量age
        int *height;//新建同名变量height,但是此height是指针变量
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
            impl.isa = &_NSConcreteStackBlock;
            impl.Flags = flags;
            impl.FuncPtr = fp;
            Desc = desc;
        }
    };
    

    __main_block_impl_0内部,新建的age是int,而height是指针int *。

    执行height = 180;

    执行block();

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) 
    {
      int age = __cself->age; // bound by copy
      int *height = __cself->height; // bound by copy
      NSLog((NSString *)&__NSConstantStringImpl__var_folders_91_sht6gqgs1xj8b0_gczwc0ygc0000gn_T_main_48f888_mi_0, age, (*height));
    }
    

    调用的时候,age是值传递,height是指针

    由于height传的值是指针,*height已经修改为180,因此,block内部的height也被修改,因此最后打印出来的height是180

    在这里插入图片描述

    block内部访问全局变量

    如果是全局变量或者static修饰的全局变量,运行结果又有什么不一样呢?
    int age = 10;
    static int height = 170;
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            void(^block)(void) = ^{
                NSLog(@"调用该block----%d, %d", age, height);
            };
            age = 20;
            height = 190;
            block();
        }
        return 0;
    }
    
    运行结果:调用该block----20, 190
    

    转换为底层代码后:

    在这里插入图片描述

    从底层源码可以看出,__main_block_impl_0内部没有新定义age,或者height。
    说明block内部并没有捕获外部的全局变量。

    最后调用函数,打印的age和height是全局变量。

    通过以上代码,我们可以发现,block访问外部变量,有一个变量捕获机制(capture)捕获机制

    怎么理解捕获呢?

    在block内部专门新建一个变量,用来存储外部的值,称为捕获。
    通过以上例子,可以得出:

    block访问外部变量总结:

    img

    为什么使用auto修饰的变量,block捕获的值,而使用static修饰的局部变量,block捕获的是指针呢?

    这是因为,auto修饰的变量,随时可能被销毁,因此,需要及时把值捕获进去。
    而static修饰的变量,在程序整个生命周期都存在,所以,可以对变量进行修改,因此只需要捕获指针即可。
    全局变量,存储在静态全局区,整个程序的生命周期都存在,因此,不需要捕获

    - (void)test
    {
        void(^block)(void) = ^{
            NSLog(@"调用该block----%@", self);
        };
        block();
    }
    
    问:该block里面的self,是否会被捕获?

    同样,使用clang转换为底层代码,可以看到block定义:

    struct __YZPerson__test_block_impl_0 {
      struct __block_impl impl;
      struct __YZPerson__test_block_desc_0* Desc;
      YZPerson *self;
      __YZPerson__test_block_impl_0(void *fp, struct __YZPerson__test_block_desc_0 *desc, YZPerson *_self, int flags=0) : self(_self) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    可以看到,捕获到了self。

    问:为什么会捕获self呢?
    - (void)test
    {
        void(^block)(void) = ^{
            NSLog(@"调用该block----%@", self);
        };
        block();
    }
    
    最后转化为:
    
    static void _I_YZPerson_test(YZPerson * self, SEL _cmd) {
        void(*block)(void) = ((void (*)())&__YZPerson__test_block_impl_0((void *)__YZPerson__test_block_func_0, &__YZPerson__test_block_desc_0_DATA, self, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    

    可以看出,在test函数里面,其实是有两个隐式变量:self和SEL类型的_cmd
    self是以参数传进去的,因此,self属于局部变量,需要进行捕获。

    既然这样的话,那么:

    - (void)test
    {
        void(^block)(void) = ^{
            NSLog(@"调用该block----%@", _name);
        };
        block();
    }
    
    _name又是如何存在的呢?捕获还是不捕获?捕获的话是直接捕获还是怎样的呢?

    不多说,咱还是直接看底层代码:

    在这里插入图片描述

    从图片中可以看到,block并没有捕获name,而是通过捕获的self,访问的_name。
    其实可以理解,因为name属于YZPerson里面的一个属性,_name是YZPerson里面的一个成员变量,_name其实是self->_name,因此,是通过捕获self,访问_name成员变量的。

    这样说明了,在block内部通过访问成员变量,就相当于里面引用了self,因此,还是需要留意循环引用的问题。

    block类型

    block有三种类型:

    __NSGlobalBlock__
    __NSStackBlock__
    __NSMallocBlock__
    

    block的三种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承NSBlock类型,再往上是继承NSObject类型。

    举个例子:

    void(^block)(void) = ^{
        NSLog(@"调用该block----");
    };
    
    NSLog(@"1-%@", [block class]);
    NSLog(@"2-%@", [[block class] superclass]);
    NSLog(@"3-%@", [[[block class] superclass] superclass]);
    NSLog(@"4-%@", [[[[block class] superclass] superclass] superclass]);
    NSLog(@"5-%@", [[[[[block class] superclass] superclass] superclass] superclass]);
    NSLog(@"6-%@", [[[[[[block class] superclass] superclass] superclass] superclass] superclass]);
    
    运行结果:
    2020-03-25 11:08:14.326871+0800 block学习[53532:3297757] 1-__NSGlobalBlock__
    2020-03-25 11:08:14.327226+0800 block学习[53532:3297757] 2-__NSGlobalBlock
    2020-03-25 11:08:14.327276+0800 block学习[53532:3297757] 3-NSBlock
    2020-03-25 11:08:14.327317+0800 block学习[53532:3297757] 4-NSObject
    2020-03-25 11:08:14.327349+0800 block学习[53532:3297757] 5-(null)
    2020-03-25 11:08:14.327377+0800 block学习[53532:3297757] 6-(null)
    

    可以看出,该block的类型是 NSGlobalBlock,其继承关系是:NSGlobalBlock : __NSGlobalBlock : NSBlock : NSObject

    至于为什么NSObject的superclass是nil可以参考iOS中对象的本质

    问:那,三种类型具体什么时候是哪种类型呢?

    先上个总结图:

    在这里插入图片描述

    具体的实验结果可以参考iOS之Block基本使用

    其中,在ARC下,你会发现,

    int a = 3;//局部变量
    void(^block)(void) = ^{
        NSLog(@"调用了block, a = %d", a);
    };
    NSLog(@"%@", block);
    结果:<__NSMallocBlock__: 0x28343ea60>
    

    按照之前的总结图,block的存储类型不应该是NSStackBlock吗?怎么打印出来的却是NSMallocBlock?

    这是因为,在ARC中,系统以及自动帮我们做了copy操作。从而将本应该是NSStackBlock经过copy操作后,变为NSMallocBlock。

    每一种类型的block调用copy后的结果如下:
    在这里插入图片描述

    为什么ARC需要帮我们把本存储在NSStackBlock的block经过copy操作,转移存储在NSMallocBlock上呢?

    一个在MRC下的例子:

    void(^block)(void);
    void test()
    {
        int age = 10;
        block = ^{
           NSLog(@"调用该block----%d", age);
        };
    }
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            test();
            block();
        }
        return 0;
    }
    
    结果:
    调用该block-----272632728
    

    可以看到,age的值不是10,而是一串莫名其妙的数字

    这是因为,block引用了auto变量,block类型是NSStackBlock。
    在test()括号执行完毕后,block其实已经被释放了,再次调用block,里面的age就不是10了。
    因此,我们需要将存在栈上的block通过copy操作,转移存储在堆上。将生命周期交给程序员自己控制。

    总结:

    在ARC环境下,编译器会根据以下情况自动将栈上的block复制到堆上:

    block作为函数返回值时
    将block赋值给strong指针时(即block有强指针引用)
    block作为Cocoa API中方法名含有usingBlock的方法参数时
    block作为GCD API的方法参数时
    三个block类型具体存储在哪一个区域
    在这里插入图片描述

    block与copy、retain、release操作

    对不同类型的block,调用其retainCount,观看其有何不同点:

    以下是验证程序:

    NSGlobalBlock

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        //没有访问auto变量,存储在NSGlobalBlock
        void(^block)(void) = ^{
            
        };
        NSLog(@"%@", block);
        [block retain];
        [block retain];
        [block retain];
        NSLog(@"[block retainCount] = %d", [block retainCount]);
        NSLog(@"%@", block);
    }
    
    打印结果:
    2020-07-15 18:42:26.133369+0800 block2[9804:1002328] <__NSGlobalBlock__: 0x10fd29178>
    2020-07-15 18:42:26.133506+0800 block2[9804:1002328] [block retainCount] = 1
    2020-07-15 18:42:26.133586+0800 block2[9804:1002328] <__NSGlobalBlock__: 0x10fd29178>
    

    NSStackBlock

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        int a = 3;//局部变量
        //访问auto变量,存储在NSStackBlock
        void(^block)(void) = ^{
            NSLog(@"调用了block, a = %d", a);
        };
        NSLog(@"%@", block);
        [block retain];
        [block retain];
        [block retain];
        NSLog(@"[block retainCount] = %d", [block retainCount]);
        NSLog(@"%@", block);
    }
    
    打印结果:
    2020-07-15 18:43:43.936774+0800 block2[9825:1003381] <__NSStackBlock__: 0x7ffeebdf7f88>
    2020-07-15 18:43:43.936910+0800 block2[9825:1003381] [block retainCount] = 1
    2020-07-15 18:43:43.936996+0800 block2[9825:1003381] <__NSStackBlock__: 0x7ffeebdf7f88>
    

    NSMallocBlock

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        int a = 3;//局部变量
        //访问auto变量,存储在NSStackBlock
        void(^block)(void) = [^{
            NSLog(@"调用了block, a = %d", a);
        } copy];//调用copy,存储在NSMallocBlock
        NSLog(@"%@", block);
        [block retain];
        [block retain];
        [block retain];
        NSLog(@"[block retainCount] = %d", [block retainCount]);
        NSLog(@"%@", block);
    }
    
    打印结果:
    2020-07-15 18:44:48.964249+0800 block2[9842:1004304] <__NSMallocBlock__: 0x600000cd53e0>
    2020-07-15 18:44:48.964366+0800 block2[9842:1004304] [block retainCount] = 1
    2020-07-15 18:44:48.964456+0800 block2[9842:1004304] <__NSMallocBlock__: 0x600000cd53e0>
    

    block与copy、retain、release操作的总结:

    不同于NSObjec的copy、retain、release操作:

    Block_copy与copy等效,Block_release与release等效;

    对Block不管是retain、copy、release都不会改变引用计数retainCount,retainCount 始终是1;

    NSGlobalBlock:retain、release、copy操作都无效;

    NSStackBlock:retain、release操作无效,必须注意的是,NSStackBlock在函数返回后,Block内存将被回收。即使retain也没用。容易犯的错误是[[mutableAarry addObject:stackBlock],在函数出栈后,从mutableAarry中取到的stackBlock已经被回收,变成了野指针。正确的做法是先将stackBlock copy到堆上,然后加入数组:[mutableAarry addObject:[[stackBlock copy] autorelease]]。
    支持copy,copy之后生成新的NSMallocBlock类型对象。

    NSMallocBlock:支持retain、release,虽然retainCount始终是1,但内存管理器中仍然会增加、减少计数。
    copy之后不会生成新的对象,只是增加了一次引用,类似retain;

    尽量不要对Block使用retain操作。

  • 相关阅读:
    XCode编译器介绍
    iOS程序的启动过程介绍
    浅谈观察者、工厂、简单工厂设计模式
    iPhone4/4s 5.1.1版本越狱后无法连接iTunes,出现0xE8000012错误的解决方法
    【转】iOS App 自定义 URL Scheme 设计
    【转】iPhone通讯录AddressBook.framework和AddressBookUI.framework的应用
    iOS6正式版不完美越狱教程(附安装讯飞输入法)
    批处理中setlocal enabledelayedexpansion的作用
    应用审核reject理由汇总
    Hudson安装和配置
  • 原文地址:https://www.cnblogs.com/r360/p/15772243.html
Copyright © 2020-2023  润新知