OC内存管理教程之ARC(二)——自动引用计数规则

前言:

       第一篇介绍了Objective-C在MRC下的内存管理,本篇讲述ARC所引起的变化。
       实际上,“引用计数式的内存管理”的本质在ARC中并没有改变。就算开启了ARC,编译器关于对象的内存管理还是会使用引用计数的形式。只不过ARC自动帮我们处理了计数的相关部分,开发者不再需要时刻关心当前计数为几,自己在使用对象的时候有没有及时retain或者release。ARC最大的好处就是,在你没有对内存管理有很好的理解的情况下,也能愉快地开发简单的app。O__O "…我就是这种情况。。。。但不建议这样干。。。。
       在xcode里面,不仅可以设置全局是否使用ARC,甚至还可以对每个类文件进行设置是否使用ARC,在一个应用程序中混合ARC有效和无效的二进制形式。这样在引用一些年代比较久远的使用非ARC模式开发的库或者类时,就显得尤为方便。

ARC模式需要满足以下条件:
  • 使用xcode4.2或以上版本;
  • 使用LLVM编译器3.0或以上版本;
  • 编译器选项中设置ARC为有效;

所以现阶段的环境,都支持ARC(屁话,现在都已经是xcode8.3的年代了~)。只要设置成ARC有效就行了。
同时,设置了ARC有效时,有些规则是要遵守的:
  • 不能使用 retain/release/retainCount/autorelease
  • 不能使用 NSAllocateObject/NSDeallocateObject
  • 须遵守内存管理的方法命名规则
  • 不要显示调用dealloc
  • 使用 @autorelease 块代替NSAutoreleasePool
  • 不能使用区域(NSZone)
  • 对象型变量不能作为C语言结固体的成员
  • 显示转换“id”和"void *"
如果第一次接触可能不是很看得懂,有个概念即可。
从这里开始讲解的内容,只适用于ARC模式,如果需要实验,先将Xcode开启ARC模式。

所有权修饰符:

       虽然说ARC可以帮助我们管理对象在内存中的生命周期,但不代表我们就可以什么也不用管了。ARC有效时,苹果提供了几个关于所有权的修饰符给开发者使用。开发者正确地使用修饰符,可以帮助编译器的ARC机制更好地运行,更好地管理我们创建的对象。(注意是ARC有效时,以下几个修饰符都是ARC有效时使用的)
  • __strong修饰符
  • __weak修饰符
  • __unsafe_unretained修饰符
  • __autoreleasing修饰符
       我的理解是,分别把他们看作:强指针,弱指针,不安全的弱指针,自动释放指针。在非ARC模式下,编译器是通过引用计数是否为0来决定要不要废弃当前对象。而在ARC模式中,编译器是通过有没有强指针指向当前对象来决定要不要废弃当前对象。只要没有强指针指向这个对象,就废弃它。(什么?你前面不是说ARC和非ARC的本质都是引用计数吗?在这里怎么就成了强弱指针了呢?别着急,它们并不矛盾。有强指针指向的时候,引用计数+1,强指针废弃了,引用计数-1。仍然保证引用计数的机制合理运行。)

__strong修饰符:

这是默认的修饰符。即在创建对象时,什么修饰符都不写,那么它默认就被添加上了__strong修饰符成为一个强指针。
    //以下两种写法在ARC有效时是等价的
    id obj = [[NSObject alloc] init];
    id __strong obj = [[NSObject alloc] init];
那么在ARC有效时,__strong修饰符是怎么样的运行机制呢?
这里用回上篇中的Person类作为展示。
{
        Person __strong *p1 = [[Person alloc] init];
        //p1是个强指针,一般情况下都会省略__strong修饰符
        //使用p1
        //使用结束后不再需要且不允许调用[p1 release]方法
    }
    /*
     在这里p1的生命周期结束,p1被废弃
     Person对象已经没有强指针指向它了
     编译器此时废弃内存中的Person对象
     */
     
如果有多个指针指向这个对象,其实情况也一样。只要记住,一个对象只要在没有强指针指向时,才会被废弃。
但是,只有强指针是不够的。因为只有强指针会引发一个很重要的问题,就是循环引用,相信读者都听过这个概念。但什么是循环引用呢?
假如有个这样的Person类和Dog类。
Person.h
#import <Foundation/Foundation.h>
#import "Dog.h"

