美文网首页iOS技术图谱
iOS技术图谱之再谈Block

iOS技术图谱之再谈Block

作者: iOS大蝠 | 来源:发表于2019-11-20 15:56 被阅读0次

    Block 最早出现是在 Mac OS X 10.6 和 iOS 4 中,作为对 C 语言的扩展,用来实现匿名函数的特性,在如今 Objective-C 开发的项目中 Block 随处可见。Block 为 Objective-C 提供了强大的函数式编程能力,为日常开发带来了极大的便利。那么对于 Block,你又了解多少?

    初识

    A Short Practical Guide to Blocks 文章中, Apple 列举了几种在系统框架 API 中 Block 的使用场景:

    • 任务完成回调
    • 通知回调
    • 错误回调
    • 枚举
    • 视图动画以及变换
    • 排序
      在Clang 9的官方文档中,Block的实现规范本文的开头是这样描述Block的:
    struct Block_literal_1 {
        void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
        int flags;
        int reserved;
        void (*invoke)(void *, ...);
        struct Block_descriptor_1 {
        unsigned long int reserved;         // NULL
            unsigned long int size;         // sizeof(struct Block_literal_1)
            // optional helper functions
            void (*copy_helper)(void *dst, void *src);     // IFF (1<<25)
            void (*dispose_helper)(void *src);             // IFF (1<<25)
            // required ABI.2010.3.16
            const char *signature;                         // IFF (1<<30)
        } *descriptor;
        // imported variables
    };
    
    

    Block是个包含isa指针的结构体,熟悉Objective-C的同学都知道,Objective-C中的对象也是一个包含isa指针的结构体,所有Block也可以当做是一个对象。通过注释发现Block可以被初始化NSConcreteStackBlock或NSConcreteGlobalBlock。(以下所述内容默认环境为ARC)

    类型

    区块有以下几种:

    // the raw data space for runtime classes for blocks
    // class+meta used for stack, malloc, and collectable based blocks
    BLOCK_EXPORT void * _NSConcreteMallocBlock[32]
        __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
    BLOCK_EXPORT void * _NSConcreteAutoBlock[32]
        __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
    BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32]
        __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
    BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32]
        __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
    // declared in Block.h
    // BLOCK_EXPORT void * _NSConcreteGlobalBlock[32];
    // BLOCK_EXPORT void * _NSConcreteStackBlock[32];
    

    其中NSConcreteAutoBlock,NSConcreteFinalizingBlock,NSConcreteWeakBlockVariable只在GC环境下使用。

    NSConcreteGlobalBlock

    未捕获任何变量或仅捕获的变量为以下类型的Block是NSConcreteGlobalBlock。

    静态变量
    整体变量
    静态变量

    NSLog(@"%@",^(void) {});
    

    NSConcreteStackBlock
    只要捕获了以上三种类型以外的变量的Block是NSConcreteStackBlock。

    int c;
    NSLog(@"%@",^(void) { c; });
    

    NSConcreteMallocBlock

    系统不提供直接创建NSConcreteMallocBlock的方式,但是可以对NSConcreteStackBlock进行复制操作来生成NSConcreteMallocBlock。

    以下情况,块会进行复制操作:

    • 手动执行copy方法
    • 将Block赋值给__strong修饰符修饰(系统替换)的Block或id对象
    • 作为方法的返回值
    • 系统API中包含usingBlock的方法
    int c;
    id block = ^(void) {
        c;
    };
    NSLog(@"%@",block);
    

    生命周期

    先看一张内存段分布图:

    也就是说 NSConcreteStackBlock 是由编译器自动管理,超过作用域之外就会自动释放了。而 NSConcreteMallocBlock 是由程序员自己管理,如果没有被强引用也会被销毁。NSConcreteGlobalBlock 由于存在于全局区,所以会一直伴随着应用程序。

    变量

    任意类型的变量都可以在 Block 中被访问,但是能够被修改的变量只有以下三种:

    • 静态变量
    • 全局变量
    • 静态全局变量

    全局变量和静态全局变量由于存在于全局区作用域广,所以在 Block 内部能够直接修改。那么对于静态变量是怎么实现修改的?

    可以使用 Clang 提供的命令来进一步的分析:

    clang -rewrite-objc Hello.m
    

    使用 clang -rewrite-objc 命令对代码的转换并不与实际编译过程相同,但是转换后的代码可读性更高,可以更好的帮助理解 Block 的机制。

    该命令可以将 Objective-C 的代码转成 C++ 代码。

    int a;
    static int b;
    - (void)main
    {
        static int c;
        int d;
        ^(void) {
            a++;
            b++;
            c++;
            d;
        };
    }
    

    以上代码转换后变为:

    int a;
    static int b;
    
    struct __Hello__main_block_impl_0 {
      struct __block_impl impl;
      struct __Hello__main_block_desc_0* Desc;
      int *c;
      int d;
      __Hello__main_block_impl_0(void *fp, struct __Hello__main_block_desc_0 *desc, int *_c, int _d, int flags=0) : c(_c), d(_d) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __Hello__main_block_func_0(struct __Hello__main_block_impl_0 *__cself) {
      int *c = __cself->c; // bound by copy
      int d = __cself->d; // bound by copy
    
            a++;
            b++;
            (*c)++;
            d;
        }
    
    static struct __Hello__main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __Hello__main_block_desc_0_DATA = { 0, sizeof(struct __Hello__main_block_impl_0)};
    
    static void _I_Hello_main(Hello * self, SEL _cmd) {
        static int c;
        int d;
        ((void (*)())&__Hello__main_block_impl_0((void *)__Hello__main_block_func_0, &__Hello__main_block_desc_0_DATA, &c, d));
    }
    

    原 Block 转换为 __Hello__main_block_impl_0 结构体,内部定义了指针 c 以及变量 d。原 main 方法转换为 _I_Hello_main 函数,函数中构造了 __Hello__main_block_impl_0,传入了 c 变量的地址,以及 d 变量的值。这就是为什么局部变量 d 不能被修改,而静态变量 c 可以被修改的原因。c 是指针传递,而 d 是值传递。之所以使用指针传递,是因为作用域的限制,通过指针进行作用域扩展,在 C 语言中是很常见且简单的做法。

    那么为什么 d 不使用指针传递,这是因为局部变量是存储在栈上,其生命周期是不稳定,Block 中通过指针访问到的局部变量可能已经销毁了。而静态变量是存储在静态数据存储区的,与应用程序生命周期一致,是可以保证正确访问的变量。

    __Hello__main_block_func_0 是 Block 的执行函数与 void (*invoke)(void *, ...) 对应,该函数的入参为 Block 实例。由于 a、b 是全局变量,所以在函数内部直接进行 + 操作,而对 c 进行 + 操作是通过指针来执行的,对于 d 不能进行 + 操作,所以在开发阶段编译器直接进行了报错。

    Property 和 Ivar

    如果在 Block 内部对 Property 或 Ivar 进行修改,发现是可以修改成功的,实际上 Property 内部操作的还是 Ivar,所以需要了解下为何 Ivar 可以在 Block 内部修改。

    {
        int _b;
    }
    
    - (void)main
    {
        ^(void) {
            _b++;
        };
    }
    

    以上代码转换后变为:

    extern "C" unsigned long OBJC_IVAR_$_Hello$_b;
    
    struct __Hello__main_block_impl_0 {
      struct __block_impl impl;
      struct __Hello__main_block_desc_0* Desc;
      Hello *self;
      __Hello__main_block_impl_0(void *fp, struct __Hello__main_block_desc_0 *desc, Hello *_self, int flags=0) : self(_self) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __Hello__main_block_func_0(struct __Hello__main_block_impl_0 *__cself) {
      Hello *self = __cself->self; // bound by copy
    
            (*(int *)((char *)self + OBJC_IVAR_$_Hello$_b))++;
        }
    static void __Hello__main_block_copy_0(struct __Hello__main_block_impl_0*dst, struct __Hello__main_block_impl_0*src) {_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    static void __Hello__main_block_dispose_0(struct __Hello__main_block_impl_0*src) {_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    static struct __Hello__main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __Hello__main_block_impl_0*, struct __Hello__main_block_impl_0*);
      void (*dispose)(struct __Hello__main_block_impl_0*);
    } __Hello__main_block_desc_0_DATA = { 0, sizeof(struct __Hello__main_block_impl_0), __Hello__main_block_copy_0, __Hello__main_block_dispose_0};
    
    static void _I_Hello_main(Hello * self, SEL _cmd) {
        ((void (*)())&__Hello__main_block_impl_0((void *)__Hello__main_block_func_0, &__Hello__main_block_desc_0_DATA, self, 570425344));
    }
    

    可以看到这次转换后的代码多了一个 OBJC_IVAR__Hello_b 全局变量,这个全局变量表示变量 b 的内存偏移量,并且在 Block 内部引用了 self(这也是为什么 Block 中使用 Ivar 也会造成循环引用的原因),在 _Hello__main_block_func_0 函数中使用 self 作为基地址 + OBJC_IVAR_Hello_b 偏移量的方式获取到内存地址,然后进行 + 操作。(这里还多了两个函数: copy 和 dispose ,后文会解释)

    __block 修饰符

    上文中我们对局部变量 d 进行 + 操作时,编译器提示我们需要加 __block 修饰符,这个 __block 修饰符是什么?为什么加上之后就可以在 Block 中对局部变量进行修改?

    __block int a = 1;
    ^(void) {
       a++;
    };
    

    以上代码转换后变为:

    struct __Block_byref_a_0 {
      void *__isa;
    __Block_byref_a_0 *__forwarding;
     int __flags;
     int __size;
     int a;
    };
    
    struct __Hello__main_block_impl_0 {
      struct __block_impl impl;
      struct __Hello__main_block_desc_0* Desc;
      __Block_byref_a_0 *a; // by ref
      __Hello__main_block_impl_0(void *fp, struct __Hello__main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __Hello__main_block_func_0(struct __Hello__main_block_impl_0 *__cself) {
      __Block_byref_a_0 *a = __cself->a; // bound by ref
    
            (a->__forwarding->a)++;
        }
    static void __Hello__main_block_copy_0(struct __Hello__main_block_impl_0*dst, struct __Hello__main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __Hello__main_block_dispose_0(struct __Hello__main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static struct __Hello__main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __Hello__main_block_impl_0*, struct __Hello__main_block_impl_0*);
      void (*dispose)(struct __Hello__main_block_impl_0*);
    } __Hello__main_block_desc_0_DATA = { 0, sizeof(struct __Hello__main_block_impl_0), __Hello__main_block_copy_0, __Hello__main_block_dispose_0};
    
    static void _I_Hello_main(Hello * self, SEL _cmd) {
        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1};
        ((void (*)())&__Hello__main_block_impl_0((void *)__Hello__main_block_func_0, &__Hello__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    }
    

    在 __Hello__main_block_impl_0 中,局部变量 a 变成了 __Block_byref_a_0, 这个 struct 中持有 int 类型的变量 a,并且还有 __Block_byref_a_0 类型的指针 __forwarding。

    可以发现 __Block_byref_a_0 并没有声明在 __Hello__main_block_impl_0。是因为当有多个 Block 引用了用一个 __block 修饰的变量的情况下,可以复用 __Block_byref_a_0。

    在 _I_Hello_main 函数的实现中,先是构建了 __Block_byref_a_0,将 isa 指向 (void*)0 也就是 NULL,将 __forwarding 指向自身,并且将 int 类型的 a 初始化为 1。在构建 __Hello__main_block_impl_0 的时候,将 __Block_byref_a_0 的地址传入了构造函数中,通过指针传递,解决了作用域限制的问题,达到了在 Block 调用函数中使用 __Block_byref_a_0 的目的。

    在 __Hello__main_block_func_0 中使用 (a->__forwarding->a)++ 的方式来使局部变量 a 进行 + 操作。第一个 a 指的是 __Block_byref_a_0,由于 __forwarding 指向自身,所以 a->__forwarding 还是 __Block_byref_a_0。第二个 a 就是 __Block_byref_a_0 中的变量 a。


    源码

    关于 Block 的底层实现源码,可以参考这个 libclosure-67,本文只介绍关于 copy 相关的源码,更多细节读者可以自行研读源码。

    _Block_copy

    上文提到对 NSConcreteStackBlock 进行 copy 操作后可以生成 NSConcreteMallocBlock ,在 runtime.c 中可以看到 copy 的具体实现:

    // Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
    void *_Block_copy(const void *arg) {
        struct Block_layout *aBlock;
    
        if (!arg) return NULL;
    
        // The following would be better done as a switch statement
        aBlock = (struct Block_layout *)arg;
        // 1
        if (aBlock->flags & BLOCK_NEEDS_FREE) {
            // latches on high
            latching_incr_int(&aBlock->flags);
            return aBlock;
        }
        // 2
        else if (aBlock->flags & BLOCK_IS_GLOBAL) {
            return aBlock;
        }
        // 3
        else {
            // Its a stack block.  Make a copy.
            struct Block_layout *result = malloc(aBlock->descriptor->size);
            if (!result) return NULL;
            memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
            // reset refcount
            result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
            result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
            _Block_call_copy_helper(result, aBlock);
            // Set isa last so memory analysis tools see a fully-initialized object.
            result->isa = _NSConcreteMallocBlock;
            return result;
        }
    }
    

    实现中有多个针对 flags 的条件判断,flags 在 Block_private.h是这样定义的:

    // Values for Block_layout->flags to describe block objects
    enum {
        BLOCK_DEALLOCATING =      (0x0001),  // runtime
        BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
        BLOCK_NEEDS_FREE =        (1 << 24), // runtime
        BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
        BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
        BLOCK_IS_GC =             (1 << 27), // runtime
        BLOCK_IS_GLOBAL =         (1 << 28), // compiler
        BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
        BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
        BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
    };
    

    其中 BLOCK_REFCOUNT_MASK 表示 NSConcreteStackBlock,BLOCK_NEEDS_FREE 表示 NSConcreteMallocBlock,BLOCK_IS_GLOBAL 表示 NSConcreteGlobalBlock。

    1、如果是 NSConcreteMallocBlock,则对引用计数递增并且返回 aBlock。
    2、如果是 NSConcreteGlobalBlock 则直接返回 aBlock。
    3、如果是 NSConcreteStackBlock ,首先分配一块与原 Block 大小相同的内存,然后使用 memmove() 函数将原 Block 的所有元数据按位复制到 result 上,接着将 result 的引用计数置为 0(注释表示这是不需要的,可能是防止某种异常情况出现),之后将 result 的 flags 置为 BLOCK_NEEDS_FREE,引用计数置为 2(注释表示逻辑引用计数为 1,Block 的引用计数以 2 为单位,每次递增2),再调用 _Block_call_copy_helper 函数,这个函数只在 Block 内引用了对象类型或 __block 修饰变量的情况下才会有作用(这种情况转换后的代码中会生成 copy 和 dispose 函数,这两个函数用来管理对象内存的),对于 Block 中的对象类型的 copy 都是指针 copy,生成一个指针指向原对象,最后将 result 的 isa 置为 NSConcreteMallocBlock。

    _Block_byref_copy

    前面提到对于 __block 修饰的变量最终会转换成 __Block_byref_a_0。在 Block 由栈 copy 到堆的时候,__Block_byref_a_0 也会有 copy 行为,_Block_byref_copy 的具体实现为:

    // Runtime entry points for maintaining the sharing knowledge of byref data blocks.
    
    // A closure has been copied and its fixup routine is asking us to fix up the reference to the shared byref data
    // Closures that aren't copied must still work, so everyone always accesses variables after dereferencing the forwarding ptr.
    // We ask if the byref pointer that we know about has already been copied to the heap, and if so, increment and return it.
    // Otherwise we need to copy it and update the stack forwarding pointer
    static struct Block_byref *_Block_byref_copy(const void *arg) {
        struct Block_byref *src = (struct Block_byref *)arg;
        // 1
        if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
            // src points to stack
            struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
            copy->isa = NULL;
            // byref value 4 is logical refcount of 2: one for caller, one for stack
            copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
            copy->forwarding = copy; // patch heap copy to point to itself
            src->forwarding = copy;  // patch stack to point to heap copy
            copy->size = src->size;
            // 2
            if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
                // Trust copy helper to copy everything of interest
                // If more than one field shows up in a byref block this is wrong XXX
                struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
                struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
                copy2->byref_keep = src2->byref_keep;
                copy2->byref_destroy = src2->byref_destroy;
    
                if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
                    struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                    struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                    copy3->layout = src3->layout;
                }
    
                (*src2->byref_keep)(copy, src);
            }
            else {
                // Bitwise copy.
                // This copy includes Block_byref_3, if any.
                memmove(copy+1, src+1, src->size - sizeof(*src));
            }
        }
        // already copied to heap
        // 3
        else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
            latching_incr_int(&src->forwarding->flags);
        }
        // 4
        return src->forwarding;
    }
    

    1、如果是栈上的 Block_byref 则分配与原 Block_byref 大小相同的内存,将 isa 置为 NULL,将 copy 后的 Block_byref 置为 BLOCK_BYREF_NEEDS_FREE,引用逻辑计数为 2,调用方和栈各有一份,copy 后的 Block_byref 的 forwarding 指向自己,原 Block_byref 的 forwarding 指向 copy 后的 Block_byref,最后赋值 size。
    2、对 Block_byref 是否含有对象类型进行判断,并针对不同情况进行内存管理。
    3、如果是堆上的 Block_byref 则对其引用计算递增。



    4、返回堆上的 Block_byref。

    之所以 __block 修饰的变量可以在 Block 中被修改,是因为在 Block 被 copy 到堆上时, Block_byref 也被 copy 到了堆上,并且栈和堆中的 Block_byref 都指向了堆中的 Block_byref。

    相关文章

      网友评论

        本文标题:iOS技术图谱之再谈Block

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