iOS Block学习笔记(七) -- Block的存储域以及Block在栈上与在堆上

通过前面的知识, Block语法执行时, Block底层会转化成Block的结构体类型的自动变量存储在栈上, 在__block变量初始化时, 会转化成__block变量的结构体类型的自动变量存储在栈上. 所谓结构体自动变量, 就是在栈上生成的结构体的实例, 如下表:

名称 实质
Block 栈上Block的结构体实例
__block变量 栈上__block变量的结构体的实例

同时,我们知道Block也是Objective-C的对象, 通过isa指针, 上面出现的Block基本是_NSConcreteStackBlock, 表示该对象存储在栈上.

实际上,Block对象有如下几种类型, 不同的类型, 存储在应用程序的不同的内存区域:

名称 设置对象的存储域
_NSConcreteStackBlock
_NSConcreteGlobalBlock 数据域(.data域)
_NSConcreteMallocBlock

关于应用程序的内存布局, 可以参考以下文章 linux系统进程的内存布局

isa = &_NSConcreteGlobalBlock对象的Block, 会存储在数据域, 因为它创建在全局域, 因此不会截获自动变量, 全局只需要一个实例即可, 因此将该Block存储在数据域. 有以下两种情况创建的Block是_NSConcreteGlobalBlock:

  • 记述全局变量的地方有Block语法时
  • Block语法的表达式中不使用截获的自动变量时候(只截获静态变量, 或者不截获变量)

通过以下Demo可以支持上面的结论, 前4个Block都是_NSConcreteGlobalBlock:

void (^global_block)(void) = ^{};
static int static_global_val = 1;
int global_val = 2;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        static int static_val = 3;
        int num = 4;

        void (^global_static_val_block)(void) = ^{ static_global_val = 0; };
        void (^local_static_val_block)(void) = ^{ static_val = 0; };
        void (^normal_block)(void) = ^{};
        void (^stack_block)(void) = ^{int a = num;};

        NSLog(@"Global Block: %@\n", global_block);
        NSLog(@"Global Static val Block: %@\n", global_static_val_block);
        NSLog(@"Local Static val Block: %@\n", local_static_val_block);
        NSLog(@"Normal Block: %@\n", normal_block);
        NSLog(@"Stack Block: %@\n", stack_block);
        NSLog(@"Stack Block: %@\n", ^{int a = num;});

//        Global Block: <__NSGlobalBlock__: 0x100001060>
//        Global Static val Block: <__NSGlobalBlock__: 0x1000010a0>
//        Local Static val Block: <__NSGlobalBlock__: 0x1000010e0>
//        Normal Block: <__NSGlobalBlock__: 0x100001120>
//        Stack Block: <__NSMallocBlock__: 0x1007078b0>
//        Stack Block: <__NSStackBlock__: 0x7ffeefbff548>
    }
    return 0;
}

最后两个一个是_NSConcreteMallocBlock, 一个是_NSConcreteStackBlock, 两者实现一样,为何不是同一类Block?

我们知道栈上的自动变量, 在超出其作用域以后,其内存会被废弃回收. Block自动变量也是如此, 当栈上的Block所属的变量作用域结束, 那么该Block就会被废弃, 同时__block变量也存储在栈上,超出其作用域时, 也会被废弃.

Block3.3.jpg

让我们思考一下以下场景,__block int val是__block变量结构体在栈上, 而创建的Block结构体实例也是在栈上, 但是我们在超出其作用域时, 也能够修改val的值, 同时此时Block变成了__NSMallocBlock__:

// 全局blk变量
void (^blk)(void);

int main(int argc, const char * argv[]) {
    {
        // val在作用域内有效
        __block int val = 10;

        blk = ^{
            NSLog(@"val = %d", val);
            val = 0;
            NSLog(@"val = %d", val);
        };
    }

    // 此时 val不在作用域内, val应该失效

    blk();
    NSLog(@"blk: %@", blk);
    return 0;
}
//2018-11-06 10:51:15.758231+0800 Block-Demo[40051:1639002] val = 10
//2018-11-06 10:51:15.758633+0800 Block-Demo[40051:1639002] val = 0
//2018-11-06 10:52:30.711561+0800 Block-Demo[40088:1643359] blk: <__NSMallocBlock__: 0x10053d360>

"Blocks"提供了底层将栈上的Block和__block变量copy到堆上的方法, 这样即使栈上的Block和__block变量失效, 堆上的Block和__block变量依然能够使用.

