老生常谈之Block

前面有一篇介绍Block的博客,主要介绍了Block的简单使用技巧。这篇博客主要更加深入地了解一下Block。包括:Block的实现、__Block的原理以及Block的存储域三方面。

Block的实现

首先我们使用Xcode创建一个Project,点击File-->New-->Project,选择macOS中Application的Command Line Tool,然后设置Project Name即可。你好发现这个工程值包含了一个main.m文件,然后我们做如下更改(更改后的代码如下):

#import <stdio.h>

int main(int argc, const char * argv[]) {
    printf("Hello World");
    return 0;
}

这个是我们最常见的C代码,导入stdio.h,然后打印出来Hello World。接下来我们写一个最简单的block,没有返回值,没有传入参数:

#import <stdio.h>

int main(int argc, const char * argv[]) {

    void (^blk)(void) = ^{
        printf("Hello Worldddd");
    };
    blk();
    return 0;
}

打印出来的结果相当于调用了blk输出的结果。接下来我们在item中跳转到main.m所在文件夹然后执行如下命令:

clang -rewrite-objc main.m

你会发现在当前文件夹下生成了一个.cpp文件,它是经过clang编译器编译之后的文件,打开之后里面大概有5百多行,其实我们看下面的这些代码就足够了:

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;
  }
};
#ifndef BLOCK_IMPL
#define BLOCK_IMPL
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        printf("Hello Worldddd");
    }

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;
}

其中包含三个结构体:

__main_block_impl_0、__block_impl、__main_block_desc_0

和两个方法:

__main_block_func_0、main

main就是我们写的main函数。
至此,你能知道的就是:Block看上去很特别,其实就是作为及其普通的C语言源代码来处理的。编译器会把Block的源代码转换成一般的C语言编译器能处理的源代码,并作为极为普通的C语言源代码被编译。
接下来对编译的内容来一个分解,首先是

^{printf("Hello Worldddd")};

变换后的源代码如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        printf("Hello Worldddd");
    }

也就是现在变成了一个静态方法,其命名方式为:Block所属的函数名(main)和该Block语法在函数出现的顺序值(0)来给经过clang变换的函数命名。该方法的参数相当于我们在OC里面的指向自身的self。我们看一下该参数的声明:

struct __main_block_impl_0 *__cself

你会发现它其实是一个结构体,该结构体的声明如下:

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;
  }
};

该结构体中你会发现里面有一个构造函数,你忽略构造函数,会发现该结构体就很简单了,只是包含了impl和 Desc两个属性变量。其中impl也是一个结构体,它的结构如下:

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

从属性变量的名字我们可以猜测出该结构体各个属性的含义:

  1. isa:isa指针,指向父类的指针。
  2. Flags:一个标记
  3. Reserved:预留区域,用于以后的使用。
  4. FuncPtr:这个很重要,是一个函数指针。后面会详细说明它的作用。

第二个变量是Desc,也是一个结构体:

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)};

这个结构体就比较简单了,一个预留位,一个是指代该Block大小的属性,后面又包含了一个该实例:

__main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

预留位为0,大小为传入结构体的大小。接下来就是很重要的构造函数了:

 __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;
  }

其中的&_NSConcreteStackBlock用于初始化impl的isa指针;flags为0;FuncPtr是构造函数传过来的fp函数指针;Desc为一个block的描述。到这里三个结构体和一个函数就介绍完了。接下来看一下main函数里面上述构造函数是如何调用的:

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

感觉好复杂,我们先做一个转换:

struct __main_block_impl_0 tmpeImpl = __main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)

struct __main_block_impl_0 *blk = &tmpeImpl;

也就是说把结构体的实例的指针赋值给blk。接下来再看一下构造函数的的初始化,其实赋值就变成了这样:

impl.isa = &_NSConcreteStackBLock;
impl.Flags = 0;
impl.FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;

现在在看一下调用block的那句代码:

blk();

转换成了:

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

这个转换不是太明白,但是知道他的作用就是把blk当做参数传进去,调用的FuncPtr所指向的函数,也就是__ block block func _ 0。

到这里就大体了解了Block的实现,其实就是C的几个结构体和方法,经过赋值和调用,进而实现了Block。
另外Block其实实质上也是OC的对象。

__Block的原理

先看一个简单的例子:

#import <stdio.h>

int main(int argc, const char * argv[]) {
    int i = 3;
    void (^blk)(void) = ^{
        printf("Hello World,%d",i);
    };
    blk();
    return 0;
}

使用clang编译后是这样的:


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

        printf("Hello World,%d",i);
    }

int main(int argc, const char * argv[]) {
    int i = 3;
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, i));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

