iOS | 内存管理(MRC和ARC的区别以及实现)

自动引用计数(ARC)是指内存管理中对引用采取自动计数的技术。

使用ARC,就无需再次键入retain或者release代码,这降低了程序崩溃,内存泄漏等风险的同时,很大程度上减少了开发程序的工作量。ARC技术使得编译器清楚目标对象,并能立刻释放那些不再被使用的对象。如此一来,应用程序将具有可预测性,且能流畅运行,速度也将大幅提升。

MRC

人工引用计数(Manual Reference Counting)

内存管理的思考方式:

  • 自己生成的对象,自己持有
  • 非自己生成的对象,自己也能持有
  • 不需要自己持有的对象时释放
  • 无法释放非自己持有的对象
对象操作oc方法
生成并持有对象alloc/new/copy/mutableCopy 等方法
持有对象retain 方法
释放对象release 方法
废弃对象dealloc 方法
自己生成的对象,自己持有
  • alloc
  • new
  • copy
  • mutablecopy
id obj = [[NSObject alloc] init];
id obj = [NSObject new];
// 两者完全一致,生成并持有对象

// NSCopying和NSMutableCopying

非自己生成的对象,自己也能持有
id obj = [NSMutableArray array];
// 取得的对象存在,但自己不持有
[obj retain];
// 自己持有对象
不再需要自己持有的对象时释放

自己持有的对象,一旦不再需要,持有者有义务释放该对象。

id obj = [NSMutableArray array];
// 取得的对象存在,但自己不持有
[obj retain];
// 自己持有对象
[obj release];
// 释放对象
// 对象不可再被访问

array方法的实现

- (id) object
{
	id obj = [[NSObject alloc] init];
	// 自己持有对象
	[obj autorelease];
	// 释放后取得的对象存在,但自己不持有该对象
	return obj;
}

release方法时调用后立即释放,而autorelease方法则是不立即释放而是注册到autoreleasepool中,在超出指定的生存范围时能够自动并正确释放(调用release)。

id obj1 = [obj0 object];
// 取得对象存在但不持有
[obj1 retain];
// 自己持有对象
无法释放非自己持有的对象
  • 已经释放过的对象再一次释放。
id obj = [NSObject alloc] init];
[obj release];
[obj release];
  • 自己还未持有的对象进行释放
id obj1 = [obj0 object];
[obj1 release];

alloc/retain/release/dealloc的实现

id obj = [NSObject alloc];
+(id)alloc
{
	return [self allocWithZone: NSDefaultMallocZone()];
}

+(id)allocWithZone:(NSZone*)z
{
	return NSAllocateObject(self, 0, z);
}


//
struct obj_layout
{
    NSUInteger retained;
};

inline id NSAllocateObject (Class aClass, NSUInteger extraBytes, NSZone *zone)
{
    int size = 计算容纳对象所需内存大小;
    id new = NSZoneMalloc(zone, self);
    memset(new, o, size);
    new = (id)&((struct obj_layput *)new)[1];
}

NSDefaultMallocZone和NSZoneMalloc中包含的NSZone时防止内存碎片化而引入的结构,对内存分配的区域本身进行多重化管理,根据使用对象的目的,对象的大小分配内存,从而提高了内存管理的效率。

去掉NSZone后代码简化版

struct obj_layout
{
    NSUInteger retained;
};

+(id) alloc
{
	int size = sizeof(struct obj_layout) + 对象大小;
	struct obj_layout *p = (struct obj_layout *)calloc(1, size);
	return (id)(p+1);
}

方法中的retain整数用来保持引用计数并将其写入对象内存头部。

对象的引用计数通过retainCount来实现

id obj = [[NSObject alloc] init];
[obj retainCount];

- (NSUInteger)retainCount
{
	return NSExtraRefCount(self) + 1;
}
inline NSUInteger NSExtraRefCount(id anObject)
{
    return ((struct obj_layout *)anObject)[-1].retained;
}

通过retain方法可使retained变量加1,通过release方法可使retained变量减1

retain方法的实现:

-(id)retain
{
	NSIncrementExtraRefCount(self);
	return self;
}

inline void NSIncrementExtraRefCount(id anObject)
{
    if (((struct obj_layout *) anObject)[-1].retained == UINT_MAX - 1)
        [NSException raise: NSInternalInconsistencyException format: @"....."];
    ((struct obj_layout *) anObject)[-1].retained++;
}

