iOS 内存管理

一、似乎每个人在学习iOS过程中都考虑过的问题

1、alloc retain release delloc做了什么?
2、autoreleasepool 是怎样实现的?
3、__unsafe_unretained 是什么?

所以我将在这篇文章详细的从ARC解释到iOS的内存管理。

二、从ARC说起

说 iOS 的内存管理,就不得不从 ARC(Automatic Reference Counting / 自动引用计数) 说起, ARC 是 WWDC2011 和 iOS5 引入的变化。ARC 是 LLVM 3.0 编译器的特性,用来自动管理内存。

与Java中的GC不同,ARC是编译器特性,而不是基于运行时的,所以ARC其实是在编译阶段自动帮开发者插入了内存管理的代码,而不是实时监控与回收内存。

ARC的内存管理规则可以简述为:
  1. 每个对象都有一个「被引用计数」。
  2. 对象被持有,「被引用计数」+ 1 。
  3. 对象被放弃持有,「被引用计数」 - 1 。
  4. 「引用计数」= 0,释放对象。
ARC内存管理规则

三、你需要知道

  1. 包含NSObject类的Foundation框架并没有公开。
  2. Core Foundation 框架源代码,以及通过NSObject进行内存管理的部分源代码是公开的。
  3. GNUstep是Foundation的互换框架。
  • GNUstep 也是 GNU 计划之一。将 Cocoa Objective-C 软件库以自由软件方式重新实现。某种意义上,GNUstep 和 Foundation 框架的实现是相似的。

四、alloc retain release dealloc 的实现

1. GNU – alloc

查看 GNUStep 中的 alloc 函数。

GNUstep/modules/core/base/Source/NSObject.m alloc:

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

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

GNUstep/modules/core/base/Source/NSObject.m NSAllocateObject:

struct obj_layout {
      NSUInteger retained;
};
 
NSAllocateObject(Class aClass, NSUInteger extraBytes, NSZone *zone)
{
      int size = 计算容纳对象所需内存大小;
      id new = NSZoneCalloc(zone, 1, size);
      memset (new, 0, size);
      new = (id)&((obj)new)[1];
}

NSAllocateObject函数通过调用NSZoneCalloc函数来分配存放对象所需的空间,之后将内存空间置为nil,之后返回作为对象而使用的指针。

将上面的代码做简化处理:

GNUstep/modules/core/base/Source/NSObject.m alloc 简化版本:

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)
      return [self allocWithZone: NSDefaultMallocZone()];
}

alloc类方法用struct obj_layout中的retained整数来保存引用计数,并将其写入对象的内存头部,该对象内存块全部置为0后返回。

一个对象的表示如下图

对象的表示
2. GNU – retain

GNUstep/modules/core/base/Source/NSObject.m retainCount:

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

GNUstep/modules/core/base/Source/NSObject.m retain:

- (id) retain
{
      NSIncrementExtraRefCount(self);
      return self;
}
 
