iOS的Block

Block的实现

  block 的数据结构定义如下

image

  对应的结构体定义如下:

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};
struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

  通过该图,我们可以知道,一个 block 实例实际上由 6 部分构成:

1、isa 指针,所有对象都有该指针,用于实现对象相关的功能。

2、flags,用于按 bit 位表示一些 block 的附加信息。

3、reserved,保留变量。

4、invoke,函数指针,指向具体的 block 实现的函数调用地址。

5、descriptor, 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针。

6、variables,capture 过来的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。

  block的声明与定义如下图: image

  首先我们来看四个函数

void test1() {
    int a = 10; 
    void (^block)() = ^{ 
        NSLog(@"a is %d", a); 
    }; 
    a = 20; 
    block(); // 10
} 

void test2() {
    __block int a = 10; 
    void (^block)() = ^{ 
        NSLog(@"a is %d", a); 
    }; 
    a = 20; 
    block(); // 20
} 

void test3() {

    static int a = 10; 
    void (^block)() = ^{ 
        NSLog(@"a is %d", a); 
    }; 
    a = 20; 
    block(); // 20

} 


int a = 10; 
void test4() {
    void (^block)() = ^{
        NSLog(@"a is %d", a); 
    };
    a = 20;
    block();//20
}

  使用clang将test1函数编译成c++代码,在命令行中输入clang -rewrite-objc main.c即可在目录中看到 clang 输出了一个名为 main.cpp 的文件。该文件就是 block在c++语言实现。

  注:如若碰到’UIKit/UIKit.h’ file not found之类的错误,请参考Objective-C编译成C++代码报错

#ifndef BLOCK_IMPL
#define BLOCK_IMPL
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

struct __test1_block_impl_0 {
  struct __block_impl impl;
  struct __test1_block_desc_0* Desc;
  int a;
  __test1_block_impl_0(void *fp, struct __test1_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __test1_block_func_0(struct __test1_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_yt_3w8z3x6s6_53pnlcw6jk8vcr0000gn_T_main_f8cd6c_mi_0, a);
    }

static struct __test1_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __test1_block_desc_0_DATA = { 0, sizeof(struct __test1_block_impl_0)};
void test1()
{
    int a = 10;

    void (*block)() = ((void (*)())&__test1_block_impl_0((void *)__test1_block_func_0, &__test1_block_desc_0_DATA, a));
    a = 20;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

int main(int argc, char * argv[]) {
    test1();



}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

  可以看到a是作为值传递进去的。   接下来我们打印这3处a的值,结果如下:

2017-08-09 18:51:24.427 TestBounds[21998:295458] block定义前:0x7fff5278c57c
2017-08-09 18:51:24.481 TestBounds[21998:295458] block定义后:0x7fff5278c57c
2017-08-09 18:51:24.481 TestBounds[21998:295458] block内部:0x60000004fc80

  a在block中和block外部地址是不一样的,外面的地址是在栈中,而在ARC下block访问了外部变量会拷贝一份到堆上,此时的block并不会对变量a造成影响。

  再来看看test2函数编译成c++代码。

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

struct __test2_block_impl_0 {
  struct __block_impl impl;
  struct __test2_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __test2_block_impl_0(void *fp, struct __test2_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 __test2_block_func_0(struct __test2_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_yt_3w8z3x6s6_53pnlcw6jk8vcr0000gn_T_main_e4454e_mi_0, (a->__forwarding->a));
    }
static void __test2_block_copy_0(struct __test2_block_impl_0*dst, struct __test2_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __test2_block_dispose_0(struct __test2_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __test2_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __test2_block_impl_0*, struct __test2_block_impl_0*);
  void (*dispose)(struct __test2_block_impl_0*);
} __test2_block_desc_0_DATA = { 0, sizeof(struct __test2_block_impl_0), __test2_block_copy_0, __test2_block_dispose_0};
void test2() {
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
    void (*block)() = ((void (*)())&__test2_block_impl_0((void *)__test2_block_func_0, &__test2_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    (a.__forwarding->a) = 20;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

int main(int argc, char * argv[]) {
    test2();
}

  可以看到a是作为引用传入函数中去,这是因为使用了__block关键字造成的效果。我们再来打印a的地址。

2017-08-09 19:04:30.735 TestBounds[22354:303315] block定义前:0x7fff5b540578
2017-08-09 19:04:30.863 TestBounds[22354:303315] block定义后:0x60800003fe58
2017-08-09 19:04:30.863 TestBounds[22354:303315] block内部:0x60800003fe58

  可以看到使用__block定义的变量在block中拷贝到堆上去后对后来的a的地址也产生了变化,a在定义前是栈区,但只要进入了block区域,就变成了堆区。 总结如下:

  • 对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的

  • 对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的

  利用上述方法也可以解释test3和test4输出结果原因。

Block的内存管理

  在 Objective-C 语言中,一共有 3 种类型的 block:

1、_NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量。

2、_NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。

3、_NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。

  而在MRC和ARC中有些区别 无论当前环境是ARC还是MRC,只要block没有访问外部变量,block始终在全局区

MRC情况下:

  • block如果如果访问外部变量,block在栈里

  • 不能对block使用retain,否则不能保存在堆里

  • 只有使用copy才能放在堆里

ARC情况下:

  • block如果访问外部变量,block在堆里

  • block可以使用copy和strong,并且block是一个对象

Block的循环引用

  如果要在block中直接使用外部强指针会发生循环引用,使用以下代码在block外部实现可解决

__weak typeof(self) weakSelf = self;

  如果在block内部使用延时操作还使用弱指针的话会取不到弱指针,此时需要在block内部再将弱指针强引用一下

__strong typeof(self) strongSelf = weakSelf;

本文参考:

谈Objective-C block的实现

iOS中__block 关键字的底层实现原理

最近的文章

iOS静态库中的Category

  一个项目中使用了一个包含 category 的静态库,但是此项目在运行过程中,该静态库调用category增加的方法处,却报selector not recognized异常。产生的原因  苹果官方文档中的这个 Q&A QA1490:Building Objective-C static libraries with categories 已经说明了这个问题产生的原因:  这个异常是因为标准 UNIX 静态库、linker 以及 Objective-C 的动态性三者之间的实现导...…

继续阅读
更早的文章

VLCMediaPlayer的集成与使用

  VLC Media Player (VideoLAN) 为 Windows、Linux、OS X、Android、iOS、Windows Phone等平台提供一个视频播放器、解码器。它可以播放来自网络、摄像头、磁盘、光驱的文件,支持包括MPEG 1/2/4, H264, VC-1, DivX, WMV, Vorbis, AC3, AAC等格式的解码。在 Windows 和 Linux 上的 VLC 是使用C++/Qt写成,提供了一致的用户体验。同时 VLC 还专门为 OS X 提供了原...…

继续阅读