这几天为了巩固知识,从 iOS 的各个知识点开始学习,希望自己对每一个知识理解的更加深入的了解。这次来分享一下 block 的学习笔记。

block 简介

block 被当做扩展特性而被加入 GCC 编译器中的。自从 OS X 10.4 和 iOS 4.0 之后,这个特性被加入了 Clang 中。因此我们今天使用的 block 在 C、C++、Objective-C 和 Objective-C++ 中均可使用。

对于 block 的语法,只放一张图即可。在之后的 block 系列文章中会详细说明其用法。

C 中的 block

说起 Xcode 的默认编译器 clang ,不得不提及 clang 在整个 编译 – 链接 过程中所起到的作用。在编译期, clang 首先对 Objective-C 代码做分析检查,确保代码中没有任何明显的错误,然后将其转换成为低级的类汇编代码,即我们经常说的中间码。

在学习 Objective-C 中的 block ,会经常使用的 clang 的 -rewrite-objc 命令来将 block 的语法转换成C语言的 struct 结构,从而供我们学习参考。

先从最简单的C语言中的 block 看起:

#include

void (^outside)(void) = ^{

printf("Hello block!\n");

};

int main () {

outside();

return 0;

}

然后使用 clang -rewrite-objc 命令对 blockTest.c 进行 block 语法转换,得到 blockTest.cpp 这个文件。

在精简代码后,选取出主要关注的代码片段。

struct __block_impl {

void *isa;

int Flags;

int Reserved;

void *FuncPtr;

};

struct __outside_block_impl_0 {

struct __block_impl impl;

struct __outside_block_desc_0* Desc;

__outside_block_impl_0(void *fp, struct __outside_block_desc_0 *desc, int flags=0) {

impl.isa = &_NSConcreteGlobalBlock;

impl.Flags = flags;

impl.FuncPtr = fp;

Desc = desc;

}

};

static void __outside_block_func_0(struct __outside_block_impl_0 *__cself) {

printf("Hello block!\n");

}

static struct __outside_block_desc_0 {

size_t reserved;

size_t Block_size;

} __outside_block_desc_0_DATA = {

0,

sizeof(struct __outside_block_impl_0)

};

int main () {

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

return 0;

}

代码可能有些难懂,逐一来分析。

static void __outside_block_func_0(struct __outside_block_impl_0 *__cself) {

printf("Hello block!\n");

}

这个函数应该是和源代码最相近的部分。并且,源代码中的 block 名被重新组合成一种新的字符串形式,而生成了这个函数的函数名。在参数上发现其实这个参数名又是一种新的字符串组合形式(__xxx_block_impl_y:这里的 xxx 是 block 名称,y 是该函数出现的顺序值)。

继续来看看参数 __cself 的声明:

struct __outside_block_impl_0 {

struct __block_impl impl;

struct __outside_block_desc_0* Desc;

// 构造函数

__outside_block_impl_0(void *fp, struct __outside_block_desc_0 *desc, int flags=0) {

impl.isa = &_NSConcreteGlobalBlock;

impl.Flags = flags;

impl.FuncPtr = fp;

Desc = desc;

}

};

第一个成员impl,是 __block_impl 类型,结构体在生成文件中也是出现的:

struct __block_impl {

void *isa;

int Flags;

int Reserved;

void *FuncPtr;

};

  • isa指针:指向一个类对象。在非 GC 模式下有三种类型:_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock。

  • Flags:block 的负载信息(引用计数和类型信息),按位存储。在下面有详细说明。

  • Reserved:保留变量。

  • FuncPtr:指向 block 函数地址的指针。

在 runtime 的源码中,对于 Flags 的枚举要比文档中描述的更加详细,其定义如下。

enum {

BLOCK_DEALLOCATING =      (0x0001),  // runtime

BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime

BLOCK_NEEDS_FREE =        (1 << 24), // runtime

BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler

BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code

BLOCK_IS_GC =             (1 << 27), // runtime

BLOCK_IS_GLOBAL =         (1 << 28), // compiler

BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE

BLOCK_HAS_SIGNATURE  =    (1 << 30)  // compiler

};

在 clang 的官方文档中,有这么一句话:

