iOS底层原理 - 探寻block本质 之 __block

面试题引发的思考:

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

// 打印结果
Demo[1234:567890] 大括号已经结束

由打印结果可知:
大括号已经结束,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产生强引用,所以也可以解决循环引用问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容