iOS学习开发iOS开发记录iOS学习笔记

iOS-Block的实现

2019-04-16  本文已影响6人  FlyElephant

Block是C语言的扩充功能,是带有自动变量的匿名函数。block 将同一逻辑的代码放在一个块,使代码更简洁紧凑,易于阅读,比函数使用更方便,代码更美观,开发中受到广泛的使用。

block 的底层实现

将main.m中的代码通过clang编译成main.cpp代码:

int main(int argc, const char * argv[]) {
    // insert code here...
    void (^blk)(void) = ^{ printf("FlyElephant---Block\n"); };
    blk();
    return 0;
}

main.cpp代码:

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("FlyElephant---Block\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(int argc, const char * argv[]) {

    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

block通过__main_block_impl_0初始化,后序工作通过 __block_impl ,__main_block_desc_0实现。

__block_impl

__block_impl结构体代码如下:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
impl.isa = &_NSConcreteStackBlock; 
impl.isa = &_NSConcreteMallocBlock; 
impl.isa = &_NSConcreteGlobalBlock;
  __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;
  }

其中FuncPtr指针指向的block函数:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
 printf("FlyElephant---Block\n"); }

__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;
  }
};

__main_block_desc_0

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_block_impl_0,返回impl,执行impl->FuncPtr.

block 捕获外部变量

int main(int argc, const char * argv[]) {
    // insert code here...
    int localValue = 1;
    void (^blk)(void) = ^{ printf("FlyElephant = %d\n", localValue); };
    blk();
    return 0;
}

block捕获外部变量编译之后的cpp代码如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int localValue;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _localValue, int flags=0) : localValue(_localValue) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int localValue = __cself->localValue; // bound by copy
 printf("FlyElephant = %d\n", localValue); }

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(int argc, const char * argv[]) {

    int localValue = 1;
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, localValue));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

__block_impl定义不变:

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

block通过参数传递获取到了localValue,保存到结构体中的同名变量中。赋值的时候通过__cself来赋值:

 int localValue = __cself->localValue; // bound by copy

不过暂时还不能还不能修改值,如果修改localValue会报错:

Variable is not assignable (missing __block type specifier)

内存区域

讨论block的存储区域我们先了解一下C/C++编译的程序在内存中的分布情况 :

1.栈区(stack)— 程序运行时由编译器自动分配,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。程序结束时由编译器自动释放。

2.堆区(heap) — 在内存开辟另一块存储区域。一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是不一样,分配方式类似于链表。用malloc, calloc, realloc等分配内存的函数分配得到的就是在堆上。

3.全局区(静态区)(static)—编译器编译时即分配内存。全局变量和静态变量的存储是放在一块的。对于C语言初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。而C++则没有这个区别 - 程序结束后由系统释放。

4.文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放

5.程序代码区—存放函数体的二进制代码。

block访问变量有两种方式一种是静态变量,全局变量和__block形式。

静态变量,全局变量

block使用静态变量,全局变量,全局静态变量代码:

int global_val = 1;
static int static_global_val = 2;
int main(int argc, const char * argv[]) {
    // insert code here...
    static int static_val = 3;
    void(^blk)(void) = ^ {
        global_val = 2;
        static_global_val = 3;
        static_val = 4;
    };
    return 0;
}

编译之后的代码:

int global_val = 1;
static int static_global_val = 2;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_val = __cself->static_val; // bound by copy

        global_val = 2;
        static_global_val = 3;
        (*static_val) = 4;
    }

对静态全局变量和全局变量访问和转换之前一样,对静态变量的方式是将指针保存了起来。block对于其自动变量而言没有将指针保存起来,是因为自动变量超出其作用域之后就会被废弃。

__block

通过__block看下代码:

    __block int localValue = 0;
    void (^blk)(void) = ^{
        localValue = 1;
    };

编译之后代码:

struct __Block_byref_localValue_0 {
  void *__isa;
__Block_byref_localValue_0 *__forwarding;
 int __flags;
 int __size;
 int localValue;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_localValue_0 *localValue; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_localValue_0 *_localValue, int flags=0) : localValue(_localValue->__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_localValue_0 *localValue = __cself->localValue; // bound by ref

        (localValue->__forwarding->localValue) = 1;
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->localValue, (void*)src->localValue, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->localValue, 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(int argc, const char * argv[]) {

    __attribute__((__blocks__(byref))) __Block_byref_localValue_0 localValue = {(void*)0,(__Block_byref_localValue_0 *)&localValue, 0, sizeof(__Block_byref_localValue_0), 0};
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_localValue_0 *)&localValue, 570425344));
    return 0;
}

相对之前的代码多了不少代码,主要新增代码:

OC源码中的 __block localValue 翻译后变成了 __Block_byref_intValue_0 结构体指针变量 intValue,通过指针传递到 block 内,与静态变量的指针传递是一致的。__Block_byref_intValue_0 这个结构体的字段需要注意:

struct __Block_byref_localValue_0 {
  void *__isa;
__Block_byref_localValue_0 *__forwarding;
 int __flags;
 int __size;
 int localValue;
};
__block变量结构体.png

结构体里面还多了个 __forwarding 指向自己的指针变量,这与block的类型有关系。

block类型

block 有三种类型 NSConcreteGlobalBlock,NSConcreteStackBlock和NSConcreteMallocBlock。

NSConcreteGlobalBlock

_NSConcreteGlobalBlock 类型的 block 处于内存的 ROData 段,不捕获局部变量,运行不依赖上下文,内存管理比较简单。

*block 字面量写在全局作用域时,编译之后也是_NSConcreteGlobalBlock类型。

