内存管理

概述

三种主要的内存管理方式:

  1. 显示内存释放:C 中的 free、C++ 中的 delete;
  2. 基于引用计数:C++ 中的智能指针(smart pointer),Objective-C 中使用的方式;
  3. 垃圾回收机制:Java、JavaScript、C#;

OS X (macOS) 过去支持垃圾回收,现在已经废除了。iOS 则一直不支持垃圾回收。

引用计数

iOS 支持 MRC (Mannul Reference Counting) 手动引用计数与 ARC (Automatic Reference Counting) 自动引用计数两种管理机制。

MRC

手动引用计数主要有以下的几个相关的操作:

  1. 生成(分配一个对象,并且引用计数置为 1):alloccpoynew
  2. 持有(将一个对象的引用计数自增 1):retain
  3. 释放(将一个对象的引用计数自减 1):releaseautorelease
  4. 废弃(释放一个对象,引用计数降为 0 应该废弃):dealloc
  5. 显示(显示一个对象的引用计数):retainCount

为什么要引入 autorelease

  • 在初始化一个对象的时候自动指明,如果对象的引用计数降为 0,则自动释放这个对象。

  • 比如在未使用 autorelease  的时候一个代码可能是这么写的:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    - (People *)createPeople {
        People *people = [[People alloc] init];
        people.name = @"小王";
        people.age = 10;
        people.male = YES;
        return people;
    }
    
    - (void)func2 {
        People *people = [self createPeople];
        [people trainingDog];
        [people release];
    }
    
  •  如果使用了这个关键字,则代码会被写成这样:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    - (People *)createPeople {
        People *people = [[[People alloc] init] autorelease];
        people.name = @"小王";
        people.age = 10;
        people.male = YES;
        return people;
    }
    
    - (void)func2 {
        People *people = [self createPeople];
        [people trainingDog];
    }
    

什么是 AutoReleasePool

  • 线程在一个 Autorelease Pool 的上下文中执行,线程任务完成后销毁,主线程不会销毁;

  • 在一次事件循环结束后,Autorelease Pool 中声明的变量会被自动释放;

  • 如果在这个线程中定义了大量的变量,在释放变量时则会导致短时间的内存占用过高。比如下面的代码:

    1
    2
    3
    4
    
        for (int i = 0; i < 100; i++) {
            NSString *fileContents = [NSString stringWithContentsOfURL:urlArray[i] encoding:NSUnicodeStringEncoding error:nil];
            NSLog(@"%@", fileContents);
        }
    

    使用 AutoReleasePool 则可以写成下面的形式:

    1
    2
    3
    4
    5
    6
    
        for (int i = 0; i < 100; i++) {
            NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
            NSString *fileContents = [NSString stringWithContentsOfURL:urlArray[i] encoding:NSUnicodeStringEncoding error:nil];
            NSLog(@"%@", fileContents);
            [pool release];
        }
    

    也可以用比较语法糖的写法:

    1
    2
    3
    4
    5
    6
    
        for (int i = 0; i < 100; i++) {
            @autoreleasepool {
                NSString *fileContents = [NSString stringWithContentsOfURL:urlArray[i] encoding:NSUnicodeStringEncoding error:nil];
                NSLog(@"%@", fileContents);
            }
        }
    

如何解决循环引用?

  • 循环引用指两个对象相互持有,导致无法被释放的状态。比如:

    1
    2
    3
    4
    5
    6
    7
    
    @interface People : NSObjec
    @property(nonatomic, retain) Dog *dog;
    @end
    
    @interface Dog : NSObject
    @property(nonatomic, retain) People *owner;
    @end
    
  • 解决循环引用,只需要将其中一个持有改为赋值即可:

    1
    2
    3
    4
    5
    6
    7
    
    @interface People : NSObjec
    @property(nonatomic, retain) Dog *dog;
    @end
    
    @interface Dog : NSObject
    @property(nonatomic, assign) People *owner;
    @end
    

ARC

系统会检测出何时需要保持对象,何时需要自动释放对象,何时需要释放对象,编译器会管理好对象的内存,会在何时的地方插入 retain, releaseautorelease,通过生成正确的代码去自动释放或者保持对象。

相比于 MRC,ARC 使用的是以下的几个关键词:

  1. 生成仍然保留 allocnewcpoy
  2. 持有使用关键词 strong(相对于 MRC 中的关键词 retain);
  3. 不持有则使用关键词 weak(相对于 MRC 中的关键词 assign区别),当对象没有被强持有时置为 nil
  4. 释放保留了之前的两个关键词,引入了新的关键词 dealloc,它的好处是无需要释放父类;

两种拷贝协议:

  1. NSCopying:调用 copy 方法,需要实现 NSCopying 协议,否则 Crash。比如下面的调用:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    @class People;
    @interface Dog : NSObject <NSCopying>
    @property(nonatomic, strong) NSString *name;
    @property(nonatomic, assign) NSInteger weight;
    @property(nonatomic, weak) People *owner;
    
    @end
    
    @implementation Dog
    - (id)copyWithZone:(nullable NSZone *)zone {
        Dog *dog = [[Dog alloc] init];
        dog.name = self.name;
        dog.weight = self.weight;
        dog.owner = self.owner;
        return dog;
    }
    @end
    
  2. NSMutableCopying:字符串、数组、字典等对象,需要进行深拷贝对象,需要实现 NSMutableCopying。比如下面的调用:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    @class People;
    @interface Dog : NSObject <NSMutableCopying>
    @property(nonatomic, strong) NSString *name;
    @property(nonatomic, assign) NSInteger weight;
    @property(nonatomic, weak) People *owner;
    
    @end
    
    @implementation Dog
    - (id)mutableCopyWithZone:(nullable NSZone *)zone {
        Dog *dog = [[Dog alloc] init];
        dog.name = self.name;
        dog.weight = self.weight;
        dog.owner = self.owner;
        return dog;
    }
    @end
    

    这个协议的实现可以设计成 不可变与可变 两种形式,其区别就在于外界是否可以对对象进行修改(我感觉就类似于 c++ 中的引用与常量引用)。