OC-Block的本质

给你一囗甜甜゛ 提交于 2020-01-28 05:19:15

1.block的本质

block其实就是封装了函数调用以及环境变量调用的对象。

2.block的定义

return_type (^blockName)(parameters)

3.block的本质结构

新建工程,在main.m中添加如下的代码:

int age = 10;
void (^blockame)(void) = ^{
    NSLog(@"logBlock = %d", age);
};

blockame();

用命令行将main.m转成c++的语言,如下:

xcrun -sdk iphoneos clang  -arch arm64 -rewrite-objc main.m 

生成的main.cpp打开,查看如下:

从上图可以看出,c++把block转成了__main_block_impl_0 这种结构体。C++命名block是有格式的,格式如下:__XXX类名_block定义所在的方法名_block_impl_0 ,与block相关的结构体命名大致都类似__main_block_impl_0 结构体定义如下:

其中,__main_block_impl_0这个结构体有三个属性:__block_impl结构体类型的 impl、__main_block_desc_0结构类型的desc,还有外部获取到的变量 age。从定义中可知:把fp赋值给了impl的FuncPtr,把desc赋值给了结构体本身的Desc。那么__block_impl结构体定义如下:

__block_impl内部有个isa指针,也可以说明,block是一个对象。因为从__main_block_impl_0这个结构体定义中可知把fp赋值给了impl的FuncPtr,那么这个fp的值是什么呢?回到这个图:

可以知道,__main_block_impl_0在初始化的时候,把__main_block_func_0 (命名规则如下:__XXX类名_block定义所在的方法名_block_func_0 当做结构体初始化参数fp传了进入,查看__main_block_func_0的定义如下:

从定义可知,__main_block_func_0这个函数,主要是把block大括号内部的代码封装起来了。

__main_block_impl_0结构图体的另外一个参数 __main_block_desc_0_DATA又是什么呢?如下图:

可知__main_block_desc_0_DATA 这个参数的类型是__main_block_desc_0 (命名规则如下:__XXX类名_block定义所在的方法名_block_desc_0 )的结构体类型,里面主要存储的是reserved(是否持有)和block结构体本身的大小。

所以,block是一个将block大括号内部的方法封装成__main_block_func_0的函数存储在__main_block_impl_0的结构体对象中。

4.block捕获变量

为了保证block内部能够正常访问外部的变量,block有个变量捕获机制。如下图:

一般我们在方法内部定义的变量,如果不是用static修饰的,那么都是默认用auto修饰的。auto修饰的变量是自动变量,离开作用域就会被销毁。

4.1 捕获auto变量

例如下面的代码:

int age = 10;
void (^blockame)(void) = ^{
    NSLog(@"logBlock = %d", age);
};
blockame();

转成C++的代码如下:

从上图可知,在构造block函数的时候,已经把age的值传进去了,传进去之后,我们来看一下做了什么操作:

从上图可知,在创建block的结构体时,已经把int age 捕获到作为结构体的成员变量,但是在构造block的结构体时,把age的值作为_age的值 赋值给了age。其中age(_age)就是相当于age = _age。所以,在构造block的结构体时,auto的自动变量的值已经存到了block的结构体中了。

4.2 捕获static变量

例如下面的代码:

static int age = 10;
void (^blockame)(void) = ^{
    NSLog(@"logBlock = %d", age);
};
age = 20;
blockame();

打印出来的结果不是10,而是20. 这是为什么呢?将代码转成c++的代码,如下图:

与block捕获auto变量不一样的地方是,捕获static的变量时,传入的不是age的值,而是age的地址。查看生成的__main_block_impl_0 的结构体如下:

生成的block结构体也持有age的这个地址,所以在外部把age的值修改完后,block内部age的地址并没有被修改,而这个地址指向的值已经修改了,所以,block内部age的值也被修改了。

4.3 捕获全局变量

查看如下的代码:

int age_ = 10;
static int height_ = 10;

int main(int argc, char * argv[]) {
    @autoreleasepool {
        void (^blockame)(void) = ^{
            NSLog(@"age_ = %d, height_ = %d", age_, height_);
        };
        age_ = 20;
        height_ = 20;
        blockame();
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

这个定义了两个全局变量,将代码转成C++的代码如下:

从上图中,我们可知,在构造这个block的结构体的时候,并没有把age_和height_的值传入到__main_block_impl_0的构造方法中,查看__main_block_impl_0的结构体的定义如下:

从上图可知,age_和Height_ 并没有捕获到__main_block_impl_0 的结构体内部中,所以全局变量,并不会被block捕获。查看block封装的__main_block_func_0 的函数,如下:

可知,age_和height_ 是直接获取全局变量的值。

4.4 其他有意思的例子

如果有个Person类,Person类有个test的方法,方法写了一个block,如下图:

#import "Person.h"

@implementation Person

- (void)test {
    void (^block)(void) = ^{
        NSLog(@"%@", self);
    };
    block();
}

@end

那么block内部的self会被捕获到吗?同样,转成C++的代码如下:

从上图 block的构造函数可以看到已经把self传进去了,那么继续看__Person_test_block_impl_0 结构体的定义如下:

从上图可知,block结构体已经把Person 对象的self捕获到block的内部了。那么为什么能捕获到呢?

首先,test的Person对象的一个对象方法,其内部隐藏了两个参数即self和_cmd,即 :

其次,self是test方法的内部变量,相当于auto修饰的自动变量,所以self会被block会捕获自动到。

 

如果将代码替换如下:

#import "Person.h"

@implementation Person

- (void)test {
    void (^block)(void) = ^{
        NSLog(@"%@", self.name);
    };
    block();
}

@end

不是打印self也是self.name,我们猜想是不是会捕获到_name呢?实际上不是的,block仍然会捕获的是self,然后通过self来获取name的值。将代码装成C++,如下图:

首先可以看到是将self传入到构造函数里,而不是self.name传入构造函数里。其次,查看__Person_test_block_impl_0的结构体如下:

从结构体的定义可知,将Person的对象self捕获到block结构体的成员变量了呢,那么block内部是怎么获取到name的值呢?就需要查看block封装的函数了,如下图:

从objc_msgSend的方法中,可知,是通过给self发送消息获取name的值。

4.5 block的内存布局

5.block的类型

block有三种类型,可以通过class方法或者isa指针查看具体的类型,但是最终都是继承于NSBlock的类型。

5.1 NSGlobalBlock类型

没有访问block外部auto变量的block都是存放在全局区的,也就是globalBlock。如下图:

5.2 NSStackBlock类型

访问了block外部auto变量的block都是存放在栈区上,也就是stackBlock。如下图:

提供一份代码如下:

void(^block)(void);
void BlockTest() {
    int a = 10;
    block = ^{
        NSLog(@"我是test的block = %d", a);
    };
    block();
}

int main(int argc, char * argv[]) {
    @autoreleasepool {
        BlockTest();
        block();
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

打印出来的结果如下:

为什么第一次调用block的时候,a的值不是10了呢?

在MRC下,因为BlockTest方法里面的block是NSStackBlock类型,会存放在栈空间上。而栈空间的特点就是,方法执行完后,会回收方法内部存放在栈空间内存的变量。而block转成C++代码后,block结构体内部的成员变量也是存放在栈区的,当执行完BlockTest方法后,其内存也会被回收,block内部变量的值也就会错乱了。而在ARC下编译器会自动为代码添加copy操作,那么BlockTest方法内部的block就被复制到了堆区上,就不会有相同的问题了。

5.3 NSMallocBlock类型

__NSStackBlock__调用了copy就会将原本存放在栈区上的block复制到堆上,同时栈区的block会被销毁。NSGlobalBlock 进行copy操作后,依然是NSGlobalBlock。

5.4 block的copy

将NSStackBlock进行copy操作,就可以将类型转化成NSMallocBlock类型,并将block所处的栈区,复制到堆区上。在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况:

  • block作为函数返回值时(例如massory)
  • 将block赋值给__strong指针的时候
  • block作为GCD API的方法参数时,例如,dispatch_once..., dispatch_after...等等
  • block作为Cocoa API(Foundation框架)中方法名含有usingBlock的方法参数时,例如:array对象的enumerateObjectsUsingBlock:方法。

6.__block修饰变量的原理

我们都知道如果block内部要修改外部的变量时,需要用__block来修饰才可以。这是为什么呢?使用__block需要注意什么?__block的内存管理又是什么样的呢?

6.1 为什么没有用__block修饰的变量不能在block内部修改

这个需要从block的本质上入手,我们将下面的代码转成C++源码来了解一下:

        int a = 10;
        void (^blockTest)(void) = ^{
            NSLog(@"---a = %d---", a);
        };
        blockTest();

C++的源码如下:

首先,我们知道block会转成block结构体,而结构体内部有一个int a的成员变量,当调用block的构造函数时,是把a的值复制给了block的结构体,而不是把a的地址赋值给了block的结构体;而block的结构体有一个成员变量funPtr,这个变量存放的是block内部代码封装的函数,也就是对应上图的__main_block_func_0 的那个方法。__main_block_func_0这个方法中,定义了一个 int a (其作用域在__main_block_func_0 函数里面), 这个a的值取的是block里面的a的值,如果修改了__main_block_func_0 这个方法里面的a的值,也只是修改了block内部int a 的值,也不是修改main函数里面的int a(a的作用域在main函数的范围内)的值。相当于,不能在一个方法的内部,修改另外一个方法的局部变量的值。

6.1 __block修饰变量需要注意事项

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

6.2 __block修饰变量的原理

我们都知道,block可以捕获block上下文的环境变量到block结构体的内部,一般变量的类型是什么样的,block内部持有的变量也是什么类型的。例如:

int a = 10;
void (^blockTest)(void) = ^{
    NSLog(@"---a = %d---", a);
};
blockTest();

转成c++语言后,如下:

可知,如果没有用__block修改的变量,其源码并没有对变量做任何的处理,block的结构体持有的也是int a。但是如果在int a 上添加__block,那么情况就会不一样了,如下:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        __block int a = 10;
        void (^blockTest)(void) = ^{
            a = 20;
            NSLog(@"---a = %d---", a);
        };
        blockTest();
        
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

转成C++源码后,如下:

main函数中的

__block int a = 10;

被转化成了

__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};

查看__Block_byref_a_0 的结构体如下:

可见,__Block_byref_a_0 有5个成员变量:第一个是isa指针,第二个是指向自身类型的__forwarding指针,第三个是一个标记flag,第四个是它的大小,第五个是变量值,名字和变量名同名。

再看生成的block结构体,如下图:

此时,block的结构体持有的也不是int a, 而是__Block_byref_a_0 结构体类型的指针a,指针a的对象有个int 的a。__Block_impl 结构体的构造函数时,会把__Block_byref_a_0这个类型的a的地址赋值给block的结构体。也就是说,__block修饰变量的时候,编译器会把该变量包装成一个对象,而这个对象会赋值给block的结构体对象的成员变量。

那么block又是如何通过__Block_byref_a_0 这个结构体修改变量a的呢?查看修改a = 20; 这句代码转成的源码,如下:

首先,block会先取出 __Block_byref_a_0 类型的a, 然后通过a的指向自己的farwarding变量,再找到int 类型的a 并赋值。

如果在block外部打印a的值,如下:

NSLog(@"a = %d", a);

那么此时的a,取的值也是先取 __Block_byref_a_0 类型的a, 然后通过a的指向自己的farwarding变量,再找到int 类型的a的值。

所以,为什么__block修饰的变量能在block内部修改?因为__block将变量包装成对象,然后在把捕获到的变量封装在block的结构体里面,block内部存储的变量为结构体指针,也就可以通过指针找到内存地址进而修改变量的值。

7.__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变量产生内存管理。当blcokcopy到堆上时
会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对__block变量形成强引用(相当于retain)

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

__block copy内存管理

blockcopy到堆上时,block内部引用的__block变量也会被复制到堆上,并且持有变量,如果block复制到堆上的同时,__block变量已经存在堆上了,则不会复制。

当block从堆中移除的话,就会调用dispose函数,也就是__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数,会自动释放引用的__block变量。

 

__block 释放内存管理

block内部决定什么时候将变量复制到堆中,什么时候对变量做引用计数的操作。

__block修饰的变量在block结构体中一直都是强引用,而其他类型的是由传入的对象指针类型决定。

一段代码更深入的观察一下。

首先需要说明的一点是对象在OC中,默认声明自带__strong所有权修饰符的 . 
在转换出来的源码中,我们也可以看到,Block捕获了__block,并且强引用了,因为在__Block_byref_person_1结构体中,有一个变量是 Person *__strong person; ,这个默认也是带__strong所有权修饰符的。

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

将上述代码转化为c++代码查看不同变量之间的区别

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  
  int number;
  NSObject *__strong object;
  NSObject *__weak weakObj;
  __Block_byref_age_0 *age; // by ref
  __Block_byref_person_1 *person; // by ref
  __Block_byref_weakPerson_2 *weakPerson; // by ref
  
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _number, NSObject *__strong _object, NSObject *__weak _weakObj, __Block_byref_age_0 *_age, __Block_byref_person_1 *_person, __Block_byref_weakPerson_2 *_weakPerson, int flags=0) : number(_number), object(_object), weakObj(_weakObj), age(_age->__forwarding), person(_person->__forwarding), weakPerson(_weakPerson->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

上述__main_block_impl_0结构体中看出,没有使用__block修饰的变量(object 和 weadObj)则根据他们本身被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捕获的对象类型的变量。weakPerson是弱指针,所以__Block_byref_weakPerson_2weakPerson就是弱引用,person是强指针,所以__Block_byref_person_1对person就是强引用。

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_object_assign((void*)&dst->object, (void*)src->object, 3);
    _Block_object_assign((void*)&dst->weakObj, (void*)src->weakObj, 3);
    _Block_object_assign((void*)&dst->person, (void*)src->person, 8);
    _Block_object_assign((void*)&dst->weakPerson, (void*)src->weakPerson, 8);
}

__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_object_dispose((void*)src->object, 3);
    _Block_object_dispose((void*)src->weakObj, 3);
    _Block_object_dispose((void*)src->person, 8);
    _Block_object_dispose((void*)src->weakPerson, 8);
    
}

被__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结构体及其声明

__Block_byref_person_0结构体

typedef void (*Block)(void);
struct __Block_byref_person_0 {
  void *__isa;  // 8 内存空间
__Block_byref_person_0 *__forwarding; // 8
 int __flags; // 4
 int __size;  // 4
 void (*__Block_byref_id_object_copy)(void*, void*); // 8
 void (*__Block_byref_id_object_dispose)(void*); // 8
 Person *__strong person; // 8
};
// 8 + 8 + 4 + 4 + 8 + 8 + 8 = 48 
// __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对象进行强引用或者弱引用。

强引用示意图

如果使用__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内部的变量是强引用还是弱引用。

弱引用示意图

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

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;的引用。以保证结构体和结构体内部的对象可以正常释放。

8.循环引用
8.1循环引用导致内存泄漏

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

运行代码打印内容

block的copy[55423:9158212] 大括号结束啦

可以发现大括号结束之后,person依然没有被释放,产生了循环引用。

Person对象和block对象相互之间产生了强引用,导致双方都不会被释放,进而造成内存泄漏。

8.2 解决循环引用问题 - ARC

首先为了能随时执行block,我们肯定希望person对block对强引用,而block内部对person的引用为弱引用最好。

使用__weak 和 __unsafe_unretained修饰符可以解决循环引用的问题

我们上面也提到过__weak会使block内部将指针变为弱指针。block对person对象为弱指针的话,也就不会出现相互引用而导致不会被释放了。

使用`__weak`和`__unsafe_unretained`修饰

__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也可以解决循环引用

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

8.3 __strong 和 __weak

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


在block内部重新使用__strong修饰self变量是为了在block内部有一个强指针指向weakSelf避免在block调用的时候weakSelf已经被销毁。主要是在多线程中使用 , 实际开发环境中 , 线程比较简单 , __strong ,一般可省略. 还是用一个例子来说明吧.

如果block里面不加__strong __typeof(weakSelf)strongSelf = weakSelf会如何呢?

 
#import "ViewController.h"
#import "Student.h"
 
@interface ViewController ()
@end
 
@implementation ViewController
 
- (void)viewDidLoad {
    [super viewDidLoad];
 
    Student *student = [[Student alloc]init];
    student.name = @"Hello World";
    __weak typeof(student) weakStu = student;
    
    student.study = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"my name is = %@",weakStu.name);
        });
    };
 
    student.study();
}