release方法的实现

- (void)release
{
	if (NSDecrementExtraRefCountWasZero(self))
		[self dealloc];
}

BOOL NSDecrementExtraRefCountWasZero (id anObject)
{
    if ((struct obj_layout *) anObject)[-1].retained == 0) {
        return YES;
    }
    else
    {
        ((struct obj_layout *)anObject)[-1].retained--;
        return NO;
    }
}

dealloc方法的实现

-(void)dealloc
{
	NSDeallocateObject(self);
}

inline void NSDeallocateObject (id anObject)
{
    struct obj_layout *o = &((struct obj_layout *) anObject)[-1];
    free(o);
}
// 废弃alloc分配的内存块

实际上苹果是通过散列表的形式实现引用计数的,散列表的键值为内存块地址的散列值。

散列表存储信息为引用计数以及内存块地址。

autorelease

autorelease会类似c语言中的自动变量来对待对象实例,超出作用域时则将对象释放。

{
    int a;
}

autorelease就是类似 { } 的作用,使用方法如下:

  • 生成并持有NSAutoreleasePool对象
  • 调用已分配对象的autorelease实例方法。
  • 废弃NSAutoreleasePool对象。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain]; //等同于[obj release]

在Cocoa框架中,相当于程序主循环的NSRunLoop或者在其他程序可运行的地方,对NSAutoreleasePool 对象进行生成、持有和废弃处理。因此开发者不一定非得使用NSAutoreleasePool对象来进行开发工作。

但是在大量产生autorelease对象时,只有不废弃NSAutoreleasePool对象,那么生成的对象就不能被释放,因此可能出现内存不足的情况。例如:

for (int i = 0; i < 图像数; ++i)
{
    /*
     * 读入图像
     * 大量产生autorelease对象
     * 没有废弃NSAutoreleasePool 对象
     * 导致最终内存不足
     */
    [pool drain];
    /*
    * 通过drain方法autorelease对象被release
    */
}

Cocoa框架中也有很多类方法返回autorelease对象,比如NSMutableArray类的arrayWithCapacity类方法。

id array = [NSMutableArray arrayWithCapacity:1];

等同于

id array = [[[NSMutableArray alloc] initWithCapacity:1] autorelease];
autorelease的实现

[obj autorelease]

-(id)autorelease
{
	[NSAutoreleasePool addObject:self];
}
+(void)addObject:(id)anObj
{
	NSAutorelease *pool = 取得正在使用的NSAutorelease对象;
	if (pool != nil)
    {
    	[pool addObject:anObj];
    }
    else
    {
    	NSLog(@"AutoreleasePool 对象非存在状态下调用");
    }
}

如果嵌套生成或持有多个NSAutoreleasePool对象,则会使用最内侧的对象。

[pool drain]

- (void)drain
{
	[self dealloc];
}

- (void) dealloc
{
	[self emptyPool];
	[array release];
}

- (void) emtpyPool
{
	for (id obj in array)
    {
    	[obj release];
    }
}

ARC

与MRC的引用计数式内存管理在本质上没有太大变化,只是自动帮我们处理引用计数的部分。

通过__strong修饰符可以不必再次键入retain或者release,自动地实现了上述的四种内存管理思考方式。

但引用计数式内存管理会发生"循环引用"的问题。

{
    id test0 = [[Test alloc] init]; /* 对象A */
    /*
     * test0 持有Test对象A的强引用
     */
    
    id test1 = [[Test alloc] init]; /* 对象B */
    /*
     * test1 持有Test对象B的强引用
     */
    [test0 setObject:test1];
    /*
     * 此时持有Test对象B的强引用变量为test1和对象A的obj_成员变量
     */
    [test1 setObject:test0];
    /*
     * 此时持有Test对象A的强引用变量为test0和对象B的_obj变量
     */
}
/*
* 因为test0变量超出其作用域,强引用失效,所以自动释放对象A
*
* 因为test1变量超出其作用域,强引用失效,所以自动释放对象B
*
* 此时持有对象A的强引用变量为对象B的_obj,持有对象B的强引用变量为对象A的_obj
*
* 发生内存泄漏!(应当废弃的对象在超出其生命周期后依然存在)
*/

同时像下面的情况,虽然只有一个对象,但对象持有其自身时也会发生循环引用。

id test = [[Test alloc] init];
[test setObject:test];

