iOS Block的本质(四)

1. block内修改变量的值

int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
Block block = ^ {
// age = 20; // 无法修改
NSLog(@"%d",age);
};
block();
}
return 0;
}
  1. 默认情况下block不能修改外部的局部变量。通过之前对源码的分析可以知道。
  2. age是在main函数内部声明的,说明age的内存存在于main函数的栈空间内部,但是block内部的代码在__main_block_func_0函数内部。__main_block_func_0函数内部无法访问age变量的内存空间,两个函数的栈空间不一样,__main_block_func_0内部拿到的age是block结构体内部的age,因此无法在__main_block_func_0函数内部去修改main函数内部的变量。

方式一:age使用static修饰。

  • 前文提到过static修饰的age变量传递到block内部的是指针,在__main_block_func_0函数内部就可以拿到age变量的内存地址,因此就可以在block内部修改age的值。

方式二:__block

  1. __block用于解决block内部不能修改auto变量值的问题,__block不能修饰静态变量(static) 和全局变量。

    • __block int age = 10;
  2. 编译器会将__block修饰的变量包装成一个对象,查看其底层c++源码。

  3. 上述源码中可以发现,首先被__block修饰的age变量声明变为名为age的__Block_byref_age_0结构体,也就是说加上__block修饰的话捕获到的block内的变量为__Block_byref_age_0类型的结构体。

  4. 通过下图查看__Block_byref_age_0结构体内存储哪些元素。

    • __isa指针 :__Block_byref_age_0中也有isa指针也就是说__Block_byref_age_0本质也一个对象。
    • __forwarding:__forwarding是__Block_byref_age_0结构体类型的,并且__forwarding存储的值为(__Block_byref_age_0 *)&age,即结构体自己的内存地址。
    • __flags :0
    • __size :sizeof(__Block_byref_age_0),即__Block_byref_age_0所占用的内存空间。
    • age :真正存储变量的地方,这里存储局部变量10。
    • 接着将__Block_byref_age_0结构体age存入__main_block_impl_0结构体中,并赋值给__Block_byref_age_0 *age;
    • 之后调用block,首先取出__main_block_impl_0中的age,通过age结构体拿到__forwarding指针,上面提到过__forwarding中保存的就是__Block_byref_age_0结构体本身,这里也就是age(__Block_byref_age_0),在通过__forwarding拿到结构体中的age(10)变量并修改其值。
    • 后续NSLog中使用age时也通过同样的方式获取age的值。
  5. 为什么要通过__forwarding获取age变量的值?

  • __forwarding是指向自己的指针。这样的做法是为了方便内存管理,之后内存管理章节会详细解释。
  • 到此为止,__block为什么能修改变量的值已经很清晰了。__block将变量包装成对象,然后在把age封装在结构体里面,block内部存储的变量为结构体指针,也就可以通过指针找到内存地址进而修改变量的值。

2. __block修饰对象类型

