iOS中的Block详解2(附面试题) - 底层原理总结

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

我们查看源码可以看到确实如此

强指针引用

将上述代码转移到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进行一次copy操作,以保证person不会被销毁。堆空间的block自己销毁之后也会对持有的对象进行release操作。

也就是说栈空间上的block不会对对象强引用,堆空间的block有能力持有外部调用的对象,即对象进行强引用或去除强引用的操作。

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

将代码转化为c++来看一下上述代码之间的差别。__weak修饰变量,需要告知编译器使用ARC环境及版本号,否则会报错,添加说明-fobjc-arc -fobjc-runtime=ios=8.0.0

xcrun -sdk iponeos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

__weak修饰变量

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

__main_block_copy_0和__main_block_dispose_0

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

__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对象,以及8。

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

_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函数内部会调用_Block_object_dispose函数。

_Block_object_dispose会对person对象做释放操作,类似于release,也就是断开对person对象的引用,而person究竟是否被释放还是取决于person对象自己的引用计数。

总结

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

2.当block内部访问了对象类型的auto变量时,如果block是在栈上,block内部不会对person产生强引用。

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

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

问题

1.下列代码中person在何时销毁?

- (void)touchesBegan:(NSSet *)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");
}

打印结果:

打印结果

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

2.下列代码person在何时销毁?
- (void)touchesBegan:(NSSet *)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");
}

打印结果:

打印结果

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

3.通过示例代码进行总结
- (void)touchesBegan:(NSSet *)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(), ^{
        
        NSLog(@"weakP ----- %@",waekP);
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"person ----- %@",person);
        });
    });
    NSLog(@"touchBegin----------End");
}

打印结果:

打印结果
- (void)touchesBegan:(NSSet *)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(), ^{
        
        NSLog(@"person ----- %@",person);
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"weakP ----- %@",waekP);
        });
    });
    NSLog(@"touchBegin----------End");
}

打印结果:

打印结果

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 int age = 10;

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

__block修饰的变量其底层源码

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

你可能感兴趣的:(iOS中的Block详解2(附面试题) - 底层原理总结)