iOS Block(三)---- 本质篇

大纲

  • block对对象变量的捕获
  • __main_block_copy_0 和 __main_block_dispose_0
  • block内修改变量的值
  • __block内存管理
  • __forwarding指针

block对对象变量的捕获

block一般使用过程中都是对对象变量的捕获,那么对象变量的捕获同基本数据类型变量相同吗?

思考:当在block中访问的为对象类型时,对象什么时候会销毁?

typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            
            block = ^{
                NSLog(@"------block内部%d",person.age);
            };
        } // 执行完毕,person没有被释放
        NSLog(@"--------");
    } // person 释放
    return 0; 
}

大括号执行完毕之后,person依然不会被释放。
在这里插入图片描述
由此可以看出,block有一个强引用,引用person,所以block不被销毁的话,person也不会被销毁。

在MRC环境下即使block还在, person却被释放掉了。因为MRC环境下block在栈空间,栈空间对外面的person不会进行强引用。

//MRC环境下代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            block = ^{
                NSLog(@"------block内部%d",person.age);
            };
            [person release];
        } // person被释放
        NSLog(@"--------");
    }
    return 0;
}

block调用copy操作之后,person不会被释放。

block = [^{
   NSLog(@"------block内部%d",person.age);
} copy];

上文中也提到过,只需要对栈空间的block进行一次copy操作,将栈空间的block拷贝到堆中,person就不会被释放,说明堆空间的block可能会对person进行一次retain操作,以保证person不会被销毁。堆空间的block自己销毁之后也会对持有的对象进行release操作。

也就是说栈空间上的block不会对对象强引用,堆空间的block有能力持有外部调用的对象,即对对象进行强引用或去除强引用的操作。

_weak

_weak添加后,person在作用域执行完毕之后就会被销毁

typedef void (^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            
            __weak Person *waekPerson = person;
            block = ^{
                NSLog(@"------block内部%d",waekPerson.age);
            };
        }
        NSLog(@"--------");
    }
    return 0;
}

转化为C++看看
在这里插入图片描述
可以看到在结构体中同样是以_weak修饰

__main_block_copy_0 和 __main_block_dispose_0

当block中捕获对象类型的变量时,我们发现block结构体__main_block_impl_0的描述结构体__main_block_desc_0中多了两个参数copy和dispose函数,查看源码:

在这里插入图片描述

可以看到

  • copydispose函数中传入的都是__main_block_impl_0结构体本身。

copy本质就是__main_block_copy_0函数,__main_block_copy_0函数内部调用_Block_object_assign函数,在这里,_Block_object_assign中传入的是person对象的地址,person对象,以及8。(没写错,就是8)

dispose本质就是__main_block_dispose_0函数,__main_block_dispose_0函数内部调用_Block_object_dispose函数,这里,_Block_object_dispose函数传入的参数是person对象,以及8。

_Block_object_assign函数调用时机及作用

当block进行copy操作的时候就会自动调用__main_block_desc_0内部的__main_block_copy_0函数,__main_block_copy_0函数内部会调用_Block_object_assign函数。

_Block_object_assign函数会自动根据__main_block_impl_0结构体内部的person是什么类型的指针,对person对象产生强引用或者弱引用。可以理解为_Block_object_assign函数内部会对person进行引用计数器的操作,如果__main_block_impl_0结构体内person指针是__strong类型,则为强引用,引用计数+1,如果__main_block_impl_0结构体内person指针是__weak类型,则为弱引用,引用计数不变

_Block_object_dispose函数调用时机及作用

当block从堆中移除时就会自动调用__main_block_desc_0中的__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数。

_Block_object_dispose会对person对象做释放操作,类似于release,也就是断开对person对象的引用,而person究竟是否被释放还是取决于person对象自己的引用计数。

小结:

  1. 一旦block中捕获的变量为对象类型,block结构体中的__main_block_desc_0会出两个参数copydispose。因为访问的是个对象,block希望拥有这个对象,就需要对对象进行引用,也就是进行内存管理的操作。比如说对对象进行retarn操作,因此一旦block捕获的变量是对象类型就会会自动生成copydispose来对内部引用的对象进行内存管理。
  2. 当block内部访问了对象类型的auto变量时,如果block是在栈上,block内部不会对person产生强引用。不论block结构体内部的变量是__strong修饰还是__weak修饰,都不会对变量产生强引用。
  3. 如果block被拷贝到堆上。_copy函数会调用__Block_object_assign_函数,根据auto变量的修饰符(_strong_weak_unsafe_unretained)做出相应的操作,形成强引用或者弱引用
  4. 如果block从堆中移除,dispose函数会调用__Block_object_dispose函数,自动释放引用的auto变量。

block内修改变量的值

还记得我们在介绍篇中说到的_block说明符吗?

int main ()
{
    __block int age = 10 ;//两个_
      
    Myblock block = ^(void){
        age = 20 ;
        NSLog(@"%d",age);
    };

    block();
}

我们将它转化为C++看一下
在这里插入图片描述
由此可发现,__block修饰的age变量声明变为age__Block_byref_age_0结构体,也就说加上__block修饰的话捕获到的block内的变量为__Block_byref_age_0类型的结构体

通过下图查看__Block_byref_age_0结构体内存储哪些元素。
在这里插入图片描述

  • __isa指针:说明__Block_byref_age_0本质也一个对象
  • __forwarding:结构体自己的内存地址
  • __flags:0
  • __size:即__Block_byref_age_0所占用的内存空间。

接着将__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的值。
在这里插入图片描述
到此为止,__block为什么能修改变量的值已经很清晰了。__block将变量包装成对象,然后在把age封装在结构体里面,block内部存储的变量为结构体指针,也就可以通过指针找到内存地址进而修改变量的值。

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

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

在这里插入图片描述

__block内存管理

前面说到block捕获对象类型的变量时,block中的__main_block_desc_0结构体内部会自动添加copydispose函数对捕获的变量进行内存管理

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

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

首先通过一张图看一下block复制到堆上时内存变化

在这里插入图片描述

blockcopy到堆上时,block内部引用的__block变量也会被复制到堆上,并且持有变量,如果block复制到堆上的同时,__block变量已经存在堆上了,则不会复制。

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

在这里插入图片描述

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

一段代码更深入的观察一下

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

转化为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;
  }
};

上述__main_block_impl_0结构体中看出,没有使用__block修饰的变量(object 和 weadObj)则根据他们本身被block捕获的指针类型对他们进行强引用或弱引用,而一旦使用__block修饰的变量,__main_block_impl_0结构体内一律使用强指针引用生成的结构体

__forwarding指针

上面提到过的__forwarding指针指向的是结构体自己。当使用变量的时候,通过结构体找到__forwarding指针找到相应的变量。这样设计目的是为了方便内存管理。通过上面对__block变量的内存管理分析我们知道,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));
        }

可以看到,当修改__block修饰的变量时,是根据变量生成的结构体这里是__Block_byref_age_0找到其中__forwarding指针,__forwarding指针指向的是结构体自己因此可以找到age变量进行修改。

当block在栈中时,__Block_byref_age_0v结构体内的__forwarding```指针指向结构体自己。

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

在这里插入图片描述

最后

贴上大神文章iOS底层原理总结 - 探寻block的本质(二

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值