iOS 进阶—— iOS内存管理

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

  • alloc retain release delloc 做了什么?
  • autoreleasepool 是怎样实现的?
  • __unsafe_unretained 是什么?
  • Block 是怎样实现的
  • 什么时候会引起循环引用,什么时候不会引起循环引用?

所以我将在本篇博文中详细的从 ARC 解释到 iOS 的内存管理,以及 Block 相关的原理、源码。

2 从 ARC 说起

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

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

ARC 的内存管理规则可以简述为:

  • 每个对象都有一个『被引用计数』
  • 对象被持有,『被引用计数』+1
  • 对象被放弃持有,『被引用计数』-1
  • 『引用计数』=0,释放对象

3 你需要知道

  • 包含 NSObject 类的 Foundation 框架并没有公开
  • Core Foundation 框架源代码,以及通过 NSObject 进行内存管理的部分源代码是公开的。
  • GNUstep 是 Foundation 框架的互换框架

GNUstep 也是 GNU 计划之一。将 Cocoa Objective-C 软件库以自由软件方式重新实现

某种意义上,GNUstep 和 Foundation 框架的实现是相似的

通过 GNUstep 的源码来分析 Foundation 的内存管理

4 alloc retain release dealloc 的实现

4.1 GNU – alloc

查看 GNUStep 中的 alloc 函数。

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


  1. + (id) alloc 
  2.  
  3.  
  4. return [self allocWithZone: NSDefaultMallocZone()]; 
  5.  
  6. }  
  7.   
  8.  
  9. + (id) allocWithZone: (NSZone*)z 
  10.  
  11.  
  12. return NSAllocateObject (self, 0, z); 
  13.  
  14. }  

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


  1. struct obj_layout { 
  2.  
  3. NSUInteger retained; 
  4.  
  5. }; 
  6.  
  7.   
  8.  
  9. NSAllocateObject(Class aClass, NSUInteger extraBytes, NSZone *zone) 
  10.  
  11.  
  12. int size = 计算容纳对象所需内存大小; 
  13.  
  14. id new = NSZoneCalloc(zone, 1, size); 
  15.  
  16. memset (new, 0, size); 
  17.  
  18. new = (id)&((obj)new)[1]; 
  19.  
  20. }  

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

我们将上面的代码做简化整理:

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


  1. struct obj_layout { 
  2.  
  3. NSUInteger retained; 
  4.  
  5. }; 
  6.  
  7.   
  8.  
  9. + (id) alloc 
  10.  
  11.  
  12. int size = sizeof(struct obj_layout) + 对象大小; 
  13.  
  14. struct obj_layout *p = (struct obj_layout *)calloc(1, size); 
  15.  
  16. return (id)(p+1) 
  17.  
  18. return [self allocWithZone: NSDefaultMallocZone()]; 
  19.  
  20. }  

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

一个对象的表示便如下图:

4.2 GNU – retain

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


  1. - (NSUInteger) retainCount 
  2.  
  3.  
  4. return NSExtraRefCount(self) + 1; 
  5.  
  6.   
  7.  
  8. inline NSUInteger 
  9.  
  10. NSExtraRefCount(id anObject) 
  11.  
  12.  
  13. return ((obj_layout)anObject)[-1].retained; 
  14.  
  15. }  

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


  1. - (id) retain 
  2.  
  3.  
  4. NSIncrementExtraRefCount(self); 
  5.  
  6. return self; 
  7.  
  8.  
  9.   
  10.  
  11. inline void 
  12.  
  13. NSIncrementExtraRefCount(id anObject) 
  14.  
  15.  
  16. if (((obj)anObject)[-1].retained == UINT_MAX - 1) 
  17.  
  18. [NSException raise: NSInternalInconsistencyException 
  19.  
  20. format: @"NSIncrementExtraRefCount() asked to increment too far”]; 
  21.  
  22. ((obj_layout)anObject)[-1].retained++; 
  23.  
  24. }  

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

4.3 GNU – release

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

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


  1. - (oneway void) release 
  2.  
  3.  
  4. if (NSDecrementExtraRefCountWasZero(self)) 
  5.  
  6.  
  7. [self dealloc]; 
  8.  
  9.  
  10.  
  11.   
  12.  
  13. BOOL 
  14.  
  15. NSDecrementExtraRefCountWasZero(id anObject) 
  16.  
  17.  
  18. if (((obj)anObject)[-1].retained == 0) 
  19.  
  20.  
  21. return YES; 
  22.  
  23.  
  24. ((obj)anObject)[-1].retained--; 
  25.  
  26. return NO; 
  27.  
  28. }  