@interface Person : NSObject{
    //宠物,默认是__strong
    Dog *_dog;
}

-(void)setDog:(Dog *)dog;

@end
Person.m
#import "Person.h"
@implementation Person

-(void)setDog:(Dog *)dog
{
    _dog = dog;
}

@end

Dog.h
#import <Foundation/Foundation.h>
#import "Person.h"

@interface Dog : NSObject{
    //主人 默认是__strong
    Person *_owner;
}

-(void)setOwner:(Person *)owner;

@end
Dog.m
#import "Dog.h"
@implementation Dog

-(void)setOwner:(Person *)owner
{
    _owner = owner;
}

@end

如果在main函数里这样使用:
int main(int argc, char *argv[])
{
    Person *p1 = [[Person alloc] init];
    Dog *d1 = [[Dog alloc] init];
    
    [p1 setDog:d1];
    [d1 setOwner:p1];
    
    return 0;
}
/*
 p1和d1指针失效
 但是Person对象和Dog对象,因为形成了循环引用
 互相有强指针指向对方
 因为Person对象和Dog对象都不会被废弃
 造成内存泄漏
 */
用一张图说明


为了解决这个问题,苹果提供了__weak修饰符。

__weak修饰符:

Person __weak *p1 = [[Person alloc] init];
如果这样写,编译器是无法通过的。会报Assigning retained object to weak property object will be released after assignment的警告。
因为Person对象并没有强指针指向它,一诞生就马上被废弃了,然后p1就被置为nil了。
但有一种写法是不会报错的,且是正确可以通过编译的,就是使用alloc/new/copy/mutableCopy族以外的创建对象的方法:
id __weak obj3 = [NSMutableArray array];
为什么呢?这里先不介绍,留在文章的第三部分,关于autorelease和返回值那一块介绍。


考虑以下方式:
{
    Person *p1 = [[Person alloc] init];
    Person __weak *p2 = p1;
    
    //如果把p1指向空,Person对象又没有了指向它的强指针。会被废弃。
    //此时通过p2已经找不回那个Person对象了。而且p2会被编译器置为nil
    p1 = nil;
    NSLog(@"p2: %@", p2); //输出为null

}

由此可以猜测,弱指针不会使得引用计数+1,虽然通过弱指针同样可以使用到对象,但是弱指针不作为对象是否废弃的标准。
同时,查看上面代码的最后一行输出,为null。这是__weak修饰符的一个很重要特性,当一个弱指针指向的对象被废弃了,它会自动被编译器置为nil,开发者不再需要手动为p2置空,这样可以有效地避免野指针的产生。

那么有了__weak之后,又怎么可以避免循环引用呢?还是使用上面的例子,只需要修改一行代码。
把Dog类里主人的属性设为__weak:
#import <Foundation/Foundation.h>
#import "Person.h"

@interface Dog : NSObject{
    //主人 默认是__strong,手动设为__weak
    Person  __weak *_owner;
}

-(void)setOwner:(Person *)owner;

@end
一张图解释:

如此便解决了循环引用的问题。

__unsafe_unretained修饰符:

我之所以把它理解为不安全的弱指针,是因为它本质也是一个弱指针,除此之外和__weak不同的地方在于,__weak修饰的指针在所指向的对象被废弃的时候,会被编译器置空,避免野指针的产生。而__unsafe_unretained修饰的指针是不会被自动置空的,这样就存在野指针的风险。因此不建议使用__unsafe_unretained修饰符。我目前也未能很好地理解它有什么存在的价值。不过幸运的是,基本上现在也可以不太管它了,它主要用于ios5及以前的时候。

__autoreleasing修饰符:

顾名思义,把它称之为自动释放修饰符。
已经知道在非ARC的模式下,有一个autoreleasepool(自动释放池)的概念,可以避免重复为对象的引用计数+1和-1,以及避免管理多个对象时的繁琐。在ARC模式下,也同样有autoreleasepool的概念。它们的作用是相似的,就是延迟对象的废弃。在非ARC模式和ARC模式下使用autoreleasepool的区别可以用下图来展示:

