iOS-Block的实现

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;
};
  • isa 指向实例对象,block 本身也是一个 Objective-C 对象。block 的三种类型:_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock,即当代码执行时,isa 有三种取值 :
impl.isa = &_NSConcreteStackBlock; 
impl.isa = &_NSConcreteMallocBlock; 
impl.isa = &_NSConcreteGlobalBlock;
  • Flags 按位承载 block 的附加信息;
  • Reserved 保留变量;
  • FuncPtr 函数指针,指向 Block 要执行的函数
    以上的四个字段均在初始化的时候完成:
  __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;
  }
};
  • impl block 实现的结构体变量;
  • Desc 描述 block 的结构体变量;
  • __main_block_impl_0 结构体的构造函数,初始化结构体变量 impl、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)};
  • reserved 结构体信息保留字段
  • Block_size 结构体大小

整个实现过程就是初始化__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;
}

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

  • __Block_byref_localValue_0 结构体:用于封装 __block 修饰的外部变量。
  • _Block_object_assign 函数:当 block 从栈拷贝到堆时,调用此函数。
  • _Block_object_dispose 函数:当 block 从堆内存释放时,调用此函数。

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 自动拷贝到堆上,例如:

  • block 作为函数返回值返回时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
  • block 被赋值给 __strong id 类型的对象或 block 的成员变量时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
  • block 作为参数被传入方法名带有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 时。这些方法会在内部对传递进来的 block 调用 copy 或 _Block_copy 进行拷贝;

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

  • block 作为方法或函数的参数传递时;
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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 142,680评论 1 300
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 61,177评论 1 256
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 94,249评论 0 212
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 40,981评论 0 175
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 48,746评论 1 255
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,763评论 1 176
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,362评论 2 268
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,151评论 0 165
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 28,964评论 6 229
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,501评论 0 213
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,285评论 2 215
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,614评论 1 229
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,232评论 0 31
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,117评论 2 213
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,498评论 3 204
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,615评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,012评论 0 167
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,512评论 2 230
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,594评论 2 231

推荐阅读更多精彩内容

  • 《Objective-C高级编程》这本书就讲了三个东西:自动引用计数、block、GCD,偏向于从原理上对这些内容...
    WeiHing阅读 9,672评论 10 69
  • Block概要 Block:带有自动变量的匿名函数。 匿名函数:没有函数名的函数,一对{}包裹的内容是匿名函数的作...
    zweic阅读 492评论 0 2
  • Block基础回顾 1.什么是Block? 带有局部变量的匿名函数(名字不重要,知道怎么用就行),差不多就与C语言...
    YeeChain阅读 6,661评论 5 62
  • 深冬的傍晚时分,蒙蒙细雨笼罩着整座城市。一位上了年纪的老妇人独自走进一家电影院,在第五排正当中的位置坐了下来。她衣...
    晴空月阅读 508评论 9 2
  • 谁的低语 行走在大山的胸膛上 精读 嶙峋山石 一页页风干的 历史 山...
    望断天涯路_poet阅读 257评论 0 4