int main(int argc, const char * argv[]) {
@autoreleasepool {
__block Person *person = [[Person alloc] init];
NSLog(@"%@",person);
Block block = ^{
person = [[Person alloc] init];
NSLog(@"%@",person);
};
block();
}
return 0;
}
  1. 生成c++源码分析

  2. 通过源码查看,将对象包装在一个新的结构体中。结构体内部会有一个person对象,不一样的地方是结构体内部添加了内存管理的两个函数__Block_byref_id_object_copy__Block_byref_id_object_dispose

  3. 上文提到当block中捕获对象类型的变量时,block中的__main_block_desc_0结构体内部会自动添加copy和dispose函数对捕获的变量进行内存管理。

  4. 那么同样的当block内部捕获__block修饰的对象类型的变量时,__Block_byref_person_0结构体内部也会自动添加__Block_byref_id_object_copy__Block_byref_id_object_dispose对被__block包装成结构体的对象进行内存管理。

  5. 当block内存在栈上时,并不会对__block变量产生内存管理。当blcok被copy到堆上时会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_\Block_object_assign函数会对__block变量形成强引用(相当于retain)

    • 首先通过下图看一下block复制到堆上时内存变化
  6. 当block被copy到堆上时,block内部引用的__block变量也会被复制到堆上,并且持有变量,如果block复制到堆上的同时,__block变量已经存在堆上了,则不会复制。

  7. 当block从堆中移除的话,就会调用dispose函数,也就是__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数,会自动释放引用的__block变量。

  8. block内部决定什么时候将变量复制到堆中,什么时候对变量做引用计数的操作。

  9. __block修饰的变量在block结构体中一直都是强引用,而其他类型的是由传入的对象指针类型决定。

  10. 通过下面代码深入的观察一下。

    typedef void (^Block)(void);
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    int number = 20;
    __block int age = 10; NSObject *object = [[NSObject alloc] init];
    __weak NSObject *weakObj = object; Person *p = [[Person alloc] init];
    __block Person *person = p;
    __block __weak Person *weakPerson = p; Block block = ^ {
    NSLog(@"%d",number); // 局部变量
    NSLog(@"%d",age); // __block修饰的局部变量
    NSLog(@"%p",object); // 对象类型的局部变量
    NSLog(@"%p",weakObj); // __weak修饰的对象类型的局部变量
    NSLog(@"%p",person); // __block修饰的对象类型的局部变量
    NSLog(@"%p",weakPerson); // __block,__weak修饰的对象类型的局部变量
    };
    block();
    }
    return 0;
    }
  11. 将上述代码转化为c++代码查看不同变量之间的区别

    struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc; int number;
    NSObject *__strong object;
    NSObject *__weak weakObj;
    __Block_byref_age_0 *age; // by ref
    __Block_byref_person_1 *person; // by ref
    __Block_byref_weakPerson_2 *weakPerson; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _number, NSObject *__strong _object, NSObject *__weak _weakObj, __Block_byref_age_0 *_age, __Block_byref_person_1 *_person, __Block_byref_weakPerson_2 *_weakPerson, int flags=0) : number(_number), object(_object), weakObj(_weakObj), age(_age->__forwarding), person(_person->__forwarding), weakPerson(_weakPerson->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
    }
    };
  12. 上述__main_block_impl_0结构体中看出,没有使用__block修饰的变量(object 和 weakObj)则根据他们本身被block捕获的指针类型对他们进行强引用或弱引用,而一旦使用__block修饰的变量,__main_block_impl_0结构体内一律使用强指针引用生成的结构体。

  13. 接着我们来看__block修饰的变量生成的结构体有什么不同。

    struct __Block_byref_age_0 {
    void *__isa;
    __Block_byref_age_0 *__forwarding;
    int __flags;
    int __size;
    int age;
    }; struct __Block_byref_person_1 {
    void *__isa;
    __Block_byref_person_1 *__forwarding;
    int __flags;
    int __size;
    void (*__Block_byref_id_object_copy)(void*, void*);
    void (*__Block_byref_id_object_dispose)(void*);
    Person *__strong person;
    }; struct __Block_byref_weakPerson_2 {
    void *__isa;
    __Block_byref_weakPerson_2 *__forwarding;
    int __flags;
    int __size;
    void (*__Block_byref_id_object_copy)(void*, void*);
    void (*__Block_byref_id_object_dispose)(void*);
    Person *__weak weakPerson;
    };
  14. 如上面分析的那样,__block修饰对象类型的变量生成的结构体内部多了__Block_byref_id_object_copy和__Block_byref_id_object_dispose两个函数,用来对对象类型的变量进行内存管理的操作。而结构体对对象的引用类型,则取决于block捕获的对象类型的变量。weakPerson是弱指针,所以__Block_byref_weakPerson_2对weakPerson就是弱引用,person是强指针,所以__Block_byref_person_1对person就是强引用。

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign((void*)&dst->object, (void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);
    _Block_object_assign((void*)&dst->weakObj, (void*)src->weakObj, 3/*BLOCK_FIELD_IS_OBJECT*/);
    _Block_object_assign((void*)&dst->person, (void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign((void*)&dst->weakPerson, (void*)src->weakPerson, 8/*BLOCK_FIELD_IS_BYREF*/);
    }
  15. __main_block_copy_0函数中会根据变量是强弱指针及有没有被__block修饰做出不同的处理,强指针在block内部产生强引用,弱指针在block内部产生弱引用。被__block修饰的变量最后的参数传入的是8,没有被__block修饰的变量最后的参数传入的是3。

  16. 当block从堆中移除时通过dispose函数来释放他们。

    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_dispose((void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);
    _Block_object_dispose((void*)src->weakObj, 3/*BLOCK_FIELD_IS_OBJECT*/);
    _Block_object_dispose((void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_dispose((void*)src->weakPerson, 8/*BLOCK_FIELD_IS_BYREF*/); }

3. __forwarding指针

  1. 上面提到过__forwarding指针指向的是结构体自己。当使用变量的时候,通过结构体找到__forwarding指针,在通过__forwarding指针找到相应的变量。这样设计的目的是为了方便内存管理。通过上面对__block变量的内存管理分析我们知道,block被复制到堆上时,会将block中引用的变量也复制到堆中。

  2. 我们重回到源码中。当在block中修改__block修饰的变量时。

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_age_0 *age = __cself->age; // bound by ref
    (age->__forwarding->age) = 20;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_jm_dztwxsdn7bvbz__xj2vlp8980000gn_T_main_b05610_mi_0,(age->__forwarding->age));
    }
  3. 通过源码可以知道,当修改__block修饰的变量时,是根据变量生成的结构体这里是__Block_byref_age_0找到其中__forwarding指针,__forwarding指针指向的是结构体自己因此可以找到age变量进行修改。

  4. 当block在栈中时,__Block_byref_age_0结构体内的__forwarding指针指向结构体自己。

  5. 而当block被复制到堆中时,栈中的__Block_byref_age_0结构体也会被复制到堆中一份,而此时栈中的__Block_byref_age_0结构体中的__forwarding指针指向的就是堆中的__Block_byref_age_0结构体,堆中__Block_byref_age_0结构体内的__forwarding指针依然指向自己。

  6. 此时当对age进行修改时

    // 栈中的age
    __Block_byref_age_0 *age = __cself->age; // bound by ref
    // age->__forwarding获取堆中的age结构体
    // age->__forwarding->age 修改堆中age结构体的age变量
    (age->__forwarding->age) = 20;
  7. 通过__forwarding指针巧妙的将修改的变量赋值在堆中的__Block_byref_age_0中。

  8. 我们通过一张图展示__forwarding指针的作用

  9. 因此block内部拿到的变量实际就是在堆上的。当block进行copy被复制到堆上时,_Block_object_assign函数内做的这一系列操作。

4. 拓展

  1. 以下代码是否可以正确执行

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    NSMutableArray *array = [NSMutableArray array];
    Block block = ^{
    [array addObject: @"5"];
    [array addObject: @"5"];
    NSLog(@"%@",array);
    };
    block();
    }
    return 0;
    }
    • 可以正确执行,因为在block块中仅仅是使用了array的内存地址,往内存地址中添加内容,并没有修改arry的内存地址,因此array不需要使用__block修饰也可以正确编译。
    • 因此当仅仅是使用局部变量的内存地址,而不是修改的时候,尽量不要添加__block,通过上述分析我们知道一旦添加了__block修饰符,系统会自动创建相应的结构体,占用不必要的内存空间。
  2. 上面提到过__block修饰的age变量在编译时会被封装为结构体,那么当在外部使用age变量的时候,使用的是__Block_byref_age_0结构体呢?还是__Block_byref_age_0结构体内的age变量呢?

    • 为了验证上述问题,同样使用自定义结构体的方式来查看其内部结构
    typedef void (^Block)(void);
    
    struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
    }; struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
    void (*copy)(void);
    void (*dispose)(void);
    }; struct __Block_byref_age_0 {
    void *__isa;
    struct __Block_byref_age_0 *__forwarding;
    int __flags;
    int __size;
    int age;
    };
    struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    struct __Block_byref_age_0 *age; // by ref
    }; int main(int argc, const char * argv[]) {
    @autoreleasepool {
    __block int age = 10;
    Block block = ^{
    age = 20;
    NSLog(@"age is %d",age);
    };
    block();
    struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;
    NSLog(@"%p",&age);
    }
    return 0;
    }
    1. 打印断点查看结构体内部结构

    2. 通过查看blockImpl结构体其中的内容,找到age结构体,其中重点观察两个元素:

    3. __forwarding其中存储的地址确实是age结构体变量自己的地址

    4. age中存储这修改后的变量20。

    5. 上面也提到过,在block中使用或修改age的时候都是通过结构体__Block_byref_age_0找到__forwarding在找到变量age的。

    6. 另外apple为了隐藏__Block_byref_age_0结构体的实现,打印age变量的地址发现其实是__Block_byref_age_0结构体内age变量的地址。

    7. 通过上图的计算可以发现打印age的地址同__Block_byref_age_0结构体内age值的地址相同。也就是说外面使用的age,代表的就是结构体内的age值。所以直接拿来用的age就是之前声明的int age。