Block3.4.jpg

在从栈上将Block变量复制到堆上以后, 会同时存在栈上和堆上的Block和__block变量,一共两份, 并且会进行如下操作:

  • 在Block结构体数据copy到堆上以后, 栈上的Block保持不变, 将堆上Block的isa设置成&_NSConcreteMallocBlock.
  • __block变量的结构体数据调用impl->Desc->copycopy到堆上以后, 栈上的__block变量的结构体的__forwarding指针会被修改指向堆上的__block变量, 而堆上的__block变量__forwarding指针不变, 依然指向堆上的__block变量, 具体栈上和堆上的内存结构如下图:
Block3.5.jpg

此时, 不论Block/__block变量在栈上还是堆上, 通过__forwarding会访问同一个变量.

Block从栈上copy到堆上

既然有些情况下Block会自动从栈上copy到堆上, 那么编译器是如何实现的呢?

typedef int (^blk_t)(int);
blk_t func(int rate) {
  return ^(int count) { return rate * count; };
}

实际该源码会返回一个创建在栈上的Block, 即在该方法返回时, 栈上的Block会被废弃, 我们通过ARC编译器源码可得:

blk_t func(int rate){
  //1. 通过Block语法生成的Block, 即配置在栈上的Block用结构体实例赋值给相当于Block类型的变量tmp中.
  blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);

  //2. _Block_copy函数将栈上的Block复制到堆上,复制后,将堆上的地址作为指针赋值给变量tmp.
  tmp = objc_retainBlock(tmp);// _Block_copy(tmp)

  //3. 将注册在堆上的Block作为Objective-C对象注册到autoreleasePool中, 然后返回该对象
  return objc_autoreleaseReturnValue(tmp);

  //4. 函数调用结束,在栈上的Block会被自动销毁.
}

ARC中的大多数情况,编译器都会自动帮我们调用copy函数,将栈上的Block copy到堆上.但是向方法或者函数中的参数传递Block时, 需要我们手动调用copy方法.

-(id) getBlockArray {
  int val = 10;
  return [[NSArray alloc] initWithObjects: ^{NSLog(@"blk: %d", val);}, nil];// Block没有调用copy方法
}


typedef void (^blk_t)(void);
id obj = getBlockArray();
blk_t blk = (blk_t)[obj objectAtIndex:0];
blk(); // 这里执行时会报异常, getBlockArray函数执行完成以后, 栈上的Block被废弃,当执行blk(),时导致坏内存访问

因此, 要手动调用copy方法:

-(id) getBlockArray {
  int val = 10;
  return [[NSArray alloc] initWithObjects: [^{NSLog(@"blk: %d", val);} copy], nil];// 手动调用copy方法
}

typedef void (^blk_t)(void);
id obj = getBlockArray();
blk_t blk = (blk_t)[obj objectAtIndex:0];
blk(); // 这里可以正常执行

ps: 实际上, 我在iOS12上Xcode10默认配置验证的时候,发现第一种情况即使不添加copy也能够正常运行, 在数组中的Block是__NSMallocBlock__, 可能LLVM某个版本以后这些地方都做了优化.

以下两种情况无需手动调用copy方法:

  1. Cocoa框架的方法且方法名中含有 usingBlock等时, 例如 NSArray - enumerateObjectsUsingBlock:
  2. GCD的API, 例如 dispatch_async

前面是配置在栈上的Block进行copy操作, 没有问题, 如果原本Block就在堆上,调用copy操作也不会域问题, 具体的总结如下:

Block类 原存储域 调用copy效果
_NSConcreteStackBlock 从栈copy到堆
_NSConcreteGlobalBlock 数据域(.data域) 什么也不做
_NSConcreteMallocBlock 引用计数+1

__block变量来说, 当Block被从栈copy到堆上, __block变量也会受到影响, 具体来说还是看下图:

Block3.5.jpg

在栈上的情况, Block中的指针只是指向栈上的__block变量, 而当Block/__block变量被copy到堆上以后, 堆上Block会持有堆上__block变量. 而堆上的Block再次被调用copy时, 只是Block的引用计数+1而已, 而__block变量如果被多个堆上Block持有也只涉及到引用记数的变化. 一旦Block/__block变量的引用计数为0, 就会自动从堆上释放内存.这里Block/__block变量在堆上的内存管理与Objective-C对象完全一致.

参考资料

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

推荐阅读更多精彩内容