文档章节

block介绍(三)揭开神秘面纱(上)

大喵哥
 大喵哥
发布于 2014/08/14 18:16
字数 1750
阅读 37
收藏 0

上一篇我们总结了各个情况下,block及其引用到的内存位置情况。

接下来几篇,我们将剖析编译器转码以及运行时库源码来一探block的究竟。

 

block到底是什么

我们使用clang的rewrite-objc命令来获取转码后的代码。

 

1、block的底层实现

我们来看看最简单的一个block:

简单的block

这个block仅仅打印栈变量i和j的值,其被clang转码为:

简单block的转码

首先是一个结构体__main_block_impl_0(从图二中的最后一行可以看到,block是一个指向__main_block_impl_0的指针,初始化后被类型强转为函数指针),其中包含的__block_impl是一个公共实现(学过c语言的同学都知道,__main_block_impl_0的这种写法表示其可以被类型强转为__block_impl类型):

struct__block_impl {
  void*isa;
  intFlags;
  intReserved;
  void*FuncPtr;
};



 isa指针说明block可以成为一个objc 对象。

__main_block_impl_0的意思是main函数中的第0个block的implementation,这就是这个block的主体了。

 这个结构体的构造函数的参数:

  1. block实际执行代码所在的函数的指针,当block真正被执行时,实际上是调用了这个函数,其命名也是类似的方式。
  2. block的描述结构体,注意这个结构体声明结束时就创建了一个唯一的desc,这个desc包含了block的大小,以及复制和析构block时需要额外调用的函数。
  3. 接下来是block所引用到的变量们
  4. 最后是一个标记值,内部实现需要用到的。(我用计算器看了一下,570425344这个值等于1<<29,即BLOCK_HAS_DESCRIPTOR这个枚举值)

所以,我们可以看到:

  1. 为什么上一篇我们说j已经不是原来的j了,因为j是作为参数传入了block的构造函数,进行了值复制。
  2. 带有__block标记的变量会被取地址来传入构造函数,为修改其值奠定了基础

 

接下来是block执行函数__main_block_func_0:

其唯一的参数是__main_block_impl_0的指针,我们看到printf语句的数据来源都取自__cself这个指针,比较有意思的是i的取值方式(带有__block标记的变量i被转码为一个结构体),先取__forward指针,再取i,这为将i复制到堆中奠定了基础。

再下来是预定义好的两个复制/释放辅助函数,其作用后面会讲到。 

最后是block的描述信息结构体 __main_block_desc_0,其包含block的内存占用长度,已经复制/释放辅助函数的指针,其声明结束时,就创建了一个名为__main_block_desc_0_DATA的结构体,我们看它构造时传入的值,这个DATA结构体的作用就一目了然了:

长度用sizeof计算,辅助函数的指针分别为上面预定义的两个辅助函数。

注意,如果这个block没有使用到需要在block复制时进行copy/retian的变量,那么desc中不会有辅助函数

 

至此,一个block所有的部件我们都看齐全了,一个主体,一个真正的执行代码函数,一个描述信息(可能包含两个辅助函数)。

 

2、构造一个block

我们进入main函数:

图一中的第三行(block的声明),在图二中,转化为一个函数指针的声明,并且都没有被赋予初始值。

而图一中的最后一行(创建一个block),在图二中,成为了对__main_block_impl_0的构造函数的调用,传入的参数的意义上面我们已经讲过了。

所以构造一个block就是创建了__main_block_impl_0 这个c++类的实例。

 

3、调用一个block

调用一个block的写法很简单,与调用c语言函数的语法一样:

blk();



其转码后的语句:

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




将blk这个函数指针类型强转为__block_impl类型,然后取其执行函数指针,然后将此指针类型强转为返回void*并接收一个__block_impl*的函数指针,最后调用这个函数,传入强转为__block_impl*类型的blk,

即调用了前述的函数__main_block_func_0

 

4、objective-c类成员函数中的block

源码如下:

- (void)of1
{
    OBJ1* oj =self;
    void(^oblk)(void) = ^{ printf("%d\n", oj.oi);};
    Block_copy(oblk);
}



这里我故意将self赋值给oj这个变量,是为了验证前一章提出的一个结论:无法通过简单的间接引用self来防止retain循环,要避免循环,我们需要__block标记(多谢楼下网友的提醒)

转码如下:

