美文网首页
block的本质2

block的本质2

作者: CoderJRHuo | 来源:发表于2020-05-26 10:26 被阅读0次

    1. block对对象变量的捕获

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

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

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

    大括号执行完毕之后,person依然不会被释放。上一篇文章提到过,personauto变量,是存放在堆空间的,传入的block的变量同样为person,即block有一个强引用引用person,所以block不被销毁的话,peroson也不会销毁。

    查看源代码确实如此:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      // block强引用person对象
      Person *person;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *_person, int flags=0) : person(_person) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    将上述代码转移到MRC环境下,在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有能力持有外部调用的对象,即对对象进行强引用或去除强引用的操作。

    1.1 __weak修饰的对象

    __weak修饰之后,person在作用域执行完毕之后就被销毁了。

    Person *person = [[Person alloc] init];
    person.age = 10;
                           
    __weak Person *waekPerson = person;
    block = ^{
        NSLog(@"------block内部%d",waekPerson.age);
    };
    

    将代码转化为c++来看一下上述代码之间的差别。

    __weak修饰变量,需要告知编译器使用ARC环境及版本号否则会报错,添加说明-fobjc-arc -fobjc-runtime=ios-8.0.0

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      // __weak waekPerson
      Person *__weak waekPerson;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _waekPerson, int flags=0) : waekPerson(_waekPerson) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    通过底层的源码我们发现__weak修饰的变量,在生成的结构体__main_block_impl_0中也是使用__weak修饰。

    1.2 __main_block_copy_0__main_block_dispose_0

    block中捕获对象类型的变量时,我们发现block结构体__main_block_impl_0的描述结构体__main_block_desc_0中多了两个参数copydispose函数:

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->waekPerson, (void*)src->waekPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->waekPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      // copy
      void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      // dispose
      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};
    

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

    copy本质就是__main_block_copy_0函数,__main_block_copy_0函数内部调用_Block_object_assign函数,_Block_object_assign中传入的是person对象的地址,person对象,以及3

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

    _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,引用计数-1,也就是断开对person对象的引用,而person究竟是否被释放还是取决于person对象自己的引用计数。

    1.3 总结

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

    2. block内部访问了对象类型的auto变量时,如果block是在栈上,block内部不会对person产生强引用。不论block结构体内部的变量是__strong修饰还是__weak修饰,都不会对变量产生强引用

    3. 如果block通过copy函数被拷贝到堆上。copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会根据auto变量的修饰符(__strong__weakunsafe_unretained)做出相应的操作,形成强引用或者弱引用

    4. 如果block从堆中移除,dispose函数会调用_Block_object_dispose函数,自动释放引用的auto对象变量。

    1.4 问题

    1.4.1 下列代码person在何时销毁 ?
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        Person *person = [[Person alloc] init];
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",person);
        });
        NSLog(@"touchBegin----------End");
    }
    

    输出:

    2020-01-15 18:22:22.977821+0800 block的本质2[80273:5341601] touchBegin----------End
    2020-01-15 18:22:25.978097+0800 block的本质2[80273:5341601] <Person: 0x600003377b90>
    2020-01-15 18:22:25.978437+0800 block的本质2[80273:5341601] Person对象销毁了
    

    上文提到过ARC环境中,block作为GCD API的方法参数时会自动进行copy操作,因此block在堆空间,并且使用强引用访问person对象,因此block内部copy函数会对person进行强引用,引用计数+1。当block执行完毕需要被销毁时,调用dispose函数释放对person对象的引用,引用计数-1,person没有强指针指向时才会被销毁。

    1.4.2 下列代码person在何时销毁 ?
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        Person *person = [[Person alloc] init];
        
        __weak Person *waekP = person;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",waekP);
        });
        NSLog(@"touchBegin----------End");
    }
    

    输出:

    2020-01-15 18:25:39.667015+0800 block的本质2[80460:5345623] touchBegin----------End
    2020-01-15 18:25:39.667528+0800 block的本质2[80460:5345623] Person对象销毁了
    2020-01-15 18:25:42.667212+0800 block的本质2[80460:5345623] (null)
    

    blockweakp__weak弱引用,因此block内部copy函数会对person同样进行弱引用,当大括号执行完毕时,person对象没有强指针引用就会被释放。因此block块执行的时候打印null

    1.4.3 通过示例代码进行总结。
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        Person *person = [[Person alloc] init];
        
        __weak Person *waekP = person;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
            // 对waekP不会进行retain操作,引用计数不变
            NSLog(@"weakP ----- %@",waekP);
            
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                
                // 对person对象进行retain操作,引用计数+1
                // 在此处的block块没有执行之前,person对象不会销毁
                NSLog(@"person ----- %@",person);
            });// block执行完毕,person对象引用计数-1,person对象释放
        });
        NSLog(@"touchBegin----------End");
    }
    

    person对象4s之后销毁

    2020-01-15 18:28:12.476409+0800 block的本质2[80594:5348763] touchBegin----------End
    2020-01-15 18:28:13.476639+0800 block的本质2[80594:5348763] weakP ----- <Person: 0x60000307c1e0>
    2020-01-15 18:28:16.477223+0800 block的本质2[80594:5348763] person ----- <Person: 0x60000307c1e0>
    2020-01-15 18:28:16.477604+0800 block的本质2[80594:5348763] Person对象销毁了
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        Person *person = [[Person alloc] init];
        
        __weak Person *waekP = person;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
            // person对象引用计数+1
            NSLog(@"person ----- %@",person);
    
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                // person对象引用计数不变
                // 当执行到此block的时候,person对象已经在第一层block执行完毕后释放
                NSLog(@"weakP ----- %@",waekP);
            });
        });// 执行完block,block释放,person对象引用计数-1,person对象释放
        NSLog(@"touchBegin----------End");
    }
    

    person对象1s后销毁

    2020-01-15 18:30:42.891689+0800 block的本质2[80725:5351578] touchBegin----------End
    2020-01-15 18:30:43.891941+0800 block的本质2[80725:5351578] person ----- <Person: 0x6000027a41d0>
    2020-01-15 18:30:43.892261+0800 block的本质2[80725:5351578] Person对象销毁了
    2020-01-15 18:30:47.183073+0800 block的本质2[80725:5351578] weakP ----- (null)
    

    2 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内部拿到的ageblock结构体内部的age,因此无法在__main_block_func_0函数内部去修改main函数内部的变量。

    方式一:使用static修饰。

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

    方式二:使用__block修饰

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

    __block int age = 10;
    

    2.1 使用__block修饰基本数据类型的底层结构

    编译器会将__block修饰的变量包装成一个对象,查看其底层c++源码:

    // 经过__block修饰之后的变量结构体
    typedef void(*Block)(void);
    struct __Block_byref_age_0 {
      void *__isa; // isa指针
    __Block_byref_age_0 *__forwarding; // 存储结构体自己的地址
     int __flags;
     int __size; // 变量占用的空
     int age; // age变量
    };
    
    // block结构体
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_age_0 *age; // 经过__block修饰之后,捕获到的不再是对象类型,而是__Block_byref_age_0结构体
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    // block执行的代码
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        __Block_byref_age_0 *age = __cself->age; // 拿到age结构体
    
        /**  age->__forwarding->age 形式访问age变量:
        在这里先通过age结构体找到__forwarding指针,
        因为__forwarding指针指向的是结构体自己,
        再找到age变量
        */
        (age->__forwarding->age) = 20;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_jp_wpw4tlhn0qn91rgns6wx4dh00000gn_T_main_69be6d_mi_0,(age->__forwarding->age));
    }
    
    // block copy时会调用
    // 方法内部会调用_Block_object_assign
    // _Block_object_assign内部会对age进行引用计数操作
    // age在block内部是什么类型的指针,即对age产生强引用或者弱引用
    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 销毁时会调用
    // 方法内部会调用_Block_object_dispose
    // _Block_object_dispose内部会断开对block所捕获变量的引用
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    // 
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      // __main_block_copy_0,copy操作
      void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      // __main_block_dispose_0,dispose操作
      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[]) {
        NSString * appDelegateClassName;
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
            // age是结构体__Block_byref_age_0类型
            // 对应的参数分别是:isa、__forwarding、__flags、__size、age
            __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
            
            // block声明:传入上面的age结构体参数 (__Block_byref_age_0 *)&age
            Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
            
            // block调用:((__block_impl *)block)->FuncPtr
            ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
            
            
        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
        }
    
        return UIApplicationMain(argc, argv, __null, appDelegateClassName);
    }
    

    上述源码中可以发现

    首先被__block修饰的age变量声明变为名为age__Block_byref_age_0结构体,也就是说加上__block修饰的话捕获到的block内的变量为__Block_byref_age_0类型的结构体。

    通过下图查看__Block_byref_age_0结构体内存储哪些成员变量。

    [图片上传失败...(image-911d0e-1590459939395)]

    1. __isa指针:__Block_byref_age_0中也有isa指针也就是说__Block_byref_age_0本质也一个对象。
    2. __forwarding指针:__forwarding__Block_byref_age_0结构体类型的,并且__forwarding存储的值为(__Block_byref_age_0 *)&age,即结构体自己的内存地址。
    3. __flags0
    4. __sizesizeof(__Block_byref_age_0),即__Block_byref_age_0结构体所占用的内存空间。
    5. age:真正存储变量的地方,这里存储局部变量10

    接着在block的声明中,将__Block_byref_age_0结构体age存入__main_block_impl_0结构体中,并赋值给成员__Block_byref_age_0 *age

    [图片上传失败...(image-e70b56-1590459939395)]

    之后调用block,首先取出__main_block_impl_0中的age,通过age结构体拿到__forwarding指针,上面提到过__forwarding中保存的就是__Block_byref_age_0结构体本身,这里也就是age(__Block_byref_age_0),再通过__forwarding拿到结构体中的age(初始值是10)变量并修改其值。

    后续NSLog中使用age时也通过同样的方式获取age的值。

    [图片上传失败...(image-637a06-1590459939395)]

    为什么要通过__forwarding获取age变量的值?

    __forwarding是指向自己的指针。这样的做法是为了方便内存管理,之后内存管理章节会详细解释

    到此为止,__block为什么能修改变量的值已经很清晰了。__block将变量包装成对象,然后在把age封装在结构体里面,block内部存储的变量为结构体指针,也就可以通过指针找到内存地址进而修改成员变量的值。

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

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

    typedef void(*Block)(void);
    struct __Block_byref_person_0 {
      void *__isa;
    __Block_byref_person_0 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*);
     void (*__Block_byref_id_object_dispose)(void*);
     Person *__strong person;
    };
    

    __Block_byref_id_object_copy__Block_byref_id_object_dispose函数的调用时机及作用在__block内存管理部分详细分析。

    问题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.3 __Block_byref_age_0结构体内部的成员变量age细节

    上面提到过__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;
    }
    

    打印断点查看结构体内部结构

    block_validation

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

    1. __forwarding其中存储的地址确实是age结构体变量自己的地址
    2. age中存储这修改后的变量20。

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

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

    block_block_byref

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

    还可以通过lldb来查看底层结构体age成员的内存地址:p/x &(blockImpl->age->age)

    这样更加证明了访问的age其实就是访问了底层包装结构体内的成员变量age

    3. __block的内存管理

    上文提到当block中捕获__block修饰的基本数据类型的变量时,block中的描述函数__main_block_desc_0结构体内部会自动添加copydispose函数对捕获的变量进行内存管理。

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

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

    首先通过一张图看一下block复制到堆上时内存变化,假设有2个block内部同时访问__block修饰的变量:

    [图片上传失败...(image-629dd4-1590459939395)]

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

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

    [图片上传失败...(image-229467-1590459939395)]

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

    __block修饰的变量在block自己本省的内部结构体中一直都是强引用,而对象类型的是根据外部传入的对象指针类型(__block__weak)决定。

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

    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;
      // block 强引用 对象类型
      NSObject *__strong object;
      // block 弱引用 对象类型
      NSObject *__weak weakObj;
      // block 统一强引用__block修饰的变量
      __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;
      }
    };
    
    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};
    

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

    接着我们来看由__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;
    };
    

    如上面分析的那样,__block修饰对象类型的变量生成的结构体内部多了__Block_byref_id_object_copy__Block_byref_id_object_dispose两个函数,用来对对象类型的变量进行内存管理的操作。

    __Block结构体对对象的引用类型,则取决于block捕获的对象类型的变量。weakPerson是弱指针,所以__Block_byref_weakPerson_2weakPerson就是弱引用,person是强指针,所以__Block_byref_person_1person就是强引用。

    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*/);
    }
    

    __main_block_copy_0函数中会根据变量是强弱指针及有没有被__block修饰做出不同的处理,强指针在block内部产生强引用,弱指针在block内部产生弱引用。被__block修饰的变量最后的参数传入的是8,没有被__block修饰的变量最后的参数传入的是3。

    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.1 __forwarding指针

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

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

    此时当对age进行修改时:

    // 栈中的age
    __Block_byref_age_0 *age = __cself->age; // bound by ref
    // age->__forwarding获取堆中的age结构体
    // age->__forwarding->age 修改堆中age结构体的age变量
    (age->__forwarding->age) = 20;
    

    通过__forwarding指针巧妙的将修改的变量复制在堆中的__Block_byref_age_0结构体中。这样的话不管是通过栈空间的__forwarding指针或者堆空间的__forwarding指针都可以正确的访问成员变量。

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

    [图片上传失败...(image-ee13f5-1590459939395)]

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

    3.2 被__block修饰的对象类型的内存管理

    使用以下代码,生成c++代码查看内部实现:

    typedef void (^Block)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block Person *person = [[Person alloc] init];
            Block block = ^ {
                NSLog(@"%p", person);
            };
            block();
        }
        return 0;
    }
    

    来到源码查看被__block修饰之后的底层结构。

    __Block_byref_person_0结构体:

    typedef void (*Block)(void);
    struct __Block_byref_person_0 {
      void *__isa;  // 8 内存空间
    __Block_byref_person_0 *__forwarding; // 8
     int __flags; // 4
     int __size;  // 4
     void (*__Block_byref_id_object_copy)(void*, void*); // 8
     void (*__Block_byref_id_object_dispose)(void*); // 8
     Person *__strong person; // 8
    };
    // 8 + 8 + 4 + 4 + 8 + 8 + 8 = 48 
    

    __Block_byref_person_0结构体的声明:

    __attribute__((__blocks__(byref))) __Block_byref_person_0 person = {
        (void*)0,
        (__Block_byref_person_0 *)&person,
        33554432,
        sizeof(__Block_byref_person_0),
        __Block_byref_id_object_copy_131,
        __Block_byref_id_object_dispose_131,
        
        ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"))
    };
    

    之前提到过__block修饰的对象类型生成的结构体中新增加了两个函数void (*__Block_byref_id_object_copy)(void*, void*)void (*__Block_byref_id_object_dispose)(void*)。这两个函数为__block修饰的对象提供了内存管理的操作。

    可以看出为void (*__Block_byref_id_object_copy)(void*, void*)void (*__Block_byref_id_object_dispose)(void*)赋值的分别为__Block_byref_id_object_copy_131__Block_byref_id_object_dispose_131,找到这两个函数:

    static void __Block_byref_id_object_copy_131(void *dst, void *src) {
     _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
    }
    static void __Block_byref_id_object_dispose_131(void *src) {
     _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
    }
    

    从上述源码中可以发现__Block_byref_id_object_copy_131函数中同样调用了_Block_object_assign函数,而_Block_object_assign函数内部拿到dst指针(即block对象自己的地址值)加上40个字节。

    并且_Block_object_assign最后传入的参数是131,同block直接对对象进行内存管理传入的参数38都不同。可以猜想_Block_object_assign内部根据传入的参数不同进行不同的操作的。

    通过对上面__Block_byref_person_0结构体占用内存计算发现,__Block_byref_person_0结构体占用的内存为48字节。而block结构体的地址+40字节的内存恰好指向的就为person指针(到达person对象的内存刚好是40字节)。

    也就是说copy函数会将person的地址传入_Block_object_assign函数,_Block_object_assign中对Person对象进行强引用或者弱引用。

    [图片上传失败...(image-478b65-1590459939395)]

    如果使用__weak修饰变量:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Person *person = [[Person alloc] init];
            __block __weak Person *weakPerson = person;
            Block block = ^ {
                NSLog(@"%p", weakPerson);
            };
            block();
        }
        return 0;
    }
    

    查看源码:

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_weakPerson_0 *weakPerson; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_weakPerson_0 *_weakPerson, int flags=0) : weakPerson(_weakPerson->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    __main_block_impl_0中没有任何变化,__main_block_impl_0weakPerson依然是强引用,但是__Block_byref_weakPerson_0中对weakPerson变为了__weak指针:

    struct __Block_byref_weakPerson_0 {
      void *__isa;
    __Block_byref_weakPerson_0 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*);
     void (*__Block_byref_id_object_dispose)(void*);
     // __weak 弱引用对象
     Person *__weak weakPerson;
    };
    

    也就是说无论如何block内部中对__block修饰变量生成的结构体(__Block_byref_weakPerson_0)都是强引用。

    __Block_byref_weakPerson_0结构体内部对外部变量的引用取决于传入block内部的变量是强引用还是弱引用。

    [图片上传失败...(image-d7ca3a-1590459939395)]

    MRC环境下,尽管调用了copy操作,__block结构体不会对person产生强引用,依然是弱引用。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block Person *person = [[Person alloc] init];
            Block block = [^ {
                NSLog(@"%p", person);
            } copy];
            [person release];
            block();
            [block release];
        }
        return 0;
    }
    

    上述代码即使block还没有释放,但是person会先释放:

    block的copy[50480:8737001] -[Person dealloc]
    block的copy[50480:8737001] 0x100669a50
    

    block从堆中移除的时候。会调用dispose函数,block块中去除对__Block_byref_person_0 *person结构体对象的引用,__Block_byref_person_0结构体中也会调用__Block_byref_id_object_dispose操作去除对Person *person的引用。以保block证结构体和__block结构体内部的对象可以正常释放。

    4. 循环引用

    循环引用导致内存泄漏。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Person *person = [[Person alloc] init];
            person.age = 10;
            person.block = ^{
                NSLog(@"%d",person.age);
            };
        }
        NSLog(@"大括号结束啦");
        return 0;
    }
    

    运行代码打印内容

    2020-01-16 13:55:03.145970+0800 block的本质2[34643:6183880] 大括号结束啦
    

    可以发现大括号结束之后,perso依然没有被释放,产生了循环引用。

    通过一张图看一下他们之间的内存结构

    [图片上传失败...(image-f11d80-1590459939396)]

    上图中可以发现,person对象和block对象相互之间产生了强引用,导致双方都不会被释放,进而造成内存泄漏。

    4.1 解决循环引用问题 - ARC

    首先为了能随时执行block,我们肯定希望personblock强引用,而block内部对person的引用为弱引用最好。

    使用__weak__unsafe_unretained修饰符可以解决循环引用的问题

    我们上面也提到过__weak会使block内部将指针变为弱指针。blockperson对象为弱指针的话,也就不会出现相互引用而导致不会被释放了。

    [图片上传失败...(image-4e16a-1590459939396)]

    Person *person = [[Person alloc] init];
    __weak typeof(person) weakPerson = person
    
    __weak__unsafe_unretained的区别

    __weak不会产生强引用,指向的对象销毁时,会自动将指针置为nil。因此一般通过__weak来解决问题。

    __unsafe_unretained不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变。

    使用__block也可以解决循环引用的问题。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block Person *person = [[Person alloc] init];
            person.age = 10;
            person.block = ^{
                NSLog(@"%d",person.age);
                person = nil;
            };
            person.block();
        }
        NSLog(@"大括号结束啦");
        return 0;
    }
    

    上述代码之间的相互引用可以使用下图表示

    [图片上传失败...(image-51bef7-1590459939396)]

    上面我们提到过,在block内部使用变量使用的其实是__block修饰的变量生成的结构体__Block_byref_person_0内部的person对象,那么当person对象置为nil也就断开了结构体对person的强引用,那么三角的循环引用就自动断开。该释放的时候就会释放了。

    但是有弊端,必须执行block,因为是在block内部将person对象置为nil的。也就是说在block执行之前代码是因为循环引用导致内存泄漏的。

    4.2 解决循环引用问题 - MRC

    使用__unsafe_unretained解决。在MRC环境下不支持使用__weak,使用原理同ARC环境下相同,这里不在赘述。

    使用__block也能解决循环引用的问题。因为上文__block内存管理中提到过,MRC环境下,尽管调用了copy操作,__block结构体不会对person产生强引用,依然是弱引用。因此同样可以解决循环引用的问题。

    4.3 __strong__weak

    __weak typeof(self) weakSelf = self;
    person.block = ^{
        __strong typeof(weakSelf) myself = weakSelf;
        NSLog(@"age is %d", myself->_age);
    };
    

    block内部重新使用__strong修饰self变量是为了在block内部有一个强指针指向weakSelf避免在block调用的时候weakSelf已经被销毁。

    相关文章

      网友评论

          本文标题:block的本质2

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