The flags field is set to zero unless there are variables imported into the Block that need helper functions for program level Block_copy() and Block_release() operations, in which case the (1<<25) flags bit is set.

也就是说,一般情况下,一个 block 的 flags 成员默认设置为 0。如果当 block 需要 Block_copy() 和 Block_release 这类拷贝辅助函数,则会设置成 1 ,也就是 BLOCK_HAS_COPY_DISPOSE 类型。可以搜索到大量讲述 Block_copy 方法的博文,其中涉及到了 BLOCK_HAS_COPY_DISPOSE 。

总结一下枚举类的用法,前 16 位即起到标记作用,又可记录引用计数:

  • BLOCK_DEALLOCATING:释放标记。一般常用 BLOCK_NEEDS_FREE 做 位与 操作,一同传入 Flags ,告知该 block 可释放。

  • BLOCK_REFCOUNT_MASK:一般参与判断引用计数,是一个可选用参数。

  • BLOCK_NEEDS_FREE:通过设置该枚举位,来告知该 block 可释放。意在说明 block 是 heap block ,即我们常说的 _NSConcreteMallocBlock 。

  • BLOCK_HAS_COPY_DISPOSE:是否拥有拷贝辅助函数(a copy helper function)。

  • BLOCK_HAS_CTOR:是否拥有 block 析构函数(dispose function)。

  • BLOCK_IS_GC:是否启用 GC 机制(Garbage Collection)。

  • BLOCK_HAS_SIGNATURE:与 BLOCK_USE_STRET 相对,判断是否当前 block 拥有一个签名。用于 runtime 时动态调用。

我们返回结构体 __outside_block_impl_0 继续看第二个成员 Desc 指针。以下是 __outside_block_desc_0 结构体声明。

static struct __outside_block_desc_0 {

size_t reserved;

size_t Block_size;

} __outside_block_desc_0_DATA = {

0,

sizeof(struct __outside_block_impl_0)

};

其中两个成员也可以从名字看出,描述的是 block 的预留区空间和 block 的大小。其中size_t类型在64位环境下应为long unsigned int,该宏定义在 C标准库 的 stddef.h 中。__outside_block_desc_0_DATA 是该结构体类型的环境量,使用成员对齐方式进行快捷构造。

再来看最重要的部分,即 __outside_block_impl_0 的构造函数。

// 构造函数

__outside_block_impl_0(void *fp, struct __outside_block_desc_0 *desc, int flags=0) {

impl.isa = &_NSConcreteGlobalBlock;

impl.Flags = flags;

impl.FuncPtr = fp;

Desc = desc;

}

这里的所有过程除了 &_NSConcreteGlobalBlock 以外都比较好理解。先跳过这部分,放在文章最后进行分析。继续看一下入口函数 main()。

int main () {

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

return 0;

}

去掉强制转换部分,增强可读性:

outside -> FuncPtr(outside);

也就是说,在执行我们定义的 block 的时候,会访问 impl 的 FuncPrt 这个函数指针。而在初始化(析构)时,这个函数会被指向 block 的执行函数体,也就是一开始分析的 __outside_block_func_0 方法。并且传入自身为参数。所以上文所提及的 __cself 参数,其实可以理解为面向对象中的所属对象,在 C++ 中我们常用 this 指针描述;而在 Objective-C 中,常常使用 self 。

写到这里,笔者有一些很有意思的联想。在 Objective-C 的设计中,为了突出对象与方法间的所属关系,经常会传递一个指针作为参数。例如在许多 Foundation 框架中的 Delegate 方法,第一个参数往往是委托方法的发起者本身。

最后再来看一下之前略过的 _NSConcreteGlobalBlock 。

对于任意一个对象的 isa 指针,其指向的对象是自身的类对象;而类对象的 isa 指针,指向的是元类(meta class)。而 block 虽然也是对象,但其结构是异于 NSObject 的。最新版本的 object 结构如下:

struct objc_class : objc_object {

// Class ISA;

Class superclass;

cache_t cache;             // formerly cache pointer and vtable

class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

}

其中 object 的 isa 指针是从 objc_objcet 中继承而来的。而 block 为了模拟 object 结构,也用到了 isa 对其进行了分类。其中 _NSConcreteGlobalBlock 就是其中之一。

