Block学习

152 阅读5分钟

block 的设计非常有意思

我会先把各种情况下的 block 转成 c++,看看它的底层实现,再做总结,以防自己忘记😄

转换命令:

clang -x objective-c -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m

基础情况

    int b = 100;
    NSObject *c = [[NSObject alloc] init];
    // global
    EmptyBlock block = ^{
        printf("");
    };
    [self printBlockClass:block];
    // stack
    [self printBlockClass:^{
        printf("%ld", b);
    }];
     // malloc
    block = ^{
        printf("%ld", c);
    };
    [self printBlockClass:block];

最基本的 block 是全局 block,不引用局部变量,可能引用全局变量,也可能不引用任何变量,存放在数据段中:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
    printf("");
}

static struct __ViewController__viewDidLoad_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)};

可以看到 global 类型的 block 的初始化函数中,将 isa 设置成 StackBlock,但实际看汇编代码就可以知道,根本就不会走初始化函数,从数据段中取出来就直接用了。

引用了局部变量的 block

引用了基础类型的局部变量:

struct __ViewController__viewDidLoad_block_impl_1 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_1* Desc;
  int b;
  __ViewController__viewDidLoad_block_impl_1(void *fp, struct __ViewController__viewDidLoad_block_desc_1 *desc, int _b, int flags=0) : b(_b) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __ViewController__viewDidLoad_block_func_1(struct __ViewController__viewDidLoad_block_impl_1 *__cself) {
    int b = __cself->b; // bound by copy
    printf("%ld", b);
}

static struct __ViewController__viewDidLoad_block_desc_1 {
  size_t reserved;
  size_t Block_size;
} __ViewController__viewDidLoad_block_desc_1_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_1)};

struct __ViewController__viewDidLoad_block_impl_2 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_2* Desc;
  NSObject *__strong c;
  __ViewController__viewDidLoad_block_impl_2(void *fp, struct __ViewController__viewDidLoad_block_desc_2 *desc, NSObject *__strong _c, int flags=0) : c(_c) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __ViewController__viewDidLoad_block_func_2(struct __ViewController__viewDidLoad_block_impl_2 *__cself) {
    NSObject *__strong c = __cself->c; // bound by copy
    printf("%ld", c);
}

static void __ViewController__viewDidLoad_block_copy_2(struct __ViewController__viewDidLoad_block_impl_2*dst, struct __ViewController__viewDidLoad_block_impl_2*src) {
    _Block_object_assign((void*)&dst->c, (void*)src->c, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __ViewController__viewDidLoad_block_dispose_2(struct __ViewController__viewDidLoad_block_impl_2*src) {
    _Block_object_dispose((void*)src->c, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static struct __ViewController__viewDidLoad_block_desc_2 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __ViewController__viewDidLoad_block_impl_2*, struct __ViewController__viewDidLoad_block_impl_2*);
  void (*dispose)(struct __ViewController__viewDidLoad_block_impl_2*);
} __ViewController__viewDidLoad_block_desc_2_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_2), __ViewController__viewDidLoad_block_copy_2, __ViewController__viewDidLoad_block_dispose_2};

对比可知:

  • block 将引用的局部变量放在了自身结构体内
  • 不论是基本数据类型还是对象,结构体内的变量的类型跟外部的都一样,比如 int 和 NSObject *
  • 对象类型还包含了内存管理修饰符 __strong
  • 对象类型还定义了 copy 和 dispose 函数去管理对象的 copy 和 release,在 copy 结构体时会调用,这两个函数被包含在了 desc 结构体中
  • stackblock 不持有被引用的对象。因为 stackblock 没有被拷贝,所以被引用对象的 copy 函数也没有调用

__weak 修饰了局部变量的 block

struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  NSObject *__weak wc;
};
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
    NSObject *__weak wc = __cself->wc; // bound by copy
    printf("%p", wc);
}
static void __ViewController__viewDidLoad_block_copy_0(struct __ViewController__viewDidLoad_block_impl_0*dst, struct __ViewController__viewDidLoad_block_impl_0*src) {_Block_object_assign((void*)&dst->wc, (void*)src->wc, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __ViewController__viewDidLoad_block_dispose_0(struct __ViewController__viewDidLoad_block_impl_0*src) {_Block_object_dispose((void*)src->wc, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __ViewController__viewDidLoad_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __ViewController__viewDidLoad_block_impl_0*, struct __ViewController__viewDidLoad_block_impl_0*);
  void (*dispose)(struct __ViewController__viewDidLoad_block_impl_0*);
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0), __ViewController__viewDidLoad_block_copy_0, __ViewController__viewDidLoad_block_dispose_0};

睁大了眼睛看,其实跟上面的区别很小,就是 wc 的修饰符变成了 __weak

_Block_object_assign/_Block_object_dispose 函数会根据修饰符妥善处理好内存管理的

__block 修饰了局部变量的 block

    __block int a = 10;
    EmptyBlock block = ^{
        printf("%ld", a);
    };
    a = 20;

转出来的 c++ 代码(desc 结构体,copy/dispose 函数我没有列举):

struct __Block_byref_a_0 {
    void *__isa;
    __Block_byref_a_0 *__forwarding;
    int __flags;
    int __size;
    int a;
};

struct __ViewController__viewDidLoad_block_impl_0 {
    struct __block_impl impl;
    struct __ViewController__viewDidLoad_block_desc_0* Desc;
    __Block_byref_a_0 *a; // by ref
};
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
    __Block_byref_a_0 *a = __cself->a; // bound by ref
    printf("%ld", (a->__forwarding->a));
}

这次居然搞出来一个结构体来包装被引用的局部变量

为什么不直接取地址,我觉得原因有如下:

  • 只能将被引用的变量放在堆中,因为不知道 block 什么时候会被释放,放在栈中是不行的。既然放在堆上,堆的分配策略至少会分配16个字节,所以多放点东西,也不会太心疼空间😂
  • 另外的原因,接着看

forwarding 什么作用

因为 block 会被拷贝到堆上,这样就有了两个 block 了,为了方便处理,会在拷贝之后,把栈上的 block 中的 forwaring 指向堆上 block 中的 a

外部怎么用被引用的变量?

(a.__forwarding->a) = 20;

外部也是用的被包装后的结构体

__weak 修饰了局部变量的 block

    NSObject *c = [[NSObject alloc] init];
    __block __weak typeof(c) wc = c;
    EmptyBlock block = ^{
        wc = [[NSObject alloc] init];
        printf("%p", wc);
    };
struct __Block_byref_wc_0 {
    void *__isa;
    __Block_byref_wc_0 *__forwarding;
    int __flags;
    int __size;
    void (*__Block_byref_id_object_copy)(void*, void*);
    void (*__Block_byref_id_object_dispose)(void*);
    NSObject *__weak wc;
};

struct __ViewController__viewDidLoad_block_impl_0 {
    struct __block_impl impl;
    struct __ViewController__viewDidLoad_block_desc_0* Desc;
    __Block_byref_wc_0 *wc; // by ref
    __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, __Block_byref_wc_0 *_wc, int flags=0) : wc(_wc->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
  }
};
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
    __Block_byref_wc_0 *wc = __cself->wc; // bound by ref
    (wc->__forwarding->wc) = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
        printf("%p", (wc->__forwarding->wc));
    }
static void __ViewController__viewDidLoad_block_copy_0(struct __ViewController__viewDidLoad_block_impl_0*dst, struct __ViewController__viewDidLoad_block_impl_0*src) {
    _Block_object_assign((void*)&dst->wc, (void*)src->wc, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __ViewController__viewDidLoad_block_dispose_0(struct __ViewController__viewDidLoad_block_impl_0*src) {
    _Block_object_dispose((void*)src->wc, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static struct __ViewController__viewDidLoad_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __ViewController__viewDidLoad_block_impl_0*, struct __ViewController__viewDidLoad_block_impl_0*);
  void (*dispose)(struct __ViewController__viewDidLoad_block_impl_0*);
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0), __ViewController__viewDidLoad_block_copy_0, __ViewController__viewDidLoad_block_dispose_0};

有如下变化:

  • 被引用变量被包装成结构体之后,结构体中又多出来了 copy/dispose 两个函数变量
  • desc 结构体中也有两个 copy/dispose 函数,但是与上面的又不一样

desc 中的内存管理函数是管理 block 结构体中的成员变量 wc 的,block 对 wc 永远都是 strong

被引用变量包装成的结构体中的内存管理函数是处理结构体中的 wc 变量的,此时的 wc 是 __weak

所以把被引用的变量包装成结构体主要是因为这个

总结

对不同类型的 block 进行 copy 时,都做了什么

  • Global: 什么也不做
  • Malloc: 引用计数加1
  • Stack: 复制到堆上

什么时候会 copy block

ARC情况下,编译器会自动根据情况对 block 进行 copy:

  • block 作为返回值时
  • 赋值给 __strong 类型变量
  • Cocoa API 中有 名为UsingBlock的参数时