深入研究Block用weakSelf、strongSelf、@weakify、@strongify解决循环引用

前言

在上篇中,仔细分析了一下Block的实现原理以及__block捕获外部变量的原理。然而实际使用Block过程中,还是会遇到一些问题,比如Retain Circle的问题。

目录

  • 1.Retain Circle的由来
  • 2.__weak、__strong的实现原理
  • 3.weakSelf、strongSelf的用途
  • 4.@weakify、@strongify实现原理

一. Retain Circle的由来

循环引用的问题相信大家都很理解了,这里还是简单的提一下。

当A对象里面强引用了B对象,B对象又强引用了A对象,这样两者的retainCount值一直都无法为0,于是内存始终无法释放,导致内存泄露。所谓的内存泄露就是本应该释放的对象,在其生命周期结束之后依旧存在。

这是2个对象之间的,相应的,这种循环还能存在于3,4……个对象之间,只要相互形成环,就会导致Retain Cicle的问题。

当然也存在自身引用自身的,当一个对象内部的一个obj,强引用的自身,也会导致循环引用的问题出现。常见的就是block里面引用的问题。

二.__weak、__strong的实现原理

在ARC环境下,id类型和对象类型和C语言其他类型不同,类型前必须加上所有权的修饰符。

所有权修饰符总共有4种:

1.__strong修饰符
2.__weak修饰符
3.__unsafe_unretained修饰符
4.__autoreleasing修饰符

一般我们如果不写,默认的修饰符是__strong。

要想弄清楚__strong,__weak的实现原理,我们就需要研究研究clang(LLVM编译器)和objc4 Objective-C runtime库了。

关于clang有一份关于ARC详细的文档,有兴趣的可以仔细研究一下文档里面的说明和例子,很有帮助。

以下的讲解,也会来自于上述文档中的函数说明。

1.__strong的实现原理

(1)对象持有自己

首先我们先来看看生成的对象持有自己的情况,利用alloc/new/copy/mutableCopy生成对象。

当我们声明了一个__strong对象

 
  1. {

  2. id __strong obj = [[NSObject alloc] init];

  3. }

LLVM编译器会把上述代码转换成下面的样子

id __attribute__((objc_ownership(strong))) obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));

相应的会调用

 
  1. id obj = objc_msgSend(NSObject, @selector(alloc));

  2. objc_msgSend(obj,selector(init));

  3. objc_release(obj);

上述这些方法都好理解。在ARC有效的时候就会自动插入release代码,在作用域结束的时候自动释放。

(2)对象不持有自己

生成对象的时候不用alloc/new/copy/mutableCopy等方法。

 
  1. {

  2. id __strong obj = [NSMutableArray array];

  3. }

LLVM编译器会把上述代码转换成下面的样子

id __attribute__((objc_ownership(strong))) array = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"));

查看LLVM文档,其实是下述的过程

相应的会调用

 
  1. id obj = objc_msgSend(NSMutableArray, @selector(array));

  2. objc_retainAutoreleasedReturnValue(obj);

  3. objc_release(obj);

与之前对象会持有自己的情况不同,这里多了一个objc_retainAutoreleasedReturnValue函数。

这里有3个函数需要说明:
1.id objc_retainAutoreleaseReturnValue(id value)

id objc_retainAutoreleaseReturnValue(id value);
Precondition: value is null or a pointer to a valid object.

If value is null, this call has no effect. Otherwise, it performs a retain operation followed by the operation described in objc_autoreleaseReturnValue.

Equivalent to the following code:
id objc_retainAutoreleaseReturnValue(id value) {
return objc_autoreleaseReturnValue(objc_retain(value));
}

Always returns value

2.id objc_retainAutoreleasedReturnValue(id value)

id objc_retainAutoreleasedReturnValue(id value);
Precondition: value is null or a pointer to a valid object.

If value is null, this call has no effect. Otherwise, it attempts to accept a hand off of a retain count from a call to objc_autoreleaseReturnValue on value in a recently-called function or something it calls. If that fails, it performs a retain operation exactly like objc_retain.

Always returns value

3.id objc_autoreleaseReturnValue(id value)

id objc_autoreleaseReturnValue(id value);
Precondition: value is null or a pointer to a valid object.

If value is null, this call has no effect. Otherwise, it makes a best effort to hand off ownership of a retain count on the object to a call toobjc_retainAutoreleasedReturnValue for the same object in an enclosing call frame. If this is not possible, the object is autoreleased as above.

Always returns value

这3个函数其实都是在描述一件事情。 it makes a best effort to hand off ownership of a retain count on the object to a call to objc_retainAutoreleasedReturnValue for the same object in an enclosing call frame。

这属于LLVM编译器的一个优化。objc_retainAutoreleasedReturnValue函数是用于自己持有(retain)对象的函数,它持有的对象应为返回注册在autoreleasepool中对象的方法或者是函数的返回值。

在ARC中原本对象生成之后是要注册到autoreleasepool中,但是调用了objc_autoreleasedReturnValue 之后,紧接着调用了 objc_retainAutoreleasedReturnValue,objc_autoreleasedReturnValue函数会去检查该函数方法或者函数调用方的执行命令列表,如果里面有objc_retainAutoreleasedReturnValue()方法,那么该对象就直接返回给方法或者函数的调用方。达到了即使对象不注册到autoreleasepool中,也可以返回拿到相应的对象。

