Block原理分析(1)

前情提要

1.闭包、Block是一个带有自动变量值(可以截获自动变量值)的匿名函数。截获的含义是保存该自动变量的瞬间值。
2.OC中如果要改变Block截获的外部自动变量的值,需要在该变量前加上__block修饰符。Swift不用,系统会自动处理这些问题。延伸到对象,对于Swift来说系统也会处理但OC不同。OC下,当从外部截获一个对象(NSMutableArray* )mArray时,在Block内调用NSMutableArray的方法(addObject:)没问题,因为Block捕获的是NSMutableArray类对象用的结构体实例指针。但若对其进行操作,比如赋值,就会产生编译错误,要在变量前加上__block。Swift下直接用就行了。
3.从C代码的角度分析closure/block。OC可以直接用clang -rewrite-objc 原文件名来分析,Swift下的clang ..我没有找到对应的命令,所以只看OC的。在分析之前,先介绍两个小概念。
--1.OC&Swift中的self
OC Code:

- (void) method:(int)arg {
    NSLog(@"%p %d\n", self, arg);
}
MyObject *obj = [[MyObject alloc] init];
[obj method: 10];

转换成C Code:

void _I_MyObject_method_(struct MyObject *self, SEL _cmd, int arg) {
    NSLog(@"%p %d\n", self, arg);
}
MyObject *obj = objc_msgSend(objc_getClass("MyObject"), sel_registerName("alloc"));
obj = objc_msgSend(obj, sel_registerName("init"));
objc_msgSend(obj, sel_registerName("method:"), 10);

最后一条执行语句,也就是[obj method: 10]转换成objc_msgSend(obj, sel_registerName("method:"), 10)objc_msgSend(obj, sel_registerName("method:"), 10)函数根据指定的对象和函数名,从对象持有类(MyObject)的结构体中检索[XX method:10]对应的函数-->void _I_MyObject_method_(struct MyObject *self, SEL _cmd, int arg)的指针并调用。此时,objc_msgSend函数的第一个参数obj作为_I_MyObject_method_(struct MyObject *self, SEL _cmd, int arg)的第一个参数self进行传递。即self就是Myobject类的对象-obj自己。

--2.OC中的id

*id为objc_object结构体的结构体指针。
id在C语言中的声明:

typedef struct objc_object {
    Class isa;
} *id;

*Class为objc_class结构体的结构体指针。
Class在C语言中的声明:

typedef struct objc_class {
    Class isa;
} *Class;

这与objc_object结构体相同。然而,objc_object结构体和objc_class结构体归根结底是在各个对象(id)和类(Class)的实现中使用的最基本的结构体。

例:

类,MyObject

@interface MyObject : NSObject {
    int val0;
    int val1;
}
@end

使用MyObject创建的对象newObject = [MyObject new],就是基于objc_object创建了一个该类(MyObject)的结构体实例。通过isa保持该类(MyObject)的结构体实例指针。

struct newObject {
    Class isa;
    int val0;
    int val1;
    ...;
}

而各类的结构体是就是基于objc_class的 class_t结构体。

struct class_t {
    struct class_t *isa;
    struct class_t *superclass;
    Cache cache;
    IMP *vtable;
    uintptr_t data_NEVER_USE;
    ...;
};

class_t中持有,声明的成员变量,方法的名称,方法的实现(函数指针),属性以及父类的指针,并被OC运行时库所使用的。

对象中的isa指针和类中的isa指针的区别在于,对象的是Class类型,只负责指向他的所属类。类的isa指针是class_t类型,包含了class_t结构体中的所有内容(比如方法,父class等等),指向它自己的元类(metaclass)。他的元类中就包含了各种方法,变量等等。而他的metaclass的isa指向了NSObject,superclass指向它父类的元类。以ViewController举例:

static void OBJC_CLASS_SETUP_$_ViewController(void ) {
    OBJC_METACLASS_$_ViewController.isa = &OBJC_METACLASS_$_NSObject;
    OBJC_METACLASS_$_ViewController.superclass = &OBJC_METACLASS_$_UIViewController;
    OBJC_METACLASS_$_ViewController.cache = &_objc_empty_cache;
    OBJC_CLASS_$_ViewController.isa = &OBJC_METACLASS_$_ViewController;
    OBJC_CLASS_$_ViewController.superclass = &OBJC_CLASS_$_UIViewController;
    OBJC_CLASS_$_ViewController.cache = &_objc_empty_cache;
}

