iOS技术图谱之再谈Block

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__b 全局变量,这个全局变量表示变量 b 的内存偏移量,并且在 Block 内部引用了 self(这也是为什么 Block 中使用 Ivar 也会造成循环引用的原因),在 _Hello__main_block_func_0 函数中使用 self 作为基地址 + OBJC_IVAR_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)