一星一度的周末又走了,这周加班iOSTalk来晚了。有人会说Swift都已经横空出世了,Objective-C在排行榜都已经在持续下跌了,未来Swift将取代Objective-C成为苹果开发的主流语言,那么深入研究Obiective-C的意义何在?那要是这么说。人终有一死,那你今天还浪费那粮食干啥呢?
从自认为是持有长远眼光的人来看不再需要深入了解Objective-C是明智的选择。然而在我看来,现在深入了解OC还是很有意义的。就如Block来说吧,Block的源码转化之后是C语言结构体,进一步了解C你敢说它啥时候会没用吗?还有就是新兴语言Swift,打开Swift SDK头文件,结构体类型随处可见,连基本的数据类型Int都是源于结构体,所以说研究研究还是有点用处滴!好了,自我安慰完毕,开始枯燥的OC代码之旅。
有人也肯定会觉得微信毕竟是信手拈来的浅阅读,你写一大串的纯理论会有人耐心的看么,其实我也知道不会,我自己有的时候看到短短进度条的文章,也失去阅读的兴趣,虽然说知道作者写这么多也着实不易。所以我想到了一个办法提升文章被阅读的可能性,具体措施在文末揭晓。
前面两篇写了block的基本使用以及基本的Block的实现源码。这篇将揭晓Block的最后一幕,内容是block的几个使用技巧以及其底层源码解析,里面包含对和栈的知识,如果你还不了解你可以通过历史记录查看我之前写的一篇「内存管理之引用计数」。
前面讲到Block会捕获外部变量,但是当你试图在Block里面修改捕获的外部变量时。就是出现编译错误,解决的一种办法是将外部变量使用 __block
修饰符修饰。下面是添加 __block
修饰的外部变量代码:
#include <stdio.h> int main(int argc, const char * argv[]) { __block int val = 10; void (^blk)(void) = ^{ val = 1; }; return 0; }
该代码可进行编译。变换后如下:
struct __Block_byref_val_0 { void *__isa; __Block_byref_val_0 *__forwarding; int __flags; int __size; int val; }; // [1] struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_val_0 *val; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val; // bound by ref (val->__forwarding->val) = 1; } // [2] static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { _Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/); } static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->val, 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[]) { __Block_byref_val_0 val = { (void*)0, (__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10 }; blk = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344); return 0; }
我们可以发现加上 __block
说明符,源码量就急剧的增加了(已做简化)。
__block
修饰的变量变成了 __Block_byref_val_0
结构体。[1] __Block_byref_val_0
结构体实例的成员变量__forwarding持有该实例自身的指针。 __main_block_func_0
结构体实例持有指向 __block
变量的 __Block_byref_val_0
结构体实例的指针。 __Block_byref_val_0
结构体实例的成员变量 __forwarding
持有指向该实例自身的指针。通过成员变量 __forwarding
访问成员变量val的地址,从而可以修改自动变量。 Block_byref_i_0
结构体相关的内存管理,所以 main_block_desc_0
中增加了 copy
和 dispose
函数指针,对于在调用前后修改相应变量的引用计数。 __block
变量转换成了 __block
变量的结构体类型的自动变量。所谓结构体类型的自动变量,即栈上生成的该结构体的实例。如下表所示:
名称 | 实质 | |
---|---|---|
Block | 栈上Block的结构体实例 | |
__block变量 | 栈上__block变量的结构体实例 |
前面我们看到Block的类型说明_NSConcreteStackBlock.虽然该类没有出现已变换源代码中,但还有与之相识的类,如:
他们分别对应的存储区域如下所示:
类 | 设置对象的存储区域 |
---|---|
_NSConcreteStackBlock | 栈 |
_NSConcreteGlobalBlock | 程序的数据区域(.data区域) |
_NSConcreteMallocBlock | 堆 |
定义Block时期内存区域分配在栈中,其Block类型为 __NSConcreteStackBlock
类对象.
那么 _NSConcreteMallocBlock
类型的对象由何而来?这里肯定有人还在疑惑为什么 __block
变量转化而来的结构体为生成指向自身的 __forwarding
指针变量。其目的是为了能然超出范围的Block也有效。有人会说设置个全局的Block不就可以搞定了么。
行不行先来段代码看看:
void (^blk)(); if (/* some condition */) { blk = ^{ NSLog(@"Block A"); }; }else { blk = ^{ NSLog(@"Block B"); }; } blk();
看这段代码我声明了全局的Block变量blk,然后在if语句中定义。如果你不理解block那么就很容易写出这样的代码,其实这段代码是很危险的。因为全局的blk变量是分配在栈上的。在if和else语句中定义的blk内容,编译器会给每个块分配好栈内存,然后等离开了相应的范围之后,编译器有可能把分配给块的内存覆写了。如果编译器未覆写这块栈内存则程序照常运行,如果这块内容被覆写那么程序就会崩溃。
解决上面问题的方法就是使用 copy
方法,将block拷贝到堆中。拷贝完之后就是接下来要将的 _NSConcreteMallocBlock
类型。该类型是带有引用计数的对象,如果在ARC下,只要引用计数不为0,可以随意的访问,后继的内存管理就交给编译器来完成了。
还有一种类型是 _NSConcreteGlobalBlock
类型,这类Block不会捕捉任何状态的外部变量。块所使用的整个内存区域,在编译器已经完全的确定了,不需要每次调用时在栈中创建,如下就是一个全局快:
void (^blk)() = { NSLog("This is a global block"); } void main() { }
如果Block配置在栈中,则在Block中使用的 __block
变量也分配在栈中。当Block被复制到堆中时, __block
变量也一并被复制在堆中,并被Block所持有。如果非配在堆中的Block被废弃,那么它所使用的 __block
变量也就被释放了。下面来看堆和栈上 __block
混用的例子
__block int val = 0; void (^blk) (void) = [^{val++;} copy]; ++val; blk(); NSLog(@"val:%d",val);
执行结果为:
val: 2
在Block中和在Block外修改 __block
变量完全等效,它是怎么实现的呢?
是因为执行 copy
方法之后,Block被复制到堆中,其内部捕获的 __block
变量也一并被复制。而此时分配在栈上的val任然存在的,栈上的 __block
变量val会将原本指向自身的 __forwarding
指针指向复制到堆中的 __block
变量val的地址。这样堆中的 __block
变量被修改之后就等同于栈上的block被修改。
通过该功能,无论是在Block的语法中、Block语法外使用 __block
变量,还是 __block
变量配置在栈上还是堆上,都可以顺利地访问一个 __block
变量。
先来看一段Blcok截获可变数组对象的例子:
blk blk; { NSMutableArray *array = [[NSMutableArray alloc] init]; blk = ^(id obj){ [array addObject:obj]; NSLog(@"arrayCount = %lu",(unsigned long)array.count); }; } blk([[NSObject alloc] init]); blk([[NSObject alloc] init]); blk([[NSObject alloc] init]);
执行该段代码的结果为
arrayCount = 1; arrayCount = 2; arrayCount = 3;
从表面上看是没什么问题,运行的结果也是正确的。而实际上如果我们大量的调用block向可变数组中添加对象元素程序会强制结束。原因是block截获的NSMutableArray对象是分配在栈上的,随着当可变数组元素增加到一定程度会造成栈溢出。
解决方法是调用 copy
方法,形式如下:
blk = [^(id obj){ [array addObject:obj]; NSLog(@"arrayCount = %lu",(unsigned long)array.count); } copy];
在实际项目中对于Block最常见的问题应该是循环引用。如果Block中使用了 __strong
修饰符的对象,那么当block从栈复制到堆时,该对象为Block所持有。这样容易造成循环引用,比较明显的我想大家肯定遇到过,我们来看一个比较隐蔽的,源代码如下:
typedef void (^blk_t)(void); @interface MyObject : NSObject @property (nonatomic, strong) id obj; @property (nonatomic, strong) blk_t blk; @end @implementation MyObject - (id)init { self = [super init]; _blk = ^{NSLog(@"obj = %@",_obj);}; return self; } @end
通过编译器我们可以看到造成了循环引用,即Block语法内部使用了 _obj
变量,是因为 _obj
变量实际上截获了self。对编译器来说, _obj
变量只不过是对象的成员变量罢了。
解决的方法便是便是通过 __weak
修饰符来修饰会被Block捕获的变量:
id __weak obj = _obj; _blk = ^{NSLog(@"obj = %@",obj);};
还有一种定义设置weak变量的方式,可以用于宏定义。代码如下:
#define WS(weakSelf) __weak __typeof(&*self)weakSelf = self;
调用 WS(ws)
之后 ws
就变成为了 __weak
修饰符修饰的 self
了。
如果你觉得我写的东西对你有点价值的话,希望你能为我增加一个关注量。我的公众号「iOSTalk」。
对于微信的长文不适合作为浅阅读,以及个人公众号受微信团队“歧视”的问题。我也想过搭建自己的技术博客,之前使用零碎的时间折腾Octopress一个星期才让自己网址可以访问。兴奋之余就没有继续折腾页面的部署了。后来有幸获成了《程序员头条》的管理员。编辑文章之余,在上面发表了我的第一遍文章「iOS面试题集锦」为我带来了不少关注量,非常感谢iOS开发: iOSDevTip 博主后继我还会将我觉得不易浅阅读的好文发表到《程序员头条》。欢迎大伙关注。点击「阅读原文」直达《程序员头条》
本文由程序员头条管理员蒋小飞原创文章,转载务必注明出处。
蒋小飞微信公众号iOSTalk: JoneTalk
我叫小飞,iOS开发者,现就职于智能家居领域,坐标浙江杭州.iOSTalk用于记录iOS开发技巧,以及发表个人成长之路的一些观点.