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

本篇主要是对小码哥底层视频学习的总结。方便日后复习。
上一篇《iOS底层原理总结 - 探寻block本质(一)》:
https://www.jianshu.com/p/deb04ce08d1a

本篇学习总结:

  • blcok对对象变量的捕获
  • block内修改变量的值
  • __block内存管理
  • 循环引用问题即解决方式

好了,带着问题,我们一一开始阅读吧

一.blcok对对象变量的捕获

上节中我们讨论了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);
            };
        } // ARC环境下执行完毕,person没有被释放
        NSLog(@"--------");
    } // ARC环境下person 释放
    return 0;
}

//打印结果如下:
--------
person delloc

大括号执行完毕之后,person对象依然没有被释放,上一篇文章中提到过,person为auto局部变量,传入block的变量同样为person,即block有一个强引用指针指向person,所以block不被销毁的话,person对象也不会被销毁。

查看一下c++文件


强指针引用.png

如果将ARC环境改为MRC,又会是怎么样的结果呢?

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 release];//MRC下需要手动释放对象空间
        } //当block还未被释放时,person对象已经被释放了
        NSLog(@"--------");
    } 
    return 0;

//打印结果如下:
person delloc
--------
}

block调用copy操作之后,person不会被释放。

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);
            } copy];
            [person release];
        }
        NSLog(@"--------");
    } /
    return 0;
}

//打印结果如下:
--------

在MRC环境下,只需要将栈空间的block进行一次copy操作,就能将栈空间的block拷贝到堆上,person对象不会被释放,说明拷贝到堆上的block对person进行一次retain操作,以保证person不会被销毁。堆空间的block自己销毁之后也会对person对象进行一个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 *weakPerson = person;
            
            block = ^{
                NSLog(@"------block内部%d",weakPerson.age);
            };
            
        }
        NSLog(@"--------");
    }
    return 0;
}

//打印结果如下:
person delloc
--------

我们转化为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

查看c++代码

__weak修饰变量.png

总结一下:

1.__weak/__strong 这类修饰符只能修饰对象,不可修饰基本数据类型
2.MRC环境下不管是__weak/__strong修饰的对象,栈上的block都不会对对象进行强引用,局部对象一出作用域就会自动被释放。
3.ARC环境下堆上的block默认对对象时强引用,只有等block内存释放的时候才会释放引用对象。__weak修饰的对象,堆上的block对其是弱引用,局部对象一出作用域就会自动被释放。

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

__main_block_copy_0、__main_block_dispose_0函数.png

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

这里说的copy就是__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类型,则为弱引用,引用计数不变。

这里的dispose函数指的是_Block_object_dispose函数,当block从堆上移除时就会自动调用__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数,_Block_object_dispose会对person对象做释放操作,类似于release,也就是断开对person对象的引用
person究竟是否被释放还取决于person对象自己的引用计数。

总结一下:

1.一旦block中捕获的变量为对象类型,block结构体中的__main_block_desc_0会多出两个参数copydispose。因为访问的是个对象,block希望拥有这个对象,就需要对对象进行引用,也就是进行内存管理的操作,比如说对象进行retain操作,当block从堆上移除时调用dispose函数。copy跟dispose方法内部调用看上面总结。
2.当block内部访问了对象类型变量时,如果block在栈上,block内部不会对person产生强引用,不论block结构体内部的变量是__strong修饰还是__weak修饰,都不会对变量产生强引用
3.如果block被拷贝到堆上copy函数会调用_Block_object_assign函数,根据变量的修饰符(__strong,__weak,unsafe_unretained)做出相应的操作,形成强应用还是弱引用。
4.如果block从堆上移除,dispose函数会调用_Block_object_dispose函数,自动释放引用的变量。

问题
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");
}
打印结果.png

上文提到过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 *weakP = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@", weakP);
    });
    NSLog(@"touchBegin----------End");
}
打印内容.png

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

3.下列person对象何时释放呢?

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    Person *person = [[Person alloc] init];
    
    __weak Person *weakP = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        NSLog(@"weakP ----- %@", weakP);
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"person ----- %@",person);
        });
    });
    NSLog(@"touchBegin----------End");
}
打印结果.png

