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

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

发表于 14小时前| 546次阅读| 来源 CSDN| 5 条评论| 作者 刘耀柱
width="22" height="16" src="http://hits.sinajs.cn/A1/weiboshare.html?url=http%3A%2F%2Fwww.csdn.net%2Farticle%2F2015-11-12%2F2826198&type=3&count=&appkey=&title=%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E6%98%AF%E5%9C%A8%E7%A8%8B%E5%BA%8F%E9%9C%80%E8%A6%81%E6%97%B6%E7%A8%8B%E5%BA%8F%E5%91%98%E5%88%86%E9%85%8D%E4%B8%80%E6%AE%B5%E5%86%85%E5%AD%98%E7%A9%BA%E9%97%B4%EF%BC%8C%E8%80%8C%E5%BD%93%E4%BD%BF%E7%94%A8%E5%AE%8C%E4%B9%8B%E5%90%8E%E5%B0%86%E5%AE%83%E9%87%8A%E6%94%BE%E3%80%82%E5%A6%82%E6%9E%9C%E7%A8%8B%E5%BA%8F%E5%91%98%E5%AF%B9%E5%86%85%E5%AD%98%E8%B5%84%E6%BA%90%E4%BD%BF%E7%94%A8%E4%B8%8D%E5%BD%93%EF%BC%8C%E6%9C%89%E6%97%B6%E4%B8%8D%E4%BB%85%E4%BC%9A%E9%80%A0%E6%88%90%E5%86%85%E5%AD%98%E8%B5%84%E6%BA%90%E6%B5%AA%E8%B4%B9%EF%BC%8C%E7%94%9A%E8%87%B3%E4%BC%9A%E5%AF%BC%E8%87%B4%E7%A8%8B%E5%BA%8Fcrach%E3%80%82%E6%9C%AC%E6%96%87%E4%BD%9C%E8%80%85%E4%BB%8E%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5%E5%BC%80%E5%A7%8B%EF%BC%8C%E5%89%96%E6%9E%90%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E7%9A%84%E6%96%B9%E6%B3%95%E4%B8%8E%E9%97%AE%E9%A2%98%E3%80%82&pic=&ralateUid=&language=zh_cn&rnd=1447374916061" frameborder="0" scrolling="no" allowtransparency="true"> 摘要:内存管理是在程序需要时程序员分配一段内存空间,而当使用完之后将它释放。如果程序员对内存资源使用不当,有时不仅会造成内存资源浪费,甚至会导致程序crach。本文作者从基本概念开始,剖析内存管理的方法与问题。

CSDN移动将持续为您优选移动开发的精华内容,共同探讨移动开发的技术热点话题,涵盖移动应用、开发工具、移动游戏及引擎、智能硬件、物联网等方方面面。如果您想投稿、寻求《近匠》报道,或给文章挑错,欢迎发送邮件至tangxy#csdn.net(请把#改成@)。 


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


memory management from apple document

基本概念

引用计数(Reference Count)

为了解释引用计数,我们做一个类比:员工在办公室使用灯的情景。


引用Pro Multithreading and Memory Management for iOS and OS X的图

  • 当第一个人进入办公室时,他需要使用灯,于是开灯,引用计数为1;
  • 当另一个人进入办公室时,他也需要灯,引用计数为2;每当多一个人进入办公室时,引用计数加1;
  • 当有一个人离开办公室时,引用计数减1,当引用计数为0时,也就是最后一个人离开办公室时,他不再需要使用灯,关灯离开办公室。

内存管理规则

从上面员工在办公室使用灯的例子,我们对比一下灯的动作与Objective-C对象的动作有什么相似之处:


因为我们是通过引用计数来管理灯,那么我们也可以通过引用计数来管理使用Objective-C对象。


引用Pro Multithreading and Memory Management for iOS and OS X的图

而Objective-C对象的动作对应有哪些方法以及这些方法对引用计数有什么影响?


当你alloc一个对象objc,此时RC=1;在某个地方你又retain这个对象objc,此时RC加1,也就是RC=2;由于调用alloc/retain一次,对应需要调用release一次来释放对象objc,所以你需要release对象objc两次,此时RC=0;而当RC=0时,系统会自动调用dealloc方法释放对象。

Autorelease Pool