iOS Block的本质(四)的更多相关文章

  1. iOS Block的本质(一)

    iOS Block的本质(一) 1.对block有一个基本的认识 block本质上也是一个oc对象,他内部也有一个isa指针.block是封装了函数调用以及函数调用环境的OC对象. 2.探寻block ...

  2. # iOS Block的本质(三)

    iOS Block的本质(三) 上一篇文章iOS Block的本质(二)中已经介绍过block变量的捕获,本文继续探寻block的本质. 1. block对对象变量的捕获,ARC 环境 block一般 ...

  3. iOS Block的本质(二)

    iOS Block的本质(二) 1. 介绍引入block本质 通过上一篇文章Block的本质(一)已经基本对block的底层结构有了基本的认识,block的底层就是__main_block_impl_ ...

  4. IOS Block的本质

    #import "HMViewController.h" #import "HMPerson.h" @interface HMViewController () ...

  5. iOS底层原理总结 - 探寻block的本质(一)

        面试题 block的原理是怎样的?本质是什么? __block的作用是什么?有什么使用注意点? block的属性修饰词为什么是copy?使用block有哪些使用注意? block在修改NSMu ...

  6. iOS block从零开始

    iOS block从零开始 在iOS4.0之后,block横空出世,它本身封装了一段代码并将这段代码当做变量,通过block()的方式进行回调. block的结构 先来一段简单的代码看看: void ...

  7. iOS中常用的四种数据持久化方法简介

    iOS中常用的四种数据持久化方法简介 iOS中的数据持久化方式,基本上有以下四种:属性列表.对象归档.SQLite3和Core Data 1.属性列表涉及到的主要类:NSUserDefaults,一般 ...

  8. iOS block 机制

    本文要将block的以下机制,并配合具体代码详细描述: block 与 外部变量 block 的存储域:栈块.堆块.全局块 定义 块与函数类似,只不过是直接定义在另一个函数里,和定义它的那个函数共享同 ...

  9. ipa包如何打包?ios打包ipa的四种方法分享

      今天带来的内容是ios打包ipa的四种方法.总结一下,目前.app包转为.ipa包的方法有以下几种,下面一起来看看吧!    1.Apple推荐的方式,即实用xcode的archive功能 Xco ...