block内部对person对象先进行弱引用,再进行强引用,当第一个GCD函数执行完毕后,person对象被block弱引用着,不会对引用计数造成变化,第二个GCD函数对person对象是强引用,只能等第二个block执行完毕之后才可释放person对象,因此第二个block执行完毕后会打印两次person对象。

4.下面这种情况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)(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");
}
打印结果.png

block内部对person对象先进行强引用,再进行弱引用,当第一个GCD函数执行之前,person对象被block强引用着,第一个GCD函数执行完毕后,随着block释放而释放,第二个GCD函数没有之前person对象已经释放了,因此第二个block块执行的时候打印null。

二.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的内存存在栈空间,但是block内部的代码在__main_block_func_0函数内部。__main_block_func_0函数内部无法访问age变量的内存空间,两个函数的栈空间不一样,__main_block_func_0内部拿到的age是block结构体内部的age,因为无法在__main_block_func_0函数内部去修改main函数内部的变量。

如果我们想要在block内部修改局部变量的数值,可以采用以下两种方式
1.局部变量使用static修饰

前文提到过static修饰的局部变量被block捕获后转化成指针类型的变量,在__main_block_func_0函数内部可以拿到age变量的内存地址,因为就可以在block内部修改age的值。

2.__block修饰局部变量

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

__block int age = 10;

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

_block修饰的变量源码.png

解释一下上面的代码

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

通过下图查看__Block_byref_age_0结构体内存储哪些元素。


__Block_byref_age_0赋值.png

isa指针__Block_byref_age_0结构体也有一个isa指针,说明__Block_byref_age_0的本质也是一个对象。
__forwarding__forwarding__Block_byref_age_0 结构体类型的,并且__Block_byref_age_0存储的值为(__Block_byref_age_0 *)&age,即结构体自己的内存地址。
** __flags**:默认传入0。
__sizesizeof(__Block_byref_age_0)__Block_byref_age_0所占用的空间。
age:真正存储变量的地方,这里存储局部变量为10
接着将__Block_byref_age_0结构体中的变量存入__main_block_impl_0结构体中,并赋值给__Block_byref_age_0 *age;

__Block_byref_age_0 *age赋值.png

之后调用block,首先取出__main_block_impl_0中的age,通过age结构体拿到__forwarding指针,上面提到过__forwarding中保存的就是__Block_byref_age_0结构体本身,这里也就是age(__Block_byref_age_0),在通过__forwarding拿到结构体中的age(10)变量并修改其值。
后续NSLog中使用age时也通过同样的方式获取age的值。

修改结构体内的age值.png

到此为止,__block修饰的局部变量在block内部可以进行修改,__block将变量包装成对象,然后在把age结构体封装到main_block_impl结构体里面,block内部存储的变量为结构体指针,也就可以通过指针找到内存地址进而修改变量的值。

用图总结一下:


__block修饰变量.png

3.__block修饰对象类型
那么如果变量本身就是对象类型呢?通过以下代码生成c++源码查看。

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

__block修饰对象类型源码.png

__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的内存地址,往内存地址中添加内容,并没有修改array的内存地址,因此array不需要使用__block修饰也可以正确编译。上面说的是在block内部可以修改__block修饰的变量值。
因为当仅仅是使用局部变量的内存地址,而不是修改的时候,类似上述的代码,尽量不要添加__block,因为添加__block修饰符之后,系统会自动创建相应的结构体,占用不必要的内存空间。

2.上面提到的__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_byref_age_0结构体.png

通过查看main_block_impl结构体其中的内容,找到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变量的地址。

age的内存地址推算.png

通过上图的计算可以发现打印age的地址同__Block_byref_age_0结构体内的age值的地址相同,也就是说外面使用的age,代表的就是结构体内的age值,所以直接拿来用的age就是之前声明的int age
总结一下__block修饰对象类型的底层实现:

__block修饰对象.png

三.__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修饰变量产生内存管理(基本数据类型,对象类型)。当block被copy到堆上时会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对__block变量形成强弱引用。

首先看一张图看一下block复制到堆上的内存变化


__block copy内存管理.png