在开发中,我们常常都会使用到局部变量,局部变量一个特点就是当它超过作用域时,就会自动释放。而autorelease pool跟局部变量类似,当执行代码超过autorelease pool块时,所有放在autorelease pool的对象都会自动调用release。它的工作原理如下:

  • 创建一个NSAutoreleasePool对象;
  • 在autorelease pool块的对象调用autorelease方法;
  • 释放NSAutoreleasePool对象。


引用Pro Multithreading and Memory Management for iOS and OS X的图

iOS 5/OS X Lion前的(等下会介绍引入ARC的写法)实例代码如下:

  1. NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];  
  2.   
  3. // put object into pool  
  4. id obj = [[NSObject alloc] init];  
  5. [obj autorelease];  
  6.   
  7. [pool drain];  
  8.   
  9. /* 超过autorelease pool作用域范围时,obj会自动调用release方法 */  
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

// put object into pool
id obj = [[NSObject alloc] init];
[obj autorelease];

[pool drain];

/* 超过autorelease pool作用域范围时,obj会自动调用release方法 */

由于放在autorelease pool的对象并不会马上释放,如果有大量图片数据放在这里的话,将会导致内存不足。

  1. for (int i = 0; i < numberOfImages; i++)  
  2. {  
  3.       /*   处理图片,例如加载 
  4.        *   太多autoreleased objects存在 
  5.        *   由于NSAutoreleasePool对象没有被释放 
  6.        *   在某个时刻,会导致内存不足  
  7.        */  
  8. }  
for (int i = 0; i < numberOfImages; i++)
{
      /*   处理图片,例如加载
       *   太多autoreleased objects存在
       *   由于NSAutoreleasePool对象没有被释放
       *   在某个时刻,会导致内存不足 
       */
}

ARC管理方法

iOS/OS X内存管理方法有两种:手动引用计数(Manual Reference Counting)和自动引用计数(Automatic Reference Counting)。从OS X Lion和iOS 5开始,不再需要程序员手动调用retain和release方法来管理Objective-C对象的内存,而是引入一种新的内存管理机制Automatic Reference Counting(ARC),简单来说,它让编译器来代替程序员来自动加入retain和release方法来持有和放弃对象的所有权。

在ARC内存管理机制中,id和其他对象类型变量必须是以下四个ownership qualifiers其中一个来修饰:

  • __strong(默认,如果不指定其他,编译器就默认加入)
  • __weak
  • __unsafe_unretained
  • __autoreleasing

所以在管理Objective-C对象内存的时候,你必须选择其中一个,下面会用一些列子来逐个解释它们的含义以及如何选择它们。

__strong ownership qualifier

如果我想创建一个字符串,使用完之后将它释放调用,使用MRC管理内存的写法应该是这样:

  1. {  
  2.     NSString *text = @"Hello, world";    //@"Hello, world"对象的RC=1  
  3.     NSLog(@"%@", text);  
  4.     [text release];                      //@"Hello, world"对象的RC=0  
  5. }  
{
    NSString *text = @"Hello, world";    //@"Hello, world"对象的RC=1
    NSLog(@"%@", text);
    [text release];                      //@"Hello, world"对象的RC=0
}

而如果是使用ARC方式的话,就text对象无需调用release方法,而是当text变量超过作用域时,编译器来自动加入[text release]方法来释放内存。

  1. {  
  2.     NSString *text = @"Hello, world";    //@"Hello, world"对象的RC=1  
  3.     NSLog(@"%@", text);  
  4. }  
  5. /* 
  6.  *  当text超过作用域时,@"Hello, world"对象会自动释放,RC=0 
  7.  */  
{
    NSString *text = @"Hello, world";    //@"Hello, world"对象的RC=1
    NSLog(@"%@", text);
}
/*
 *  当text超过作用域时,@"Hello, world"对象会自动释放,RC=0
 */

而当你将text赋值给其他变量anotherText时,MRC需要retain一下来持有所有权,当text和anotherText使用完之后,各个调用release方法来释放。

  1. {  
  2.     NSString *text = @"Hello, world";    //@"Hello, world"对象的RC=1  
  3.     NSLog(@"%@", text);  
  4.   
  5.     NSString *anotherText = text;        //@"Hello, world"对象的RC=1  
  6.     [anotherText retain];                //@"Hello, world"对象的RC=2  
  7.     NSLog(@"%@", anotherText);  
  8.   
  9.     [text release];                      //@"Hello, world"对象的RC=1  
  10.     [anotherText release];               //@"Hello, world"对象的RC=0  
  11. }  
{
    NSString *text = @"Hello, world";    //@"Hello, world"对象的RC=1
    NSLog(@"%@", text);

    NSString *anotherText = text;        //@"Hello, world"对象的RC=1
    [anotherText retain];                //@"Hello, world"对象的RC=2
    NSLog(@"%@", anotherText);

    [text release];                      //@"Hello, world"对象的RC=1
    [anotherText release];               //@"Hello, world"对象的RC=0
}