2.__weak的实现原理

声明一个__weak对象

 
  1. {

  2. id __weak obj = strongObj;

  3. }

假设这里的strongObj是一个已经声明好了的对象。

LLVM转换成对应的代码

id __attribute__((objc_ownership(none))) obj1 = strongObj;

相应的会调用

 
  1. id obj ;

  2. objc_initWeak(&obj,strongObj);

  3. objc_destoryWeak(&obj);

看看文档描述

id objc_initWeak(id *object, id value);
Precondition: object is a valid pointer which has not been registered as a __weak object.

value is null or a pointer to a valid object.
If value is a null pointer or the object to which it points has begun deallocation, object is zero-initialized. Otherwise, object
is registered as a __weak object pointing to value

Equivalent to the following code:
id objc_initWeak(id object, id value) { object = nil;
return objc_storeWeak(object, value);
}

Returns the value of object after the call.
Does not need to be atomic with respect to calls to objc_storeWeak on object

objc_initWeak的实现其实是这样的

 
  1. id objc_initWeak(id *object, id value) {

  2. *object = nil;

  3. return objc_storeWeak(object, value);

  4. }

会把传入的object变成0或者nil,然后执行objc_storeWeak函数。

那么objc_destoryWeak函数是干什么的呢?

void objc_destroyWeak(id *object);
Precondition: object is a valid pointer which either contains a null pointer or has been registered as a __weak object.

object is unregistered as a weak object, if it ever was. The current value of object is left unspecified; otherwise, equivalent to the following code:

void objc_destroyWeak(id *object) {
objc_storeWeak(object, nil);
}

Does not need to be atomic with respect to calls to objc_storeWeak on object

objc_destoryWeak函数的实现

 
  1. void objc_destroyWeak(id *object) {

  2. objc_storeWeak(object, nil);

  3. }

也是会去调用objc_storeWeak函数。objc_initWeak和objc_destroyWeak函数都会去调用objc_storeWeak函数,唯一不同的是调用的入参不同,一个是value,一个是nil。

那么重点就都落在objc_storeWeak函数上了。

id objc_storeWeak(id *object, id value);
Precondition: object is a valid pointer which either contains a null pointer or has been registered as a __weak object. value
is null or a pointer to a valid object.

If value is a null pointer or the object to which it points has begun deallocation, object is assigned null and unregistered as a __weak object. Otherwise, object is registered as a __weak object or has its registration updated to point to value

Returns the value of object after the call.

objc_storeWeak函数的用途就很明显了。由于weak表也是用Hash table实现的,所以objc_storeWeak函数就把第一个入参的变量地址注册到weak表中,然后根据第二个入参来决定是否移除。如果第二个参数为0,那么就把__weak变量从weak表中删除记录,并从引用计数表中删除对应的键值记录。

所以如果__weak引用的原对象如果被释放了,那么对应的__weak对象就会被指为nil。原来就是通过objc_storeWeak函数这些函数来实现的。

以上就是ARC中__strong和__weak的简单的实现原理,更加详细的还请大家去看看这一章开头提到的那个LLVM文档,里面说明的很详细。

三.weakSelf、strongSelf的用途

在提weakSelf、strongSelf之前,我们先引入一个Retain Cicle的例子。

假设自定义的一个student类

例子1:

 
  1. #import <Foundation/Foundation.h>

  2. typedef void(^Study)();

  3. @interface Student : NSObject

  4. @property (copy , nonatomic) NSString *name;

  5. @property (copy , nonatomic) Study study;

  6. @end

 
  1. #import "ViewController.h"

  2. #import "Student.h"

  3.  
  4. @interface ViewController ()

  5. @end

  6.  
  7. @implementation ViewController

  8.  
  9. - (void)viewDidLoad {

  10. [super viewDidLoad];

  11.  
  12. Student *student = [[Student alloc]init];

  13. student.name = @"Hello World";

  14.  
  15. student.study = ^{

  16. NSLog(@"my name is = %@",student.name);

  17. };

  18. }

到这里,大家应该看出来了,这里肯定出现了循环引用了。student的study的Block里面强引用了student自身。根据上篇文章的分析,可以知道,_NSConcreteMallocBlock捕获了外部的对象,会在内部持有它。retainCount值会加一。

我们用Instruments来观察一下。添加Leak观察器。

当程序运行起来之后,在Leak Checks观察器里面应该可以看到红色的❌,点击它就会看到内存leak了。有2个泄露的对象。Block和Student相互循环引用了。

打开Cycles & Roots 观察一下循环的环。

这里形成环的原因block里面持有student本身,student本身又持有block。

那再看一个例子2:

 
  1. #import "ViewController.h"

  2. #import "Student.h"

  3.  
  4. @interface ViewController ()

  5. @end

  6.  
  7. @implementation ViewController

  8.  
  9. - (void)viewDidLoad {

  10. [super viewDidLoad];

  11.  
  12. Student *student = [[Student alloc]init];

  13. student.name = @"Hello World";

  14.  
  15. student.study = ^(NSString * name){

  16. NSLog(@"my name is = %@",name);

  17. };

  18. student.study(student.name);

  19. }

我把block新传入一个参数,传入的是student.name。这个时候会引起循环引用么?

答案肯定是不会。

如上图,并不会出现内存泄露。原因是因为,student是作为形参传递进block的,block并不会捕获形参到block内部进行持有。所以肯定不会造成循环引用。

