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里面也能改变原变量。

你可能感兴趣的:(Block在内存中的位置)