关于 block 类型将会在 block 系列其他文中介绍,这里由于我们的 block 是处在全局位置,所以其类型为 _NSConcreteGlobalBlock。

在学习 C 中的 block ,通过 clang 的语义转换将 block 语法使用 C 语言描述,使得我们更进一步的深入学习 block 的内部实现。

对于 clang -rewrite-objc 一种误区

很多时候,会想当然的认为,在编译期,clang 对代码进行语义判断之后,会像 -rewrite-objc 一样对代码进行转译成 C 语言,进而转换成中间码。但是,该命令并不能代表编译后所执行的代码。

在巧哥很久之前谈Objective-C Block的实现的文中,有这么一个代码片段:

http://blog.devtang.com/2013/07/28/a-look-inside-blocks/

#include

int main() {

^{ printf("Hello, World!\n"); } ();

return 0;

}

在使用 -rewrite-objc 进行语法转换后,所显示的 block 类型为 _NSConcreteStackBlock 。而根据我们对于 block 的认知,当 block 没有引用外部的变量对象时,其类型应为 _NSConcreteGlobalBlock。难道,clang 对于 Objective-C 中的 block 和 C 中的 block 处理,会有差异吗?其实不是的,我们来做这个实验:

#include

void (^outside)(void) = ^(void) {

printf("Hello, block!\n");

};

int main() {

void (^inside)(void) = ^(void) {

printf("Hello, block!\n");

};

printf("outside: %p\n", outside);

printf("inside:  %p\n", inside);

return 0;

}

outside: 0x10d48e040

inside:  0x10d48e080

从输出结果上看,两个 block 被存储在同一区域,也就是 .data 常量区。

可是在 main 函数内声明的 block 类型,通过 clang -rewrite-objc 工具转换后仍为 _NSConcreteStackBlock 栈存储 block 类型。从这个侧面,可以明白其实 clang 对语法的解释转换,不一定出现在编译过程中。而在编译期间转换成中间码的过程中,在新版本的 clang 编译器已经不需要解释成c的语法进行过度,从而翻译成中间码。而是,在语法检测后,直接转至中间码,提交至 llvm 进行链接处理。

所以,通过 clang -rewrite-objc 命令,仅将扩展语法通过可读性更高的 C 语法进行改写,而不是编译期中的子编译过程。我们仅仅通过他来了解 block 真正的结构就已经足够了。

尾声

这篇文章讲述了 block 的结构以及指向 block 函数体的具体方式。在以后的 block 系列学习笔记中,还会继续记录 block 类型、 block 使用等相关知识。


参考资料

A look inside blocks (Block_copy)

http://t.cn/zQZYfN4

clang官方文档:block 扩展语法

http://t.cn/RzhtuNt

若想查看更多的iOS Source Probe文章,收录在这个Github仓库中。

https://github.com/Desgard/iOS-Source-Probe