再改一下。看例子3:

 
  1. #import "ViewController.h"

  2. #import "Student.h"

  3.  
  4. @interface ViewController ()

  5. @property (copy,nonatomic) NSString *name;

  6. @property (strong, nonatomic) Student *stu;

  7. @end

  8.  
  9. @implementation ViewController

  10.  
  11. - (void)viewDidLoad {

  12. [super viewDidLoad];

  13.  
  14. Student *student = [[Student alloc]init];

  15.  
  16. self.name = @"halfrost";

  17. self.stu = student;

  18.  
  19. student.study = ^{

  20. NSLog(@"my name is = %@",self.name);

  21. };

  22.  
  23. student.study();

  24. }

这样会形成循环引用么?

答案也是否定的。

ViewController虽然强引用着student,但是student里面的blcok强引用的是viewController的name属性,并没有形成环。如果把上述的self.name改成self,也依旧不会产生循环引用。因为他们都没有强引用这个block。

那遇到循环引用我们改如何处理呢??类比平时我们经常写的delegate,可以知道,只要有一边是__weak就可以打破循环。

先说一种做法,利用__block解决循环的做法。例子4:

 
  1. #import "ViewController.h"

  2. #import "Student.h"

  3.  
  4. @interface ViewController ()

  5. @end

  6.  
  7. @implementation ViewController

  8.  
  9. - (void)viewDidLoad {

  10. [super viewDidLoad];

  11.  
  12. Student *student = [[Student alloc]init];

  13.  
  14. __block Student *stu = student;

  15. student.name = @"Hello World";

  16. student.study = ^{

  17. NSLog(@"my name is = %@",stu.name);

  18. stu = nil;

  19. };

  20. }

这样写会循环么?看上去应该不会。但是实际上却是会的。

由于没有执行study这个block,现在student持有该block,block持有__block变量,__block变量又持有student对象。3者形成了环,导致了循环引用了。
想打破环就需要破坏掉其中一个引用。__block不持有student即可。

只需要执行一下block即可。例子5:

 
  1. #import "ViewController.h"

  2. #import "Student.h"

  3.  
  4. @interface ViewController ()

  5. @end

  6.  
  7. @implementation ViewController

  8.  
  9. - (void)viewDidLoad {

  10. [super viewDidLoad];

  11.  
  12. Student *student = [[Student alloc]init];

  13. student.name = @"Hello World";

  14. __block Student *stu = student;

  15.  
  16. student.study = ^{

  17. NSLog(@"my name is = %@",stu.name);

  18. stu = nil;

  19. };

  20.  
  21. student.study();

  22. }

这样就不会循环引用了。

使用__block解决循环引用虽然可以控制对象持有时间,在block中还能动态的控制是__block变量的值,可以赋值nil,也可以赋值其他的值,但是有一个唯一的缺点就是需要执行一次block才行。否则还是会造成循环引用。

值得注意的是,在ARC下__block会导致对象被retain,有可能导致循环引用。而在MRC下,则不会retain这个对象,也不会导致循环引用。

接下来可以正式开始讲讲weakSelf 和 strongSelf的用法了。

1.weakSelf

说道weakSelf,需要先来区分几种写法。
__weak __typeof(self)weakSelf = self; 这是AFN里面的写法。。

#define WEAKSELF typeof(self) __weak weakSelf = self; 这是我们平时的写法。。

先区分__typeof() 和 typeof()
由于笔者一直很崇拜AFNetWorking的作者,这个库里面的代码都很整洁,里面各方面的代码都可以当做代码范本来阅读。遇到不懂疑惑的,都要深究,肯定会有收获。这里就是一处,平时我们的写法是不带__的,AFN里面用这种写法有什么特殊的用途么?

在SOF上能找到相关的答案

__typeof__() and __typeof() are compiler-specific extensions to the C language, because standard C does not include such an operator. Standard C requires compilers to prefix language extensions with a double-underscore (which is also why you should never do so for your own functions, variables, etc.)
typeof() is exactly the same, but throws the underscores out the window with the understanding that every modern compiler supports it. (Actually, now that I think about it, Visual C++ might not. It does support decltype() though, which generally provides the same behaviour as typeof().)
All three mean the same thing, but none are standard C so a conforming compiler may choose to make any mean something different.

其实两者都是一样的东西,只不过是C里面不同的标准,兼容性不同罢了。

更加详细的官方说明

那么抽象出来就是这2种写法。
#define WEAKSELF __weak typeof(self)weakSelf = self;
#define WEAKSELF typeof(self) __weak weakSelf = self;

这样子看就清楚了,两种写法就是完全一样的。

我们可以用WEAKSELF来解决循环引用的问题。例子6:

 
  1. #import "ViewController.h"

  2. #import "Student.h"

  3.  
  4. @interface ViewController ()

  5. @end

  6.  
  7. @implementation ViewController

  8.  
  9. - (void)viewDidLoad {

  10. [super viewDidLoad];

  11.  
  12. Student *student = [[Student alloc]init];

  13. student.name = @"Hello World";

  14. __weak typeof(student) weakSelf = student;

  15.  
  16. student.study = ^{

  17. NSLog(@"my name is = %@",weakSelf.name);

  18. };

  19.  
  20. student.study();

  21. }

这样就解决了循环引用的问题了。