也就是在main函数调用的时候把i传到了构造函数里,然后通过i(_i)对结构体的属性变量i赋值,i变量现在已经成为了结构体的一个树形变量。在构造函数执行时把i赋值。在 main block_func 0里面通过 cself调用,这个变量实际是在声明block时,被复制到了结构体变量i,因此不会影响变量i的值。当我们尝试在Block中去修改时,你会得到如下错误:

Variable is not assignable(missing __block type specifier)

提示我们加上__block,接下来我们将源代码做如下修改:

int main(int argc, const char * argv[]) {
    __block int i = 3;
    void (^blk)(void) = ^{
        i = i + 3;
        printf("Hello World,%d",i);
    };
    blk();
    return 0;
}

运行一下你会发现你成功对i的值进行了修改!用clang进行编译,结果如下:

struct __Block_byref_i_0 {
  void *__isa;
__Block_byref_i_0 *__forwarding;
 int __flags;
 int __size;
 int i;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_i_0 *i; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__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_i_0 *i = __cself->i; // bound by ref

        (i->__forwarding->i) = (i->__forwarding->i) + 3;
        printf("Hello World,%d",(i->__forwarding->i));
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 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_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 3};
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

你会发现多了一个__ Block byref _i 0的结构体,然后多了两个copy和dispose函数。
看一下main函数里面的i,此时也不再是一个简单的基本类型int,而是一个初始化的 Block byref i _ 0的结构体,该结构体有个属性变量为i,然后把3赋值给了那个属性变量。该结构体还有一个指向自己的指针 forwarding,它被赋值为i的地址。
现在 _ main block func 0在实现中使用了指向该变量的指针,所以达到了修改外部变量的作用。

Block的存储域

Block的存储域有以下几种:

  1. _ NSConcreteStackBlock,该类的对象Block设置在栈上
  2. _ NSConcreteGlobalBlock,该类的Block设置在程序的数据区(.data)域中。
  3. _ NSConcreteMallocBlcok,该类的Block设置在堆上
    下面这张图展示了Block的存储域:

我们前面看到的都是在Stack的Block,但是你可以在OC工程中打印一下你声明的block的isa,你会发现它其实是Malloc的block,也就是在堆上的block。如图:

还有一种情况是Global的block:

在ARC中,只有NSConcreteGlobalBlock和NSConcreteMallockBlock两种类型的block。因为我们最简单的block在工程中打印出来的都是MallocBlock。也许是因为苹果把对象都放到了堆管理,而Block也是对象,所以也放到了堆上。

此时我们也许会有个疑问:Block超出了变量作用域为什么还能存在呢?
对于Global的Block,变量作用域之外也可以通过指针安全使用,但是设置在栈上的就比较尴尬了,作用域结束后,Block也会 被废弃。为了使Block超出变量作用域还可以存在,Block提供了将Block和 __ block变量从栈上复制到堆上的方法来解决这个问题。这样就算栈上的废弃,堆上的Block还可以继续存在。

看一下对Block进行复制,结果如何:

如果对Block进行了copy操作,__ block的变量也会受到影响,当 block的变量配置在栈上,复制之后它将从栈复制到堆上并被Blcok持有,如果是堆上的 block变量,Blcok复制之后该变量被Block持有。
如果两个block(block1,block2)同时都是用 block变量,如果block1被复制到了堆上,那么 block变量也会在block1复制到堆的同时复制到堆上,当block2再是用到 block变量的时候,只是增加堆上 block变量的引用计数,不会再次复制。如果堆上的block1和block2被废弃了,那么它所是用的 block变量也就被释放了(如果block1被废弃,而block2没有被废弃,那么 block变量的引用计数-1,直到最后使用 block变量的block被废弃的同时,堆上的 block也会被释放)。
理解了上面刚才说的复制之后,现在回过来思考另一个问题: block的时候转换的结构体中的 forwarding指针有什么作用呢?(下面代码中的 __ forwarding)

struct __Block_byref_i_0 {
  void *__isa;
__Block_byref_i_0 *__forwarding;
 int __flags;
 int __size;
 int i;
};

其实是这样的:栈上的 block变量用结构体实例在 block变量从栈复制到堆上的时候,会将成员变量 forwarding的值替换为复制目标堆上的 block变量用结构体实例的地址。通过该操作之后,无论是在Block语法中、Block语法外使用 block变量,还是 block变量配置在栈上或者堆上,都可以顺利地访问同一个 __ block变量。

以上便是对block的进一步介绍,主要参考了《Objective-C高级编程 iOS与OS X多线程和内存管理》一书。

转载请注明来源:

时间: 2024-10-02 15:15:43

老生常谈之Block的相关文章

