Object-C 内存管理模式初探(一)

前言

**
Apple在Xcode4.2版本开始,只要LLVM版本在3.0或以上版本,并设置ARC有效,在用户编译代码时,编译器会自动进行内存管理。
本文介绍了手工内存管理的原理以及ARC的规则和实现。

- 基本概念

首先我们需要了解一些内存管理的基本概念。
˙引用计数
我们以办公室为例,当第一个人来到办公室的时候,办公室就要开灯,但是随着办公室的人数增多,灯并不会增多。但是,当第一个人离开办公室的时候,办公室是不能关灯的,因为还有其他人在工作,只有当最后一个人离开办公室的时候,办公室才可以关灯。
引用计数是以同样的方式工作,当一个对象生成的时候,该对象每多一个持有者,该对象的引用计数就 “+1”,每当有一个持有者放弃持有了,该对象的引用计数就“-1”,当对象的引用技术为0的时候,既是该对象没有任何持有者了,此时,该对象的内存空间被释放。
˙生命周期
由引用计数的概念可知,对象的生命周期主要分三个阶段
1. 生成
2. 持有
3. 释放
·内存管理思想
1. 自己生成的对象,自己持有
2. 非自己生成的对象,自己也能持有
3. 不再需要自己持有的对象时释放对象
4. 非自己持有的对象自己无法释放

-手工内存管理

·Object-C对象操作方法分别对应上文内存管理思想
1. alloc/new/copy/mutableCopy (自己生成并持有对象)
2. retain (持有对象)
3. release (释放对象)
4. dealloc (释放对象)

-
OC中函数命名是有命名空间限制的,以 alloc/new/copy/mutableCopy 开头的自定义函数若有父类,则必须先调用 “[super xxxx]”并返回指定类型的返回值。

·alloc/reatain/release/dealloc 方法的实现
OS X及iOS的源码大部分开源在Apple Open Source.但是由于Cocoa框架中的相关代码未公开,此处借鉴GUNstep中相关框架的简化源码。

·alloc方法首先创建了一个struct,改struct中包含对象的引用计数,然后将引用计数的值写到OBJECT内存区域的开始位置,随后将该对象的内存块全部置0。

alloc 源码简化版

struct obj_layout
{
  NSUInteger retained;
};

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

对象的引用计数则可通过实例方法 retainCount 获得。retainCount 通过访问对象的第一个内存块来获得,因为初始化时,对象的所有内存块都被置0,所有 NSExtraRefCount 获取到的值是0,通过对结果“+1”,使我们得到的引用计数的值是 “1”。

- (NSUInteger)retainCount
{
  return NSExtraRefCount(self) + 1;
}

inline NSUInteger

NSExtraRefCount(id object)
{
  return ((struct obj_layout *)(object))[-1].retained;
}

·retain方法的实现是通过使对象的 retainCount + 1,同样,release 方法则是 retainCount - 1。

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

inline void

NSIcrementExtraRefCount(id object)
{
  if (((struct obj_layout *)object)[-1].retained > UINT64_MAX - 1)
    [NSException raise:NSInternalInconsistencyException format:@"NSIcrementExtraRefCount() asked to increase too far"];
  ((struct obj_layout *)object)[-1].retained ++;
}

·最后,当对象的 retainCount 的值为 0 的时候,会调用对象的dealloc方法释放对象的内存块。

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

inline void

NSDeallocateObjec(id object)
{
  struct obj_layout *o = &((struct obj_layout *)object)[-1];
  free(o);
}

上述代码只释放由 alloc 分配的代码块。

·苹果的实现
通过在NSObject 类的 alloc方法上设置断点,我们利用Xcode的调试器(lldb)以及iOS追溯其过程。其调用函数如下

+ alloc
+ allocWithZone
class_creatInstance
calloc

用同样的方法,我们可以获知 retainCount/retain/release 的实现

-retainCount
__CFDoExtraRefOperation
CFBasicHashGetCountOfKey

-retain
__CFDoExtraRefOperation
CFBasicHashAddValue

-release
__CFDoExtraRefOperation
CFBasicHashRemoveValue
(CFBasicHashRemoveValue 返回0时,-release 调用 dealloc)

以上我们可以发现,三个方法全都调用了一个名为 __CFDoExtraRefOperation 的函数,由于其是 CF 开头的,所以我们可以知道该函数属于 CoreFoundation 框架。
进一步观察后我们发现,苹果的实现就是基于以内存块地址为键值的散列表来构造一个引用计数表,其中引用计数以及对象内存地址存储为键值。
与GUNstep的实现有所不同,苹果将引用计数从对象内存地址的内存块头部剥离出来,简化了对象内存块的管理,同时还使追溯内存块变得不在单纯依赖于对象,只要引用计数表中还有该对象的记录,即使对象被销毁,依旧可以通过表中存储的内存地址寻址到该对象的位置。
引用计数散列表