解决循环应用的问题一定要分析清楚哪里出现了循环引用,只需要把其中一环加上weakSelf这类似的宏,就可以解决循环引用。如果分析不清楚,就只能无脑添加weakSelf、strongSelf,这样的做法不可取。

在上面的例子3中,就完全不存在循环引用,要是无脑加weakSelf、strongSelf是不对的。在例子6中,也只需要加一个weakSelf就可以了,也不需要加strongSelf。

曾经在segmentfault也看到过这样一个问题,问:为什么iOS的Masonry中的self不会循环引用?

 
  1. UIButton *testButton = [[UIButton alloc] init];

  2. [self.view addSubview:testButton];

  3. testButton.backgroundColor = [UIColor redColor];

  4. [testButton mas_makeConstraints:^(MASConstraintMaker *make) {

  5. make.width.equalTo(@100);

  6. make.height.equalTo(@100);

  7. make.left.equalTo(self.view.mas_left);

  8. make.top.equalTo(self.view.mas_top);

  9. }];

  10. [testButton bk_addEventHandler:^(id sender) {

  11. [self dismissViewControllerAnimated:YES completion:nil];

  12. } forControlEvents:UIControlEventTouchUpInside];

如果我用blocksKit的bk_addEventHandler
方法, 其中使用strong self, 该viewController就无法dealloc, 我理解是因为,self retain self.view, retain testButton, retain self. 但是如果只用Mansonry的mas_makeConstraints
方法, 同样使用strong self, 该viewController却能正常dealloc, 请问这是为什么, 为什么Masonry没有导致循环引用?

看到这里,读者应该就应该能回答这个问题了。

 
  1. - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {

  2. self.translatesAutoresizingMaskIntoConstraints = NO;

  3. MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];

  4. block(constraintMaker);

  5. return [constraintMaker install];

  6. }

在Masonry这个block中,block仅仅捕获了self的translatesAutoresizingMaskIntoConstraints变量,但是并没有持有self。

上述描述有误,感谢@酷酷的哀殿 耐心指点

更正如下:

关于 Masonry ,它捕获了变量 self,然后对其执行了setTranslatesAutoresizingMaskIntoConstraints:方法。但是,因为执行完毕后,block会被销毁,没有形成环。所以,没有引起循环依赖。

2.strongSelf

上面介绍完了weakSelf,既然weakSelf能完美解决Retain Circle的问题了,那为何还需要strongSelf呢?

还是先从AFN经典说起,以下是AFN其中的一段代码:

 
  1. #pragma mark - NSOperation

  2.  
  3. - (void)setCompletionBlock:(void (^)(void))block {

  4. [self.lock lock];

  5. if (!block) {

  6. [super setCompletionBlock:nil];

  7. } else {

  8. __weak __typeof(self)weakSelf = self;

  9. [super setCompletionBlock:^ {

  10. __strong __typeof(weakSelf)strongSelf = weakSelf;

  11.  
  12. #pragma clang diagnostic push

  13. #pragma clang diagnostic ignored "-Wgnu"

  14. dispatch_group_t group = strongSelf.completionGroup ?: url_request_operation_completion_group();

  15. dispatch_queue_t queue = strongSelf.completionQueue ?: dispatch_get_main_queue();

  16. #pragma clang diagnostic pop

  17.  
  18. dispatch_group_async(group, queue, ^{

  19. block();

  20. });

  21.  
  22. dispatch_group_notify(group, url_request_operation_completion_queue(), ^{

  23. [strongSelf setCompletionBlock:nil];

  24. });

  25. }];

  26. }

  27. [self.lock unlock];

  28. }

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

 
  1. #import "ViewController.h"

  2. #import "Student.h"

  3.  
  4. @interface ViewController ()

  5. @end

  6.  
  7. @implementation ViewController

  8.  
  9. - (void)viewDidLoad {

  10. [super viewDidLoad];

  11.  
  12. Student *student = [[Student alloc]init];

  13. student.name = @"Hello World";

  14. __weak typeof(student) weakSelf = student;

  15.  
  16. student.study = ^{

  17. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

  18. NSLog(@"my name is = %@",weakSelf.name);

  19. });

  20. };

  21.  
  22. student.study();

  23. }

输出:

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进行一直进行强引用。

 
  1. #import "ViewController.h"

  2. #import "Student.h"

  3.  
  4. @interface ViewController ()

  5. @end

  6.  
  7. @implementation ViewController

  8.  
  9. - (void)viewDidLoad {

  10. [super viewDidLoad];

  11.  
  12. Student *student = [[Student alloc]init];

  13.  
  14. student.name = @"Hello World";

  15. __weak typeof(student) weakSelf = student;

  16.  
  17. student.study = ^{

  18. __strong typeof(student) strongSelf = weakSelf;

  19. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

  20. NSLog(@"my name is = %@",strongSelf.name);

  21. });

  22.  
  23. };

  24.  
  25. student.study();

  26. }

输出

my name is = Hello World

至此,我们就明白了weakSelf、strongSelf的用途了。

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

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

关于Retain Circle最后总结一下,有3种方式可以解决循环引用。

结合《Effective Objective-C 2.0》(编写高质量iOS与OS X代码的52个有效方法)这本书的例子,来总结一下。