随机推荐

  1. webpack 小demo

    1 安装node.js 2 安装cnpm 3 安装webpack cnpm install --save-dev webpack 对于大多数项目,我们建议本地安装.这可以使我们在引入破坏式变更的依赖时 ...

  2. IsPostBack深入探讨

    1IsPostBack介绍 IsPostBack是Page类有一个bool类型的属性,用来判断针对当前Form的请求是第一次还是非第一次请求.当IsPostBack=true时表示非第一次请求,我们称 ...

  3. LeetCode: 557Reverse Words in a String III(easy)

    题目: Given a string, you need to reverse the order of characters in each word within a sentence while ...

  4. input输入框修改后自动跳到最后一个字符

    <input class="m-form-control" onpaste="return false" placeholder="直播间名称& ...

  5. 洛谷 - P3768 - 简单的数学题 - 欧拉函数 - 莫比乌斯反演

    https://www.luogu.org/problemnew/show/P3768 \(F(n)=\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{n}ijgcd(i ...

  6. 地理位置(navigation.geolocation)与本地存储(seessionStorage、localStorage)

    一.地理位置( geolocation ): navigator.geolocation对象: 1.单次请求: //navigator.geolocation.getCurrentPosition( ...

  7. 2013 Noip提高组 Day2

    3288积木大赛 正文 题目描述 春春幼儿园举办了一年一度的“积木大赛”.今年比赛的内容是搭建一座宽度为n的大厦,大厦可以看成由n块宽度为1的积木组成,第i块积木的最终高度需要是hi. 在搭建开始之前 ...

  8. jsp内置对象分析

    1.html表单的提交方式比较: 1.1.get方式:将表单内容经过编码之后 ,通过URL发送, 使用get方式发送时有255个字符的限制. 1.2.post方式:将表单的内容通过http发送,pos ...

  9. bryce1010专题训练——LCT&&树链剖分

    LCT&&树链剖分专题 参考: https://blog.csdn.net/forever_wjs/article/details/52116682

  10. mybatis实现简单的增删查改

    接触一个新技术,首先去了解它的一些基本概念,这项技术用在什么方面的.这样学习起来,方向性也会更强一些.我对于mybatis的理解是,它是一个封装了JDBC的java框架.所能实现的功能是对数据库进行增 ...