使用弱引用__weak可以实现,弱引用不持有对象,只是持有该对象的强引用,如果该对象的所有强持有者都释放,则对象废弃,即使此时仍有弱持有者。

_unsafe_unretained 和weak的不同之处在于前者在赋值给带有strong修饰符的变量必须确保被赋值对象确实存在,不然其不会像weak一样被设置为nil而是造成悬浮指针。(存在是历史遗留问题,有了weak后基本不用它了)

strong修饰符类似于c++中的std::shared_ptr指针,而weak修饰符类似于c++中的std::weak_ptr,在c++中没有strong,weak强烈推荐使用这两个指针。

ARC,MRC区别

说了这么多总结一下ARC,MRC的区别吧。MRC类似于c++中的普通指针,程序猿要手动的进行生成持有释放废弃操作,但是可以将其注册到autoreleasepool中(类似c++的作用域),在作用域消失时也就是autoreleasepool对象释放时会释放所有注册在它里面的对象。而ARC则是类似于c++的智能指针,不需要显示的对对象实例进行释放,出现了strong,weak等修饰符,采用自动引用计数的方法,使得当一个对象实例在强引用计数为0时,则废弃这个对象实例,释放其所占的内存块。

ARC的规则:

  • 在ARC有效时编译代码一定要遵守以下规则,
  • 不能使用NSAllocateObject/NSDeallocateObject
  • 须遵守内存管理的命名规则
    • init返回的必须是实例对象
  • 不要显式地调用dealloc
  • 使用@autorelease块代替NSAutoreleasePool
  • 不能使用NSZone
  • 对象型变量不能作为C语言结构体成员
  • 显式转换id和void*
    • 不能强制转换
    • 如果只是想单纯地赋值可以通过__bridge转换,但可能造成悬空指针问题,不推荐。

ARC中的属性:

属性声明所有权修饰符
assign_unsafe__unretained修饰符
copy__strong修饰符
retain__strong修饰符
strong__strong修饰符
unsafe_unretained_unsafe__unretained修饰符
weak__weak修饰符
  • **assign:**对应到__unsafe_unretained, 表明setter仅做赋值,不增加对象的引用计数,用于基本数据类型

  • __weak修饰符赋值后不会注册到autoreleasepool中,只会在使用时注册,而且每次使用都会注册一次,在autoreleasepool结束后会全部释放。

  • **copy:**对应到__strong,但是赋值操作比较特殊:赋值时进行copy而非retain操作,原来的值可变则深拷贝,不可变则浅拷贝。

ARC的实现

__strong

先看如下一段代码:

{
// ARC中默认会在对象前添加一个修饰符__strong
id obj = [[NSObject alloc] init];
//<==>等价于
id __strong obj = [[NSObject alloc] init];
}

根据runtime特性,它的实际调用如下:

{
// 消息转发
id obj = objc_msgSend(NSObject,@selector(alloc));
objc_msgSend(obj,@selector(init));
// 编译器在obj作用域结束时自动插入release
objc_release(obj);
}

当然这里是以alloc/new/copy/mutableCopy生成的对象,这种对象会被当前的变量所持有,引用计数会加1.那如果不是用被持有的方式生成对象呢?
看下面这段代码:

{
id obj = [NSMutableArray array];
}

这种方式生成的对象不会被obj持有,通常情况下会被注册到autoreleasepool中.但也有特殊情况,上面的代码可以转换成如下代码:

{
// 消息转发
id obj = objc_msgSend(NSMutableArray,@selector(array));
// 调用objc_retainAutoreleasedReturnValue函数
objc_retainAutoreleasedReturnValue(obj);
// 编译器在obj作用域结束时自动插入release
objc_release(obj);
}

这里介绍两个相关函数:

  • objc_retainAutoreleasedReturnValue():这个函数的作用是返回注册在autoreleasepool当中的对象.
  • objc_retainAutoreleaseReturnValue():这个函数一般是和objc_retainAutoreleasedReturnValue()成对出现的.目的是注册对象到autoreleasepool中.但不仅限于此.
    为何说不仅限于此呢?原因在于,objc_retainAutoreleaseReturnValue()函数在发现对象调用了方法或者函数之后又调用了objc_retainAutoreleasedReturnValue(),那么就不会再把返回的对象注册到autoreleasepool中了,而是直接把对象传递过去.
    这样的好处显而易见:不用再去autoreleasepool中取出对象,传递出去,而是越过autoreleasepool直接传递,提升了性能.