EOCNetworkFetcher.h

 
  1. typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);

  2.  
  3. @interface EOCNetworkFetcher : NSObject

  4.  
  5. @property (nonatomic, strong, readonly) NSURL *url;

  6.  
  7. - (id)initWithURL:(NSURL *)url;

  8.  
  9. - (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;

  10.  
  11. @end

EOCNetworkFetcher.m

 
  1. @interface EOCNetworkFetcher ()

  2.  
  3. @property (nonatomic, strong, readwrite) NSURL *url;

  4. @property (nonatomic, copy) EOCNetworkFetcherCompletionHandler completionHandler;

  5. @property (nonatomic, strong) NSData *downloadData;

  6.  
  7. @end

  8.  
  9. @implementation EOCNetworkFetcher

  10.  
  11. - (id)initWithURL:(NSURL *)url {

  12. if(self = [super init]) {

  13. _url = url;

  14. }

  15. return self;

  16. }

  17.  
  18. - (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion {

  19. self.completionHandler = completion;

  20. //开始网络请求

  21. dispatch_async(dispatch_get_global_queue(0, 0), ^{

  22. _downloadData = [[NSData alloc] initWithContentsOfURL:_url];

  23. dispatch_async(dispatch_get_main_queue(), ^{

  24. //网络请求完成

  25. [self p_requestCompleted];

  26. });

  27. });

  28. }

  29.  
  30. - (void)p_requestCompleted {

  31. if(_completionHandler) {

  32. _completionHandler(_downloadData);

  33. }

  34. }

  35.  
  36. @end

EOCClass.m

 
  1. @implementation EOCClass {

  2. EOCNetworkFetcher *_networkFetcher;

  3. NSData *_fetchedData;

  4. }

  5.  
  6. - (void)downloadData {

  7. NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"];

  8. _networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];

  9. [_networkFetcher startWithCompletionHandler:^(NSData *data) {

  10. _fetchedData = data;

  11. }];

  12. }

  13. @end

在这个例子中,存在3者之间形成环

1、completion handler的block因为要设置_fetchedData实例变量的值,所以它必须捕获self变量,也就是说handler块保留了EOCClass实例;

2、EOCClass实例通过strong实例变量保留了EOCNetworkFetcher,最后EOCNetworkFetcher实例对象也会保留了handler的block。

书上说的3种方法来打破循环。

方法一:手动释放EOCNetworkFetcher使用之后持有的_networkFetcher,这样可以打破循环引用

 
  1. - (void)downloadData {

  2. NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"];

  3. _networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];

  4. [_networkFetcher startWithCompletionHandler:^(NSData *data) {

  5. _fetchedData = data;

  6. _networkFetcher = nil;//加上此行,打破循环引用

  7. }];

  8. }

方法二:直接释放block。因为在使用完对象之后需要人为手动释放,如果忘记释放就会造成循环引用了。如果使用完completion handler之后直接释放block即可。打破循环引用

 
  1. - (void)p_requestCompleted {

  2. if(_completionHandler) {

  3. _completionHandler(_downloadData);

  4. }

  5. self.completionHandler = nil;//加上此行,打破循环引用

  6. }

方法三:使用weakSelf、strongSelf

 
  1. - (void)downloadData {

  2. __weak __typeof(self) weakSelf = self;

  3. NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"];

  4. _networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];

  5. [_networkFetcher startWithCompletionHandler:^(NSData *data) {

  6. __typeof(&*weakSelf) strongSelf = weakSelf;

  7. if (strongSelf) {

  8. strongSelf.fetchedData = data;

  9. }

  10. }];

  11. }

四.@weakify、@strongify实现原理

上面讲完了weakSelf、strongSelf之后,接下来再讲讲@weakify、@strongify,这两个关键字是RAC中避免Block循环引用而开发的2个宏,这2个宏的实现过程很牛,值得我们学习。

@weakify、@strongify的作用和weakSelf、strongSelf对应的一样。这里我们具体看看大神是怎么实现这2个宏的。

直接从源码看起来。

 
  1. #define weakify(...) \

  2. rac_keywordify \

  3. metamacro_foreach_cxt(rac_weakify_,, __weak, __VA_ARGS__)

  4.  
  5.  
  6. #define strongify(...) \

  7. rac_keywordify \

  8. _Pragma("clang diagnostic push") \

  9. _Pragma("clang diagnostic ignored \"-Wshadow\"") \

  10. metamacro_foreach(rac_strongify_,, __VA_ARGS__) \

  11. _Pragma("clang diagnostic pop")

看到这种宏定义,咋一看什么都不知道。那就只能一层层的往下看。

1. weakify

先从weakify(...)开始。

 
  1. #if DEBUG

  2. #define rac_keywordify autoreleasepool {}

  3. #else

  4. #define rac_keywordify try {} @catch (...) {}

  5. #endif

这里在debug模式下使用@autoreleasepool是为了维持编译器的分析能力,而使用@try/@catch 是为了防止插入一些不必要的autoreleasepool。rac_keywordify 实际上就是autoreleasepool {}
的宏替换。因为有了autoreleasepool {}的宏替换,所以weakify要加上@,形成@autoreleasepool {}。

 
  1. #define metamacro_foreach_cxt(MACRO, SEP, CONTEXT, ...) \

  2. metamacro_concat(metamacro_foreach_cxt, metamacro_argcount(__VA_ARGS__))(MACRO, SEP, CONTEXT, __VA_ARGS__)