老生常谈遮罩层 滚动条的问题_javascript技巧

今天遇到的问题是,在弹出层后面的 遮罩层,因为有滚动条,导致滚动条下面不可视区域没有遮罩层,解决方式是加的css. js代码 <script type="text/javascript"> //显示灰色JS遮罩层 function showBg(ct,content){ var bH=$(document).height(); var bW=$("body").width()+16; var objWH=getObjWh(ct); $("#fu

CSS魔法堂:不得不说的Containing Block

前言  <CSS魔法堂:重新认识Box Model.IFC.BFC和Collapsing margins>中提到在没有floated兄弟盒子时,line box的左右边框会与所属的containing block的左右content edge相接触.那到底什么是containing block(abbr. CB)呢? containing block在CSS的visual formatting model中十分重要的理论基础,因为盒子的宽/高度自动值/相对值的计算,相对/浮动/绝对定位,均依赖

ARC中block块作为属性的使用笔记

ARC中block块作为属性的使用笔记 block较难理解,根据在内存中的分布情况就分为3种类型,根据使用的情形又分为很多很多种.虽然用起来容易,但使用不当会造成内存泄露,虽然都是这么说,但你真的研究过为什么会泄露吗?为什么有些时候外部变量进入block的时候会导致引用计数+1呢?   本人做过MRC以及ARC的开发,但大势所趋,ARC将是以后开发的主要模式,即使有MRC也是ARC混编MRC的代码,所以,本文的block的一些使用上的心得都基于ARC的,已经不考虑MRC的了,请看官注意,MRC与

ARC下block使用情况

ARC与MRC的block有着一些区别,笔记整理ARC的block,仅仅是自己参考的笔记,详情请参考 http://www.cnbluebox.com/?p=255   在开始之前,请新建一个Model类,写几个如下的属性,用于后面测试block的特性.     Block的类型与内存管理 根据Block在内存中的位置分为三种类型NSGlobalBlock,NSStackBlock, NSMallocBlock. NSGlobalBlock:类似函数,位于text段: NSStackBlock:

UIButton的两种block传值方式

UIButton的两种block传值方式 方式1 - 作为属性来传值 BlockView.h 与 BlockView.m // // BlockView.h // Block // // Created by YouXianMing on 15/1/14. // Copyright (c) 2015年 YouXianMing. All rights reserved. // #import <UIKit/UIKit.h> @class BlockView; /** 定义枚举值 */ typed

CSS教程:block element与inline element元素详解

块元素(block element)一般是其他元素的容器元素,块元素一般都从新行开始,它可以容纳内联元素和其他块元素,常见块元素是段落标签'P"."form"这个块元素比较特殊,它只能用来容纳其他块元素. 如果没有css的作用,块元素会顺序以每次另起一行的方式一直往下排.而有了css以后,我们可以改变这种html的默认布局模式,把块元素摆放到你想要的位置上去.而不是每次都愚蠢的另起一行.需要指出的是,table标签也是块元素的一种,table based layout和css

块(Block),元素(Element),修饰符(Modifier)

文章简介:BEM代表块(Block),元素(Element),修饰符(Modifier).这些术语的含意将在本文进一步阐述. 什么是BEM? BEM代表块(Block),元素(Element),修饰符(Modifier).这些术语的含意将在本文进一步阐述. 编程方法论中一个最常见的例子就是面向对象编程(OOP).这一编程范例出现在许多语言中.在某种程度上,BEM和OOP是相似的.它是一种用代码和一系列模式来描述现实情况的方法,它只考虑程序实体而无所谓使用什么编程语言. 我们使用BEM的原则创建了

内联元素(inline element)和块元素(block element)

文章简介:CSS里有哪些常见的块级元素和行内元素?  根据CSS规范的规定,每一个网页元素都有一个display属性,用于确定该元素的类型,每一个元素都有默认的display属性值,比如div元素,它的默认display属性值为"block",成为"块级"元素(block-level):而span元素的默认display属性值为"inline",称为"行内"元素. div这样的块级元素,就会自动占据一定矩形空间,可以通过设置高

老生常谈:首页位置消失的原因和解决办法

这是一个老生常谈的小问题:如果有一天,你SEO你的网站,发现"首页位置"下的"1"变成了"--"或者"0",消失了,你该怎么办呢? 这个问题很多站长们都可能会遇到. 网站首页位置消失的原因有很多,大致分为这三种: 一.网站本身的原因.例如优化或改版过度.长期未更新.内容雷同.网站作弊.出现违禁信息等等. 二.可能被友链连累.如果你的网站的友情链接出了问题,那么它的被降权一般也会影响到你的网站. 三.百度自身的原因.可能是百度算