__weak

weak修饰符想必大家都非常熟悉,它有一个众所周知的特性:用weak修饰的对象在销毁后会被自动置为nil.另外还补充一点:凡是用weak修饰过的对象,必定是注册到autoreleasepool中的对象.
看下面的代码:

{
// obj默认有__strong修饰
id obj = [[NSObject alloc] init];
id __weak obj1 = obj;
}

实际过程如下:

{
// 省略obj的实现
id obj1;
// 通过objc_initWeak初始化变量
objc_initWeak(&obj1,obj);
// 通过objc_destroyWeak释放变量
objc_destroyWeak(&obj1);
}
  • objc_initWeak()函数的作用是将obj1初始化为0,然后将obj作为参数传递到这个函数中objc_storeWeak(&obj1,obj)
  • objc_destroyWeak()函数则将0作为参数来调用:objc_storeWeak(&obj1,0)
  • objc_storeWeak()函数的作用是以第二个参数(obj || 0)作为key,第一个参数(&obj1)作为value,将第一个参数的地址注册到weak表中.当key为0,即从weak表中删除变量地址.

那么weak表中的对象是如何被释放的呢?

  • 从weak表中获取废弃对象的键值记录.
  • 将记录中所有包含__weak的变量地址,赋值为nil.
  • 从weak表中删除该记录.
  • 从引用计数表中删除对应的记录.

这就是__weak修饰的变量会在释放后自动置为nil的原因.同时,因为weak修饰之后涉及到注册到weak表等相关操作,如果大量使用weak可能会造成不必要的CPU资源浪费,所以书里指出尽量在循环引用中使用weak.
这里不得不提到另外一个和__weak相近的属性:__unsafe_unretained,它与weak的区别在于,释放对象后不会对其置为nil,在某些特定的场合下,需要延迟释放的时候,可以考虑用这个属性修饰.

好了,下一个问题,看如下代码:

{
id __weak obj1 = obj;
// 这里使用了obj1这个用weak修饰的变量
NSLog(@"%@",obj1);
}

在weak变量被使用的情况下,实际过程如下:

{
id obj1;
objc_initWeak(&obj1,obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@",tmp);
objc_destroyWeak(&obj1);
}

从这段实现代码中我们可以看出如下几点:

  • 当我们使用weak修饰的对象时,实际过程中产生了一个tmp对象,因为objc_loadWeakRetained()函数会从weak表中取出weak修饰的对象,所以tmp会对这个取出的对象进行一次强引用.
  • 因为上述原因,weak修饰的对象在当前变量作用域结束前都可以放心使用.
  • objc_autorelease()会将tmp对象也注册到autoreleasepool中.所以当大量使用weak对象的时候,注册到autoreleasepool的对象会大量增加.解决方案就是用一个__strong修饰的临时变量来使用.
{
id __weak obj1 = obj;
id tmp = obj1;
// 后面使用tmp即可
}

延伸一下:为什么有循环引用block内用weakObject的时候最好能在block内套一层strongObject?

  • 在异步线程中weakObject可能会被销毁,所以需要套一层strong.
  • 如果内部有耗时的循环语句,频繁使用weakObject也会增加内存损耗.

!!! 为什么访问weak修饰的对象就会访问注册到自动释放池的对象呢?

因为weak不会引起对象的引用计数器变化,因此,该对象在运行过程中很有可能会被释放。所以,需要将对象注册到自动释放池中并在autoreleasePool销毁时释放对象占用的内存。

__autoreleasing

它的主要作用就是将对象注册到autoreleasepool中.没啥好说的.

最后补充几种在ARC环境下获取引用计数的方法,但并不一定准确:ARC的一些引用计数优化,以及多线程的中的竞态条件问题,有兴趣的可以自己去了解一下.

(1) 使用_objc_rootRetainCount()私有函数
OBJC_EXTERN int _objc_rootRetainCount(id);
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    id obj = [[NSObject alloc] init];
    NSLog(@"%d",_objc_rootRetainCount(obj));
}
@end

(2) 使用KVC
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    id obj = [[NSObject alloc] init];
    NSLog(@"%d",[[obj valueForKey:@"retainCount"] integerValue]);
}
@end

(3) 使用CFGetRetainCount()
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    id obj = [[NSObject alloc] init];
    NSLog(@"%d",CFGetRetainCount((__bridge CFTypeRef)(obj)));
}
@end
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值