__VA_ARGS__:总体来说就是将左边宏中 ... 的内容原样抄写在右边 __VA_ARGS__ 所在的位置。它是一个可变参数的宏,是新的C99规范中新增的,目前似乎只有gcc支持(VC从VC2005开始支持)。

那么我们使用@weakify(self)传入进去。__VA_ARGS__相当于self。此时我们可以把最新开始的weakify套下来。于是就变成了这样:

rac_weakify_,, __weak, __VA_ARGS__整体替换MACRO, SEP, CONTEXT, ...

这里需要注意的是,源码中就是给的两个","逗号是连着的,所以我们也要等效替换参数,相当于SEP是空值。

替换完成之后就是下面这个样子:

 
  1. autoreleasepool {}

  2. metamacro_concat(metamacro_foreach_cxt, metamacro_argcount(self))(rac_weakify_, , __weak, self)

现在我们需要弄懂的就是metamacro_concat 和 metamacro_argcount是干什么用的。

继续看看metamacro_concat 的实现

 
  1. #define metamacro_concat(A, B) \

  2. metamacro_concat_(A, B)

  3.  
  4.  
  5. #define metamacro_concat_(A, B) A ## B

## 是宏连接符。举个例子:

假设宏定义为#define XNAME(n) x##n,代码为:XNAME(4),则在预编译时,宏发现XNAME(4)与XNAME(n)匹配,则令 n 为 4,然后将右边的n的内容也变为4,然后将整个XNAME(4)替换为 x##n,亦即 x4,故 最终结果为 XNAME(4) 变为 x4。所以A##B就是AB。

metamacro_argcount 的实现

 
  1. #define metamacro_argcount(...) \

  2. metamacro_at(20, __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)

  3.  
  4.  
  5. #define metamacro_at(N, ...) \

  6. metamacro_concat(metamacro_at, N)(__VA_ARGS__)

metamacro_concat是上面讲过的连接符,那么metamacro_at, N = metamacro_atN,由于N = 20,于是metamacro_atN = metamacro_at20。

 
  1. #define metamacro_at0(...) metamacro_head(__VA_ARGS__)

  2. #define metamacro_at1(_0, ...) metamacro_head(__VA_ARGS__)

  3. #define metamacro_at2(_0, _1, ...) metamacro_head(__VA_ARGS__)

  4. #define metamacro_at3(_0, _1, _2, ...) metamacro_head(__VA_ARGS__)

  5. #define metamacro_at4(_0, _1, _2, _3, ...) metamacro_head(__VA_ARGS__)

  6. #define metamacro_at5(_0, _1, _2, _3, _4, ...) metamacro_head(__VA_ARGS__)

  7. #define metamacro_at6(_0, _1, _2, _3, _4, _5, ...) metamacro_head(__VA_ARGS__)

  8. #define metamacro_at7(_0, _1, _2, _3, _4, _5, _6, ...) metamacro_head(__VA_ARGS__)

  9. #define metamacro_at8(_0, _1, _2, _3, _4, _5, _6, _7, ...) metamacro_head(__VA_ARGS__)

  10. #define metamacro_at9(_0, _1, _2, _3, _4, _5, _6, _7, _8, ...) metamacro_head(__VA_ARGS__)

  11. #define metamacro_at10(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, ...) metamacro_head(__VA_ARGS__)

  12. #define metamacro_at11(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, ...) metamacro_head(__VA_ARGS__)

  13. #define metamacro_at12(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, ...) metamacro_head(__VA_ARGS__)

  14. #define metamacro_at13(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, ...) metamacro_head(__VA_ARGS__)

  15. #define metamacro_at14(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, ...) metamacro_head(__VA_ARGS__)

  16. #define metamacro_at15(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, ...) metamacro_head(__VA_ARGS__)

  17. #define metamacro_at16(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, ...) metamacro_head(__VA_ARGS__)

  18. #define metamacro_at17(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, ...) metamacro_head(__VA_ARGS__)

  19. #define metamacro_at18(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, ...) metamacro_head(__VA_ARGS__)

  20. #define metamacro_at19(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, ...) metamacro_head(__VA_ARGS__)

  21. #define metamacro_at20(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, ...) metamacro_head(__VA_ARGS__)

metamacro_at20的作用就是截取前20个参数,剩下的参数传入metamacro_head。

 
  1. #define metamacro_head(...) \

  2. metamacro_head_(__VA_ARGS__, 0)

  3.  
  4.  
  5. #define metamacro_head_(FIRST, ...) FIRST

metamacro_head的作用返回第一个参数。返回到上一级metamacro_at20,如果我们从最源头的@weakify(self),传递进来,那么metamacro_at20(self,20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1),截取前20个参数,最后一个留给metamacro_head_(1),那么就应该返回1。

metamacro_concat(metamacro_foreach_cxt, metamacro_argcount(self)) = metamacro_concat(metamacro_foreach_cxt, 1) 最终可以替换成metamacro_foreach_cxt1。