而使用ARC的话,并不需要调用retain和release方法来持有跟释放对象。

  1. {  
  2.     NSString *text = @"Hello, world";    //@"Hello, world"对象的RC=1  
  3.     NSLog(@"%@", text);  
  4.   
  5.     NSString *anotherText = text;        //@"Hello, world"对象的RC=2  
  6.     NSLog(@"%@", anotherText);  
  7. }  
  8. /* 
  9.  *  当text和anotherText超过作用域时,会自动调用[text release]和[anotherText release]方法, @"Hello, world"对象的RC=0 
  10.  */  
{
    NSString *text = @"Hello, world";    //@"Hello, world"对象的RC=1
    NSLog(@"%@", text);

    NSString *anotherText = text;        //@"Hello, world"对象的RC=2
    NSLog(@"%@", anotherText);
}
/*
 *  当text和anotherText超过作用域时,会自动调用[text release]和[anotherText release]方法, @"Hello, world"对象的RC=0
 */

除了当__strong变量超过作用域时,编译器会自动加入release语句来释放内存,如果你将__strong变量重新赋给它其他值,那么编译器也会自动加入release语句来释放变量指向之前的对象。例如:

  1. {  
  2.     NSString *text = @"Hello, world";    //@"Hello, world"对象的RC=1  
  3.     NSString *anotherText = text;        //@"Hello, world"对象的RC=2  
  4.     NSString *anotherText = @"Sam Lau";  // 由于anotherText对象引用另一个对象@"Sam Lau",那么就会自动调用[anotherText release]方法,使得@"Hello, world"对象的RC=1, @"Sam Lau"对象的RC=1  
  5. }  
  6. /* 
  7.  *  当text和anotherText超过作用域时,会自动调用[text release]和[anotherText release]方法, 
  8.  *  @"Hello, world"对象的RC=0和@"Sam Lau"对象的RC=0 
  9.  */  
{
    NSString *text = @"Hello, world";    //@"Hello, world"对象的RC=1
    NSString *anotherText = text;        //@"Hello, world"对象的RC=2
    NSString *anotherText = @"Sam Lau";  // 由于anotherText对象引用另一个对象@"Sam Lau",那么就会自动调用[anotherText release]方法,使得@"Hello, world"对象的RC=1, @"Sam Lau"对象的RC=1
}
/*
 *  当text和anotherText超过作用域时,会自动调用[text release]和[anotherText release]方法,
 *  @"Hello, world"对象的RC=0和@"Sam Lau"对象的RC=0
 */
如果变量var被__strong修饰,当变量var指向某个对象objc,那么变量var持有某个对象objc的所有权。

前面已经提过内存管理的四条规则:


我们总结一下编译器是按以下方法来实现的:

  • 对于规则1和规则2,是通过__strong变量来实现;
  • 对于规则3来说,当变量超过它的作用域或被赋值或成员变量被丢弃时就能实现;
  • 对于规则4,当RC=0时,系统就会自动调用。

__weak ownership qualifier

其实编译器根据__strong修饰符来管理对象内存。但是__strong并不能解决引用循环(Reference Cycle)问题:对象A持有对象B,反过来,对象B持有对象A;这样会导致不能释放内存造成内存泄露问题。

引用Pro Multithreading and Memory Management for iOS and OS X的图

举一个简单的例子,有一个类Test有个属性objc,有两个对象test1和test2的属性objc互相引用test1和test2:

  1. @interface Test : NSObject  
  2. @property (strong, nonatomic) id objc;  
  3. @end  
