美文网首页
第37条:理解“块”这一概念

第37条:理解“块”这一概念

作者: MrSYLong | 来源:发表于2018-10-14 21:52 被阅读10次

块可以实现闭包,Mac OS X10.4与iOS4.0之后的系统,都含有正常执行块所需的运行期组件。

块的基础知识

块用“^”符号来表示,后面跟着一对花括号,括号里面是块的实现代码。

// 最简单的块
^{
    // 实现代码
}

块就是一个值,有块类型变量,可以把块赋值给块变量。

// 将块赋值给块变量
void (^someBlock) () = ^ {
    // 实现代码
};

块类型的语法:

return_type (^block_name) (parameters)
// 示例:
int (^addBlock) (int a, int b) = ^(int a, int b){
    return a + b;
};

块强大之处:在声明它的范围里,所有变量都可以为其所捕获,默认情况下,为块所捕获的变量,是不可以在块里修改的。不过,声明变量的时候加上__block修饰符,就可以在块内修改了。

如果块所捕获的变量是对象类型,那么会自动保留它。系统在释放块的时候,会一并将捕获的对象释放,以便平衡捕获时所执行的保留操作。

块本身可视为对象。块本身也和其他对象一样,有引用计数。

如果将块定义在Objective-C类的实例方法中,那么除了可以访问类的所有实例变量,还可以使用self变量。块总能修改实例变量,所以声明时无须加__block。不过,如果通过读取或写入操作捕获了实例变量,那么自动把self变量也一并捕获了,因为实例变量是与self所指代的实例关联在一起的。

- (void)anInstanceMethod
{
    void (^someBlock) () = ^ {
        _anInstanceVariable = @"Something";
    };
}

注意:上面代码,由于块里没有明确使用self变量,所有很容易忘记self变量其实也为块所捕获,self也是对象,它所指代的对象保留了块,块又在捕获self时也将其保留,所以上面代码导致"保留环",造成内存泄露。
块的内部结构

每个Objective-C对象都占据着某个内存区域。因为实例变量的个数及对象所包含的关联数据互不相同,所以每个对象做占据的内存区域有大有小。

块对象的内存布局.png

isa指针是指向Class对象指针。

invoke变量,是一个函数指针,指向块的实现代码。

descriptor变量是指向结构体的指针,每个块里都包含此结构体,其中声明了块对象的总体大小,还声明了copy与dispose这两个辅助函数所对应的函数指针,前者要保留捕获的对象,后者则将之释放。

块还会把它捕获的所有变量都拷贝一份,不是对象本身,而是执行对象的指针变量。这些拷贝放在descriptor变量后面。

全局块、栈块及堆块

定义块的时候,其所占的内存区域是分配在栈上的。

void (^someBlock) ();
if (/* some condition */) {
    someBlock = ^ {
        NSLog(@"Block A");
    };
} else {
    someBlock = ^ {
        NSLog(@"Block B");
    };
}
someBlock;

上面代码,定义在if及else中的两个块都分配在栈内存中,离开相应的范围,编译器可能会把分配给块的内存覆写掉,所以出现可能会出现崩溃的情况。

为解决此问题,可给块对象发送copy消息,将块从栈复制到堆,块就可以在定义它之外的范围使用了,块也成了带引用计数的对象了,后续的复制都不再是真的的执行复制,只是递增块对象的引用计数。不再使用这个块时,ARC自动释放,MRC手动管理引用计数。当引用计数为0后,“分配在堆上的块”会像其他对象一样,被系统回收,“分配在栈上的块”则无须明确释放,因为栈内存本来就自动回收。

“全局块”:不会捕捉任何状态,运行时也无须有状态来参与,所使用的内存区域,在编译期就已经完全确定,可以声明在全局内存里,它的拷贝操作是空操作,因为全局块绝不可能为系统所回收。

相关文章

网友评论

      本文标题:第37条:理解“块”这一概念

      本文链接:https://www.haomeiwen.com/subject/tsgtzftx.html