在源码中继续搜寻。

 
  1. // metamacro_foreach_cxt expansions

  2. #define metamacro_foreach_cxt0(MACRO, SEP, CONTEXT)

  3. #define metamacro_foreach_cxt1(MACRO, SEP, CONTEXT, _0) MACRO(0, CONTEXT, _0)

  4.  
  5. #define metamacro_foreach_cxt2(MACRO, SEP, CONTEXT, _0, _1) \

  6. metamacro_foreach_cxt1(MACRO, SEP, CONTEXT, _0) \

  7. SEP \

  8. MACRO(1, CONTEXT, _1)

  9.  
  10. #define metamacro_foreach_cxt3(MACRO, SEP, CONTEXT, _0, _1, _2) \

  11. metamacro_foreach_cxt2(MACRO, SEP, CONTEXT, _0, _1) \

  12. SEP \

  13. MACRO(2, CONTEXT, _2)

  14.  
  15. #define metamacro_foreach_cxt4(MACRO, SEP, CONTEXT, _0, _1, _2, _3) \

  16. metamacro_foreach_cxt3(MACRO, SEP, CONTEXT, _0, _1, _2) \

  17. SEP \

  18. MACRO(3, CONTEXT, _3)

  19.  
  20. #define metamacro_foreach_cxt5(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4) \

  21. metamacro_foreach_cxt4(MACRO, SEP, CONTEXT, _0, _1, _2, _3) \

  22. SEP \

  23. MACRO(4, CONTEXT, _4)

  24.  
  25. #define metamacro_foreach_cxt6(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5) \

  26. metamacro_foreach_cxt5(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4) \

  27. SEP \

  28. MACRO(5, CONTEXT, _5)

  29.  
  30. #define metamacro_foreach_cxt7(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6) \

  31. metamacro_foreach_cxt6(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5) \

  32. SEP \

  33. MACRO(6, CONTEXT, _6)

  34.  
  35. #define metamacro_foreach_cxt8(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7) \

  36. metamacro_foreach_cxt7(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6) \

  37. SEP \

  38. MACRO(7, CONTEXT, _7)

  39.  
  40. #define metamacro_foreach_cxt9(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8) \

  41. metamacro_foreach_cxt8(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7) \

  42. SEP \

  43. MACRO(8, CONTEXT, _8)

  44.  
  45. #define metamacro_foreach_cxt10(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9) \

  46. metamacro_foreach_cxt9(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8) \

  47. SEP \

  48. MACRO(9, CONTEXT, _9)

  49.  
  50. #define metamacro_foreach_cxt11(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) \

  51. metamacro_foreach_cxt10(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9) \

  52. SEP \

  53. MACRO(10, CONTEXT, _10)

  54.  
  55. #define metamacro_foreach_cxt12(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11) \

  56. metamacro_foreach_cxt11(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) \

  57. SEP \

  58. MACRO(11, CONTEXT, _11)

  59.  
  60. #define metamacro_foreach_cxt13(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12) \

  61. metamacro_foreach_cxt12(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11) \

  62. SEP \

  63. MACRO(12, CONTEXT, _12)

  64.  
  65. #define metamacro_foreach_cxt14(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13) \

  66. metamacro_foreach_cxt13(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12) \

  67. SEP \

  68. MACRO(13, CONTEXT, _13)

  69.  
  70. #define metamacro_foreach_cxt15(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14) \

  71. metamacro_foreach_cxt14(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13) \

  72. SEP \

  73. MACRO(14, CONTEXT, _14)

  74.  
  75. #define metamacro_foreach_cxt16(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15) \

  76. metamacro_foreach_cxt15(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14) \

  77. SEP \

  78. MACRO(15, CONTEXT, _15)

  79.  
  80. #define metamacro_foreach_cxt17(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16) \

  81. metamacro_foreach_cxt16(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15) \

  82. SEP \

  83. MACRO(16, CONTEXT, _16)

  84.  
  85. #define metamacro_foreach_cxt18(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17) \

  86. metamacro_foreach_cxt17(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16) \

  87. SEP \

  88. MACRO(17, CONTEXT, _17)

  89.  
  90. #define metamacro_foreach_cxt19(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18) \

  91. metamacro_foreach_cxt18(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17) \

  92. SEP \

  93. MACRO(18, CONTEXT, _18)

  94.  
  95. #define metamacro_foreach_cxt20(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19) \

  96. metamacro_foreach_cxt19(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18) \

  97. SEP \

  98. MACRO(19, CONTEXT, _19)

metamacro_foreach_cxt这个宏定义有点像递归,这里可以看到N 最大就是20,于是metamacro_foreach_cxt19就是最大,metamacro_foreach_cxt19会生成rac_weakify_(0,__weak,_18),然后再把前18个数传入metamacro_foreach_cxt18,并生成rac_weakify_(0,__weak,_17),依次类推,一直递推到metamacro_foreach_cxt0。

#define metamacro\_foreach\_cxt0(MACRO, SEP, CONTEXT)

metamacro_foreach_cxt0就是终止条件,不做任何操作了。

于是最初的@weakify就被替换成

 
  1. autoreleasepool {}

  2. metamacro_foreach_cxt1(rac_weakify_, , __weak, self)

#define metamacro_foreach_cxt1(MACRO, SEP, CONTEXT, _0) MACRO(0, CONTEXT, _0)

代入参数

 
  1. autoreleasepool {}

  2. rac_weakify_(0,__weak,self)

最终需要解析的就是rac_weakify_

 
  1. #define rac_weakify_(INDEX, CONTEXT, VAR) \

  2. CONTEXT __typeof__(VAR) metamacro_concat(VAR, _weak_) = (VAR);

把(0,__weak,self)的参数替换进来(INDEX, CONTEXT, VAR)。
INDEX = 0, CONTEXT = __weak,VAR = self,