·autorelease
autorelease是Object-C最重要的特性之一,虽然它看上去很像 ARC ,但是实际上它更加类似 C 语言的局部变量。
autorelease通过生成一个 NSAutoReleasePool 对象,对声明为 autorelease 类型的对象都将其注册到此 NSAutoReleasePool 对象中,在该 NSAutoReleasePool 对象被废弃的时候,会对所有注册到 NSAutoReleasePool 中声明为 autorelease 类型的变量执行一次 release 操作,随后再释放自己存储 autorelease 对象的数组。
对此需要注意的是,如果在同一 NSAutoReleasePool 对象中,大量声明 autorelease 类型的变量,那么,在 [NSAutoReleasePool drain] 调用之前,所有内存池中的对象都不会释放,那么极有可能造成内存不足的情况。所以正确的生成,持有,废弃 NSAutoReleasePool 对象是非常有必要的。

  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc]init];
  for (int i = 0 ; i < imageCount; i ++)
  {
    //TODO:读入图像数据,生成大量autorelease数据
    //pool未被释放,内存不足
  }
  [pool drain];
  //================================================================
  for (int i = 0 ; i < imageCount; i ++)
  {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc]init];
    //TODO:读入图像数据,生成大量autorelease数据
    [pool drain];
    //pool被释放
  }
Tips:如果将 NSAutoreleasePool 对象声明为 autorelease 类型会导致发生异常。因为通常调用对象的 autorelease 实例方法实际都会、是调用 NSObject 的 autorelease,而 NSAutoreleasePool 的 autorelease 已经被重载,因而运行时就会出错。

ARC

指定文件的编译属性为 -fno-objc-arc 可使编译器在编译时使用非ARC的机制来编译文件。
ARC与非ARC最大的不同就在于属性修饰符

  • __strong
  • __weak
  • __unsafe_unretained ()
  • __autorelease ()

OC中对象的声明默认是强引用类型的,以下代码是等价的:

id obj = nil;
id __strong obj;

同时,强引用和弱引用的变量在初始化时默认会被赋值为nil,所以在初始化时可以不必显示的赋nil值。
·_strong
强引用用于声明和持有对象,当对象超出作用域的时候,该对象既被废弃,这符合内存管理的思想。

// ARC
{
    id __strong obj = [[NSObject alloc]init];
}
//============================================
//非 ARC
{
    id obj = [[NSObject alloc]init];
    [obj release];
}

但是,涉及到内存管理就会肯定涉及到一个概念就是循环引用,请看下面的例子:

// 声明类
@interface TestObject : NSObject
{
  id __strong obj_;
}

- (void)setObject:(id __strong)obj;

@end

@implementation TestObject

- (id)init
{
  self = [super init];
  return self;
}

- (void)setObject:(id)obj
{
  obj_ = obj;
}

@end

// 以下为循环引用

{
  id testA = [[TestObject alloc]init];
  id testB = [[TestObject alloc]init];

  [testA setObject:testB];
  [testB setObject:testA];
}

由于类的修饰符全部是强引用 ,testA的强引用有两个,分别是 testA和testB中的obj_,testB也同样,当变量超出作用域,testA和testB都被废弃后,两个对象的强引用计数并不为0,分别继续
被对方的obj持有,但是由于两者都已被废弃,所以两者的obj均无法访问,所以此时两者内存均无法被释放,此时造成内存泄露。同样道理,对自身的强引用也会造成类似的情况。
·__weak
为了避免类似情况的发生,__weak 弱引用产生了。
弱引用不持有对象实例,只提供对象的访问权限,当弱引用的对象的强引用全部失效后,弱引用既被置为 nil。这样,当上例中的TestObject的持有的对象类型是 __weak 的时候,循环引用的问题就解决了。

Tips:有一个生动的例子可以说明强引用和弱引用的区别:如果对象是一条小狗,强引用就是牵着狗的绳子,弱引用就是在一旁看着小狗的小孩。当绳子还在,狗就在,小孩也可以观察到小狗。但是当绳子没有了,小狗就会跑了,小孩也就无法再观察到小狗了。

·__unsafe_unretain
对于来ARC来说,__unsafe_unretain 修饰符表示该对象的内存管理的工作需要程序员自己来完成,编译器不再对此对象进行内存管理的工作,此类型的对象在管理不当的情况下极易造成内存泄露。
__unsafe_unretain 类型的变量类似于 __weak ,它也并不持有对象,所以,在将 __unsafe_unretain 类型的变量赋值给 __strong 类型的变量时,必须保证 __unsafe_unretain 此变量依旧存在,没有被释放,不然就会造成内存

·__autoreleaseing
在ARC中 NSAutoreleasePool 类是不可以使用的,当我们需要类似的功能的时候可以使用@autoreleasepool{} 替代,同样的,对象的 autorelease 方法也无法使用,此时就会需要 __autorelease 修饰符来替代。

后续我会列举修饰符的实现功能及其源码,并举例说明。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值