Block学习笔记4-Block存储域

回到前文中演示的__main_block_impl_0的代码

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

这里讲解一下block结构体的__main_block_impl_0的构造函数中isa指针的值类型及其含义。
首先isa指向实例对象,正好表明Block也跟一般的OC对象类似,拥有isa指针,共有三种block类型:

  • NSConcreteStackBlock:设置在栈上
  • NSConcreteGlobalBlock:设置在全局范围上,即与全局变量一样,设置在程序的数据区域
  • NSConcreteMallocBlock:设置在堆上
    下面分别举例说明这三种情况在什么时候发生,以及对变量访问的限制区别

NSConcreteGlobalBlock

OC源码:
#import <Foundation/Foundation.h>
void (^bBlock)(void)=^{NSLog(@"hahaha");};
int main(int argc, char * argv[]) {
...
C源码:
struct __bBlock_block_impl_0 {
  struct __block_impl impl;
  struct __bBlock_block_desc_0* Desc;
  __bBlock_block_impl_0(void *fp, struct __bBlock_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;//全局block,无法截获自动变量
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

当将block声明在main函数之前即函数全局变量时,impl.isa的值即为NSConcreteGlobalBlock,但是由于相当于全局变量,所以无法截获自动变量,这种block不会在执行时发生任何改变,所以将其存储于数据区域中。
所以可以推断出除了在函数的全局变量处声明block能将block类型设置为NSConcreteGlobalBlock外,在函数中声明block时,只要block的执行函数中不包含任何自动变量,即其内容不会在执行时发生改变时,就可以得到NSConcreteGlobalBlock类型的block。作者试验过,的确如此。

NSConcreteStackBlock

非某些情况下,在函数中声明的block均为NSConcreteStackBlock类型,正如前面所列举的所有例子均是生成的NSConcreteStackBlock类型。

OC源码:
        __block int i = 1;
        void (^aBlock)() = ^{
            NSLog(@"%d",i);
        };//aBlock已经被复制到了堆上
        i=2;
        aBlock();

在上述源码中,按之前所理解的,这里应该生成的是NSConcreteStackBlock类型的block,但是,在aBlock();处打上断点,Xcode截取到的aBlock类型竟然为NSConcreteMallocBlock,如下图所示:


stackBlock1.png

这是因为在ARC的环境下,声明block时默认为__strong,所以编译器就自动的将block复制到了堆上,所以在运行时得到的block为NSConcreteMallocBlock,将ARC关闭,则可得到想要的NSConcreteStackBlock。

附上ARC关闭开起的设置:


ARC设置.png

由于NSConcreteStackBlock是生成在栈上的,当其所属的变量作用域结束时,该block就会被废弃,同时配置在栈上的block变量也会被废弃,所以Block提供了将Block与__block变量复制到堆上的方法,来防止因变量作用域结束而废弃的情况,而被复制后的Block的impl.isa值即为NSConcreteMallocBlock了。
附上代码举例:
(为了验证NSConcreteStackBlock下列代码在MRC的环境下运行)

typedef void (^blk_t)(void);
@implementation MyViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    blk_t blk = [self testBlk];
    blk();
}
- (blk_t)testBlk{
    int j = 0;
    blk_t tttt = ^{NSLog(@"tttt%d",j);};
    return tttt;
}

这段代码中先定义了一个blk_t类型的block,然后在私有方法中构建一个block,将这个block作为返回值。然后在viewDidLoad 中调用testBlk获取到其返回的blk。程序运行结果:

访问NSConcreteStackBlock崩溃.png

由此可得到当testBlk函数运行结束时,在其函数作用域上的NSConcreteStackBlock类型的block tttt也被释放了,所以在后面运行blk时会出现EXC_BAD_ACCESS的崩溃。

NSConcreteMallocBlock

正如前面所列举的例子所示,在ARC有效的环境下,编译器会自动将block复制到堆上(大多数情况下)。
在此情况下编译器无法自动判断,需手动调用copy函数,将block复制到堆上:

  • 向方法或函数的参数中传递Block时

但下列两种传递参数的情况除外,编译器会自动复制block

  • Cocoa框架的方法且方法名中含有usingBlock等时
  • GCD的API

附上代码举例:(将编译环境修改回ARC)

typedef void (^blk_t)(void);
@implementation MyViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    blk_t blk = [self testBlk];
    blk();
}
- (blk_t)testBlk{
    int j = 0;
    blk_t tttt = ^{NSLog(@"tttt%d",j);};
    return tttt;
}//输出tttt0

同样的一段代码,在ARC环境下能正常运行,因为在ARC下声明tttt时,已经将其复制到堆上了,所以当testBlk运行结束时,由于blk持有了testBlk的返回结果,所以引用计数加一,tttt就没有被释放掉,所以可以正常访问。
下列代码为书中的例子,作者在运行时遇到了跟书中例子解释不同的地方,在此贴出试验代码与结果:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSDictionary *tt = [self getDic];
    blk_t blk = [tt objectForKey:@"2"];
    blk();
}

- (NSDictionary* )getDic {
    int i = 1;
    NSDictionary *ttt = [[NSDictionary alloc] initWithObjectsAndKeys:^{NSLog(@"first %d",i);},@"1",^{NSLog(@"second %d",i);} ,@"2", nil];
    return ttt;
}

上述代码中,为字典初始化了两个键值对,值均为block,但是根据书中所述,获取到的字典值应该均为NSConcreteStackBlock类型,但是经过重复试验,在ARC有效的环境下,无论是对NSArray还是NSDictionary通过initwithobject或initWithObjectsAndKeys的方式传入block时,第一个总是为NSConcreteMallocBlock,后面block的均为NSConcreteStackBlock如下图所示:

代码示例.png

输出结果如下:

  • 情况一:
blk_t blk = [tt objectForKey:@"2"];
blk();

访问的是NSConcreteStackBlock,程序崩溃在blk()处,与书中的描述相符,这是因为在NSDictionary *tt = [self getDic]执行结束时,栈上的Block被废弃,访问不存在的变量引起的崩溃。

  • 情况二:
blk_t blk = [tt objectForKey:@"1"];
blk();

访问的是NSConcreteMallocBlock,程序在执行完blk();后,崩溃在main.m的return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));函数上,目前尚未找出原因,如果有人知晓原因的话欢迎留言探讨!
作者猜测可能是NSArray与NSDictionary通过initwithobject或initWithObjectsAndKeys的方式传入变量时,NSArray与NSDictionary会对首元素进行某种操作,使其不会存储在栈上,但是既然为NSArray与NSDictionary对象所持有,那为什么在访问后会崩溃呢?这是作者一直未想明白的地方。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容