Block在内存中的位置

Block在内存中的三种位置

首先,在 MRC 下,Block 默认是分配在栈上的,除非进行显式的 copy,但是在ARC的中,一般都是分配在堆中。

@interface SecondViewController ()
typedef void (^blk)(void);
@end

@implementation SecondViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    __block int val = 1;

    //block保存在堆区
    __strong blk heapBlock = ^{
        val = 2;
    };
    NSLog(@"heapBlock: %@", heapBlock);

    //block保存在栈区
    __weak blk stackBlock = ^{
        val = 2;
    };
    NSLog(@"stackBlock: %@", stackBlock);
    
    //block保存在全局区
    blk globalBlock = ^{
    };
    NSLog(@"globalBlock: %@", globalBlock);
}

Log输出

2018-04-09 15:50:14.351940+0800 TestMRC[29483:4870456] heapBlock: <__NSMallocBlock__: 0x60000024c090>
2018-04-09 15:50:14.352184+0800 TestMRC[29483:4870456] stackBlock: <__NSStackBlock__: 0x7ffee57543d0>
2018-04-09 15:50:14.352318+0800 TestMRC[29483:4870456] globalBlock: <__NSGlobalBlock__: 0x10a4a9148>

简单来说分两种情况
A、没有引用外部变量 --- block存放在全局区
B、引用了外部变量----显式声明为weak类型的block则存放在栈区,反之则是存在在堆区的,也就是说block是strong类型的。

内存分区

iOS App的内存分配大概如下所示

image.png

从下到上,是低地址到高地址。
代码区(Code): 存放App代码;
常量区(Const):存放App声明的常量;
全局区/静态区(Global/Static):存放App的全局变量与静态变量;
堆区(heap):一般是存放对象类型,需要手动管理内存,ARC项目的话,编译器接手内存管理工作;堆区是使用链表来存储的空闲内存地址的,是不连续的,而链表的遍历方向是由低地址向高地址。
栈区(stack):存放的自动变量(没有定义static的局部变量)、一旦出了函数的作用域就会被销毁,不需要手动管理,栈区地址从高到低分配,遵循先进后出的,如果申请的栈区内存大小超过剩余的大小,就会反正栈区溢出overflow问题,一般栈区内存大小为2M。

使用block的注意事项

因为在ARC中,block还是有可能为NSStackBlock类型的,也就是说,随着作用域结束,block将会销毁回收。
所以有时候需要对block进行copy操作,手动复制到堆区内存中,防止被回收,导致block无法使用。

自动copy

以下情况,系统会将block自动复制到堆上,自动对block调用copy方法。
1、当 block 作为函数返回值返回时;
2、当 block 被赋值给__strong修饰的 id 类型的对象或 block 对象时;
3、当 block 作为参数被传入方法名带有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 时。

因为在ARC下,对象默认是用__strong修饰的,所以大部分情况下编译器都会将 block从栈自动复制到堆上,所以iOS编程中,一般声明block的property修饰符为copy,显示地说明是在堆区的,ARC会自动完成从栈区copy到堆区的操作,当然使用 strong 去修饰 block 也是没有问题的,还是一样会自动copy。

不自动copy

以下情况,系统不会自动复制到堆上,也就是说,作用域结束,block就被回收销毁。
block 作为方法或函数的参数传递。
block 作为临时变量,没有赋值给其他block

注意,上述不一定是从栈区复制到堆区,也有可能是global 区复制过去的。

__block的探究

捕获变量

  • 自动变量
    Block捕获外部自动变量是传值的方式捕获的, Block只能访问此变量的值,也非通过此变量的内存地址访问。

  • 非自动变量
    Block捕获静态变量时,是通过传递该静态变量的内存地址
    Block捕获全局区变量是,因为作用域是全局,Block也可以直接修改。

  • 总结
    Block本质上是一个匿名函数,对于函数来说,捕获变量可以理解为传递参数。

