美文网首页iOSObject-c
iOS底层原理 - 探寻block本质(三)

iOS底层原理 - 探寻block本质(三)

作者: hazydream | 来源:发表于2018-03-09 09:24 被阅读6次

    面试题引发的思考:

    Q: __block的作用是什么?有什么使用注意点?

    • __block用于解决block内部无法修改auto变量值的问题;
    • __block不能修饰全局变量、静态变量。

    Q: block内部修改的NSMutableArray,是否需要添加__block

    • 因为block内部只是使用了array的内存地址添加数据,并没有修改array的内存地址,所以array不需要__block修饰;
    • 添加__block修饰符之后,系统会创建相应的结构体,占用一定的内存空间;所以要根据相应情况添加__block修饰符,避免内存浪费。

    Q: 使用block有那些注意事项?

    • 注意循环引用问题。

    iOS底层原理 - 探寻block本质(二)中介绍到block对对象类型的变量捕获,以及对象的销毁时机。
    下面介绍如何实现在block内部修改变量的值。

    1. 修饰符__block

    需要在block内部修改变量的值,代码如下:

    typedef void (^Block)(void);
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int age = 10;
            Block block = ^{
                age = 20;  // error
            };
            block();
        }
        return 0;
    }
    

    以上代码会出现编译错误。

    C++源码

    通过源码可知:
    age是在main函数内部声明的变量,存在于main函数的栈空间内部;
    block内部实现是__main_block_func_0函数,其内部捕获age,新增一个参数存储外部的age变量的值,这个age存在于block的栈空间内部;
    所以block内部无法修改main函数内部的auto变量。

    Q: 那么该如何在block内部修改变量的值呢?

    1> 方法一:使用static变量
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            static int age = 10;
            Block block = ^{
                age = 20;
                NSLog(@"------------ %d", age);
                // 打印结果:------------ 20
            };
            block();
        }
        return 0;
    }
    

    由前文可知:局部变量都会被block捕获,auto变量值传递,static变量指针传递。
    block内部会新增一个参数存储age的指针,通过指针访问age变量的内存地址,就可以修改age的值。

    2> 方法二:使用全局变量

    全局变量在哪里都可以访问,所以block不用捕获全局变量,直接进行访问。

    以上两种方法会使变量一直存在于内存中,占用内存地址。我们可以使用__block修饰符来解决这个问题。

    3> 方法三:使用__block修饰变量
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block int age = 10;
            Block block = ^{
                age = 20;
                NSLog(@"------------ %d", age);
                // 打印结果:------------ 20
            };
            block();
        }
        return 0;
    }
    

    转化成C++源码:

    使用__block修饰变量的源码

    由源码可知:

    存值时:

    1. __block修饰的age变量会在block内部转化成名为age__Block_byref_age_0结构体,结构体包括:

      • __isa:说明__Block_byref_age_0本质也是对象
      • __flags:赋值为0
      • __forwarding:指向结构体自身的指针
      • __size:占用的内存空间
      • age:存储变量
    2. 然后__Block_byref_age_0结构体age存储在结构体__main_block_impl_0中。

    取值时:

    1. 通过_cself->ageage赋值给 __Block_byref_age_0结构体;
    2. age->__forwarding->age通过结构体指针访问成员变量来改变成员变量的值;
    3. 通过age->__forwarding->age进行取值。
    4> Q: 以下代码是否可以正确执行?
    typedef void (^Block)(void);
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSMutableArray *array = [NSMutableArray array];
            Block block = ^{
                [array addObject: @"1"];
                [array addObject: @"2"];
                NSLog(@"array - %@", array);
            };
            block();
        }
        return 0;
    }
    
    • 可以正确执行。
    • 因为block内部只是使用了array的内存地址添加数据,并没有修改array的内存地址;
    • 所以array不需要__block修饰;
    • 添加__block修饰符之后,系统会创建相应的结构体,占用一定的内存空间;所以要根据相应情况添加__block修饰符,避免内存浪费。

    2. __block内存管理

    (1) 分析一下代码:

    typedef void (^Block)(void);
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
    
            int age = 10;
            __block int weight = 60;
    
            NSObject *object = [[NSObject alloc] init];
            __weak NSObject *weakObject = object;
            __block NSObject *blockObject = object;
            __block __weak NSObject *blockWeakObject = object;
    
            Block block = ^{
                NSLog(@"%d", age); // 局部变量
                NSLog(@"%d", weight); // __block修饰的局部变量
                NSLog(@"%p", object); // 对象类型的局部变量
                NSLog(@"%p", weakObject); // __weak修饰的对象类型的局部变量
                NSLog(@"%p", blockObject); // __block修饰的对象类型的局部变量
                NSLog(@"%p", blockWeakObject); // __block、__weak修饰的对象类型的局部变量
            };
            block();
         }
        return 0;
    }
    

    使用命令行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m将代码转化成C++语言:

    __main_block_impl_0函数

    __main_block_impl_0函数可知:

    1. 未使用__block修饰的变量(objectweakObject),根据被block捕获的指针类型进行强引用或弱引用;
    2. 使用__block修饰的变量(weightblockObjectblockWeakObject),都是使用强指针引用生成的结构体。
    结构体

    由以上__block修饰的对象类型的变量生成的结构体可知:

    1. 其内部多了__Block_byref_id_object_copy__Block_byref_id_object_dispose两个函数,用来对对象类型的变量进行内存管理操作。
    2. block捕获的对象类型决定结构体对象的引用类型:
      a> blockObject是强指针,所以__Block_byref_ blockObject_1blockObject就是强引用;
      b> blockWeakObject是弱指针,所以__Block_byref_ blockWeakObject_1blockWeakObject就是弱引用。
    __main_block_copy_0函数、__main_block_dispose_0函数

    由以上C++代码可知:

    1. __main_block_copy_0函数根据变量的强弱指针及是否被__block修饰做出不同处理:
      a> 强指针在block内部产生强引用;
      b> 弱指针在block内部产生弱引用;
      c> 被__block修饰的变量最后的参数传入的是8
      d> 没有被__block修饰的变量最后的参数传入的是3
    2. __main_block_dispose_0函数会在block从堆中移除时释放这些变量。

    (2) 总结:

    block复制到堆上

    由block复制到堆上的内存变化图可知:

    1. 将block复制到堆上时,block内部引用的__block变量也被复制到堆上,此时block持有__block变量;
    2. 若将block复制到堆上时,__block变量已经在堆上,则不会再次将其复制到堆上。
    block从堆上移除

    由block从堆上移除的内存变化图可知:

    1. 将block从堆中移除时,若有别的block持有__block变量,则不会将__block变量移除;
    2. 将所有的block从堆中移除时,此时没有block持有__block变量,__block变量被移除。

    (3) __forwarding指针

    typedef void (^Block)(void);
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block int age = 10;
            Block block = ^{
                age = 20;
            };
            block();
         }
        return 0;
    }
    

    将代码转化成C++语言:

    C++代码

    由以上代码可知:

    1. 当block在栈上时,栈上的__Block_byref_age_0结构体内部__forwarding指针指向结构体自己;
    2. 当block复制到堆上时,栈上的__Block_byref_age_0结构体也会被复制到堆上,此时栈上的__Block_byref_age_0结构体内部__forwarding指针指向的是堆中的__Block_byref_age_0结构体,堆中__Block_byref_age_0结构体内的__forwarding指针依然指向自己。

    以上结论可由下图展示:

    __forwarding指针

    3. __block修饰的对象类型的内存管理

    (1) __block修饰对象类型

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

    将代码转化成C++语言:

    C++代码

    由以上C++代码可知:
    __Block_byref_person_0内部多了__Block_byref_id_object_copy__Block_byref_id_object_dispose两个函数,用来对对象类型的变量进行内存管理操作:

    对于__Block_byref_id_object_copy函数:
    a> __Block_byref_id_object_copy函数赋值为__Block_byref_id_object_copy_131函数;
    b> __Block_byref_id_object_copy_131函数调用_Block_object_assign函数;
    c> _Block_object_assign函数内部拿到dst指针(block对象的地址值)加上40个字节,即为person指针。

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

    __Block_byref_id_object_copy函数同理。

    (2) __block__weak同时修饰对象类型

    使用__block__weak同时修饰变量同理。

    block内部对__block修饰变量生成的结构体都是强引用;
    结构体内部对外部变量的引用取决于传入block内部的变量是强引用还是弱引用。

    (3) 总结

    • 当block在栈上时,不会对__block变量产生强引用;

    • 当block被copy到堆时:
      a> 会调用block内部的copy函数;
      b> copy函数内部会调用_Block_object_assign函数;
      c> _Block_object_assign函数会根据所指向对象的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain)。

    • 当block从堆中移除时:
      a> 会调用block内部的dispose函数;
      b> dispose函数内部会调用_Block_object_dispose函数;
      c> _Block_object_dispose函数会自动释放指向的对象(release)。

    4. 循环引用

    (1) 循环引用原理

    // TODO: -----------------  Person类  -----------------
    typedef void (^Block)(void);
    
    @interface Person : NSObject
    @property (nonatomic, assign) int age;
    @property (nonatomic, copy) Block myBlock;
    @end
    
    @implementation Person
    - (void)dealloc {
        NSLog(@"------------ %s", __func__);
    }
    @end
    
    // TODO: -----------------  main  -----------------
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Person *person = [[Person alloc] init];
            person.age = 10;
            person.myBlock = ^{
                NSLog(@"%d", person.age);
            };
         }
        NSLog(@"大括号已经结束");
        return 0;
    }
    
    打印结果

    由打印结果可知:
    大括号已经结束,person没有被释放,产生了循环引用。

    循环引用原理

    循环引用原理如上图所示:
    大括号结束后引用1被断开,引用2引用3没有断开,形成循环引用,进而造成内存泄漏。

    (2) 循环引用解决方法 - ARC

    解决循环引用问题,还需要保证block在person销毁前不被销毁,解决方案是:
    Person对block的引用(引用2)为强引用;block内部对Person的引用(引用3)为弱引用。

    1> 使用__weak__unsafe_unretained修饰符
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Person *person = [[Person alloc] init];
            person.age = 10;
    
            // __weak:不会产生强引用,指向对象销毁时,会自动让指针置为nil
            // __unsafe_unretained:不会产生强引用,不安全,指向对象销毁时,指针存储的地址值不变
    
            //  __weak Person *weakPerson = person;
            // __weak typeof(person) weakPerson = person;
            __unsafe_unretained typeof(person) weakPerson = person;
            person.myBlock = ^{
                NSLog(@"age - %d", weakPerson.age);
            };
        }
        NSLog(@"大括号已经结束");
        return 0;
    }
    
    2> 使用__block修饰符
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block Person *person = [[Person alloc] init];
            person.age = 10;
            person.myBlock = ^{
                NSLog(@"age - %d", person.age);
                person = nil;
            };
            person.myBlock();
        }
        NSLog(@"大括号已经结束");
        return 0;
    }
    

    使用__block修饰符打破循环引用原理如下:

    __block修饰符打破循环引用

    由上文可知:
    __block修饰person变量,会生成__Block_byref_person_0结构体,其内部包含的person对象才是block内部使用的变量。

    那么将block内部的person置为nil,三角循环引用就会断开。

    此方法要求执行block,并且在block内部将person对象置为nil

    (2) 循环引用解决方法 - MRC

    1> 使用__unsafe_unretained修饰符
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __unsafe_unretained Person *person = [[Person alloc] init];
            person.age = 10;
            person.myBlock = [^{
                NSLog(@"age - %d", person.age);
            } copy];
            [person release];
        }
        NSLog(@"大括号已经结束");
        return 0;
    }
    

    MRC环境下不支持__weak修饰符,使用__unsafe_unretained修饰符原理同ARC环境下相同,不再赘述。

    2> 使用__block修饰符
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block Person *person = [[Person alloc] init];
            person.age = 10;
            person.myBlock = [^{
                NSLog(@"age - %d", person.age);
            } copy];
            [person release];
        }
        NSLog(@"大括号已经结束");
        return 0;
    }
    

    由上文可知:
    MRC环境下,当block被copy到堆时,__block结构体不会对person产生强引用,所以也可以解决循环引用问题。

    相关文章

      网友评论

        本文标题:iOS底层原理 - 探寻block本质(三)

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