[iOS]Block本质的一些研究

一些安排和感想

从上一篇的ARC的实现细节开始,会陆续去研究一些老生常谈的话题,诸如:Block,Runtime,GCD,Runloop,Animation等知识点,并生成一些文章.目的在于巩固已有知识,同时希望能帮到一些"有缘人".

从一段代码开始

先看一段代码:

int main(){
    void (^block)(void) = ^{
        printf("block");
    };
    block();
    return 0;
}

这段代码的作用很简单:在block内进行简单的打印.但这个过程中到底发生了什么呢?在Terminal我们输入clang -rewrite-objc + 这个文件,我们可以得到如下的C++源码(过滤一些代码后):

// block 的结构体
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr; // 方法指针
};
// 当前第一个block的实现
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; // block类型
    impl.Flags = flags;
    impl.FuncPtr = fp; // 实际实现的方法指针
    Desc = desc;
  }
};
// 这个函数就是最终执行的函数:打印内容
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        printf("block");
    }
// 描述信息:block大小,版本等
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(){
    // 生成一个block变量,这个变量是通过__main_block_impl_0内的构造方法生成的
    // 传入了实际方法实现__main_block_func_0的地址,和描述信息__main_block_desc_0_DATA的地址
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    // 上一步之后__block_impl的FuncPtr指针已经指向了__main_block_func_0的地址
    // 因为__main_block_func_0方法需要传入__main_block_impl_0类型的变量,所以直接传入上面的block变量即可完成函数调用.
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

以上代码中注释了一些关键信息,充分说明了block本质是什么,总结一下:

第1步:首先通过构造函数__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)生成了一个__main_block_impl_0类型的变量block,block内的FuncPtr指针指向了__main_block_func_0的地址.
第2步:__main_block_func_0函数又有一个__main_block_impl_0类型的形参,所以这里实际调用如下:
(*block->impl.FuncPtr)(block)

Block也是一个对象

从上面的C++源码中我们不难发现这个结构体__block_impl:

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

我们对比一下objc_class结构体class_t(在runtime源码的版本为437.1中的runtime/objc-runtime-new.h):

typedef struct class_t {
    struct class_t *isa;
    struct class_t *superclass;
    Cache cache;
    IMP *vtable;
    class_rw_t *data;
} class_t;

通过对比我们可以发现,一般对象和block都有一个isa指针成员变量,所以这里可以理解block是一个对象,并且上述block的对象类型为:
_NSConcreteStackBlock.

复杂一点的block

以上的block只是最简单的block,这里看一个稍微复杂一点的block:

int main(){
    int val1 = 123;
    int val2 = 456;
    void (^block)(void) = ^{
        printf("val1:%d",val1);
    };
    val1 = 789;
    block();
    return 0;
}

这里打印的结果大家想必都猜得出来:val1=123,而不是789.这是为什么呢?
转换后的C++代码:

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;
  int val1; // 将使用的val1作为成员变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val1, int flags=0) : val1(_val1) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int val1 = __cself->val1; // bound by copy

        printf("val1:%d",val1);
    }

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 val1 = 123;
    int val2 = 456;
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val1));
    val1 = 789;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

对比之前的block的代码,这里稍微复杂了一点,因为涉及到了对于自动变量val1的截取.
我们可以看到在__main_block_impl_0结构体中,val1被当做了一个成员变量保存在了结构体中,同时实现函数__main_block_func_0中,打印的方式为

int val1 = __cself->val1; // __cself就代表__main_block_impl_0这个结构体变量
printf("val1:%d",val1);

我们再仔细观察一下这里:

void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, 
&__main_block_desc_0_DATA, val1));

这里的val1采用的值传递,而非地址传递.所以可以解释为何后面val1 = 789,并没有更改初始值.而且我们也发现val2这个变量未曾使用到,所以在C++源码中也不会被用到.

__block 修饰符

如果我们想更改val1,这里可以用__block修饰符来声明变量:

__block int val1 = 123;

但是一旦这么声明之后,转换之后的C++代码变得稍微复杂了一些:

struct __Block_byref_val1_0 {
  void *__isa;
__Block_byref_val1_0 *__forwarding;
 int __flags;
 int __size;
 int val1;
};
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val1_0 *val1; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val1_0 *_val1, int flags=0) : val1(_val1->__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_val1_0 *val1 = __cself->val1; // bound by ref

        printf("val1:%d",(val1->__forwarding->val1));
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val1, (void*)src->val1, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val1, 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(){

    __attribute__((__blocks__(byref))) __Block_byref_val1_0 val1 = {
        (void*)0,
        (__Block_byref_val1_0 *)&val1, 
        0, 
        sizeof(__Block_byref_val1_0), 
        123
};
    int val2 = 456;

    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val1_0 *)&val1, 570425344));

    (val1.__forwarding->val1) = 789;

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

    return 0;
}

很明显我们可以看出源码中多了一个__Block_byref_val1_0这个结构体,它的作用就是用来保存我们__block修饰过的变量

struct __Block_byref_val1_0 {
  void *__isa;
__Block_byref_val1_0 *__forwarding;// 指向自己的指针
 int __flags;
 int __size;
 int val1; // 保存的成员变量
};

并且在main函数中,将123这个值初始化进了__Block_byref_val1_0结构体变量中.

__attribute__((__blocks__(byref))) __Block_byref_val1_0 val1 = {
        (void*)0,
        (__Block_byref_val1_0 *)&val1, 
        0, 
        sizeof(__Block_byref_val1_0), 
        123
};

当我们想更改val1时,这时候因为val1被存进了__Block_byref_val1_0类型的结构体中,并且传进__main_block_impl_0构造方法的是(__Block_byref_val1_0 *)&val1这个val1变量地址,所以是地址传递.而这里有一个__forwarding是指向__Block_byref_val1_0自己的,所以当设置val1 = 789的时候,可以通过(val1.__forwarding->val1) = 789来改变val1.

为什么会有__forwarding指针

这里涉及到block的作用域.在ARC情况下,编译器通常会进行适当的判断将栈上的block复制到堆里面,这个想必大家都比较清楚.
为什么需要复制到堆里呢?
因为有可能这个block在当前作用域结束的时候,block内的变量还可能需要使用.如果不复制到堆里面,那么后面就无法使用了.所以这里设置了__forwarding指针指向堆的结构体实例,不管block是在栈上,还是在堆里,都可以通过__forwarding访问到相应的成员变量.
暂时到这里吧,后面的循环引用想必都十分熟悉了,就不再一一赘述了.

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

推荐阅读更多精彩内容