于是

 
  1. CONTEXT __typeof__(VAR) metamacro_concat(VAR, _weak_) = (VAR);

  2.  
  3.  
  4. 等效替换为

  5.  
  6.  
  7. __weak __typeof__(self) self_weak_ = self;

最终@weakify(self) = __weak __typeof__(self) self_weak_ = self;

这里的self_weak_ 就完全等价于我们之前写的weakSelf。

2. strongify

再继续分析strongify(...)

rac_keywordify还是和weakify一样,是autoreleasepool {},只为了前面能加上@

 
  1. _Pragma("clang diagnostic push") \

  2. _Pragma("clang diagnostic ignored \"-Wshadow\"") \

  3. _Pragma("clang diagnostic pop")

strongify比weakify多了这些_Pragma语句。

关键字_Pragma是C99里面引入的。_Pragma比#pragma(在设计上)更加合理,因而功能也有所增强。

上面的等效替换

 
  1. #pragma clang diagnostic push

  2. #pragma clang diagnostic ignored "-Wshadow"

  3. #pragma clang diagnostic pop

这里的clang语句的作用:忽略当一个局部变量或类型声明遮盖另一个变量的警告。

最初的

 
  1. #define strongify(...) \

  2. rac_keywordify \

  3. _Pragma("clang diagnostic push") \

  4. _Pragma("clang diagnostic ignored \"-Wshadow\"") \

  5. metamacro_foreach(rac_strongify_,, __VA_ARGS__) \

  6. _Pragma("clang diagnostic pop")

strongify里面需要弄清楚的就是metamacro_foreach 和 rac_strongify_。

 
  1. #define metamacro_foreach(MACRO, SEP, ...) \

  2. metamacro_foreach_cxt(metamacro_foreach_iter, SEP, MACRO, __VA_ARGS__)

  3.  
  4. #define rac_strongify_(INDEX, VAR) \

  5. __strong __typeof__(VAR) VAR = metamacro_concat(VAR, _weak_);

我们先替换一次,SEP = 空 , MACRO = rac_strongify_ , __VA_ARGS__ , 于是替换成这样。

metamacro_foreach_cxt(metamacro_foreach_iter,,rac_strongify_,self)

根据之前分析,metamacro_foreach_cxt再次等效替换,metamacro_foreach_cxt##1(metamacro_foreach_iter,,rac_strongify_,self)

根据

#define metamacro_foreach_cxt1(MACRO, SEP, CONTEXT, _0) MACRO(0, CONTEXT, _0)

再次替换成metamacro_foreach_iter(0, rac_strongify_, self)

继续看看metamacro_foreach_iter的实现

#define metamacro_foreach_iter(INDEX, MACRO, ARG) MACRO(INDEX, ARG)

最终替换成rac_strongify_(0,self)

 
  1. #define rac_strongify_(INDEX, VAR) \

  2. __strong __typeof__(VAR) VAR = metamacro_concat(VAR, _weak_);

INDEX = 0, VAR = self,于是@strongify(self)就等价于

 
  1. __strong __typeof__(VAR) VAR = metamacro_concat(VAR, _weak_);

  2.  
  3. 等价于

  4.  
  5. __strong __typeof__(self) self = self_weak_;

注意@strongify(self)只能使用在block中,如果用在block外面,会报错,因为这里会提示你Redefinition of 'self'。

总结一下

@weakify(self) = @autoreleasepool{} __weak __typeof__ (self) self_weak_ = self;

@strongify(self) = @autoreleasepool{} __strong __typeof__(self) self = self_weak_;

经过分析以后,其实@weakify(self) 和 @strongify(self) 就是比我们日常写的weakSelf、strongSelf多了一个@autoreleasepool{}而已,至于为何要用这些复杂的宏定义来做,目前我还没有理解。如果有大神指导其中的原因,还请多多指点。

更新

针对文章中给的例子3,大家都提出了疑问,为何没有检测出循环引用?其实这个例子有点不好。因为这个ViewController的引用计数一出来就是6,因为它被其他很多对象引用着。当然它是强引用了student,因为student的retainCount值是2。ViewController释放的时候才会把student的值减一。针对这个例子3,我重新抽取出中间的模型,重新举一个例子。

既然ViewController特殊,那我们就新建一个类。

 
  1. #import <Foundation/Foundation.h>

  2. #import "Student.h"

  3.  
  4. @interface Teacher : NSObject

  5. @property (copy , nonatomic) NSString *name;

  6. @property (strong, nonatomic) Student *stu;

  7. @end

 
  1. #import "ViewController.h"

  2. #import "Student.h"

  3. #import "Teacher.h"

  4.  
  5. @interface ViewController ()

  6. @end

  7.  
  8. @implementation ViewController

  9.  
  10. - (void)viewDidLoad {

  11. [super viewDidLoad];

  12.  
  13. Student *student = [[Student alloc]init];

  14. Teacher *teacher = [[Teacher alloc]init];

  15.  
  16. teacher.name = @"i'm teacher";

  17. teacher.stu = student;

  18.  
  19. student.name = @"halfrost";

  20.  
  21. student.study = ^{

  22. NSLog(@"my name is = %@",teacher.name);

  23. };

  24.  
  25. student.study();

  26. }

如图所示,还是出现了循环引用,student的block强引用了teacher,teacher又强引用了student,导致两者都无法释放。


 

文/一缕殇流化隐半边冰霜(简书作者)
原文链接:http://www.jianshu.com/p/701da54bd78c
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值