给ViewController添加了一个实例方法(instanceFunc)一个类方法(classFunc)。
Metaclass方法列表的赋值:(const struct _method_list_t *)&_OBJC_$_CLASS_METHODS_ViewController;--->OBJC_$_CLASS_METHODS_ViewController--->:

_OBJC_$_CLASS_METHODS_ViewController __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"classFunc", "v16@0:8", (void *)_C_ViewController_classFunc}}
};

实例方法被保存在了_class_ro_t _OBJC_CLASS_RO_$_ViewController这里他会赋值给_class_t OBJC_CLASS_$_ViewController也就是本类。

即元类保存了类方法。

本类保存了实例方法。

MyObject类的实例变量val0,val1被直接声明为对象的结构体成员。OC中由类生成对象就意味着,生成一个基于该类的结构体实例,这些结构体实例(对象)通过isa保持该类的结构体实例指针。举个例子就是:有3个类,C动物->C狗->C柯基,基于柯基生成的一个对象叫做KK,那么KK这个结构体就会有一个isa指向C柯基,而这个C柯基的isa指向自己的元类。当KK发送了一个消息(狼嚎时)因为C柯基本类的class_t结构体中的cache和vtable都没有这个方法,便逐级会向上传递,找不到再performslector等等。


介绍完两个小概念,开始从C代码的角度分析Block,举两个例子,一个是不截获自动变量值的另一个是截获自动变量值的:

不截获自动变量值:

.m文件下的代码:

int main() {
    void(^blk)(void) = ^{printf("Block\n");};
    blk();
    return0;
}

clang -rewrite-objc 项目文件名.m之后,摘取有用的内容:

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

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("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() {
    void(*blk)(void) = ((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);
    return0;
}

源码中的^{printf("Block\n");};转换后的代码是

static void__main_block_func_0(struct __main_block_impl_0 *__cself) {printf("Block\n");}

通过转换后的代码可知,通过Block使用的匿名函数实际上被作为简单的C语言函数来处理。另外根据Block语法所属的函数名(此处为main)和该Block语法在该函数出现的顺序值(此处为0)来给经clang变换的函数命名(void__main_block_func_0)。该函数的参数*__cself就是指向Block值的变量,__main_block_impl_0结构体的指针。

那我们来看看__main_block_impl_0结构体,去掉构造函数后:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
};

第一个成员变量是impl,他的结构体声明是:

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

impl是虚函数列表,该结构体内存放了一个标志,包括今后版本升级所需的区域及函数指针。

第二个成员变量是Desc指针,__main_block_desc_0结构体的声明是:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
};

这些也如同其成员名称所示,其结构为今后版本升级所需的区域和Block大小。
这两个成员变量看完之后,再来看一下初始化含有这些结构体的__main_block_impl_0结构体的构造函数,就是我们刚才忽略的那一部分代码,这个代码执行完成意味着_cself初始化完成:

    __main_block_impl_0(void *fp,struct__main_block_desc_0 *desc,intflags=0) {
      impl.isa = &_NSConcreteStackBlock;
      impl.Flags = flags;
      impl.FuncPtr = fp;
      Desc = desc;
  }

在分析这个结构体之前,先看一下这个构造函数的调用(在 int main() {...}中):

void(*blk)(void) = ((void(*)(void))&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA));

去掉转换部分 简化成两行代码:

struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;

该源代码将__main_block_impl_0结构体类型的自动变量(栈上生成的__main_block_impl_0结构体实例的指针),赋值给__main_block_impl_0结构体指针类型的变量blk,对应未转换前的代码就是这个赋值操作:

void(^blk)(void) = ^{printf("Block\n");};

即,将Block语法生成的Block赋给Block类型变量blk。该源代码中的Block就是__main_block_impl_0结构体类型的自动变量,即栈上生成的__main_block_impl_0结构体实例。然后指针赋值。

接下来看看__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);结构体实例构造参数。__main_block_func_0&__main_block_desc_0_DATA

__main_block_func_0是一个由Block语法转换的C语言函数指针,对应的函数就是:static void __main_block_func_0(struct__main_block_impl_0 *__cself) {printf("Block\n");}

&__main_block_desc_0_DATA是作为静态全局变量初始化的__main_block_desc_0结构体实例指针。其初始化代码如下:

__main_block_desc_0_DATA= {0,sizeof(struct__main_block_impl_0)};

由此可知,该源代码使用Block,即__main_block_impl_0结构体实例的大小,进行初始化。


下面综合来看一下,栈上的Block即__main_block_impl_0结构体实例的初始化到底是怎样完成的(相关参数展开):

struct __main_block_impl_0 {
    void *isa;
    Int Flags;
    int Reserved;
    void &FuncPtr;
    struct __main_block_desc_0* Desc;
}

该结构体会根据构造函数进行如下的初始化:

isa = &_NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;

即,向内存申请空间,并将__main_block_func_0函数指针赋值给成员变量FunPtr,为后续调用做好准备。


源码中调用blk的部分blk()对应转换后的代码是:

((void(*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

去掉转换部分:

(*blk->impl.FuncPtr)(blk);

这就是简单地使用函数指针调用函数。

阶段总结

由Block语法转换的__main_block_func_0函数的指针被赋值给栈上生成的__main_block_impl_0结构体实例的FuncPtr中,再将栈上生成的__main_block_impl_0结构体实例赋值给Block类型的变量blk,之后调用blk()
通过观察调用函数,__main_block_func_0函数的参数__cself指向Block值blk,调用:(*blk->impl.FuncPtr),参数:(blk)。由此看出Block正是作为参数进行了传递。


截获自动变量值的:

.m文件下的代码:

#include <stdio.h>
int main() {
    int unCapturedVariable = 100;
    int capturedVariable = 60;
    const char *fmt = "capturedVariable = %d/n";
    void(^blk)(void) = ^{printf(fmt, capturedVariable);};
    blk();
    return 0;
}

clang -rewrite-objc 项目文件名.m之后,摘取有用的内容:

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

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 unCapturedVariable = 100;
    int capturedVariable = 60;
    const char *fmt = "capturedVariable = %d/n";
    void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, capturedVariable));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

这与前面转换的源代码稍有差异。下面来看看不同之处。首先我们注意到,Block语法表达式中使用的自动变量(capturedVariablefmt)被作为成员变量追加到了__main_block_impl_0中。并且类型完全相同,而Block语法表达式中没有使用的自动变量(unCapturedVariable)并没有被追加进去。

因此,Block的自动变量截获只针对Block中使用的自动变量。

再来看看初始化__main_block_impl_0结构体的构造函数__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _capturedVariable, int flags=0) : fmt(_fmt), capturedVariable(_capturedVariable) {...},有什么不同。
在初始化结构实例时,根据传递给构造函数的参数对成员变量(capturedVariable和fmt)进行初始化。通过以下代码:
void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, capturedVariable));。使用执行Block语法时的自动变量capturedVariablefmt来初始化__main_block_impl_0结构体实例(系统生成的Block,或者叫右边的Block可能好理解一些)。即在该源代码中,__main_block_impl_0结构体实例的初始化方法如下(相关参数展开):

struct __main_block_impl_0 {
    void *isa;
    Int Flags;
    void &FuncPtr;
    struct __main_block_desc_0* Desc;
    char *fmt;
    int capturedVariable;
}

赋值:

impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
fmt = "capturedVariable = %d/n";
capturedVariable = 60;

由此可知在__main_block_impl_0结构体实例中,成员变量(fmtcapturedVariable)被外部自动变量值赋值。

下面再来看一下使用Block的匿名函数的实现。转换前的代码片段是:printf(fmt, capturedVariable);
转换后的是:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int capturedVariable = __cself->capturedVariable; // bound by copy
printf(fmt, capturedVariable);}

在转换后的源代码中,截获到__main_block_impl_0结构体实例的成员变量的自动变量(__cself->fmt__cself->capturedVariable), bound by copy!这些变量(fmtcapturedVariable)在Block语法表达式执行之前(没初始化__main_block_impl_0之前)就已经被声明定义在__main_block_impl_0结构体里。

总的来说,所谓“截获自动变量值”意味着在执行__main_block_func_0时,Block语法表达式所使用的自动变量值已经被保存到Block的结构体实例(__main_block_impl_0)中了。在函数中可以直接截获到它们,并使用。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,104评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,816评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,697评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,836评论 1 298
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,851评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,441评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,992评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,899评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,457评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,529评论 3 341
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,664评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,346评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,025评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,511评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,611评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,081评论 3 377
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,675评论 2 359