inline void
NSIncrementExtraRefCount(id anObject)
{
      if (((obj)anObject)[-1].retained == UINT_MAX - 1)
      [NSException raise: NSInternalInconsistencyException
      format: @"NSIncrementExtraRefCount() asked to increment too far”];
      ((obj_layout)anObject)[-1].retained++;
}

以上代码中,NSIncrementExtraRefCount首先写入了当retained变量超出最大值时发生异常的代码(因为 retained 是 NSUInteger 变量),然后进行retain ++代码。

3. GNU – release

和 retain 相应的,release 方法做的就是 retain --。

GNUstep/modules/core/base/Source/NSObject.m release

- (oneway void) release
{
if (NSDecrementExtraRefCountWasZero(self))
{
       [self dealloc];
}
}
 
BOOL
NSDecrementExtraRefCountWasZero(id anObject)
{
      if (((obj)anObject)[-1].retained == 0)
      {
            return YES;
      }
      ((obj)anObject)[-1].retained--;
      return NO;
}
4. GNU – dealloc

dealloc 将会对对象进行释放。

GNUstep/modules/core/base/Source/NSObject.m dealloc:

- (void) dealloc
{
      NSDeallocateObject (self);
}
 
inline void
NSDeallocateObject(id anObject)
{
      obj_layout o = &((obj_layout)anObject)[-1];
      free(o);
}
5. Apple 实现

在 Xcode 中 设置 Debug -> Debug Workflow -> Always Show Disassenbly 打开。这样在打断点后,可以看到更详细的方法调用。

通过在 NSObject 类的 alloc 等方法上设置断点追踪可以看到几个方法内部分别调用了:

retainCount

__CFdoExternRefOperation
CFBasicHashGetCountOfKey

retain

__CFdoExternRefOperation
CFBasicHashAddValue

release

__CFdoExternRefOperation
CFBasicHashRemoveValue

可以看到他们都调用了一个共同的__CFdoExternRefOperation方法。
该方法从前缀可以看到时包含在Core Foundation,在CFRuntime.c中可以找到,做简化后列出源码:

CFRuntime.c __CFDoExternRefOperation:

int __CFDoExternRefOperation(uintptr_t op, id obj) {
      CFBasicHashRef table = 取得对象的散列表(obj);
      int count;
 
switch (op) {
      case OPERATION_retainCount:
      count = CFBasicHashGetCountOfKey(table, obj);
      return count;
      break;
      case OPERATION_retain:
      count = CFBasicHashAddValue(table, obj);
      return obj;
      case OPERATION_release:
      count = CFBasicHashRemoveValue(table, obj);
      return 0 == count;
      }
}

所以 __CFDoExternRefOperation 是针对不同的操作,进行具体的方法调用,如果 op 是 OPERATION_retain,就去掉用具体实现 retain 的方法。

从 BasicHash 这样的方法名可以看出,其实引用计数表就是散列表。
key 为 hash(对象的地址) value 为 引用计数。

下图是 Apple 和 GNU 的实现对比:


Apple 和 GNU 的实现对比

五、autorelease 和 autorelaesepool

在苹果对于 NSAutoreleasePool 的文档中表示:

每个线程(包括主线程),都维护了一个管理 NSAutoreleasePool 的栈。当创建新的 Pool 时,他们会被添加到栈顶。当 Pool 被销毁时,他们会被从栈中移除。
autorelease 的对象会被添加到当前线程的栈顶的 Pool 中。当 Pool 被销毁,其中的对象也会被释放。
当线程结束时,所有的 Pool 被销毁释放。

对 NSAutoreleasePool 类方法和 autorelease 方法打断点,查看其运行过程,可以看到调用了以下函数:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// 等同于 objc_autoreleasePoolPush
 
id obj = [[NSObject alloc] init];
[obj autorelease];
// 等同于 objc_autorelease(obj)
 
[NSAutoreleasePool showPools];
// 查看 NSAutoreleasePool 状况
 
[pool drain];
// 等同于 objc_autoreleasePoolPop(pool)

[NSAutoreleasePool showPools] 可以看到当前线程所有 pool 的情况:

objc[21536]: ##############
objc[21536]: AUTORELEASE POOLS for thread 0x10011e3c0
objc[21536]: 2 releases pending.
objc[21536]: [0x101802000] ................ PAGE (hot) (cold)
objc[21536]: [0x101802038] ################ POOL 0x101802038
objc[21536]: [0x101802040] 0x1003062e0 NSObject
objc[21536]: ##############
Program ended with exit code: 0

在 objc4 中可以查看到 AutoreleasePoolPage:

objc4/NSObject.mm AutoreleasePoolPage
 
class AutoreleasePoolPage
{
static inline void *push()
{
      生成或者持有 NSAutoreleasePool 类对象
}
static inline void pop(void *token)
{
      废弃 NSAutoreleasePool 类对象
      releaseAll();
}
static inline id autorelease(id obj)
{
      相当于 NSAutoreleasePool 类的 addObject 类方法
      AutoreleasePoolPage *page = 取得正在使用的 AutoreleasePoolPage 实例;
}
id *add(id obj)
{
      将对象追加到内部数组
}
void releaseAll()
{
      调用内部数组中对象的 release 方法
}
};
 
void *
objc_autoreleasePoolPush(void)
{
      if (UseGC) return nil;
      return AutoreleasePoolPage::push();
}
 
void
objc_autoreleasePoolPop(void *ctxt)
{
      if (UseGC) return;
      AutoreleasePoolPage::pop(ctxt);
}

AutoreleasePoolPage 以双向链表的形式组合而成(分别对应结构中的 parent 指针和 child 指针)。
thread 指针指向当前线程。
每个 AutoreleasePoolPage 对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址。
next 指针指向下一个 add 进来的 autorelease 的对象即将存放的位置。
一个 Page 的空间被占满时,会新建一个 AutoreleasePoolPage 对象,连接链表。

AutoreleasePoolPage

六、__unsafe_unretained

有时候我们除了 __weak 和 __strong 之外也会用到 __unsafe_unretained 这个修饰符,那么我们对 __unsafe_unretained 了解多少?

__unsafe_unretained 是不安全的所有权修饰符,尽管 ARC 的内存管理是编译器的工作,但附有 __unsafe_unretained 修饰符的变量不属于编译器的内存管理对象。赋值时即不获得强引用也不获得弱引用。

来运行一段代码:

id __unsafe_unretained obj1 = nil;
{
      id __strong obj0 = [[NSObject alloc] init];
 
      obj1 = obj0;
 
      NSLog(@"A: %@", obj1);
}
 
      NSLog(@"B: %@", obj1);

运行结果:

2017-11-16 13:24:47.245220 __unsafe_unretained[55726:4408416] A:
2017-11-16 13:24:47.246670 __unsafe_unretained[55726:4408416] B:
Program ended with exit code: 0

对代码进行详细分析:

id __unsafe_unretained obj1 = nil;
{
// 自己生成并持有对象
id __strong obj0 = [[NSObject alloc] init];
 
// 因为 obj0 变量为强引用,
// 所以自己持有对象
obj1 = obj0;
 
// 虽然 obj0 变量赋值给 obj1
// 但是 obj1 变量既不持有对象的强引用,也不持有对象的弱引用
NSLog(@"A: %@", obj1);
// 输出 obj1 变量所表示的对象
}
 
NSLog(@"B: %@", obj1);
// 输出 obj1 变量所表示的对象
// obj1 变量表示的对象已经被废弃
// 所以此时获得的是悬垂指针
// 错误访问

所以,最后的 NSLog 只是碰巧正常运行,如果错误访问,会造成 crash
在使用 __unsafe_unretained 修饰符时,赋值给附有 __strong 修饰符变量时,要确保对象确实存在。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容