@interface Test : NSObject
@property (strong, nonatomic) id objc;
@end

  1. {  
  2.     Test *test1 = [Test new];        /* 对象a */  
  3.     /* test1有一个强引用到对象a */  
  4.   
  5.     Test *test2 = [Test new];        /* 对象b */  
  6.     /* test2有一个强引用到对象b */  
  7.   
  8.     test1.objc = test2;              /* 对象a的成员变量objc有一个强引用到对象b */  
  9.     test2.objc = test1;              /* 对象b的成员变量objc有一个强引用到对象a */  
  10. }  
  11. /*   当变量test1超过它作用域时,它指向a对象会自动release 
  12.  *   当变量test2超过它作用域时,它指向b对象会自动release 
  13.  *    
  14.  *   此时,b对象的objc成员变量仍持有一个强引用到对象a 
  15.  *   此时,a对象的objc成员变量仍持有一个强引用到对象b 
  16.  *   于是发生内存泄露 
  17.  */  
{
    Test *test1 = [Test new];        /* 对象a */
    /* test1有一个强引用到对象a */

    Test *test2 = [Test new];        /* 对象b */
    /* test2有一个强引用到对象b */

    test1.objc = test2;              /* 对象a的成员变量objc有一个强引用到对象b */
    test2.objc = test1;              /* 对象b的成员变量objc有一个强引用到对象a */
}
/*   当变量test1超过它作用域时,它指向a对象会自动release
 *   当变量test2超过它作用域时,它指向b对象会自动release
 *   
 *   此时,b对象的objc成员变量仍持有一个强引用到对象a
 *   此时,a对象的objc成员变量仍持有一个强引用到对象b
 *   于是发生内存泄露
 */

如何解决?于是我们引用一个__weakownership qualifier,被它修饰的变量都不持有对象的所有权,而且当变量指向的对象的RC为0时,变量设置为nil。例如:

  1. __weak NSString *text = @"Sam Lau";  
  2. NSLog(@"%@", text);  
__weak NSString *text = @"Sam Lau";
NSLog(@"%@", text);

由于text变量被__weak修饰,text并不持有@"Sam Lau"对象的所有权,@"Sam Lau"对象一创建就马上被释放,并且编译器给出警告⚠️,所以打印结果为(null)。

所以,针对刚才的引用循环问题,只需要将Test类的属性objc设置weak修饰符,那么就能解决。

  1. @interface Test : NSObject  
  2. @property (weak, nonatomic) id objc;  
  3. @end  
@interface Test : NSObject
@property (weak, nonatomic) id objc;
@end

  1. {  
  2.     Test *test1 = [Test new];        /* 对象a */  
  3.     /* test1有一个强引用到对象a */  
  4.   
  5.     Test *test2 = [Test new];        /* 对象b */  
  6.     /* test2有一个强引用到对象b */  
  7.   
  8.     test1.objc = test2;              /* 对象a的成员变量objc不持有对象b */  
  9.     test2.objc = test1;              /* 对象b的成员变量objc不持有对象a */  
  10. }  
  11. /*   当变量test1超过它作用域时,它指向a对象会自动release 
  12.  *   当变量test2超过它作用域时,它指向b对象会自动release 
  13.  */  
{
    Test *test1 = [Test new];        /* 对象a */
    /* test1有一个强引用到对象a */

    Test *test2 = [Test new];        /* 对象b */
    /* test2有一个强引用到对象b */

    test1.objc = test2;              /* 对象a的成员变量objc不持有对象b */
    test2.objc = test1;              /* 对象b的成员变量objc不持有对象a */
}
/*   当变量test1超过它作用域时,它指向a对象会自动release
 *   当变量test2超过它作用域时,它指向b对象会自动release
 */

__unsafe_unretained ownership qualifier

__unsafe_unretained ownership qualifier,正如名字所示,它是不安全的。它跟__weak相似,被它修饰的变量都不持有对象的所有权,但当变量指向的对象的RC为0时,变量并不设置为nil,而是继续保存对象的地址;这样的话,对象有可能已经释放,但继续访问,就会造成非法访问(Invalid Access)。例子如下:

  1. __unsafe_unretained id obj0 = nil;  
  2. {  
  3.     id obj1 = [[NSObject alloc] init];     // 对象A  
  4.     /* 由于obj1是强引用,所以obj1持有对象A的所有权,对象A的RC=1 */  
  5.     obj0 = obj1;  
  6.     /* 由于obj0是__unsafe_unretained,它不持有对象A的所有权,但能够引用它,对象A的RC=1 */  
  7.     NSLog(@"A: %@", obj0);  
  8. }  
  9. /* 当obj1超过它的作用域时,它指向的对象A将会自动释放 */  
  10. NSLog(@"B: %@", obj0);  
  11. /* 由于obj0是__unsafe_unretained,当它指向的对象RC=0时,它会继续保存对象的地址,所以两个地址相同 */  