输出:

my name is = (null)
为什么输出是这样的呢?

重点就在dispatch_after这个函数里面。在study()的block结束之后,student被自动释放了。又由于dispatch_after里面捕获的__weak的student,根据__weak的实现原理,在原对象释放之后,__weak对象就会变成null,防止野指针。所以就输出了null了。

那么我们怎么才能在weakSelf之后,block里面还能继续使用weakSelf之后的对象呢?

究其根本原因就是weakSelf之后,无法控制什么时候会被释放,为了保证在block内不会被释放,需要添加__strong。

在block里面使用的__strong修饰的weakSelf是为了在函数生命周期中防止self提前释放。strongSelf是一个自动变量当block执行完毕就会释放自动变量strongSelf不会对self进行一直进行强引用。

#import "ViewController.h"
#import "Student.h"
 
@interface ViewController ()
@end
 
@implementation ViewController
 
- (void)viewDidLoad {
    [super viewDidLoad];
 
    Student *student = [[Student alloc]init];
    
    student.name = @"Hello World";
    __weak typeof(student) weakSelf = student;
    
    student.study = ^{
        __strong typeof(student) strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"my name is = %@",strongSelf.name);
        });
        
    };
 
    student.study();
}


输出

my name is = Hello World
 
至此,我们就明白了weakSelf、strongSelf的用途了。

weakSelf 是为了block不持有self,避免Retain Circle循环引用。在 Block 内如果需要访问 self 的方法、变量,建议使用 weakSelf。

strongSelf的目的是因为一旦进入block执行,假设不允许self在这个执行过程中释放,就需要加入strongSelf。block执行完后这个strongSelf 会自动释放,没有不会存在循环引用问题。如果在 Block 内有多线程的或者block的嵌套访问 self,则需要使用 strongSelf。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!