struct__OBJ1__of1_block_impl_0 {
  struct__block_impl impl;
  struct__OBJ1__of1_block_desc_0* Desc;
  OBJ1 *oj;
  __OBJ1__of1_block_impl_0(void*fp,struct__OBJ1__of1_block_desc_0 *desc, OBJ1 *_oj,intflags=0) : oj(_oj) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
staticvoid__OBJ1__of1_block_func_0(struct__OBJ1__of1_block_impl_0 *__cself) {
  OBJ1 *oj = __cself->oj;// bound by copy
 printf("%d\n", ((int(*)(id,SEL))(void*)objc_msgSend)((id)oj, sel_registerName("oi")));}



objc方法中的block与c中的block并无太多差别,只是一些标记值可能不同,为了标记其是objc方法中的blcok。

注意其构造函数的参数:OBJ1 *_oj

这个_oj在block复制到heap时,会被retain,而_oj与self根本就是相等的,所以,最终retain的就是self,所以如果当前实例持有了这个block,retain循环就形成了。

而一旦为其增加了__block标记:

- (void)of1
{
    __block OBJ1 *bSelf =self;
    ^{ printf("%d", bSelf.oi); };
}



其转码则变为:

//增加了如下行
struct__Block_byref_bSelf_0 {
  void*__isa;
__Block_byref_bSelf_0 *__forwarding;
 int__flags;
 int__size;
 void(*__Block_byref_id_object_copy)(void*,void*);
 void(*__Block_byref_id_object_dispose)(void*);
 OBJ1 *bSelf;
};
staticvoid__Block_byref_id_object_copy_131(void*dst,void*src) {
 _Block_object_assign((char*)dst +40, *(void* *) ((char*)src +40),131);
}
staticvoid__Block_byref_id_object_dispose_131(void*src) {
 _Block_object_dispose(*(void* *) ((char*)src +40),131);
}
 
//声明处变为
 
    __block __Block_byref_bSelf_0 bSelf = {(void*)0,(__Block_byref_bSelf_0 *)&bSelf,33554432,sizeof(__Block_byref_bSelf_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131,self};



clang为我们的bSelf结构体创建了自己的copy/dispose辅助函数,33554432(即1<<25 BLOCK_HAS_COPY_DISPOSE)这个值告诉系统,我们的bSelf结构体具有copy/dispose辅助函数。

而131这个参数(二进制1000 0011,即BLOCK_FIELD_IS_OBJECT (3) |BLOCK_BYREF_CALLER(128))

中的BLOCK_BYREF_CALLER在内部实现中告诉系统不要进行retain或者copy,

也就是说,在 __block bSelf 被复制至heap上时,系统会发现有辅助函数,而辅助函数调用后,并不retain或者copy 其结构体内的bSelf。

这样就避免了循环retain。

 

小结:

当我们创建一个block,并调用之,编译器为我们做的事情如下:

1.创建block所有的部件代码:一个主体,一个真正的执行代码函数,一个描述信息(可能包含两个辅助函数)。

2.将我们的创建代码转码为block_impl的构造语句。

3.将我们的执行语句转码为对block的执行函数的调用。

 

下一篇我们将剖析runtime.c的源码,并理解block的堆栈转换。

本文转载自:http://www.dreamingwish.com/frontui/article/default/block%E4%BB%8B%E7%BB%8D%EF%BC%88%E4%B8%89%EF%...

大喵哥
粉丝 13
博文 33
码字总数 1912
作品 0
广州
程序员
私信 提问
小白机器学习基础算法学习必经之路

小白机器学习基础算法学习必经之路 作者简介: 武博士,人工智能方向博士,中国移动集团IT架构师。 科研方向:自然语言处理、计算机视觉、强化学习。 已经发表SCI文章3篇。 CSDN专栏文章60篇...

GitChat技术杂谈
2018/12/06
0
0
TurboMail邮件系统新版本抢鲜看

拓波软件研发团队潜心开发长达4年,累计研发投入数千万人民币的TurboMail邮件系统新产品即将投入市场销售,小编现为您揭开TurboMail邮件系统新产品的神秘面纱,让您一睹为快。 承载着最高新的...

月亮湖泊
2016/12/09
22
0
Opera在2009美国CES上发布最新SDK

1月7日,全球知名浏览器开发商挪威Opera软件公司在2009年美国电子展揭开了Opera SDK 9.7的神秘面纱.2008年,Opera投入了大量的资源开发Opera SDK 9.7,并与合作伙伴紧密合作,以加速浏览器在各种...

红薯
2009/01/08
130
0
揭开tomcat神秘的面纱之bootstrap启动

在上文揭开tomcat神秘的面纱之bootstrap加载中,本菜鸟分析了bootstrap最终会经过初始化,加载,启动三个步骤。接着来分析启动过程。 在 的 方法中,加载完成之后就会调用 的 方法,启动容器...

一滴水的坚持
2018/11/12
0
0
揭开tomcat神秘的面纱之bootstrap启动2

在上文揭开tomcat神秘的面纱之bootstrap加载1中,本菜鸟分析道了context的启动就嘎然而止,是因为的启动过程最复杂,现在来分析context的启动过程。 是一个webApp,所有相关Context的启动都在...

一滴水的坚持
2018/11/25
0
0

没有更多内容

加载失败,请刷新页面

加载更多

OpenStack 简介和几种安装方式总结

OpenStack :是一个由NASA和Rackspace合作研发并发起的,以Apache许可证授权的自由软件和开放源代码项目。项目目标是提供实施简单、可大规模扩展、丰富、标准统一的云计算管理平台。OpenSta...

小海bug
昨天
5
0
DDD(五)

1、引言 之前学习了解了DDD中实体这一概念,那么接下来需要了解的就是值对象、唯一标识。值对象,值就是数字1、2、3,字符串“1”,“2”,“3”,值时对象的特征,对象是一个事物的具体描述...

MrYuZixian
昨天
6
0
数据库中间件MyCat

什么是MyCat? 查看官网的介绍是这样说的 一个彻底开源的,面向企业应用开发的大数据库集群 支持事务、ACID、可以替代MySQL的加强版数据库 一个可以视为MySQL集群的企业级数据库,用来替代昂贵...

沉浮_
昨天
6
0
解决Mac下VSCode打开zsh乱码

1.乱码问题 iTerm2终端使用Zsh,并且配置Zsh主题,该主题主题需要安装字体来支持箭头效果,在iTerm2中设置这个字体,但是VSCode里这个箭头还是显示乱码。 iTerm2展示如下: VSCode展示如下: 2...

HelloDeveloper
昨天
7
0
常用物流快递单号查询接口种类及对接方法

目前快递查询接口有两种方式可以对接,一是和顺丰、圆通、中通、天天、韵达、德邦这些快递公司一一对接接口,二是和快递鸟这样第三方集成接口一次性对接多家常用快递。第一种耗费时间长,但是...

程序的小猿
昨天
10
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部