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

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

作者: hazydream | 来源:发表于2018-02-05 09:49 被阅读23次

面试题引发的思考:

Q: block的属性修饰词为什么是copy?

  • 如果block是在栈上,将不会对auto变量产生强引用;
  • 如果block被copy到堆上:
    1> 会调用block内部的copy函数;
    2> copy函数内部会调用_Block_object_assign函数;
    3> _Block_object_assign函数会根据auto变量的修饰符(__strong_weak)做出相应的操作,形成强引用、弱引用(仅限于ARC时会retain,MRC时不会)
  • 如果block从堆上移除:
    1> 会调用block内部的dispose函数;
    2> dispose函数内部会调用_Block_object_dispose函数;
    3> _Block_object_dispose函数会自动释放引用的auto变量,类似于release

对象类型的auto变量

iOS底层原理 - 探寻block本质(一)中介绍到block底层原理以及block的变量捕获,那么block对对象类型变量的捕获同对基本数据类型变量的捕获是否相同?

(1) 栈空间、堆空间的强引用、弱引用

Q: 首先查看一下代码,block访问对象类型变量时,对象何时销毁?

// TODO: -----------------  Person类  -----------------
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end

@implementation Person
- (void)dealloc {
    NSLog(@"------------ Person - dealloc");
}
@end

// TODO: -----------------  main  -----------------
typedef void (^Block)(void);

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

由打印结果可知:
大括号执行完毕,person没有被释放。
person对象是在大括号内声明的局部变量,它的生命周期仅限于这个大括号内,打印结果是什么原因造成的?

上篇文章介绍到:personauto变量,person会被捕获到block内部,即block对person进行强引用; 那么block销毁之前,person是不会被销毁的。

查看源码,符合我们的结论。

强指针引用

下面我们进入MRC环境:

// MRC环境
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            block = ^{
                NSLog(@"------------ 内部 %d", person.age);
            };
            [person release];
        }  // 执行完毕,person被释放
        NSLog(@"------------ 外部");
    }
    return 0;
}
打印结果

由打印结果可知:
大括号执行完毕,person被释放。
原因是:在MRC环境下,block访问auto变量,会在栈空间,不会对person进行强引用。

对block进行copy操作,person没有被释放

// MRC环境
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            block = [^{
                NSLog(@"------------ 内部 %d", person.age);
            } copy];
            [person release];
        }
        // 执行完毕,person没有被释放
        NSLog(@"------------ 外部");
        // 需要block销毁掉,person才会被释放
        [block release];
    }
    return 0;
}
打印结果

由打印结果可知:
大括号执行完毕,person没有被释放。
原因是:对栈空间的block进行copy操作,将栈空间的block拷贝到堆中,会对person进行强引用;
堆空间的block可能会对person进行一次retain操作,保证person不被销毁,堆空间的block销毁之后会对person进行release操作。

总结可知:堆区的block对person对象有强引用作用,栈空间的block对person对象没有强引用作用。

(2) 关键字__weak

切回ARC环境,执行下列代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        {
            Person *person = [[Person alloc] init];
            person.age = 10;

            __weak Person *weakPerson = person;
            block = ^{
                NSLog(@"------------ 内部 %d", weakPerson.age);
            };
        }
        // 执行完毕,person没有被释放
        NSLog(@"------------ 外部");
    }
    return 0;
}
打印结果

将代码转化成C++,_weak修饰变量,需要告知编译器使用ARC环境及版本号:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

__weak修饰变量

由源码可知:_weak修饰的变量,在生成的__main_block_impl_0中也是使用_weak修饰。
所以对person对象是弱引用,不能改变person对象的生命周期。

(3) _main_block_copy_0函数和 __main_block_dispose_0函数

block捕获对象类型的变量时__main_block_impl_0内部结构体__main_block_desc_0中多了copy函数和dispose函数两个参数:

__main_block_copy_0、__main_block_dispose_0函数

copy函数和dispose函数中传入的都是__main_block_impl_0结构体本身。

a> copy函数本质是__main_block_copy_0函数,其内部调用_Block_object_assign函数;
_Block_object_assign中传入的是person对象的地址,person对象,以及8

b> dispose函数本质是__main_block_dispose_0函数,其内部调用_Block_object_dispose函数;
_Block_object_dispose函数传入的参数是person对象,以及8

1> _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对象产生强引用或者弱引用。

2> _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对象自己的引用计数。

总结可知:
当block内部访问了对象类型的auto变量时:

  • 如果block是在栈上,将不会对auto变量产生强引用;
  • 如果block被拷贝到堆上:
    1> 会调用block内部的copy函数;
    2> copy函数内部会调用_Block_object_assign函数;
    3> _Block_object_assign函数会根据auto变量的修饰符(__strong_weak)做出相应的操作,形成强引用,弱引用。
  • 如果block从堆上移除:
    1> 会调用block内部的dispose函数;
    2> dispose函数内部会调用_Block_object_dispose函数;
    3> _Block_object_dispose函数会自动释放引用的auto变量,类似于release

(4) 以下几个示例中person都在何时销毁?

1> 示例一:
- (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(@"------------ touchesBegan");
}
打印结果

打印结果显示:block执行完毕后person对象销毁。

iOS底层原理 - 探寻block本质(一)可知:
ARC环境中,block作为GCD API的方法参数时会自动进行copy操作,将block复制到堆上,所以block内部copy函数会对person进行强引用。

当block执行完毕需要销毁时,调用dispose函数释放对person对象的引用,person没有强指针引用时会被销毁。

2> 示例二:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    Person *person = [[Person alloc] init];

    __weak Person *weakPerson = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"------------ %@", weakPerson);
    });
    NSLog(@"------------ touchesBegan");
}
打印结果

打印结果显示:person对象先销毁,然后执行block,打印为null

block对weakPerson__weak弱引用,所以block内部copy函数会对person进行弱引用。
touchesBegan: withEvent:执行完毕时,person没有强指针引用时会被销毁,所以block执行时打印null

3> 示例三:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    Person *person = [[Person alloc] init];

    __weak Person *weakPerson = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"------------1 %@", person);
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"------------2 %@", weakPerson);
        });
    });
    NSLog(@"------------ touchesBegan");
}
打印结果

打印结果显示:外层block执行完毕后,即1秒后person对象销毁,然后执行内层block,即3秒后打印为null

外层block对person进行强引用,内层block对person进行弱引用。
当外层block执行完毕时,person有强指针引用;然后内层block执行时person没有强指针会被销毁,所以内层block执行时打印null

4> 示例四:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    Person *person = [[Person alloc] init];

    __weak Person *weakPerson = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"------------1 %@", weakPerson);

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"------------2 %@", person);
        });
    });
    NSLog(@"------------ touchesBegan");
}
打印结果

打印结果显示:外层block执行完毕后,然后执行内层block,3秒后person对象销毁。

外层block对person进行弱引用,内层block对person进行强引用。
person的强引用何时结束,person何时dealloc,所以3秒后person对象才销毁。

相关文章

网友评论

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

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