4.4 GNU – dealloc

dealloc 将会对对象进行释放。

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


  1. - (void) dealloc 
  2.  
  3.  
  4. NSDeallocateObject (self); 
  5.  
  6.   
  7.  
  8. inline void 
  9.  
  10. NSDeallocateObject(id anObject) 
  11.  
  12.  
  13. obj_layout o = &((obj_layout)anObject)[-1]; 
  14.  
  15. free(o); 
  16.  
  17. }  

4.5 Apple 实现

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

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

retainCount


  1. __CFdoExternRefOperation 
  2. CFBasicHashGetCountOfKey  

retain


  1. __CFdoExternRefOperation 
  2. CFBasicHashAddValue  

release


  1. __CFdoExternRefOperation 
  2. CFBasicHashRemoveValue  

可以看到他们都调用了一个共同的 __CFdoExternRefOperation 方法。

该方法从前缀可以看到是包含在 Core Foundation,在 CFRuntime.c 中可以找到,做简化后列出源码:

CFRuntime.c __CFDoExternRefOperation:


  1. int __CFDoExternRefOperation(uintptr_t op, id obj) { 
  2.  
  3. CFBasicHashRef table = 取得对象的散列表(obj); 
  4.  
  5. int count; 
  6.  
  7.   
  8.  
  9. switch (op) { 
  10.  
  11. case OPERATION_retainCount: 
  12.  
  13. count = CFBasicHashGetCountOfKey(table, obj); 
  14.  
  15. return count; 
  16.  
  17. break; 
  18.  
  19. case OPERATION_retain: 
  20.  
  21. count = CFBasicHashAddValue(table, obj); 
  22.  
  23. return obj; 
  24.  
  25. case OPERATION_release: 
  26.  
  27. count = CFBasicHashRemoveValue(table, obj); 
  28.  
  29. return 0 == count; 
  30.  
  31.  
  32. }  

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

从 BasicHash 这样的方法名可以看出,其实引用计数表就是散列表。

key 为 hash(对象的地址) value 为 引用计数。

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

5 autorelease 和 autorelaesepool

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

每个线程(包括主线程),都维护了一个管理 NSAutoreleasePool 的栈。当创先新的 Pool 时,他们会被添加到栈顶。当 Pool 被销毁时,他们会被从栈中移除。

autorelease 的对象会被添加到当前线程的栈顶的 Pool 中。当 Pool 被销毁,其中的对象也会被释放。

当线程结束时,所有的 Pool 被销毁释放。

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


  1. NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 
  2.  
  3. // 等同于 objc_autoreleasePoolPush 
  4.  
  5.   
  6.  
  7. id obj = [[NSObject alloc] init]; 
  8.  
  9. [obj autorelease]; 
  10.  
  11. // 等同于 objc_autorelease(obj) 
  12.  
  13.   
  14.  
  15. [NSAutoreleasePool showPools]; 
  16.  
  17. // 查看 NSAutoreleasePool 状况 
  18.  
  19.   
  20.  
  21. [pool drain]; 
  22.  
  23. // 等同于 objc_autoreleasePoolPop(pool)  

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


  1. objc[21536]: ############## 
  2.  
  3. objc[21536]: AUTORELEASE POOLS for thread 0x10011e3c0 
  4.  
  5. objc[21536]: 2 releases pending. 
  6.  
  7. objc[21536]: [0x101802000] ................ PAGE (hot) (cold) 
  8.  
  9. objc[21536]: [0x101802038] ################ POOL 0x101802038 
  10.  
  11. objc[21536]: [0x101802040] 0x1003062e0 NSObject 
  12.  
  13. objc[21536]: ############## 
  14.  
  15. Program ended with exit code: 0  

在 objc4 中可以查看到 AutoreleasePoolPage:


  1. objc4/NSObject.mm AutoreleasePoolPage 
  2.  
  3.   
  4.  
  5. class AutoreleasePoolPage 
  6.  
  7.  
  8. static inline void *push() 
  9.  
  10.  
  11. 生成或者持有 NSAutoreleasePool 类对象 
  12.  
  13.  
  14. static inline void pop(void *token) 
  15.  
  16.  
  17. 废弃 NSAutoreleasePool 类对象 
  18.  
  19. releaseAll(); 
  20.  
  21.  
  22. static inline id autorelease(id obj) 
  23.  
  24.  
  25. 相当于 NSAutoreleasePool 类的 addObject 类方法 
  26.  
  27. AutoreleasePoolPage *page = 取得正在使用的 AutoreleasePoolPage 实例; 
  28.  
  29.  
  30. id *add(id obj) 
  31.  
  32.  
  33. 将对象追加到内部数组 
  34.  
  35.  
  36. void releaseAll() 
  37.  
  38.  
  39. 调用内部数组中对象的 release 方法 
  40.  
  41.  
  42. }; 
  43.  
  44.   
  45.  
  46. void * 
  47.  
  48. objc_autoreleasePoolPush(void) 
  49.  
  50.  
  51. if (UseGC) return nil; 
  52.  
  53. return AutoreleasePoolPage::push(); 
  54.  
  55.  
  56.   
  57.  
  58. void 
  59.  
  60. objc_autoreleasePoolPop(void *ctxt) 
  61.  
  62.  
  63. if (UseGC) return; 
  64.  
  65. AutoreleasePoolPage::pop(ctxt); 
  66.  
  67. }  