void (^blk)(void) = ^{ printf("Global Block\n"); };
int main(int argc, const char * argv[]) {
    blk();
    return 0;
}
struct __blk_block_impl_0 {
  struct __block_impl impl;
  struct __blk_block_desc_0* Desc;
  __blk_block_impl_0(void *fp, struct __blk_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __blk_block_func_0(struct __blk_block_impl_0 *__cself) {
 printf("Global Block\n"); }

NSConcreteStackBlock

NSConcreteStackBlock 类型的 block 处于内存的栈区。global block 由于处在 data 段,可以通过指针安全访问,但 stack block 处在内存栈区,如果其变量作用域结束,这个 block 就被废弃,block 上的 __block 变量也同样会被废弃。

栈上的Block与__block变量.png

为了解决这个问题,block 提供了 copy 的功能,将 block 和 __block 变量从栈拷贝到堆,也就是 _NSConcreteMallocBlock。

_NSConcreteMallocBlock

当 block 从栈拷贝到堆后,当栈上变量作用域结束时,仍然可以继续使用 block,在开启 ARC 时,大部分情况下编译器通常会将创建在栈上的 block 自动拷贝到堆上,例如:

编译器不会自动调用 copy 方法:

typedef int (^blk_t)(int);
blk_t func(int rate)
{
    return ^(int count){return rate * count;};
}

上面的 block 获取了外部变量,所以是创建在栈上,当 func 函数返回给调用者时,脱离了局部变量 rate 的作用范围,如果调用者使用这个 block 就会出问题。那 ARC 开启的情况呢?运行这个 block 一切正常,编译器编译之后的代码:

blk_t func(int rate)
{
    blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);
    tmp = objc_retainBlock(tmp);
    return objc_autoreleaseReturnValue(tmp); 
}

objc_retainBlock本质上调用的是_Block_copy函数:

id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}

objc_autoreleaseReturnValue 本质上调用的是objc_autorelease函数:

id 
objc_autoreleaseReturnValue(id obj)
{
    if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;

    return objc_autorelease(obj);
}

block类型拷贝:

Block类型 源拷贝区域 Copy结果
_NSConcreteStackBlock 从栈复制到堆
_NSConcreteGlobalBlock 程序的数据区域 什么都不做
_NSConcreteMallocBlock 引用计数加1

block 内存管理

当 block 从栈内存被拷贝到堆内存时,__block 变量的变化如下图。需要说明的是,当栈上的 block 被拷贝到堆上,堆上的 block 再次被拷贝时,对 __block 变量已经没有影响了。

block拷贝.png 多个Block使用__block变量.png block废弃与变量释放.png

__forwarding

block 从栈被拷贝到堆时,__forwarding 指针变量也会指向堆区的结构体。

block 获取局部变量,当要在其他地方(超出局部变量作用范围)使用这个 block 的时候,由于访问局部变量异常,导致程序崩溃,因此需要将block从栈区拷贝到堆区。

将 block 拷贝到堆上的同时,将 __forwarding 指针指向堆上结构体。后面如果要想使用 __block 变量,只要通过 __forwarding 访问堆上变量,就不会出现程序崩溃了。

简单讲就是“不管__block变量配置在栈上还是堆上,都能正确的访问该变量。”

    __block int val = 0;
    void (^blk)(void) = [^{++val;} copy];
    ++val;
    blk();
    NSLog(@"%d", val);

👆代码中 ^{++val;} 和 ++val; 都会被转换成 ++(val.__forwarding->val);,堆上的 val 被加了两次,最后打印堆上的 val 为 2。

block 循环引用

如果在Block使用附有__strong修饰符的对象类型自动变量,那么当Block从栈复制到堆上时,该对象为Block所持有。

经典的循环引用是self与block之间的相互引用:

typedef void (^blk_t)(void);

@interface User()
{
    blk_t blk_;
}
@end

@implementation User

- (id)init
{
    self = [super init];
    blk_ = ^{NSLog(@"self = %@", self);};
    return self;
}

- (void)dealloc
{
    NSLog(@"dealloc");
}

@end

编译器会提示警告:

Capturing 'self' strongly in this block is likely to lead to a retain cycle

如果block中使用self中的属性或者成员变量:

@interface User()
{
    blk_t blk_;
}

@property (assign, nonatomic) NSInteger age;

@end

@implementation User

- (id)init
{
    self = [super init];
    blk_ = ^{
        NSLog(@"FlyElephant--%ld", (long)self.age);
    };
    return self;
}

- (void)dealloc
{
    NSLog(@"dealloc");
}

@end

编译会警告提示循环引用:

Capturing 'self' strongly in this block is likely to lead to a retain cycle

__weak修饰符解决循环引用问题:

    __weak typeof(User) *weakself = self;
    blk_ = ^{
        NSLog(@"FlyElephant--%ld", (long)weakself.age);
    };
    return self;

__block避免循环引用:

typedef void (^blk_t)(void);
@interface User : NSObject
{
    blk_t blk_;
}
@end

@implementation User

- (id)init
{
    self = [super init];
    __block id tmp = self;
    blk_ = ^{
        NSLog(@"self = %@", tmp);
        tmp = nil;
    };
    return self;
}

- (void)execBlock
{
    blk_();
}

- (void)dealloc
{
    NSLog(@"dealloc");
}

@end

int main()
{
    id object = [[User alloc] init];
    [object execBlock];
    return 0;
}

该代码没有引起循环引用。但是如果不执行execBlock实例方式,即不执行复试给成员变量blk_的Block,会循环引用并引起内存泄漏。

参考链接

http://www.galloway.me.uk/2012/10/a-look-inside-blocks-episode-1/

http://www.galloway.me.uk/2012/10/a-look-inside-blocks-episode-2/

https://www.zybuluo.com/MicroCai/note/51116

上一篇下一篇

猜你喜欢

热点阅读