当block被copy到堆上时,block内部引用的__block变量也会被复制到堆上,并且持有变量,如果block复制到堆上的同时, __block变量已经存在堆上了,则不会复制。
当block从堆上移除的话,就会调用dispose函数,也就是__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数,会自动释放引用的__block变量。

__block 释放内存管理.png

block内部决定什么时候将变量复制到堆上,什么时候对变量做应用计数的操作。
__block修饰的变量在block结构体中一直都是强引用,而其他类型的是由传入的对象指针类型决定。
还是上代码

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

上述__main_block_impl_0结构体中看出,没有使用__block修饰的变量(object和weakObj)则根据他们本身被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_byref_weakPerson_2对weakPerson就是弱引用,__Block_byref_person_1对person是强引用。

我们来看一下copy函数内部调用

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

__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指针指向的结构体是自己,所以根据__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指针.png

因此block内部拿到的变量实际就是在堆上的,当block进行copy被复制到堆上时,_Block_object_assign函数内做的这一系列操作。
被__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_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直接对对象进行内存管理传入的参数3.8都不同,可以猜想_Block_object_assign内部根据传入的参数不同进行不同的操作。
通过对上面__Block_byref_person_0结构体占用空间的计算发现__Block_byref_person_0结构体占用的空间是48个字节,而加40恰好指向的就是person指针
也就是说copy函数会将person地址传入_Block_object_assign函数,_Block_object_assign中对person对象进行强引用或者弱引用。

强引用示意图.png

如果使用__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*);
 Person *__weak weakPerson;
};

也就是说无论如何block内部中对__block修饰变量生成的结构体都是强引用,结构体内部对外部变量的引用取决于传入block内部的变量是强引用还是弱引用

弱引用示意图.png

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

MRC环境!!!
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;
}

上述代码person会先释放

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

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

__block内存管理示意图.png
四.循环引用问题

循环引用导致内存泄漏。

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

//打印结果如下:
大括号结束啦

可以发现大括号结束之后,person依然没有被释放,产生了循环引用。
通过一张图看一下他们之间的内存结构


产生循环引用示意图.png

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

首先为了能随时执行block,我们希望person对block强引用,而block内部对person的引用为弱引用最好。
使用__weak__unsafe_unretained修饰符可以解决循环引用的问题
我们上面也提到过__weak会使block内部将指针变为弱指针,block对person对象为弱指针的话,也就不会出现互相引用而导致不会被释放了。

使用`__weak`和`__unsafe_unretained`修饰 .png

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

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


使用__block也可以解决循环引用.png

上面我们提到过,在block内部使用变量使用的其实是__block修饰的变量生成的结构体__Block_byref_person_0内部的person对象,那么当person对象置为nil也就断开了结构体对person的强引用,那么三角的循环引用就会自动断开,该释放的时候也就释放了。但是有弊端,必须执行block,并且在block内部将person对象置为nil。也就说在block执行之前代码是因为循环引用导致内存泄漏的。
用一张图总结吧,方便记忆

ARC解决循环引用问题.png

解决循环引用问题 - MRC

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

用一张图总结吧,方便记忆


MRC解决循环引用问题.png

__strong和__weak

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

有时候为了避免block内部的对象在调用时被销毁,所以在block内部重新使用__strong修饰self变量。

到底为止,block的底层知识记录完毕,看一下面试题

面试题:

  • 1.block原理是什么,本质是什么

block就是一个封装函数以及函数调用环境的oc对象

  • 2.__block的作用是什么,有什么使用注意点

__block可以解决block内部无法修改auto变量值的问题,因为编译器会将__block变量包装成一个对象。
使用注意点:注意内存管理,MRC下

  • 3.block的属性修饰词为啥时copy 使用注意点

方便对block进行内存管理
注意点:循环引用,以及循环引用的解决方式

  • 4.block在修改NSMutablearray的指针变量时,还需要添加__block修饰符吗?

能不添加就不添加,因为block内部只是获取指针变量指向的对象数据,并不是改变指针变量存储的数据

本篇学习先记录到此,感谢阅读,如有错误,不吝赐教。

你可能感兴趣的:(iOS底层原理总结 - 探寻block本质(二))