使用@autoreleasepool来创建自动释放池,通过__autoreleasing修饰符把指针指向的对象注册到自动释放池里。随着@autoreleasepool块语句的结束,注册到里面的所有对象会被废弃,同样指向它们的指针也会被废弃。
@autoreleasepool {
        /*
         取得非自己生成并持有的对象
         */
        id __strong obj = [NSMutableArray array];
        
        /*
         因为变量obj为强引用
         所以自己持有对象
         
         并且该对象有编译器判断其方法名后
         自动注册到autoreleasepool
         */
    }
    
    /*
     因为变量obj超出其作用域,强引用失效
     所以自动释放自己持有的对象
     
     同时,随着@autoreleasepool块结束,
     注册到autoreleasepool中的所有对象
     被自动释放
     
     如果所有者不怎在,所以废弃对象
     */

       我的理解是,__autoreleasing修饰的变量,即我称为的自动释放指针,也可以看作是一个强指针(为什么看作是强指针后面会再说),但是它会随着@autoreleasepool块语句的结束自动被废弃。当这个自动释放指针废弃后,它原先指向的对象如果已经没有了强指针指向了,那么也会接着被废弃,如果它还有其他强指针指向,那么它可以继续存活。如下的例子可以体现:
id obj = [[NSObject alloc] init];
    @autoreleasepool {
        id __autoreleasing obj2 = obj;
        NSLog(@"%@",obj2);
    }
    NSLog(@"%@",obj);
两个NSLog输出是一样的。可以看出变量obj指向的对象没被废弃,因为obj2废弃后这个对象还有obj这个强指针指向。

       那问题又来了,为什么很多时候没有看到@autoreleasepool块语句却还是能看到__autoreleasing修饰符的使用呢?那是因为当我们创建一个应用程序的时候,UIApplication 自己会创建 main runloop,在 Cocoa 的 runloop 中实际上也是自动包含 autorelease pool 的。那这个autorelease pool什么时候释放呢?没有一个准确的时间,一般是runloop 一个 event 结束。比如执行完一个for循环。
//别忘了,默认是__strong
    id obj = [[NSObject alloc] init];
    for (int i = 0; i < 10000; i++)
    {
        id __autoreleasing obj2 = obj;
    }
        像这样没有明显地使用@autoreleasepool,就是把10000个obj2注册到main runloop下的autorelease pool里,然后执行完这个for循环后再一次性废弃这10000个__autoreleasing修饰的变量。一般来说,当需要注册到autorelease pool的变量太多的话,最好自己手动创建@autoreleasepool,不然会使得main runloop里的autorelease pool太撑,降低它的性能。
id obj = [[NSObject alloc]init];
for (int i = 0; i < 100000000; i++)
{
    @autoreleasepool
    {
        id __autoreleasing obj2 = obj;
    }
}

第一个小结

       基本上四个修饰符表现出来的基本特性就是这样子了。但是它们真正的内涵可远远不止于此,想要真正理解它们,还有很多内容需要知道。毕竟看起来越简单的东西,背后一定有着更复杂的实现。我们享受这ARC方便好用的同时,它背后一定有着更复杂的实现机理。现在可以整理一下思路,准备继续进发吧。


再议autorelease和函数返回值

还记得第一篇讲到了在非ARC(MRC)下,autorelease和函数返回值。这篇,就讲讲它们在ARC下的表现。
在 ARC 中,我们完全不需要考虑那两种类型的创建函数返回值类型的区别,ARC 会自动加入必要的代码,因此我们可以放心大胆地写类似这样的代码:
self.property = [[NSObject alloc] init];//property的定义是@property (nonatomic, retain) NSObject *property;
self.name = [NSString stringWithFormat:@"%@ %@", firstName, lastName];
以及在自己写的函数中:
+ (MyCustomClass *) myCustomClass
{
    return [[MyCustomClass alloc] init]; // 不用 autorelease
}
这些写法都是 OK 的,也不会出现内存问题。
为了进一步理解 ARC 是如何做到这一点的,我们可以参考 Clang 的 文档 。

对于 retained return value:

When returning from such a function or method, ARC retains the value at the point of evaluation of the return statement, before leaving all local scopes.

When receiving a return result from such a function or method, ARC releases the value at the end of the full-expression it is contained within, subject to the usual optimizations for local values.

可以看到基本上 ARC 就是帮我们在代码块结束的时候进行了 release:
NSObject * a = [[NSObject alloc] init];
self.property = a;
//[a release]; 我们不需要写这一句,因为 ARC 会帮我们把这一句加上