浅谈 block(1) – clang 改写后的 block 结构的更多相关文章

  1. 浅谈如何删除JSP编译后的空行

    当你在客户端用view source看JSP生成的代码时,会发现有很多空行,他们是由< %...% >后的回车换行而生成的,也就是说每一行由< %...% >包含的JSP代码到 ...

  2. IOS 浅谈闭包block的使用

    前言:对于ios初学者,block通常用于逆向传值,遍历等,会使用,但是可能心虚,会感觉block很神秘,那么下面就一起来揭开它的面纱吧. ps: 下面重点讲叙了闭包的概念,常用的语法,以及访问变量, ...

  3. 安装JDK后JRE与JVM联系浅谈

    转自安装JDK后JRE与JVM联系浅谈 安装JDK后JRE.JVM之间的关系是什么呢?那么我们要从安装JDK慢慢说起. 如果安装了JDK,会发同你的电脑有两套JRE: 一套位于 <JDK安装目录 ...

  4. 浅谈Block传值-匿名函数(代码块)

    block传值是从后往前传值---代理也是 // 使用block时, 不能访问self, 也不能直接访问属性, self.属性, 用self调用方法; 只要这样做了, block都会对其强引用一份, ...

  5. iOS开发之浅谈MVVM的架构设计与团队协作

    今天写这篇博客是想达到抛砖引玉的作用,想与大家交流一下思想,相互学习,博文中有不足之处还望大家批评指正.本篇博客的内容沿袭以往博客的风格,也是以干货为主,偶尔扯扯咸蛋(哈哈~不好好工作又开始发表博客啦 ...

  6. 浅谈Android应用保护(一):Android应用逆向的基本方法

    对于未进行保护的Android应用,有很多方法和思路对其进行逆向分析和攻击.使用一些基本的方法,就可以打破对应用安全非常重要的机密性和完整性,实现获取其内部代码.数据,修改其代码逻辑和机制等操作.这篇 ...

  7. 浅谈php生成静态页面

    一.引 言 在速度上,静态页面要比动态页面的比方php快很多,这是毫无疑问的,但是由于静态页面的灵活性较差,如果不借助数据库或其他的设备保存相关信息的话,整体的管理上比较繁琐,比方修改编辑.比方阅读权 ...

  8. 转: 浅谈C/C++中的指针和数组(二)

    转自:http://www.cnblogs.com/dolphin0520/archive/2011/11/09/2242419.html 浅谈C/C++中的指针和数组(二) 前面已经讨论了指针和数组 ...

  9. 浅谈C中的malloc和free

    转自http://bbs.bccn.net/thread-82212-1-1.html非常感谢作者 浅谈C中的malloc和free 在C语言的学习中,对内存管理这部分的知识掌握尤其重要!之前对C中的 ...

随机推荐

  1. Xamarin.Android中使用ResideMenu实现侧滑菜单

    上次使用Xamarin.Android实现了一个比较常用的功能PullToRefresh,详情见:Xamarin. Android实现下拉刷新功能 这次将实现另外一个手机App中比较常用的功能:侧滑菜 ...

  2. 我的ORM之八-- 事件

    我的ORM索引 dbo.Event 需要新建一个类,继承:IDboEvent,并设置给:dbo.Event,如: dbo.Event = MyOqlEvent.GetInstance(); 在 IDb ...

  3. Web Essentials之Markdown和自定义编辑器(Web Essentials完结)

    返回Web Essentials功能目录 本篇目录 功能 自定义编辑器 开源项目都会在项目的根目录放一个Readme.md文件来告诉读者一些重要的说明,那么就可以在VS中直接编辑Markdown文件. ...

  4. Java虚拟机10:类加载器

    类与类加载器 虚拟机设计团队把类加载阶段张的"通过一个类的全限定名来获取此类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类.实现这 ...

  5. AMD加载器实现笔记(五)

    前几篇文章对AMD规范中的config属性几乎全部支持了,这一节主要是进一步完善.到目前为止我们的加载器还无法处理环形依赖的问题,这一节就是解决环形依赖. 所谓环形依赖,指的是模块A的所有依赖项的依赖 ...

  6. php 润年 星期 天数

    详细: 闰年.星期.天 echo date(|-|L|-|); 今年是否闰年: echo date(|-|l|-|); 今天是:Tuesday echo date(|-|D|-|); 今天是:Tue ...

  7. JavaScript –类型之我晕

    每次写博我觉得取上恬当的题目比整篇行文都难,词量有限的情况下突然想到JavaScript拾遗应该会是一个非常文艺而夺目的博文题目,但我并没有急着使用,经验告诉我应该先去搜一下看有没有被用过.果不其然, ...

  8. 可拖动的DIV

    在做WEB UI设计的时候,拖动某个HTML元素已经成为一种不能忽视的用户界面模式,比较典型的应用例子就是Dialog,一个元素是怎么实现拖动的呢?其实原理非常简单,要想实现首先得了解几个基本知识. ...

  9. java 锁3

    先谈线程的状态: 具体来说有, NEW. Running. Blocked.此状态的线程阻塞,它正在等待监视器锁——等待另外一个线程释放锁(通俗说就是等它执行完synchronized了的方法/代码块 ...

  10. P,NP,NP_hard,NP_complete问题定义

    背景:在看李航的<统计学习方法时>提到了NP完全问题,于是摆之. 问题解答:以下是让我豁然开朗的解答的摘抄: 最简单的解释:P:算起来很快的问题NP:算起来不一定快,但对于任何答案我们都可 ...