美文网首页
OC之Blocks

OC之Blocks

作者: 苏沫离 | 来源:发表于2018-12-09 23:35 被阅读0次

    Blocks 是 C 语言的扩充:持有自动变量的匿名函数。

    特别说明:本文代码如无说明,均在 ARC 下编译;在 MRC 下编译会做注释

    1、Blocks 语法

    1.1、Blocks 变量声明

    Blocks 类型由返回值类型和参数类型列表构成。使用脱字符 ^ 声明 Blocks 类型的变量。

    声明Blocks类型的语法.png
    • 返回值类型:当 Blocks 被调用时返回值的类型;如 int 型。无返回值时设置为void
    • Blocks 变量名:带有脱字符 ^ 前缀并且封装在括号中
    • 参数类型列表:被封装在括号中并且位于块名称的后面,其中的参数类型以逗号分隔。如果没有参数,将参数列表设置为 void

    声明的一些简单Blocks 变量:

    //变量 sumBlock 拥有两个 int 类型参数,返回值也是 int 类型
    int (^sumBlock)(int,int);
    
    //变量 logBlock 拥有一个 NSString 类型参数,无返回值 void
    void (^logBlock)(NSString *);
    
    //变量 simpleBlock 没有参数 void,无返回值void
    void (^simpleBlock)(void);
    

    既然可以将变量设为Blocks 类型,那么也可以将函数与方法的参数、类属性设为Blocks (能够定义变量的地方就能够定义Blocks ):

    @interface ModelClass : NSObject
    
    //requestBlcok 用作对象的属性,使用 copy 修饰,具有文件作用域,被分配在堆区
    @property (nonatomic ,copy) void (^requestBlcok)(BOOL isSuccess);
    
    //requestBlcok 用于方法的参数,具有块作用域,被分配在栈区
    - (void)requestCompletion:(void(^)(BOOL isSuccess))requestBlcok;
    
    @end
    

    使用 typedefBlocks提供别名,可以简化 Blocks 类型变量名称:

    //为 block 类型提供别名 RequestBlcok
    typedef void(^RequestBlcok)(BOOL isSuccess);
    
    @interface ModelClass : NSObject
    
    //别名 RequestBlcok 用作对象的属性类型,使用 copy 修饰,具有文件作用域,被分配在堆区
    @property (nonatomic ,copy) RequestBlcok requestBlcok;
    
    //别名 RequestBlcok 用于方法的参数类型,具有块作用域,被分配在栈区
    - (void)requestCompletion:(RequestBlcok)completion;
    
    @end
    

    疑问 1:为何使用 copy 修饰属性变量 Blocks,?能用strong 嘛?
    答:当Blocks不持有自动变量时,使用 retaincopystrong 都会持有该变量;但当Blocks持有作用域之外的自动变量时,使用 retain 修饰,被持有的自动变量引用计数没有变化,可能随时释放,存在内存问题!
    而使用 copystrong 修饰的 Blocks,会调用 _Block_object_assign() 函数将持有的自动变量引用计数 加 1,保证当使用时不会被释放!

    1.2、Blocks 常量表达式

    定义Blocks常量表达式:以脱字符^开头,后跟返回值类型、参数列表、Blocks 主体:

    块常量表达式的语法.png
    • 脱字符 ^ 表明该表达式是一个常量表达式;
    • 脱字符 ^ 后的返回值类型(可选项) 设置了该Blocks 表达式返回值的类型;
    • Blocks 参数被封装在括号中传递给Blocks 主体,每个Blocks 参数都由一个参数类型和一个参数名表示,Blocks 参数之间以逗号分隔。

    Blocks 常量表达式的简单定义:

    //有返回类型的 Blocks 常量表达式
    ^int(int a ,int b){
        return a + b;
    };
    
    /* 没有设置返回类型的 Blocks 常量表达式
     * @note 返回值类型 int 可以不做定义,因为编译器会从 return 语句推断出返回值类型。
     * @note Blocks常量表达式被编译器存储在栈区
     */
    ^(int a,int b){
        return a + b;
    };
    

    Blocks 被用作方法参数,类属性时,定义Blocks常量表达式:

    ModelClass *test = [[ModelClass alloc] init];
    test.requestBlcok = ^(BOOL isSuccess) {
        printf("Block 主体,执行某些操作\n");
    };
    
    [test requestCompletion:^(BOOL isSuccess) {
        printf("Block 主体,执行某些操作\n");
    }];
    

    Blocks的声明与定义组合为一条语句:

    /* @note Blocks常量表达式被编译器存储在栈区
     * @note 通过赋值操作将其赋给Blocks变量,此时被复制到堆上或者全局静态区。
     */
    void (^logBlock)(NSString *) = ^(NSString *log){
        NSLog(@"log:%@",log);
    };
    
    //在Blocks定义中,没有参数可不使用void
    void (^simpleBlock)(void) = ^{
        
    };
    

    如果在函数体外面声明并定义 Blocks 变量,那么它具有文件作用域,由编译器静态分配,存储在全局静态区的数据区:

    static BOOL(^isEvenNumberBlock1)(int number) = ^BOOL(int number){return (number % 2) == 0 ? YES : NO;};
    

    关于 C 的内存,可以阅读 C 内存管理 一文。

    Blocks 变量声明 和 Blocks 常量表达式的语法区别
    语法元素 Blocks变量声明 Blocks常量表达式
    脱字符 ^ 标识Blocks变量声明的开始,脱字符位于Blocks变量名称之前,两者都被封装在圆括号中 标识Blocks常量表达式的开始
    名称 Blocks变量的名称是必选项 Blocks 常量表达式没有名称
    返回值类型 必选项,没有返回值时声明为void 可选项,因为编译器能够从Blocks 表达式推断出返回值类型。
    参数 必选项,没有参数时声明为void 参数列表是可选项

    2、Blocks 是一个闭包

    Blocks 是一个实现的闭包,一个允许访问其常规作用域之外变量的函数。

    在 C 函数中声明的变量具有块作用域、无链接;这意味着这些自动变量仅在该函数访问。
    与 C 函数相比,Blocks 扩大了变量的作用域,可以访问 Blocks 主体之外的自动变量。

    2.1、持有被访问的自动变量

    Blocks 访问其作用域之外的变量

    void func(void){
        NSString *log = @"打印日志";
        int a = 5;
        NSLog(@"log1:%@  : %p : %p",log,log,&log);
        NSLog(@"a  1:%d  : %p",a,&a);
        void (^simpleBlock)(void) = ^{//block 声明并赋值
            NSLog(@"log2:%@  : %p : %p",log,log,&log);
            NSLog(@"a  2:%d  : %p",a,&a);
        };
        log = @"hello word!";
        a = 6;
        NSLog(@"log3:%@  : %p : %p",log,log,&log);
        NSLog(@"a  3:%d  : %p",a,&a);
        simpleBlock();//调用 block 变量
    }
    
    /* 打印数据
    log1:打印日志  : 0x1000021c0 : 0x7ffeefbff5c8
    a  1:5  : 0x7ffeefbff5c4
    log3:hello word!  : 0x100002260 : 0x7ffeefbff5c8
    a  3:6  : 0x7ffeefbff5c4
    log2:打印日志  : 0x1000021c0 : 0x100705250
    a  2:5  : 0x100705258
     */
    

    分析打印数据:Blocks 表达式中的变量与作用域之外的自动变量,内存地址相同,但不是同一个指针指向该内存;类似于 Blocks 表达式对作用域之外的变量进行浅拷贝

    Blocks 表达式对通过浅拷贝 持有 作用域之外的自动变量。

    疑问 2:Blocks变量为何要持有作用域之外的自动变量?如何持有?

    2.2、存储类别说明符__block

    默认情况下,Blocks 常量表达式对作用域之外的局部变量无权修改,否则编译器报错 Variable is not assignable (missing __block type specifier)
    存储类别说明符 __block 用于指定将变量值设置到哪个存储区域,可以将这些变量切换为读写模式。

    __block NSString *log = @"打印日志";
    void (^simpleBlock)(void) = ^{
        log = @"hello word!";
    };
    

    __block 说明符不能与存储类别说明符 autoregisterstatic组合使用。

    2.3、Blocks访问全局变量

    Blocks常量表达式中可以直接修改全局变量,无需使用 __block 说明符。

    int a = 3;//声明并赋值一个全局变量 a
    
    int main(int argc, const char * argv[]) {
        void (^simpleBlock)(void) = ^{
            printf("a 1==== %d \n",a);
            a = 4;
            printf("a 2==== %d \n",a);
        };
        a = 5;
        simpleBlock();
        return 0;
    }
    
    /* 打印数据
    a 1==== 5
    a 2==== 4
    */
    

    在 C 语言中,下述类型变量允许在 Blocks 常量表达式中直接修改:

    • 静态变量:使用 static 修饰的变量;
    • 全局变量:具有内部链接的全局变量、具有外部链接的全局变量。

    同样的,在 OC 中对象的属性属于全局变量,也可以在 Blocks 常量表达式中直接修改。

    疑问 3:Blocks表达式中的全局变量为什么可以修改?Blocks持有该全局变量了嘛?

    3、Blocks 底层实现

    Blocks 是持有自动变量的匿名函数,允许访问其作用域之外的变量。那么,Blocks 到底是什么呢?利用 clang 将 Objective-C 代码转为 C ++ 源码:

    clang -rewrite-objc main.m
    
    //支持 ARC 并指定 Runtime 版本
    clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 ModelClass.m
    
    //指定 sdk 和真机
    xcrun -sdk iphoneos clang -rewrite-objc main.m
    
    //指定 sdk 和模拟器
    xcrun -sdk iphonesimulator clang -rewrite-objc main.m
    
    //指定具体某版本的模拟器
    xcrun -sdk iphonesimulator9.3 clang -rewrite-objc main.m
    
    //如果使用了第三SDK
    xcrun -sdk iphonesimulator9.3 clang -rewrite-objc –F      /Users/goanywhere/Downloads/nbs-newlens-ios-2.3.6  main.m
    
    3.1、分析Blocks 的本质

    变量有地址,同样的,函数也有地址,指向函数的指针中存储着函数代码的起始处地址。

    Objective-C 代码:

    int main(int argc, const char * argv[]) {
        int (^addBlock)(int,int) = ^(int a ,int b){
            return a + b;
        };
        printf("1 + 2 = %d \n",addBlock(1,2));
        return 0;
    }
    

    转换后相关的 C++ 源码:

    /* Block 结构模版
     * @param isa 指向所属类的指针,
     * @param FuncPtr 函数指针,指向Block的功能实现函数
     */
    struct __block_impl {
        void *isa;
        int Flags;
        int Reserved;
        void *FuncPtr;
    };
    
    /* Block的描述信息
     * @note Block_size 赋值 sizeof(struct __main_block_impl_0)
     *           意味着Block可能就是结构 __main_block_impl_0
     */
    static struct __main_block_desc_0 {
        size_t reserved;
        size_t Block_size;//表明 Block 所占内存空间的大小
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
    
    /// 嵌套 __block_impl 与 __main_block_desc_0
    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0* Desc;
        /* 结构体的构造函数
         * @param fp 函数指针,被赋值 __main_block_func_0()
         * @param desc 结构指针,指向结构 __main_block_desc_0
         */
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
            impl.isa = &_NSConcreteStackBlock;//isa 指针,栈内存
            impl.Flags = flags;//标志位,0
            impl.FuncPtr = fp;//函数指针,指向 Block 的功能实现函数
            Desc = desc;//描述信息:block 内存
        }
    };
    
    /* 实现 OC 代码中Block的主要功能。
     * @param __cself 结构指针,类似于 OC 中的 self 指针;指向模板 (__block_impl *)addBlock;
     *                意味着 Block 主体是一个__main_block_impl_0的结构;
     * @param int a、int b : Block 表达式的参数列表
     * @return 返回 int 类型
     */
    static int __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
        return a + b;
    }
    
    /* main() 函数的实现,分为 3 大部分:声明、实现、调用
     * 1、Block 声明:本质为函数指针 int(*addBlock)(int,int),指向构造函数 __main_block_impl_0() 的起始位置
     * 2、Block 常量表达式:获取函数地址 &__main_block_impl_0()
     *     2.1、强制类型转换 ((int (*)(int, int))
     *     2.2、构造函数 __main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))
     *     2.3、函数指针指向构造函数
     * 3、Block 的调用转为 ((int (*)(__block_impl *, int, int))((__block_impl *)addBlock)->FuncPtr)((__block_impl *)addBlock, 1, 2)
     *     3.1、函数 ((int (*)(__block_impl *, int, int))((__block_impl *)addBlock)->FuncPtr) 就是 __main_block_func_0()
     *     3.2、参数列表 ((__block_impl *)addBlock, 1, 2)
     *     3.3、__cself 指针指向模板类 (__block_impl *)addBlock
     *     3.4、调用 addBlock(1,2) 也就是调用函数指针 __block_impl.FuncPtr 指向的函数__main_block_func_0()
     */
    int main(int argc, char * argv[]) {
        int (*addBlock)(int,int) = ((int (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        printf("1 + 2 = %d \n",((int (*)(__block_impl *, int, int))((__block_impl *)addBlock)->FuncPtr)((__block_impl *)addBlock, 1, 2));
        return 0;
    }
    

    疑问 4:在构造函数__main_block_impl_0()中,isa指针指向_NSConcreteStackBlock_NSConcreteStackBlock是什么,isa指针为何指向_NSConcreteStackBlock

    小结:

    • Blocks 本质是结构__block_impl,其成员isa指针表明在内存中的位置。
    • Blocks 声明本质声明了一个特定类型的函数指针。
    • Blocks 常量表达式本质上是一个指向 __main_block_impl_0 的结构指针。
    • Blocks 调用本质是通过函数指针__main_block_impl_0.FuncPtr 调用特定函数。
    3.2、Blocks如何持有其作用域之外的自动变量?

    Blocks 与函数、方法类似,但除了是可执行代码外,Blocks 还持有与堆内存或栈内存绑定的变量

    int main(int argc, const char * argv[]) {
        int a = 3;//基本数据类型的自动变量
        NSString *string = @"Hello word!!";//OC 类:自动变量
        __weak NSString *weakString = @"弱引用";
        void (^simpleBlock)(void) = ^{
            NSLog(@"string1 ==== %@ : %p",string,string);
            NSLog(@"weakString = %@ : %p",weakString,weakString);
            printf("a1 ========= %d : %p \n",a,&a);
        };
        a = 4;
        string = @"打印日志";
        simpleBlock();
        return 0;
    }
    

    转为 C++ 代码:

    /* 将一个对象的值复制给另一个对象
     * @param dst 目的对象:接收值
     * @param src 源对象:
     * @note  BLOCK_FIELD_IS_OBJECT 代表Objective-C对象,此处相当于调用 -retain 方法
     */
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
        _Block_object_assign((void*)&dst->string, (void*)src->string, BLOCK_FIELD_IS_OBJECT);
        _Block_object_assign((void*)&dst->weakString, (void*)src->weakString, BLOCK_FIELD_IS_OBJECT);
    }
    
    /* 销毁一个对象
    */
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
        _Block_object_dispose((void*)src->string, BLOCK_FIELD_IS_OBJECT);//相当于调用 -release 方法
        _Block_object_dispose((void*)src->weakString, BLOCK_FIELD_IS_OBJECT);
    }
    
    /* Block的描述信息
     * @note 持有 复制函数 与 释放函数 的函数指针
     */
    static struct __main_block_desc_0 {
        size_t reserved;
        size_t Block_size;//表明 Block 所占内存空间的大小
        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};
    
    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0* Desc;
        NSString *__strong string; // Blocks 持有作用域之外的自动变量
        NSString *__weak weakString;
        int a;
        
        /* 结构体的构造函数
         * @param fp 函数指针,被赋值 __main_block_func_0()
         * @param desc 结构指针,指向结构 __main_block_desc_0
         * @param 作用域之外的 _string、_weakString、_a 被赋值给该结构体成员
         */
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSString *__strong _string, NSString *__weak _weakString, int _a, int flags=0) : string(_string), weakString(_weakString), a(_a) {
            impl.isa = &_NSConcreteStackBlock;//isa 指针,栈内存
            impl.Flags = flags;//标志位,0
            impl.FuncPtr = fp;//函数指针,指向 Block 的功能实现函数
            Desc = desc;//描述信息:block 内存
        }
    };
    
    /* 实现 OC 代码中Block的主要功能
     * @note __cself->string 调用的是Block对象的成员 string ,并不是作用域之外的 string 变量
     */
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        NSString *__strong string = __cself->string; // 复制的副本
        NSString *__weak weakString = __cself->weakString; // 复制的副本
        int a = __cself->a; // 复制的副本
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_d8_yqx6_fxx1gzbwsghfv7n6r8w0000gn_T_main_490ccf_mi_2,string,string);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_d8_yqx6_fxx1gzbwsghfv7n6r8w0000gn_T_main_490ccf_mi_3,weakString,weakString);
        printf("a1 ========= %d : %p \n",a,&a);
    }
    
    int main(int argc, const char * argv[]) {
        int a = 3;
        NSString *string = (NSString *)&__NSConstantStringImpl__var_folders_d8_yqx6_fxx1gzbwsghfv7n6r8w0000gn_T_main_490ccf_mi_0;
        __attribute__((objc_ownership(weak))) NSString *weakString = (NSString *)&__NSConstantStringImpl__var_folders_d8_yqx6_fxx1gzbwsghfv7n6r8w0000gn_T_main_490ccf_mi_1;
        void (*simpleBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, string, weakString, a, 570425344));
        a = 4;
        string = (NSString *)&__NSConstantStringImpl__var_folders_d8_yqx6_fxx1gzbwsghfv7n6r8w0000gn_T_main_490ccf_mi_4;
       ((void (*)(__block_impl *))((__block_impl *)simpleBlock)->FuncPtr)((__block_impl *)simpleBlock);
        return 0;
    }
    

    Blocks 持有其作用域之外的自动变量的大致过程:

    • 1、Blocks 访问其作用域之外的自动变量时,将这些变量添加为实例__main_block_impl_0的结构成员并赋值;
    • 2、当Blocks表达式从栈内存复制到堆内存或者全局静态区时,调用 _Block_object_assign() 函数将持有的自动变量也复制到堆内存或者全局静态区;
      Objective-C类对象浅拷贝其内存地址,基础数据类型拷贝其数据值;
    • 3、功能实现函数__main_block_func_0()使用的变量string是实例__main_block_impl_0的结构成员string,而不是Blocks作用域之外的自动变量string;
    3.2.1、 函数__main_block_copy_0()__main_block_dispose_0()的调用时机

    函数__main_block_copy_0()的调用时机:

    • Blocks变量调用_Block_copy() 函数 或 -copy方法时;
    • 赋值操作,将 Blocks 表达式从栈内存复制到堆内存、或者全局静态区时;
    函数 调用时机
    __main_block_copy_0() 栈上的 Block 复制到堆上时,即 调用_Block_copy() 函数 或 -copy方法时
    __main_block_dispose_0() 堆上的 Block 被废弃时

    Block从栈复制到堆上时,Block实例通过_Block_object_assign()函数,将持有的自动变量的引用计数加 1!
    Block从堆释放时,Block实例通过_Block_object_dispose()函数,释放持有自动变量的所有权!

    3.3、__Block 发挥什么作用?

    既然 Blocks 持有作用域之外的自动变量,为何使用__Block修饰之后,修改Blocks 持有的自动变量,会对作用域之外的变量产生影响呢?

    int main(int argc, const char * argv[]) {
        __block int a = 3;
        void (^simpleBlock)(void) = ^{
            printf("a 1==== %d \n",a);
            a = 4;
            printf("a 2==== %d \n",a);
        };
        a = 5;
        simpleBlock();
        return 0;
    }
    

    转为C++代码:

    /* 将一个对象的值复制给另一个对象
    * @param dst 目的对象:接收值
    * @param src 源对象:
    */
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
        _Block_object_assign((void*)&dst->a, (void*)src->a, BLOCK_FIELD_IS_BYREF);
    }
    
    /* 销毁一个对象
    */
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
        _Block_object_dispose((void*)src->a, BLOCK_FIELD_IS_BYREF);
    }
    
    /* Block的描述信息
     */
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;//表明 Block 所占内存空间的大小
      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};
    
    /* __Block 变量转为结构体
     * @note __forwarding 指针,指向外部变量指针的内存地址
     */
    struct __Block_byref_a_0 {
        void *__isa;//指向所属的类的 isa 指针
        __Block_byref_a_0 *__forwarding;
        int __flags;
        int __size;//该结构的内存大小
        int a;//该结构持有相当于原自动变量
    };
    
    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0* Desc;
        __Block_byref_a_0 *a; // by ref
        
        /* 结构体的构造函数
         * @param fp 函数指针,被赋值 __main_block_func_0()
         * @param desc 结构指针,指向结构 __main_block_desc_0
         * @param 作用域之外的 _a 被拷贝给该结构体成员
         */
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
            impl.isa = &_NSConcreteStackBlock;//isa 栈内存
            impl.Flags = flags;
            impl.FuncPtr = fp;
            Desc = desc;
        }
    };
    
    /* 实现 OC 代码中Block的主要功能
     * @note __cself->a 调用的是Block对象的成员 a ,并不是作用域之外的变量 a
     *       a->__forwarding 是作用域之外的变量 a 的指针地址
     *       a->__forwarding->a 作用域之外的变量 a 的成员
     *       (a->__forwarding->a) = 4 本质上修改的还是作用域之外的变量值
     */
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        __Block_byref_a_0 *a = __cself->a; // bound by ref
        printf("a 1==== %d \n",(a->__forwarding->a));
        (a->__forwarding->a) = 4;//修改的是作用域之外的变量值
        printf("a 2==== %d \n",(a->__forwarding->a));
    }
    
    /* mian() 函数
     * __block a 转为结构体 __Block_byref_a_0
     * a=3 被赋值给结构成员 __Block_byref_a_0.a
     * __forwarding 指针指向变量a的内存地址
     */
    int main(int argc, const char * argv[]) {
        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {
            (void*)0,// __isa 指针:指向空指针NULL
            (__Block_byref_a_0 *)&a,// __forwarding 指针
            0,
            sizeof(__Block_byref_a_0),//__size 内存大小
            3
        };
        void (*simpleBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
        (a.__forwarding->a) = 5;
        ((void (*)(__block_impl *))((__block_impl *)simpleBlock)->FuncPtr)((__block_impl *)simpleBlock);
        return 0;
    }
    

    __block 发挥的作用:

    • 1、__block 类似于强制转换,将修饰的变量转换为结构 __Block_byref_a_0
      结构成员__forwarding指向__block变量的指针地址。
    • 2、在功能函数 __main_block_func_0() 修改变量值: (a->__forwarding->a)=4,本质上修改的还是作用域之外的__block变量;
    3.4、Blocks 表达式访问的全局变量

    Blocks对象可以持有作用域之外的自动变量;但并不会持有全局变量!

    int a = 3;//全局变量
    int main(int argc, const char * argv[]) {
        void (^simpleBlock)(void) = ^{
        static int b = 13;//静态变量
        void (^simpleBlock)(void) = ^{
            a = 4;
            b = 14;
        };
        a = 5;
        b = 15;
        simpleBlock();
        return 0;
    }
    

    转为 C++ 代码:

    int a = 3;
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int *b;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_b, int flags=0) : b(_b) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        int *b = __cself->b; // bound by copy
        a = 4;
        (*b) = 14;
    }
    
    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[]) {
        static int b = 13;
        void (*simpleBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &b));
        a = 5;
        b = 15;
        ((void (*)(__block_impl *))((__block_impl *)simpleBlock)->FuncPtr)((__block_impl *)simpleBlock);
        return 0;
    }
    

    执行函数__main_block_func_0() 在全局变量a 的作用域之内,可以直接使用全局变量a,不需要复制。
    但是具有块作用域的静态变量b,在函数__main_block_func_0()的作用域之外,不能直接访问;将静态变量的地址保存在结构__main_block_impl_0,通过地址访问作用域之外的静态变量!

    4、Blocks内存管理

    Blocks常量表达式的isa指针指向 _NSConcreteStackBlock,也就是说 Blocks常量被编译器存储到栈区!与之类似的类有:_NSConcreteGlobalBlock_NSConcreteMallocBlock

    Blocks所属的类 设置对象的存储域
    _NSConcreteStackBlock 由编译器自动分配在栈区,并在合适时机释放
    _NSConcreteMallocBlock _malloc()函数分配在堆区
    _NSConcreteGlobalBlock 全局静态区,存放全局变量
    内存分区.png
    4.1、类实例_NSConcreteGlobalBlock

    Blocks作为全局变量或者类的属性时,创建的Blocks对象属于_NSConcreteGlobalBlock

    //具有文件作用域,由程序静态分配,存储在全局静态区的数据区
    static BOOL(^isEvenNumberBlock2)(int number) = ^BOOL(int number){return (number % 2) == 0 ? YES : NO;};
    int main(int argc, const char * argv[]) {
       return 0;
    }
    

    声明并定义全局变量isEvenNumberBlock2,转为 C++ 代码:

    static struct __isEvenNumberBlock2_block_impl_0 {
      struct __block_impl impl;
      struct __isEvenNumberBlock2_block_desc_0* Desc;
      __isEvenNumberBlock2_block_impl_0(void *fp, struct __isEvenNumberBlock2_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteGlobalBlock;//isa 初始化为 _NSConcreteGlobalBlock
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    全局变量isEvenNumberBlock2实例属于_NSConcreteGlobalBlock类。

    内存分析.png

    即使在块作用域内创建Blocks变量,只要Blocks不持有作用于之外的自动变量,编译器就会将Blocks变量存储在全局静态区。

    4.2、Blocks变量从栈复制到堆上

    由于Blocks可以看做 Objective-C 对象,而在 ARC 模式下编译器默认使用__strong 修饰变量;因此simpleBlock变量持有Blocks实例的所有权,类似于MRC下的-retain !因此simpleBlock变量存储在堆区。

    假如使用 __weak修饰Block变量,则该变量存储在栈区!

    从栈复制到堆上.png
    4.2.1、什么情况下栈上的Block复制到堆上?

    调用函数_Block_copy()Block变量从栈复制到堆!

    都在什么情况下,编译器会将栈上的Block变量复制到堆上呢?

    • 调用Block的实例方法-copy时:如果Block配置在栈上,那么该Block从栈复制到堆上;
    • ARC下Block 作为函数返回值时;
    • ARC下将Block表达式赋值给有__strong修饰的 id 类型或 Block类型成员变量时: 编译器自动的将对象的Block 作为参数并调用_Block_copy() 函数;与-copy效果相同!
    • 在方法名中含有usingBlockCocoa方法;
    • GCD的API传送的Block时;
    4.2.2、为什么把栈上的Block复制到堆上?

    栈上的Block变量,如果其作用域结束,该Block就会被释放;由于__Block变量也配置在栈上,其变量作用域结束,则该__Block变量也被释放。

    例子:MRC 下Block变量作为函数返回值时
    /* @note MRC 编译:Block常量表达式分配在栈内存,不被任何指针持有,当函数返回时,Block变量被释放;
    *           因此编译错误: Returning block that lives on the local stack
    *           调用 -copy 将Block变量从栈复制到堆上,解决该编译错误;
    *           需要在适当时机调用 -release 释放所有权,否则引起内存泄露;
    * @note ARC 编译,Block常量表达式作为函数返回值时会被编译成 autoreleased NSConcreteMallocBlock 类型,存储在堆内存
    *           编译器会自动释放,无需我们操心
    */
    typedef  void (^SimpleBlock)(void);
    SimpleBlock getBlockFunc(){
       int b = 20;
       return ^{
           NSLog(@"b ====== %d",b);
       };
    }
    

    注意:将 Block 从栈复制到堆上需要消耗CPU资源。如果Block在栈上也能使用,则无需复制到堆上!

    疑问6:思考一下,在此处可以使用 -autorelease 或者 -retain方法嘛?

    4.2.3、持有自动变量

    Block从栈上复制到堆上时,作用域之外的自动变量仍然存储在栈上,仅仅对应的Block结构成员被复制在堆上。

    a、Block变量调用-copy
    //MRC 下编译代码
    int main(int argc, const char * argv[]) {
       //自动变量存储在栈区,地址都是高位地址
        NSString *log = @"hello word";//栈上,高地址
        int a = 5;//栈上,高地址
        
        NSLog(@"log1:%@  : %p : %p",log,log,&log);
        NSLog(@"a  1:%d  : %p",a,&a);
        
        SimpleBlock simpleBlock =  [^{
            NSLog(@"log2:%@  : %p : %p",log,log,&log);//堆上
            NSLog(@"a  2:%d  : %p",a,&a);//堆上
        } copy];
        
        log = @"打印日志";//栈上,高地址
        a = 6;//栈上,高地址
        NSLog(@"log3:%@  : %p : %p",log,log,&log);
        NSLog(@"a  3:%d  : %p",a,&a);
        
        simpleBlock();
        
        NSLog(@"log4:%@  : %p : %p",log,log,&log);//栈上,高地址
        NSLog(@"a  4:%d  : %p",a,&a);//栈上,高地址
        
        return 0;
    }
    
    /*打印数据
    log1:hello word  : 0x1000021e0 : 0x7ffeefbff578
    a  1:5  : 0x7ffeefbff574
    log3:打印日志  : 0x100002280 : 0x7ffeefbff578
    a  3:6  : 0x7ffeefbff574
    log2:hello word  : 0x1000021e0 : 0x100502d10
    a  2:5  : 0x100502d18
    log4:打印日志  : 0x100002280 : 0x7ffeefbff578
    a  4:6  : 0x7ffeefbff574
    */
    
    持有的自动变量 Block 从栈copy到堆时的影响 Block从堆区release
    基本数据类型如int 从栈复制到堆 从堆区释放掉
    Objective-C 对象 该对象引用计数加1,堆上的Block持有该对象 该对象引用计数减1,堆上的Block释放该对象所有权
    b、对Block使用-retain-autorelease操作

    Block 能否调用使用-autorelease或者 -retain方法?

    Block调用-copy从栈复制到堆时, 底层调用_Block_object_assign()函数持有被捕获的对象(自动变量);从堆区释放时调用_Block_object_dispose()释放被捕获对象的所有权!

    使用-retain或者-autorelease 获取Block所有权,引用计数retainCount加1;但是并没有调用_Block_object_assign()函数与_Block_object_dispose(),被持有自动变量仍然在栈内存中:

    • 对于基本数据类型如int等,只要持有Block所有权,就可以正常使用这些被持有的基本数据类型;
    • 对于Objective-C类对象,捕获操作将变量指针赋值到Block结构体实例的结构成员上!对于变量指针的赋值操作,对变量的所有权没有任何影响,对变量的retainCount也没有任何影响!
      因此,捕获操作并没有捕获 Objective-C 类对象的所有权。

    我们通过retain或者autorelease操作获取的Block结构体实例的所有权,所以调用Block可以确保该对象存在!但是对被捕获的 Objective-C 类对象,Block结构体实例没有该变量的所有权,程序运行到此处,变量指针(Block结构体实例的结构成员)指向的变量可能已被释放,此时程序可能闪退!!

    被捕获的自动变量 Block 在栈上retain或者autorelease
    基本数据类型如int 没影响
    Objective-C 对象 没影响

    如果Block没有捕获自动变量,那么它被配置在全局静态区,此时又没有必要再次获取所有权了!

    获取Block 所有权的操作最好使用copy操作。

    被捕获的自动变量释放问题
    • 在MRC下,被捕获的自动变量随着Block 使用copy操作,引用计数加 1 。当我们使用release释放Block 时,Block会将持有的自动变量引用计数减 1!!
    • 在 ARC 下,Block 会随着作用域的结束而被释放,此时被Block捕获的自动变量也随之释放!!
    4.2.3、__block变量

    通过 copy操作,因为捕获的自动变量是指针,复制操作会使引用计数加 1!!那么 copyBlock 从栈复制到堆的时候,__block变量会产生什么影响呢??

    __block变量的配置存储域 Block 从栈复制到堆时的影响
    从栈复制到堆并被Block 持有
    Block 持有
    a、在一个Block中使用多个__block变量

    若在一个Block中使用__block变量,则Block持有该变量,该__block也必定配置在栈上。将Block从栈复制到堆上,所捕获的__block也被从栈上配置到堆上。

    在一个Block中使用__block变量.png

    注意__block变量若不在Block中使用,则不会被复制到堆区!Block的复制操作,对不被捕获的__block变量没有任何影响!!

    b、在多个Block中使用某个__block变量
    • 1、在多个Block中使用某个__block变量时,最初这些Block__block变量都配置在栈上。
    • 2、将任何一个Block从栈复制到堆上,这个__block变量也被从栈上复制到堆上被Block持有。
    • 3、当剩下的Block从栈复制到堆上时,被复制的Block持有__block变量,并增加__block变量的引用计数。
    在多个Block中使用__block变量.png
    c、Block被废弃

    就像Objective-C的引用计数管理一样:使用__block变量的Block持有__block。如果Block被废弃,它所持有的__block变量就被释放。

    Block的废弃和__block变量的释放.png
    d、__block变量的__forwarding指针

    通过Block 的复制,__block变量也从栈上复制到堆上,此时可同时访问栈上和堆上的__block变量!那么栈上的__block变量的__forwarding指针指向何处??堆上的__block变量的__forwarding指针又指向何处??

    将栈上的__block变量复制到堆上,栈上的__block变量__forwarding指针将指向被复制到堆上的__block变量; 堆上__block变量的__forwarding指针指向其自身!!

    __block变量从栈复制到堆上.png

    通过__block变量的__forwarding指针,无论在Block表达式之中、在Block表达式之外,无论在栈上、还是复制到堆上。都能保证程序始终访问同一个__block变量!

    e 、_Block_object_assign()函数与_Block_object_dispose()函数

    __block变量有__strong修饰时:当__block变量从栈复制到堆时,使用_Block_object_assign()函数持有赋值给__block变量的对象!当堆上的__block变量被废弃时,使用_Block_object_dispose()函数释放赋值给__block变量的对象。

    被捕获的对象 BLOCK_FIELD_IS_OBJECT
    __block变量 BLOCK_FIELD_IS_BYREF

    5、 Blocks 面试题

    • Blocks 被捕获的自动变量
    • Blocks 的本质、 __block 变量的本质
    • Blocks 内存问题:什么时候配置在堆区?什么时候配置在栈区?什么时候配置在全局区!
    • Blocks 为何要使用 -copy 操作?可不可以使用 -retain 操作、 -autorelease 操作
    • Blocks使用 -copy 操作之后要不要 -release ?被捕获的自动变量引用计数有何变化?被捕获的自动变量需不需要释放?
    • Blocks使用 -copy 操作 对 __block 变量 的影响?
    • __block 变量赋值后, __forwarding 指针的指向问题?

    参考文章/图书

    C/C++语言的闭包
    精通Objective-C
    Objective-C高级编程 iOS与OS X多线程和内存管理

    相关文章

      网友评论

          本文标题:OC之Blocks

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