至于为什么OC没有默认就给自动变量设置为指针传值,是因为栈区的随着函数作用域结束而结束,而Block可能会强引用而被copy到堆区,这时候Block还引用自动变量就不合适。

修改值

由上面可知,如果想在Block内部修改外部变量的值有两个方法可以做到

  • 传值为内存地址方式
  • 改变变量在内存中的位置

在OC中,对栈区的自动变量加了前缀__block后,Block就会创建一个带isa指针的构体来替换*原本的自动变量,里面存放着该变量的内存地址等信息。

注意,这里是替换

如原代码,定义block,让block修改自动变量 autoValue

typedef void (^MyBlock)(void);

int main(int argc, char * argv[]) {
    __block int autoValue = 1;
    MyBlock co_block = ^{
        autoValue = 2;
    };
    autoValue = 3;
    co_block();
    autoValue = 999;
    return 0;;
}

使用Clang转化为cpp
clang -rewrite-objc main.m
可得到关键cpp代码如下

typedef void (*MyBlock)(void);

struct __Block_byref_autoValue_0 {
  void *__isa;
__Block_byref_autoValue_0 *__forwarding;
 int __flags;
 int __size;
 int autoValue;
};

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

        (autoValue->__forwarding->autoValue) = 2;
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->autoValue, (void*)src->autoValue, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->autoValue, 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, char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_autoValue_0 autoValue = {(void*)0,(__Block_byref_autoValue_0 *)&autoValue, 0, sizeof(__Block_byref_autoValue_0), 1};
    MyBlock co_block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_autoValue_0 *)&autoValue, 570425344));
    (autoValue.__forwarding->autoValue) = 3;
    ((void (*)(__block_impl *))((__block_impl *)co_block)->FuncPtr)((__block_impl *)co_block);
    (autoValue.__forwarding->autoValue) = 999;
    return 0;;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

由上面cpp代码可知
当自动变量被加上__block后,原本的变量就替换为一个结构体类型,这个结构体中存在原本的自动变量int autoValue和,和一个__forwarding指针,这个指针的类型是此结构体,而Block内部访问和后续Block外面访问这个自动变量全部替换成如下
新变量->__forwarding->原变量同类型变量

(autoValue.__forwarding->autoValue) = 3;
(autoValue.__forwarding->autoValue) = 999;

注意

对自动变量加__block其实和block没有必要的联系。
就算没有block捕获自动变量,我们对一个自动变量加上__block,oc也会在编译时把他替换为一个结构体类型。如

int main(int argc, char * argv[]) {
    __block int autoValue = 1;
    autoValue = 999;
    return 0;;
}

转化后如下

struct __Block_byref_autoValue_0 {
  void *__isa;
__Block_byref_autoValue_0 *__forwarding;
 int __flags;
 int __size;
 int autoValue;
};
int main(int argc, char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_autoValue_0 autoValue = {(void*)0,(__Block_byref_autoValue_0 *)&autoValue, 0, sizeof(__Block_byref_autoValue_0), 1};
    (autoValue.__forwarding->autoValue) = 999;
    return 0;;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

对象类型也同理。

总结

__block的作用就是原地构建一个新的结构体来替换原本的自动变量,使得block捕获这个自动变量时,可以使用指针访问,这样就可以修改这个自动变量的值了。

需要注意的是
这个自动变量被替换为结构体后,其实还是一个自动变量,生命周期会随着函数结束而结束,但是平常使用的Block一般都是存放在堆区的,而且Block里面的变量也是存在堆区,Bloc捕获外面的自动变量时,会将变量从栈空间copy 到堆空间。

__block变量是在栈空间,其__forwarding指针指向自身,当变量从栈空间copy到堆空间时,原来栈空间的变量的forwarding指向了新创建的变量(堆空间上),这样block里面也能改变原变量。

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

推荐阅读更多精彩内容