对于 unretained return value:

When returning from such a function or method, ARC retains the value at the point of evaluation of the return statement, then leaves all local scopes, and then balances out the retain while ensuring that the value lives across the call boundary. In the worst case, this may involve an autorelease, but callers must not assume that the value is actually in the autorelease pool.

ARC performs no extra mandatory work on the caller side, although it may elect to do something to shorten the lifetime of the returned value.

       这个和我们之前在 MRC 中做的不是完全一样。ARC 会把对象的生命周期延长,确保调用者能拿到并且使用这个返回值,但是并不一定会使用 autorelease把对象注册到autoreleasepool中,文档写的是在 worest case 的情况下才可能会使用,因此调用者不能假设返回值真的就在 autorelease pool 中。从性能的角度,这种做法也是可以理解的。如果我们能够知道一个对象的生命周期最长应该有多长,也就没有必要使用 autorelease 了,直接使用 release 就可以。如果很多对象都使用 autorelease 的话,也会导致整个 pool 在 drain 的时候性能下降。那么问题就来了,什么时候会把返回值的对象注册到autoreleasepool呢?这取决于我们接这个返回值的变量用什么修饰符去修饰。不同修饰符修饰时,ARC会做相对应的操作。
现在就不同修饰符的变量接收返回值做不同情况的介绍:

_strong和unretained return value

       考虑以下代码:
{
     id __strong obj = [NSMutableArray array];
}
      这里补充《OC高级编程--ios与os x多线程和内存管理》这本书上贴出的关于array的源码:
+ (id) array 
{  
    return [[NSMutableArray alloc] init]; } //返回一个retained return value,但编译器扫描到当前方法是array方法,会作出优化 

       当方法全部基于 ARC 实现时,在方法 return 的时候,ARC 会调用 objc_autoreleaseReturnValue() 以替代 MRC 下的 autorelease 。在 MRC 下需要 retain 的位置,ARC 会调用 objc_retainAutoreleasedReturnValue() 。因此,ARC下的编译器会把以上代码改写成:
id obj = objc_msgSend(NSMutableArray,@selector(array));
objc_retainAutoreleasedReturnValue(obj);  // 相当于调用retain
objc_release(obj);


+ (id) array
{
    id obj = objc_msgSend(NSMutableArray, @selector(alloc)); //用一个__strong来接收
    objc_msgSend(obj, @selector(init));
    return objc_autoreleaseReturnValue(obj); //把obj注册到autorelease pool,相当于MRC下调用autorelease,到时候会自动为这个对象的引用计数-1,ARC下就是把obj这个强指针废弃掉,对象没有强指针指向,紧接着废弃这个对象
}
        虽然说ARC模式下,我们不可以自己手动执行release和retain之类的各种方法,但其实是因为编译器会帮我们插入这方面的代码来管理对象。因此可以看到编译器改写后的代码具有这些方法的影子。
        此时,ARC 可以使用一些优化技术。在调用 objc_autoreleaseReturnValue() 时,会在栈上查询 return address 以确定 return value 是否会被直接传给 objc_retainAutoreleasedReturnValue() 。 如果没传,说明返回值不能直接从提供方发送给接收方,这时就会调用 autorelease 。反之,如果返回值能顺利的从提供方传送给接收方,那么就会直接跳过 autorelease 过程,并且修改 return address 以跳过 objc_retainAutoreleasedReturnValue() 过程,这样就跳过了整个 autorelease 和 retain 的过程。通过 objc_autoreleaseReturnValue函数和objc_retainAutoreleasedReturnValue函数的写作,可以不将对象注册到autorelease pool中而直接传递,这一过程达到最优化。
       因此, 当我们使用__strong修饰的指针来接收unretained return value时,编译器就不把它注册到autorelease pool了,直接将它返回。返回后的结果,就是一个强指针,指向一个对象。

_weak和unretained return value

