美文网首页
iOS block总结(一)

iOS block总结(一)

作者: 贾小敏1234 | 来源:发表于2021-05-20 15:23 被阅读0次

    原文地址
    block总结(二)

    前言

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

    1.block的原理是怎样的?本质是什么?
    2.__block的作用是什么?有什么使用注意点?
    3.block的属性修饰词为什么是copy?使用block有哪些使用注意?
    4.block一旦没有进行copy操作,就不会在堆上
    5.block在修改NSMutableArray,需不需要添加__block?
    如果现在不是很熟悉,希望看完这篇文章,能有个新的认识。

    导读

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

    1.block的基本使用
    2.block在内存中的布局
    3.block对变量的捕获分析
    4.MRC和ARC的对比
    5.__block的分析
    6.block中内存管理问题
    7.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.
    翻译过来表达就是
    闭包是一个函数(或指向函数的指针),再加上该函数执行的外部的上下文变量(有时候也称作自由变量)。
    block 实际上就是 Objective-C 语言对于闭包的实现。

    block的基本使用

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

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

    3.block的底层结构如下图

    16c09240311d9088.png

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

    //无参无返回值 定义 和使用
    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);
    }
    

    用一幅图来表示


    16899013-d1fbb0cafc25d1c1.png

    变量捕获

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

    变量类型 捕获到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

    如图所示


    16899013-a3127ec8c11f7cf3.png
    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

    16899013-d433ea403dc9818a.png
    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

    16899013-89542ca3c750b59e.png
    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;
    }
    
    

    如图所示,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

    16899013-89542ca3c750b59e.png
    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 指针传递
    全局变量 × 直接访问

    1.auto修饰的局部变量,是值传递
    2.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 |

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

    16899013-5c2408b16e37b88e.png

    运行代码查看

    MRC下

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

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

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

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

    改为MRC方法: Build Settings 里面的Automatic Reference Counting改为NO
    如下图所示

    16899013-517d5a5c4d13579f.png

    用代码来表示

    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的情况

    1.block作为函数返回值时
    2.将block赋值给__strong指针时
    3.block作为Cocoa API中方法名含有usingBlock的方法参数时
    4.block作为GCD API的方法参数时
    5.block作为函数返回值时

    block作为函数返回值时

    16899013-99882fde2f39baed.png
    // 定义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);

    block总结(二)

    相关文章

      网友评论

          本文标题:iOS block总结(一)

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