__unsafe_unretained id obj0 = nil;
{
    id obj1 = [[NSObject alloc] init];     // 对象A
    /* 由于obj1是强引用,所以obj1持有对象A的所有权,对象A的RC=1 */
    obj0 = obj1;
    /* 由于obj0是__unsafe_unretained,它不持有对象A的所有权,但能够引用它,对象A的RC=1 */
    NSLog(@"A: %@", obj0);
}
/* 当obj1超过它的作用域时,它指向的对象A将会自动释放 */
NSLog(@"B: %@", obj0);
/* 由于obj0是__unsafe_unretained,当它指向的对象RC=0时,它会继续保存对象的地址,所以两个地址相同 */

打印结果是内存地址相同:


如果将__unsafe_unretained改为weak的话,两个打印结果将不同。

  1. __weak id obj0 = nil;  
  2. {  
  3.     id obj1 = [[NSObject alloc] init];     // 对象A  
  4.     /* 由于obj1是强引用,所以obj1持有对象A的所有权,对象A的RC=1 */  
  5.     obj0 = obj1;  
  6.     /* 由于obj0是__unsafe_unretained,它不持有对象A的所有权,但能够引用它,对象A的RC=1 */  
  7.     NSLog(@"A: %@", obj0);  
  8. }  
  9. /* 当obj1超过它的作用域时,它指向的对象A将会自动释放 */  
  10. NSLog(@"B: %@", obj0);  
  11. /* 由于obj0是__weak, 当它指向的对象RC=0时,它会自动设置为nil,所以两个打印结果将不同*/  
__weak id obj0 = nil;
{
    id obj1 = [[NSObject alloc] init];     // 对象A
    /* 由于obj1是强引用,所以obj1持有对象A的所有权,对象A的RC=1 */
    obj0 = obj1;
    /* 由于obj0是__unsafe_unretained,它不持有对象A的所有权,但能够引用它,对象A的RC=1 */
    NSLog(@"A: %@", obj0);
}
/* 当obj1超过它的作用域时,它指向的对象A将会自动释放 */
NSLog(@"B: %@", obj0);
/* 由于obj0是__weak, 当它指向的对象RC=0时,它会自动设置为nil,所以两个打印结果将不同*/


__autoreleasing ownership qualifier

引入ARC之后,让我们看看autorelease pool有哪些变化。没有ARC之前的写法如下:

  1. NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];  
  2. // put object into pool  
  3. id obj = [[NSObject alloc] init];  
  4. [obj autorelease];  
  5. [pool drain];  
  6. /* 超过autorelease pool作用域范围时,obj会自动调用release方法 */  
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// put object into pool
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
/* 超过autorelease pool作用域范围时,obj会自动调用release方法 */

引入ARC之后,写法比之前更加简洁:

  1. @autoreleasepool {  
  2.     id __autoreleasing obj = [[NSObject alloc] init];  
  3. }  
@autoreleasepool {
    id __autoreleasing obj = [[NSObject alloc] init];
}

相比之前的创建、使用和释放NSAutoreleasePool对象,现在你只需要将代码放在@autoreleasepool块即可。你也不需要调用autorelease方法了,只需要用__autoreleasing修饰变量即可。


引用Pro Multithreading and Memory Management for iOS and OS X的图

但是我们很少或基本上不使用autorelease pool。当我们使用XCode创建工程后,有一个app的入口文件main.m使用了它:

  1. int main(int argc, char * argv[]) {  
  2.     @autoreleasepool {  
  3.         return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));  
  4.     }  
  5. }  
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

Property(属性)

有了ARC之后,新的property modifier也被引入到Objective-C类的property,例如:

  1. <font color="#000000">@property (strong, nonatomic) NSString *text;</font>  
<font color="#000000">@property (strong, nonatomic) NSString *text;</font>
下面有张表来展示property modifier与ownership qualifier的对应关系:


总结

要想掌握iOS/OS X的内存管理,首先要深入理解引用计数(Reference Count)这个概念以及内存管理的规则;在没引入ARC之前,我们都是通过retain和release方法来手动管理内存,但引入ARC之后,我们可以借助编译器来帮忙自动调用retain和release方法来简化内存管理和减低出错的可能性。虽然__strong修饰符能够执行大多数内存管理,但它不能解决引用循环(Reference Cycle)问题,于是又引入另一个修饰符__weak。被__strong修饰的变量都持有对象的所有权,而被__weak修饰的变量并不持有对象所有权。下篇我们介绍使用工具如何解决常见内存问题:野指针和内存泄露。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值