AutoreleasePoolPage 以双向链表的形式组合而成(分别对应结构中的 parent 指针和 child 指针)。

thread 指针指向当前线程。

每个 AutoreleasePoolPage 对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址。

next 指针指向下一个 add 进来的 autorelease 的对象即将存放的位置。

一个 Page 的空间被占满时,会新建一个 AutoreleasePoolPage 对象,连接链表。

6 __unsafe_unretained

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

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

来运行一段代码:


  1. id __unsafe_unretained obj1 = nil; 
  2.  
  3.  
  4. id __strong obj0 = [[NSObject alloc] init];  
  5.   
  6.  
  7. obj1 = obj0;  
  8.   
  9.  
  10. NSLog(@"A: %@", obj1); 
  11.  
  12. }  
  13.   
  14.  
  15. NSLog(@"B: %@", obj1);  

运行结果:


  1. 2017-01-12 19:24:47.245220 __unsafe_unretained[55726:4408416] A: 
  2.  
  3. 2017-01-12 19:24:47.246670 __unsafe_unretained[55726:4408416] B: 
  4.  
  5. Program ended with exit code: 0  

对代码进行详细分析:


  1. id __unsafe_unretained obj1 = nil; 
  2.  
  3.  
  4. // 自己生成并持有对象 
  5.  
  6. id __strong obj0 = [[NSObject alloc] init]; 
  7.  
  8.   
  9.  
  10. // 因为 obj0 变量为强引用, 
  11.  
  12. // 所以自己持有对象 
  13.  
  14. obj1 = obj0; 
  15.  
  16.   
  17.  
  18. // 虽然 obj0 变量赋值给 obj1 
  19.  
  20. // 但是 obj1 变量既不持有对象的强引用,也不持有对象的弱引用 
  21.  
  22. NSLog(@"A: %@", obj1); 
  23.  
  24. // 输出 obj1 变量所表示的对象 
  25.  
  26.  
  27.   
  28.  
  29. NSLog(@"B: %@", obj1); 
  30.  
  31. // 输出 obj1 变量所表示的对象 
  32.  
  33. // obj1 变量表示的对象已经被废弃 
  34.  
  35. // 所以此时获得的是悬垂指针 
  36.  
  37. // 错误访问  

所以,最后的 NSLog 只是碰巧正常运行,如果错误访问,会造成 crash

在使用 __unsafe_unretained 修饰符时,赋值给附有 __strong 修饰符变量时,要确保对象确实存在

本文作者:佚名

来源:51CTO

时间: 2024-08-03 15:34:43

iOS 进阶—— iOS内存管理的相关文章

iOS/OS X 内存管理(二):借助工具解决内存问题

上一篇博客iOS/OS X内存管理(一):基本概念与原理主要讲了iOS/OSX 内存管理中引用计数和内存管理规则,以及引入ARC新的内存管理机制之后如何选择ownership qualifiers(__strong.__weak.__unsafe_unretained和__autoreleasing)来管理内存.这篇我们主要关注在实际开发中会遇到哪些内存管理问题,以及如何使用工具来调试和解决. 在往下看之前请下载实例MemoryProblems,我们将以这个工程展开如何检查和解决内存问题. 悬挂

iOS/OS X内存管理(一):基本概念与原理

