美文网首页
oc中block底层原理分析(二)

oc中block底层原理分析(二)

作者: iOS_进击的小学生 | 来源:发表于2019-04-10 21:27 被阅读0次

    一、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依然不会被释放。上一篇文章提到过,person为aotu变量,传入的block的变量同样为person,即block有一个强引用引用person,所以block不被销毁的话,peroson也不会销毁。

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

    __weak修饰的变量,在生成的__main_block_impl_0中也是使用__weak修饰。

    __main_block_copy_0 和 __main_block_dispose_0

    当block中捕获对象类型的变量时,我们发现block结构体__main_block_impl_0的描述结构体__main_block_desc_0中多了两个参数copy和dispose函数。
    copy和dispose函数中传入的都是__main_block_impl_0结构体本身。

    copy本质就是__main_block_copy_0函数,__main_block_copy_0函数内部调用_Block_object_assign函数,_Block_object_assign中传入的是person对象的地址,person对象,以及8。
    dispose本质就是__main_block_dispose_0函数,__main_block_dispose_0函数内部调用_Block_object_dispose函数,_Block_object_dispose函数传入的参数是person对象,以及8。
    总结

    1.一旦block中捕获的变量为对象类型,block结构体中的__main_block_desc_0会出两个参数copy和dispose。因为访问的是个对象,block希望拥有这个对象,就需要对对象进行引用,也就是进行内存管理的操作。比如说对对象进行retarn操作,因此一旦block捕获的变量是对象类型就会会自动生成copy和dispose来对内部引用的对象进行内存管理。

    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内修改变量的值

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int age = 10;
            Block block = ^ {
                // age = 20; // 无法修改
                NSLog(@"%d",age);
            };
            block();
        }
        return 0;
    }
    

    默认情况下block不能修改外部的局部变量。通过之前对源码的分析可以知道。

    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
    __block用于解决block内部不能修改auto变量值的问题,__block不能修饰静态变量(static) 和全局变量。

    首先被__block修饰的age变量声明变为名为age的__Block_byref_age_0结构体,也就是说加上__block修饰的话捕获到的block内的变量为__Block_byref_age_0类型的结构体。
    image.png
    接着将__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)变量并修改其值。
    为什么要通过__forwarding获取age变量的值?

    __forwarding是指向自己的指针。这样的做法是为了方便内存管理,之后内存管理章节会详细解释。
    到此为止,__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


    image.png
    __block内存管理
    上文提到当block中捕获对象类型的变量时,block中的__main_block_desc_0结构体内部会自动添加copy和dispose函数对捕获的变量进行内存管理。
    那么同样的当block内部捕获__block修饰的对象类型的变量时,__Block_byref_person_0结构体内部也会自动添加__Block_byref_id_object_copy和__Block_byref_id_object_dispose对被__block包装成结构体的对象进行内存管理。
    当block内存在栈上时,并不会对__block变量产生内存管理。当blcok被copy到堆上时
    会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对__block变量形成强引用(相当于retain)。
    __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结构体内一律使用强指针引用生成的结构体。
    如上面分析的那样,__block修饰对象类型的变量生成的结构体内部多了__Block_byref_id_object_copy和__Block_byref_id_object_dispose两个函数,用来对对象类型的变量进行内存管理的操作。而结构体对对象的引用类型,则取决于block捕获的对象类型的变量。weakPerson是弱指针,所以__Block_byref_weakPerson_2对weakPerson就是弱引用,person是强指针,所以__Block_byref_person_1对person就是强引用。
    __main_block_copy_0函数中会根据变量是强弱指针及有没有被__block修饰做出不同的处理,强指针在block内部产生强引用,弱指针在block内部产生弱引用。被__block修饰的变量最后的参数传入的是8,没有被__block修饰的变量最后的参数传入的是3。
    当block从堆中移除时通过dispose函数来释放他们。

    相关文章

      网友评论

          本文标题:oc中block底层原理分析(二)

          本文链接:https://www.haomeiwen.com/subject/dzqaiqtx.html