还记得前面说到:
    id __weak obj2 = [[NSMutableArray alloc] init];//会报对象被废弃的警告,随后打印obj2得到的是null
    id __weak obj3 = [NSMutableArray array];//不会报错
       现在可以就这个现象解释一下了。已知第一个方法返回一个retained return value,返回的时候如果没有强指针指向它,会一出生就废弃。因为编译器会自动插入一条release操作式这个对象的引用计数-1,假如没有强指针指向它为它的引用计数+1,这时候引用计数就会减为0,对象被废弃。
          有了上面的学习,都能猜到第二条之所以能过顺利编译,是因为返回的对象注册到了autorelease pool里,对象没有被废弃。所以__weak指针指向它也毫无问题。clang的文档 是这么说的:这种情况下,weak 并不会立即释放,而是会通过 objc_loadWeak 这个方法注册到 AutoreleasePool 中,以延长生命周期。同时,对于注册到autoreleasepool的对象,也可以看做有一个自动释放指针指向它,这也解释了我为什么把自动释放指针看做强指针的原因,如果不是强指针,那它就没有强指针指向也就会被废弃了啊~
       但关于__weak还有一个需要注意的问题。考虑以下代码:
{
        id __weak obj1 = obj;
        NSLog(@"%@",obj1);
    }
        该代码会被编译器转化成如下形式:
/*编译器的模拟代码*/
id obj1;
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@",tmp);
objc_destoryWeak(&obj1);
      与赋值时相比,在使用附有_weak修饰符的变量的情形下,增加了对objc_loadWeakRetained函数和objc_autorelease函数的调用。这些函数的动作如下。
(1)objc_loadWeakRetained函数取出附有__weak修饰符变量所引用的对象并retain。
(2)objc_autorelease函数讲对象注册到autoreleasepool中。
       由此可知,因为附有__weak修饰符的变量所引用的对象像这样被注册到autoreleasepool中,所以在@autoreleasepool块结束之前都可以放心使用。但是,如果大量使用附有__weak修饰符的变量,注册到autoreleasepool的对象也会大量地增加。
{
        id __weak obj1 = obj;
	NSLog(@"%@",tmp);
	NSLog(@"%@",tmp);
	NSLog(@"%@",tmp);

    }

      如此会有三个变量注册到autoreleasepool中,性能非常不友好。下面的使用可以解决这样的问题:

{
        id __weak obj1 = obj;
        id tmp = obj1;
        NSLog(@"%@",tmp);
	NSLog(@"%@",tmp);
	NSLog(@"%@",tmp);
	NSLog(@"%@",tmp);

    }
在“tmp=0”时对象仅登录到autoreleasepool一次。

_autoreleasing和unretained return value

        对于retained return value考虑以下代码:

@autoreleasepool {  
    id __autoreleasing obj = [[NSObject alloc] init];  
}  
       它的模拟代码是:

/将上面的源码转换成编译器的模拟源代码如下:  
id pool = objc_autoreleasePoolPush();  
id obj = objc_msgSend(NSObject, @selector(alloc));  
objc_msgSend(obj, @selector(init)); objc_autorelease(obj);  
objc_autoreleasePoolPop(pool);  
       可以看到对象被注册到autoreleasepool,这没什么好奇怪的,因为我们使用了__autoreleasing修饰的obj来接收。

       对于unretained return value考虑以下代码:

@autoreleasepool {  
    id __autoreleasing obj = [NSMutableArray array];  
}  
       它的模拟代码是:

//将上面的源码转换成编译器的模拟源代码如下:  
id pool = objc_autoreleasePoolPush();  
id obj = objc_msgSend(NSMutableArray, @selector(array));  
objc_retainAutoreleasedReturnValue(obj); objc_autorelease(obj);  
objc_autoreleasePoolPop(pool);  
       为了方便,再贴上array的模拟代码:

+ (id) array
{
    id obj = objc_msgSend(NSMutableArray, @selector(alloc)); //用一个__strong来接收
    objc_msgSend(obj, @selector(init));
    return objc_autoreleaseReturnValue(obj); //把obj注册到autorelease pool,相当于MRC下调用autorelease,到时候会自动为这个对象的引用计数-1,ARC下就是把obj这个强指针废弃掉,对象没有强指针指向,紧接着废弃这个对象
}
       由此可以看到,编译器在这里再一次做了优化。一般情况下array方法的返回值会通过调用objc_autoreleaseReturnValue把对象注册到autorelease pool,此时,编译器插了一条objc_retainAutoreleasedReturnValue();通过上面的学习知道这会跳过把对象注册到autoreleasepool的操作。紧接着是__autoreleasing修饰符产生的objc_autorelease(obj)方法,把对象注册到autorelease pool。这样,就可以避免因为__autoreleasing修饰符和array方法的搭配使用,而把一个对象注册到autorelease pool两次。