在Objective-C的内存管理中,其实就是引用计数(reference count)的管理.内存管理就是在程序需要时程序员分配一段内存空间,而当使用完之后将它释放.如果程序员对内存资源使用不当,有时不仅会造成内存资源浪费,甚至会导致程序crach.我们将会从引用计数和内存管理规则等基本概念开始,然后讲述有哪些内存管理方法,最后注意有哪些常见内存问题.    memory management from apple document  基本概念 引用计数(Reference Count) 为了

iOS夯实:内存管理

iOS夯实:内存管理 最近的学习计划是将iOS的机制原理好好重新打磨学习一下,总结和加入自己的思考. 有不正确的地方,多多指正. 目录: 基本信息 旧时代的细节 新时代 基本信息 Objective-C 提供了两种内存管理方式. MRR (manual retain-release) 手动内存管理这是基于reference counting实现的,由NSObject与runtime environment共同工作实现. ARC (Automatic Reference Counting)自动引用

【iOS7的一些总结】5、iOS中的内存管理

我们知道,为了更加方便地处理内存管理问题,将开发人员从繁琐的内存的分配和释放工作中解放出来而专注于产品和逻辑,iOS提供了一种有效的方法,即自动引用计数ARC.采用了ARC之后,绝大部分内存管理的问题就不需要我们继续操心了.但是从另一个方面讲,这个方便的工具也助长了程序员的懒惰情绪,很多新手和学生甚至连引用计数的含义都不清楚.的确,这部分内容相对比较难以理解,不过如果能深刻地领悟之后将会是未来学习和开发中的强大助推器. 1.关于对象所有权的概念: 当一个所有者(可以是任意的objective-c

[IOS]关于Obj-C内存管理的规则

Objective-C 2.0增加了一些新的东西,包括属性和垃圾回收.那么,我们在学习Objective-C 2.0之前,最好应该先了解,从前是什么样的,为什么Objective-C 2.0要增加这些支持. 这一切都跟Cocoa内存的管理规则有关系,我们知道,Objective-C中所有变量都定义为指针.指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址,如果使用不当,就会出错或者造成内存的泄露.要了解这些,就需要看看其内存管理的规则到底是什么样的. 这篇文章也应该做为苹果开发工具

Java进阶10 内存管理与垃圾回收

作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢!   整个教程中已经不时的出现一些内存管理和垃圾回收的相关知识.这里进行一个小小的总结. Java是在JVM所虚拟出的内存环境中运行的.内存分为栈(stack)和堆(heap)两部分.我们将分别考察这两个区域.   栈 栈的基本概念参考纸上谈兵: 栈 (stack).许多语言利用栈数据结构来记录函数调用的次序和相关变量(参考Linux从程序到进程). 在Java中,JVM中的栈记录

iOS的block内存管理

初始情况下: block本身.__block修饰的变量以及在block内部使用的变量都是在栈里的. __block修饰的变量的地址会作为实参传入block块内部(暂时先这么理解,实际比较复杂).block使用的外部变量被const拷贝到了block内部.也就是block使用的外部变量和这个外部变量本身没有关系.   copy方法之后 被拷贝的方法还是在栈上.但是拷贝之后的block已经被放在了heap(堆)上.同时__block修饰的变量被移动到了堆上,原来在栈上的已经不存在.还有block的外

iOS ARC 内存管理要点

前言 在讨论 ARC 之前,我们需要知道 Objective-C 采用的是引用计数式的内存管理方式,这一方式的特点是: 自己生成的对象自己持有.比如:NSObject * __strong object = [NSObject alloc] init];. 非自己生成的对象自己也能持有.比如:NSMutableArray * __strong array = [NSMutableArray array];. 自己持有的对象不再需要时释放. 非自己持有的对象自己无法释放. 而 ARC 则是帮助我们

理解iOS的内存管理

远古时代的故事 那些经历过手工管理内存(MRC)时代的人们,一定对 iOS 开发中的内存管理记忆犹新.那个时候大约是 2010 年,国内 iOS 开发刚刚兴起,tinyfool 大叔的大名已经如雷贯耳,而我还是一个默默无闻的刚毕业的小子.那个时候的 iOS 开发过程是这样的: 我们先写好一段 iOS 的代码,然后屏住呼吸,开始运行它,不出所料,它崩溃了.在 MRC 时代,即使是最牛逼的 iOS 开发者,也不能保证一次性就写出完美的内存管理代码.于是,我们开始一步一步调试,试着打印出每个怀疑对象的