美文网首页
每日一问01——block

每日一问01——block

作者: 巫师学徒 | 来源:发表于2017-08-24 18:00 被阅读31次

    小白篇——基本用法

    block声明——格式

    返回类型(^名字)(参数列表)

    void(^blockName)(int a,bool b,NSString *c)
    
    block表达式——格式

    ^返回类型(参数列表)

    ^(int)(int a)
    
    block作变量
    void (^block)(int a) = ^(int b) {
            NSLog(@"%d---变量",b);
        };
     block(123);
    

    执行完block(123)后打印123---变量

    block作函数参数
    //参数block的声明
    - (void)function:(int(^)(NSString *str))paramBlock {
        int i = paramBlock(@"function");
        NSLog(@"%d",i);
    }
    //调用
    [self function:^int(NSString *str) {
            NSLog(@"%@---参数",str);
            return 123;
        }];
    

    打印顺序:function---参数,123

    使用typedef声明block
    typedef int(^typeBlock)(int c);
    //函数声明
    - (void)function2:(typeBlock)paramBlock {
        int i = paramBlock(1);
        NSLog(@"%d",i);
    }
    //函数调用
    [self function2:^int(int c) {
            NSLog(@"%d---typedef参数",c);
            return c++;
        }];
    

    打印顺序 1---typedef参数,2

    block作为函数返回值
    //函数声明
    (int(^)(int a))function3 {
        return ^(int count) {
                NSLog(@"%d",i);
                return count = 0;
            };
    }
    //调用
    int(^retBlock)(int a) = [self function3];
    //用作fuc2的参数
     [self function2:retBlock];
    

    打印顺序 1,0

    block与外部变量

    在使用block中很重要的一点就是可以通过block获取上文。即block的外部变量可以放在block内部使用。

    临时变量
        int num = 10;
        void(^block1)() = ^() {
            NSLog(@"%d",num);
        };
        num = 20;
        block1();
    

    打印结果为10而不是20。从这里我们可以看出block在声明时是将num拷贝了一份到block内。外面num无论怎么变化也不会影响到内部。

    全局变量和__block
    __block int num = 10;
    void(^block1)() = ^() {
         NSLog(@"%d",num);
    };
    num = 20;
    block1();
    
    int num;
    - (void)function {
        num = 10;
        void(^block)() = ^() {
            NSLog(@"%d",num);
        };
        num = 20;
        block();
    }
    

    打印结果均为20。那么为什么全局变量拷贝到block内依然可以修改呢?

    我们可以猜想,局部变量是存储在栈上,而全局变量是存储在堆上。block拷贝变量是将num拷贝到堆上。所以对于本来就在堆上的num,block可以直接使用。

    先不说对不对,好像这个猜想挺靠谱的。和我们经常听到的
    栈上的block会自动copy到堆上
    block截取的_block变量也会同时copy到堆上
    说法貌似是一致的。

    深入了解block

    一个最简单的block

    typedef int (^Block)(void);
    int main(int argc, char * argv[]) {
        // block实现
        Block block = ^{
            return 0;
        };
        // block调用
        block();
        return 0;
    }
    

    使用clang工具翻译代码后,这个是整体的,拆分在下面

    // block内部结构体
    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    typedef int (*Block)(void);
    // block结构体
    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;
      }
    };
    // block方法实现
    static int __main_block_func_0(struct __main_block_impl_0 *__cself) {
            return 0;
     }
    // 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)};
    
    // main函数
    int main(int argc, char * argv[]) {
        // block实现
        Block block = ((int (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        // block调用
        ((int (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        return 0;
    }
    

    block内部结构

    impl结构体
    struct __block_impl {
      void *isa;  // 存储位置,_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock
      int Flags;  // 按位承载 block 的附加信息
      int Reserved;  // 保留变量
      void *FuncPtr;  // 函数指针,指向 Block 要执行的函数,即__main_block_func_0
    };
    

    1.isa 指针,所有对象都有该指针,用于实现对象相关的功能。
    这里block有3种类型。具体有什么不同下面再说。

    impl.isa = &_NSConcreteStackBlock 全局的静态 block,不会访问任何外部变量。; 
    impl.isa = &_NSConcreteMallocBlock 保存在栈中的 block,当函数返回时会被销毁; 
    impl.isa = &_NSConcreteGlobalBlock 保存在堆中的 block,当引用计数为 0 时会被销毁;
    

    2.flags,用于按 bit 位表示一些 block 的附加信息
    3.reserved,保留变量。
    4. funcPtr,函数指针,指向具体的 block 实现的函数调用地址。

    block结构体
    struct __main_block_impl_0 {
      // impl结构体,即block的主结构体
      struct __block_impl impl;
      // Desc结构体,即block的描述结构体
      struct __main_block_desc_0* Desc;
      // block结构体的构造函数
      __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;
      }
    };
    

    impl :block 实现的结构体变量,该结构体就是前面说的那个;
    desc : 描述 block 的结构体变量,主要是 size 大小,以及 copy 和 dispose 函数的指针
    __main_block_impl_0 :结构体的构造函数,初始化结构体变量 impl、Desc;
    从这里就看出block至少是将变量,函数地址通过构造函数都保存到了结构体内部了。

    block执行流程.png

    回头再看block截获变量

    我们截获变量的类型基本是以下几种
    1.全局变量 2.全局静态变量 3.局部静态变量 4.局部变量 5._block修饰的变量
    其中1.2属于一类,作用域是全局,block可以直接使用它们。而第3.4种的作用域在block外面。block想使用它就需要进行拷贝。然后通过clang工具翻译后。

    typedef int (^Block)(void);
    int a = 0;
    static int b = 0;
    int main(int argc, char * argv[]) {
        static int c = 0;
        int i = 0;
        NSMutableArray *arr = [NSMutableArray array];
        Block block = ^{
            a = 1;
            b = 1;
            c = 1;
            [arr addObject:@"1"];
            return i;
        };
        block();
        return 0;
    }
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int *c;
      NSMutableArray *arr;
      int i;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_c, NSMutableArray *_arr, int _i, int flags=0) : c(_c), arr(_arr), i(_i) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    // block实现的方法
    static int __main_block_func_0(struct __main_block_impl_0 *__cself) {
      int *c = __cself->c; // bound by copy
      NSMutableArray *arr = __cself->arr; // bound by copy
      int i = __cself->i; // bound by copy
      a = 1;
      b = 1;
      (*c) = 1;
      ((void (*)(id, SEL, ObjectType))(void *)objc_msgSend)((id)arr, sel_registerName("addObject:"), (id)(NSString *)&__NSConstantStringImpl__var_folders_k__6b9p9yt96y9dq8ds8_kvf3kh0000gn_T_main_3c9752_mi_0);
      return i;
    }
    

    我们发现静态变量传入的是指针。可以直接修改。
    而局部变量拷贝进去是const类型的,无法被修改。
    于是如果我们希望在block内部改变外部变量就需要用到__block这个关键字。

    __block

    block源码

    int main()
    {
        __block int intValue = 0;
        void (^blk)(void) = ^{
            intValue = 1;
        };
        return 0;
    }
    

    clang后

    struct __block_impl
    {
        void *isa;
        int Flags;
        int Reserved;
        void *FuncPtr;
    };
    struct __Block_byref_intValue_0
    {
        void *__isa;
        __Block_byref_intValue_0 *__forwarding;
        int __flags;
        int __size;
        int intValue;
    };
    struct __main_block_impl_0
    {
        struct __block_impl impl;
        struct __main_block_desc_0* Desc;
        __Block_byref_intValue_0 *intValue; // by ref
        __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_intValue_0 *_intValue, int flags=0) : intValue(_intValue->__forwarding)
        {
            impl.isa = &_NSConcreteStackBlock;
            impl.Flags = flags;
            impl.FuncPtr = fp;
            Desc = desc;
        }
    };
    static void __main_block_func_0(struct __main_block_impl_0 *__cself)
    {
        __Block_byref_intValue_0 *intValue = __cself->intValue; // bound by ref
        (intValue->__forwarding->intValue) = 1;
    }
    static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src)
    {
        _Block_object_assign((void*)&dst->intValue, (void*)src->intValue, 8/*BLOCK_FIELD_IS_BYREF*/);
    }
    static void __main_block_dispose_0(struct __main_block_impl_0 *src)
    {
        _Block_object_dispose((void*)src->intValue, 8/*BLOCK_FIELD_IS_BYREF*/);
    }
    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
                                 };
    int main()
    {
        __attribute__((__blocks__(byref))) __Block_byref_intValue_0 \
        intValue = 
        {
            (void*)0,
            (__Block_byref_intValue_0 *)&intValue, 
            0, 
            sizeof(__Block_byref_intValue_0), 
            0
        };
        void (*blk)(void) = (void (*)()) &__main_block_impl_0   \
                    (
                        (void *)__main_block_func_0,            \
                        &__main_block_desc_0_DATA,              \
                        (__Block_byref_intValue_0 *)&intValue,  \
                        570425344                               \
                    );
        return 0;
    }
    

    当block截获的变量为__block 修饰的变量时,多发生了下面几件事。
    __Block_byref_intValue_0 结构体:用于封装 __block 修饰的外部变量。 _Block_object_assign 函数:当 block 从栈拷贝到堆时,调用此函数。 _Block_object_dispose 函数:当 block 从堆内存释放时,调用此函数。
    __block修饰的变量被拷贝到了堆上并且变成通过指针传入block内部的。
    然后就是block结构体中多出了一个_forwarding指针。并且_forwarding指针指向外部变量自己。
    于是block结构体中的__Block_byref_intValue_0 *intValue和变量结构体中的__Block_byref_intValue_0 *__forwarding都指向了外部变量。
    这特么是不是冲突了????为了说明这个有啥用,现在开始讲block的内存管理。

    block内存管理

    吐槽##学到这真的好累啊。。。平时用惯了的block背后还有那么多事儿。不查不知道,一查吓一跳

    在前面,提到过了block的类型有三种。NSConcreteGlobalBlock_NSConcreteStackBlock_NSConcreteMallocBlock这三种block在内存中的分布如下:

    block内存分布.png
    _NSConcreteGlobalBlock

    1、当 block 字面量写在全局作用域时,即为 global block;
    2、当 block 字面量不获取任何外部变量时,即为 global block
    除了这两种方式以外,其他形式创建的block均为stack block

    创建一个简单的_NSConcreteGlobalBlock
    int main()
    {
        ^{ printf("Hello, World!\n"); } ();
        return 0;
    }
    

    clang翻译后

    struct __block_impl {
        void *isa;
        int Flags;
        int Reserved;
        void *FuncPtr;
    };
    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) {
        printf("Hello, World!\n");
    }
    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()
    {
        (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) ();
        return 0;
    }
    

    由于这个block没有引入外部变量,所以它的类型为_NSConcreteGlobalBlock。

    但clang后显示为_NSConcreteStackBlock,这里查资料原因是clang改写的实现方式和LLVM不一样,在LLVM中,开启ARC时,block应该是_NSConcreteGlobalBlock

    _NSConcreteStackBlock

    _NSConcreteStackBlock 类型的 block 处于内存的栈区,如果其变量作用域结束,这个 block 就被废弃,block 上的 __block 变量也同样会被废弃。
    所以,为了解决这个问题,block提供了copy功能,将 block 和 __block 变量从栈拷贝到堆。此时block类型变为_NSConcreteMallocBlock

    从栈拷贝到堆上

    此时impl.isa = &_NSConcreteMallocBlock;
    再看这个构造函数!

    __block修饰变量的构造函数

    block访问的永远是__forwarding指向的那块内存。所以进行 copy 操作时,会将栈上的 __forwarding 指针指向了堆上的 block 结构体实例

    小结:当block执行了copy以后,block的类型变为NSConcreteMallocBlock
    参考代码:

    int main(int argc, const char * argv[])
    {
        @autoreleasepool {
            int i = 1024;
            void (^block1)(void) = ^{
                printf("%d\n", i);
            };
            block1();
            NSLog(@"%@", block1);
        }
        return 0;
    }
    会输出 <__NSMallocBlock__: 0x100109960>
    
    block的自动拷贝与手动拷贝

    在ARC情况下,基本都会讲创建在栈上的block拷贝到堆上。除了以下这种情况:
    block 作为方法或函数的参数传递时,编译器不会自动调用 copy 方法;
    例子如下:(来自狮子书)

    - (id)getBlockArray
    {
        int val = 10;
        return [[NSArray alloc] initWithObjects: 
                                ^{NSLog(@"blk0:%d", val);}, 
                                ^{NSLog(@"blk1:%d", val);}, nil];
    }
    

    调用时

    id arr = getBlockArray();
    Block block = [arr objectAtIndex:1];//此处crash
    block();
    

    crash的原因就是block出作用域后已经被回收了,正好验证了作为参数时不进行自动拷贝的说法。

    参考的文章

    谈Objective-C block的实现
    block没那么难(一)
    block没那么难(二)
    iOS block,你要看的这都有
    感谢以上优秀文章,本文很多地方都是摘录自其中,但重要的还是得要自己理解。

    相关文章

      网友评论

          本文标题:每日一问01——block

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