深入理解iOS的block(上)

作者: iOS_小久 | 来源:发表于2020-03-07 15:52 被阅读0次

    原文地址

    前言

    在文章之前,先抛出如下问题。

    • block的原理是怎样的?本质是什么?
    • __block的作用是什么?有什么使用注意点?
    • block的属性修饰词为什么是copy?使用block有哪些使用注意?
    • block一旦没有进行copy操作,就不会在堆上
    • block在修改NSMutableArray,需不需要添加__block?

    如果现在不是很熟悉,希望看完这篇文章,能有个新的认识。

    导读

    本文主要从如下几个方面讲解block

    • block的基本使用
    • block在内存中的布局
    • block对变量的捕获分析
    • MRC和ARC的对比
    • __block的分析
    • block中内存管理问题
    • block导致的循环引用问题

    什么是block

    先介绍一下什么是闭包。在 wikipedia 上,闭包的定义是

    In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.

    翻译过来表达就是

    闭包是一个函数(或指向函数的指针),再加上该函数执行的外部的上下文变量(有时候也称作自由变量)。

    一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:1012951431, 分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!希望帮助开发者少走弯路。

    • block 实际上就是 Objective-C 语言对于闭包的实现。

    block的基本使用

    • block本质上也是一个OC对象,它内部也有个isa指针

    • block是封装了函数调用以及函数调用环境的OC对象

    • block的底层结构如下图

    无参无返回值的定义和使用

    //无参无返回值 定义 和使用
    void (^MyBlockOne)(void) = ^{
     NSLog(@"无参无返回值");
    };
    
    // 调用
    MyBlockOne();
    

    无参有返回值的定义和使用

    // 无参有返回值
    int (^MyBlockTwo)(void) = ^{
     NSLog(@"无参有返回值");
     return 2;
    };
    // 调用
    int res = MyBlockTwo();
    

    有参无返回值的定义和使用

    //有参无返回值 定义
    void (^MyBlockThree)(int a) = ^(int a){
     NSLog(@"有参无返回值 a = %d",a);
    };
    
    // 调用
    MyBlockThree(10);
    

    有参有返回值的定义和使用

    //有参有返回值
    int (^MyBlockFour)(int a) = ^(int a){
     NSLog(@"有参有返回值 a = %d",a);
     return a * 2;
    };
    MyBlockFour(4);
    

    typedef 定义Block

    实际开发中,经常需要把block作为一个属性,我们可以定义一个block

    eg:定义一个有参有返回值的block

    typedef int (^MyBlock)(int a, int b);
    

    定义属性的时候,如下即可持有这个block

    @property  (nonatomic,copy) MyBlock myBlockOne;
    

    block实现

    self.myBlockOne = ^int(int a, int b) {
     return a + b;
    };
    

    调用

    self.myBlockOne(2, 5);
    

    block 类型和数据结构

    block 数据结构分析

    生成cpp文件

    如下代码

    int age = 20;
    void (^block)(void) =  ^{
     NSLog(@"age is %d",age);
     };
    
    block();
    
    • 打开终端,cd到当前目录下

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

    生成main.cpp

    block 结构分析

    int age = 20;
    
    // block的定义
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
    // block的调用
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    

    上面的代码删除掉一些强制转换的代码就就剩下如下所示

    int age = 20;
    void (*block)(void) = &__main_block_impl_0(
     __main_block_func_0, 
     &__main_block_desc_0_DATA, 
     age
     );
    // block的调用
    block->FuncPtr(block);
    

    看出block的本质就是一个结构体对象,结构体__main_block_impl_0代码如下

    struct __main_block_impl_0 {
     struct __block_impl impl;
     struct __main_block_desc_0* Desc;
     int age;
     //构造函数(类似于OC中的init方法) _age是外面传入的
     __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
     //isa指向_NSConcreteStackBlock 说明这个block就是_NSConcreteStackBlock类型的
     impl.isa = &_NSConcreteStackBlock;
     impl.Flags = flags;
     impl.FuncPtr = fp;
     Desc = desc;
     }
    };
    

    结构体中第一个是struct __block_impl impl;

    struct __block_impl {
     void *isa;
     int Flags;
     int Reserved;
     void *FuncPtr;
    };
    

    结构体中第二个是__main_block_desc_0;

    static struct __main_block_desc_0 {
     size_t reserved;
     size_t Block_size; // 结构体__main_block_impl_0 占用的内存大小
    }
    

    结构体中第三个是age

    也就是捕获的局部变量 age

    __main_block_func_0

    //封装了block执行逻辑的函数
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
     int age = __cself->age; // bound by copy
    
     NSLog((NSString *)&__NSConstantStringImpl__var_folders_x4_920c4yq936b63mvtj4wmb32m0000gn_T_main_7f3f1b_mi_0,age);
    }
    

    用一幅图来表示

    变量捕获

    其实上面的代码我们已经看得出来变量捕获了,这里继续详细分析一下

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

    局部变量auto(自动变量)

    • 我们平时写的局部变量,默认就有 auto(自动变量,离开作用域就销毁)
    运行代码

    例如下面的代码

    int age = 20;
    void (^block)(void) =  ^{
     NSLog(@"age is %d",age);
    };
    age = 25;
    
    block();
    

    等同于

    auto int age = 20;
    void (^block)(void) =  ^{
     NSLog(@"age is %d",age);
    };
    age = 25;
    
    block();
    

    输出

    20

    分析

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

    生成main.cpp

    如图所示

    int age = 20;
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
    age = 25;
    
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    
    struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block;
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_x4_920c4yq936b63mvtj4wmb32m0000gn_T_main_d36452_mi_5);
    

    可以知道,直接把age的值 20传到了结构体__main_block_impl_0中,后面再修改age = 25并不能改变block里面的值

    局部变量 static

    static修饰的局部变量,不会被销毁

    运行代码

    eg

    static int height  = 30;
    int age = 20;
    void (^block)(void) =  ^{
     NSLog(@"age is %d height = %d",age,height);
    };
    age = 25;
    height = 35;
    block();
    

    执行结果为

    age is 20 height = 35
    

    可以看得出来,block外部修改height的值,依然能影响block内部的值

    分析

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

    生成main.cpp

    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_x4_920c4yq936b63mvtj4wmb32m0000gn_T_main_3146e1_mi_4,age,(*height));
     }
    
    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, const char * argv[]) {
     /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
     static int height = 30;
     int age = 20;
     void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
     age = 25;
     height = 35;
     ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    
    

    如图所示,age是直接值传递,height传递的是*height 也就是说直接把内存地址传进去进行修改了。

    全局变量

    运行代码
    int age1 = 11;
    static int height1 = 22;
    
    int main(int argc, const char * argv[]) {
     @autoreleasepool {
     void (^block)(void) =  ^{
     NSLog(@"age1 is %d height1 = %d",age1,height1);
     };
     age1 = 25;
     height1 = 35;
     block();
    
     }
     return 0;
    }
    

    输出结果为

    age1 is 25 height1 = 35
    
    分析

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

    生成main.cpp

    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) {
    
     NSLog((NSString *)&__NSConstantStringImpl__var_folders_x4_920c4yq936b63mvtj4wmb32m0000gn_T_main_4e8c40_mi_4,age1,height1);
    }
    
    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, const char * argv[]) {
     /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
     void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
     age1 = 25;
     height1 = 35;
     ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    
     }
     return 0;
    }
    

    从cpp文件可以看出来,并没有捕获全局变量age1和height1,访问的时候,是直接去访问的,根本不需要捕获

    小结

    变量类型 捕获到block内部 访问方式
    局部变量 auto 值传递
    局部变量 static 指针传递
    全局变量 × 直接访问
    • auto修饰的局部变量,是值传递
    • static修饰的局部变量,是指针传递

    其实也很好理解,因为auto修饰的局部变量,离开作用域就销毁了。那如果是指针传递的话,可能导致访问的时候,该变量已经销毁了。程序就会出问题。而全局变量本来就是在哪里都可以访问的,所以无需捕获。

    block类型

    block也是一个OC对象

    在进行分析block类型之前,先明确一个概念,那就是block中有isa指针的,block是一个OC对象,例如下面的代码

    void (^block)(void) =  ^{
     NSLog(@"123");
    };
    
    NSLog(@"block.class = %@",[block class]);
    NSLog(@"block.class.superclass = %@",[[block class] superclass]);
    NSLog(@"block.class.superclass.superclass = %@",[[[block class] superclass] superclass]);
    NSLog(@"block.class.superclass.superclass.superclass = %@",[[[[block class] superclass] superclass] superclass]);
    

    输出结果为

    iOS-block[18429:234959] block.class = __NSGlobalBlock__
    iOS-block[18429:234959] block.class.superclass = __NSGlobalBlock
    iOS-block[18429:234959] block.class.superclass.superclass = NSBlock
    iOS-block[18429:234959] block.class.superclass.superclass.superclass = NSObject
    

    说明了上面代码中的block的类型是__NSGlobalBlock,继承关系可以表示为__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject

    block有3种类型

    block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型

    • __NSGlobalBlock__ ( _NSConcreteGlobalBlock )
    • __NSStackBlock__ ( _NSConcreteStackBlock )
    • __NSMallocBlock__ ( _NSConcreteMallocBlock )

    其中三种不同的类型和环境对应如下

    block类型 环境
    __NSGlobalBlock__ 没有访问auto变量
    __NSStackBlock__ 访问了auto变量
    __NSMallocBlock__ __NSStackBlock__调用了copy

    其在内存中的分配如下对应

    运行代码查看

    MRC下

    注意,以下代码在MRC下测试

    注意,以下代码在MRC下测试

    注意,以下代码在MRC下测试

    因为ARC的时候,编译器做了很多的优化,往往看不到本质,

    • 改为MRC方法: Build Settings 里面的Automatic Reference Counting改为NO

    如下图所示

    用代码来表示

    void (^block)(void) =  ^{
     NSLog(@"123");
    };
    
    NSLog(@"没有访问auto block.class = %@",[block class]);
    
    auto int a = 10;
    void (^block1)(void) =  ^{
     NSLog(@"a = %d",a);
    };
    
    NSLog(@"访问了auto block1.class = %@",[block1 class]);
    
    NSLog(@"访问量auto 并且copy block1-copy.class = %@",[[block1 class] copy]);
    

    输出为

    OS-block[23542:349513] 没有访问auto block.class = __NSGlobalBlock__
    iOS-block[23542:349513] 访问了auto block1.class = __NSStackBlock__
    iOS-block[23542:349513] 访问量auto 并且copy block1-copy.class = __NSStackBlock__
    

    可以看出和上面说的

    block类型 环境
    __NSGlobalBlock__ 没有访问auto变量
    __NSStackBlock__ 访问了auto变量
    __NSMallocBlock__ __NSStackBlock__调用了copy

    是一致的

    ARC下

    在ARC下,上面的代码输出结果为下面所示,因为编译器做了copy

    iOS-block[24197:358752] 没有访问auto block.class = __NSGlobalBlock__
    iOS-block[24197:358752] 访问了auto block1.class = __NSMallocBlock__
    iOS-block[24197:358752] 访问量auto 并且copy block1-copy.class = __NSMallocBlock__
    

    block的copy

    前面说了在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,具体来说比如以下情况

    copy的情况

    • block作为函数返回值时
    • 将block赋值给__strong指针时
    • block作为Cocoa API中方法名含有usingBlock的方法参数时
    • block作为GCD API的方法参数时
    block作为函数返回值时

    // 定义Block
    typedef void (^YZBlock)(void);
    
    // 返回值为Block的函数
    YZBlock myblock()
    {
     int a = 6;
     return ^{
     NSLog(@"--------- %d",a);
     };
    }
    
    YZBlock Block = myblock();
    Block();
    NSLog(@" [Block class] = %@", [Block class]);
    

    输出为

    iOS-block[25857:385868] --------- 6
    iOS-block[25857:385868]  [Block class] = __NSMallocBlock__
    

    上述代码如果再MRC下输出__NSStackBlock__,在ARC下,自动copy,所以是__NSMallocBlock__

    将block赋值给__strong指针时
    // 定义Block
    typedef void (^YZBlock)(void);
    
    int b = 20;
    YZBlock Block2 = ^{
     NSLog(@"abc %d",b);
    };
    NSLog(@" [Block2 class] = %@", [Block2 class]);
    

    输出为

    iOS-block[26072:389164]  [Block2 class] = __NSMallocBlock__
    

    上述代码如果再MRC下输出__NSStackBlock__,在ARC下,自动copy,所以是__NSMallocBlock__

    block作为Cocoa API中方法名含有usingBlock的方法参数时

    eg:

    NSArray *array = @[@1,@4,@5];
    [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
     // code
    }];
    
    block作为GCD API的方法参数时

    eg

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    
    }); 
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
     //code to be executed after a specified delay
    });
    

    MRC下block属性的建议写法

    • @property (copy, nonatomic) void (^block)(void);

    ARC下block属性的建议写法

    • @property (strong, nonatomic) void (^block)(void);
    • @property (copy, nonatomic) void (^block)(void);

    深入理解iOS的block (下)

    另外,如果你想一起进阶,不妨添加一下交流群1012951431,选择加入一起交流,一起学习。期待你的加入!

    相关文章

      网友评论

        本文标题:深入理解iOS的block(上)

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