第二个小结:

       看到这里是不是觉得有点混乱,一会引用计数,一会强指针。补充一下说明吧:

       在MRC中,是通过引用计数来管理对象的生命周期,并没有强弱指针的概念,还是第一篇文章的例子:

int main(int argc, const charchar * argv[]) {  
    id *obj1 = [[NSObject alloc] init];  
     
    NSObject *obj2 = obj1;
    //obj2也指向了这个NSObject对象,但NSObject引用计数不会自动+1,程序员有义务为其引用计数+1  
    [obj2 retain];  
    
    //既然有了[obj2 retain],那么obj2用完之后也要有[obj2 release],retain和release方法需要成对出现。  
    [obj2 release];  
    //此时,仍然可以通过obj2指针使用到NSOjbect对象,但这是不合规范且危险的  
    NSLog(@"NSObject obj2:%@", obj2);  
      
    [obj1 release];  
    //到这里,NSObject对象的引用计数减为0了  
    //此时如果再访问NSObject对象就会报错  
    //NSLog(@"NSObject retaincount: %ld", (unsigned long)[obj2 retainCount]);  
    //所以这时候obj1和obj2都成为野指针了,这个问题会在ARC中得到解决。也可以手动设置obj1=nil,obj2=nil解决  
    return 0;  
}  

       创建一个变量指针指向一个已有对象的时候,如obj2指向NSObject对象,编译器也不会自动为对象的引用计数+1,因此,本着负责任的态度,都需要为它的引用计数+1(retain),用完再-1(release)。此外,release并不代表obj2不再指向这个对象,仅仅是让引用计数-1。那如果不是指向一个已经的对象,而是创建对象的时候,就要分以下两种情况了。

       当使用alloc/new/copy/mutableCopy方法时:NSObject *obj1 = [[NSObject alloc] init]。这样创建的对象在创建的时候引用计数就置1了,因此不需要额外地使用[obj1 retain],用完调用[obj1 release]即可。

       当使用类方法时:如id obj = [NSMutableArray array]。已知类方法内部的实质也是调用alloc/new/copy/mutableCopy来创建对象,因此一创建它们的引用计数也已经为1了,但类方法的区别在于,它会把对象注册到autorelease pool,用完这个对象不用为其release。这个pool一般是runloop下的pool,它会在某个时候自己调用drain,把注册到里面的对象引用计数-1。如果是注册到自己创建的pool,那就要记得自己调用drain方法。我们自己编写类方法的时候,也应该遵守苹果的这个规范,把对象注册到autorelease pool。

       

       在ARC中,有了强弱指针,就不用再考虑引用计数的问题了,引用计数编译器会自动帮我们处理。不然这个机制就不会叫自动引用计数了。我们只需要简单地考虑有没有强指针指向这个对象。多一个强指针指向这个对象时,编译器自动帮这个对象的引用计数+1,少一个强指针指向这个对象时,编译器自动帮这个对象的引用计数-1。如果没有强指针指向这个对象也就意味着这个对象引用计数为0,需要废弃了。而对于autorelease pool的问题,它仍旧是需要的,因为它在处理返回值问题以及批量处理对象问题上都能发挥很好的作用。所以也就有了__autoreleasing修饰符了,这个修饰符能把对象注册到autorelease pool里。但就算没有写这个修饰符,在类方法的返回值上,编译器也会机智地根据实际需要自动帮我们把对象注册到autorelease pool里(看接收的对象,不一定注册的)。而对于autorelease pool里的对象,也可以理解成有一个会自动释放的强指针指向它,这个强指针会随着它所在的pool结束而废弃。


总结:

       第二篇关于ARC的介绍到这里就结束了!如果第一次看,疑惑,懵逼是免不了的,没关系,多看几次就好了。这篇博客我尽量使用了通俗,易懂的描述来表达,基本涵盖了ARC原理的基础部分,希望你看完的话有所收获。如果觉得写得好,也欢迎转载,或者留言交流。关于ARC的系列还有第三篇,当你掌握了ARC原理的时候,它们在一般的ios开发中是怎么体现出来呢?第三篇就是讲述这方面的问题。敬请期待~


  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值