iOS 內存管理

# 前言

反復地復習iOS基礎知識和原理,打磨知識體系是非常重要的,本篇就是重新溫習iOS的內存管理。

內存管理是管理對象生命周期,在對象不需要時進行內存釋放的編程規范。

# 目錄

  • 前言

  • 目錄

  • MRC時代

    • 概要
    • Memory Management Policy 內存管理策略
    • Practical Memory Management 實際內存管理
    • 內存管理實踐
      • 使用訪問器方法使內存管理更輕松
      • 使用訪問器方法設置屬性
      • 不要在初始化和dealloc中使用訪問器方法
      • 使用弱引用來避免循環引用
      • 避免正在使用的對象被釋放
      • Collections類擁有它們所包含的對象所有權
      • 通過引用計數實現所有所有權策略
  • ARC時代

    • 概要
    • ARC 強制新規則
    • 內存泄漏
    • block使用中出現循環引用
    • NSTimer循環引用
  • 參考資料

# MRC時代

概要

Objective-C內存管理使用使用引用計數(Reference Counting)來管理內存。

在OS X 10.8以后也不再使用垃圾回收機制,iOS則從來都沒有支持垃圾回收機制。

create或者copy對象時,會計數為1,其他對象需要retain時,會增加引用計數。持有對象的所有者也可以放棄所有權,放棄所有權時減少計數,當計數為0時就會釋放對象。
如圖:

memory_management 圖片來自官方文檔

Memory Management Policy 內存管理策略

  • 通過分配內存或copy來創建任何對象
  • 使用方法 alloc, allocWithZone:, copy, copyWithZone:, mutableCopy , mutableCopyWithZone:創建對象
  • 通過retain來獲取不是自己創建對象的所有權。以下兩種情況使用retain:
  1. accessor method或者init method方法獲取所需要的對象所有權為屬性property
  2. 需要操作對象時,避免對象被釋放而導致錯誤,需要retain持有對象。
  • 發送release, autorelease消息來釋放不需要的對象。
  • 不要不是你創建的對象和沒有所有權的對象發送release消息。

Practical Memory Management 實際內存管理

  • Autorelease pools
  • 向對象發送autorelease消息,會將對象標記為延遲釋放,當對象超出當前作用域時,釋放對象。
  • AppKit frameworksUIKit frameworks在事件循環的每個周期開始時,在主線程上創建一個自動釋放池,并在此次時間循環結束時,釋放它,從而釋放在處理時生成的所有自動釋放的對象。因此,通常不需要自己創建autoreleasePool,當然,以下情況你需要自己創建和銷毀autoreleasePool
  1. 如果你編寫的代碼不是基于UI framework的程序,如command-line tool命令行工具。
  2. 如果你需要寫一個循環,創建許多臨時對象,如讀入大量的銅像同時改變圖片尺寸,圖像讀入到NSData對象,并從中生成UIImage對象,改變該對象尺寸生成新的UIImage對象。
  3. 如果你創建一個長期存在線程并且可能產生大量的autorelease對象。

autoreleasePool推薦使用以下方法:

@autoreleasepool {
 //do something
}
  • dealloc
    NSObject對象的引用計數為0時,銷毀該對象前會調用dealloc方法,用來釋放該對象擁有的所有資源,包裹實例變量指向的對象。
    例子:
 // MRC
 - (void)dealloc{
    [_firstName release];
    [_lastName release];
    [super dealloc];
 }

Important: Never invoke another object’s dealloc method directly.You must invoke the superclass’s implementation at the end of your implementation.You should not tie management of system resources to object lifetimes; see Don’t Use dealloc to Manage Scarce Resources.When an application terminates, objects may not be sent a dealloc message. Because the process’s memory is automatically cleared on exit, it is more efficient simply to allow the operating system to clean up resources than to invoke all the memory management methods.
不要直接調用另一個對象的dealloc方法。你必須在類使用結束時調用父類的實現。你不應該把系統資源與對象的生命周期綁定。
因為進程的內存退出時,對象可能無法發送dealloc消息,該方法的內存被自動退出清零,所以讓操作系統清理資源比調用所有的內存管理方法更有效。

內存管理實踐

使用訪問器方法使內存管理更輕松

如果類有一個屬性是一個對象,你必須確保使用該對象時,它不會被釋放。因此在設置時,必須聲明對象的所有權。還必須保證持有這些對象所有權的放棄。

  • 使用setget方法來實現,更方便管理內存(主要是省寫很多retainrelease)。
    例子如下:
@interface Counter : NSObject
@property (nonatomic, retain) NSNumber *count;
@end;

Counter類有一個屬性是NSNumber對象,屬性聲明了setget兩個訪問器方法,在get中就是返回synthesized實例變量,所以沒必要retain或者release

- (NSNumber *)count {
 return _count;
}

set方法:

- (void)setCount:(NSNumber *)newCount {
    [newCount retain]; // 先`retain`確保新數據不被釋放
    [_count release];  // 釋放舊對象所有權
    // Make the new assignment.
    _count = newCount;  // 將新值賦給_count
}

retain確保新數據不被釋放,釋放舊的對象所有權(Objective-C允許向nil發送消息)。你必須在[newCount retain]之后再[_count release]確保外部不會被dealloc

使用訪問器方法設置屬性

// 方法一
- (void)reset {
    NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
    [self setCount:zero];
    [zero release];
}
// 方法二
- (void)reset {
    NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
    [_count release];
    _count = zero;
}

方法二沒有對count屬性賦新值時沒有使用set訪問方法,也不會觸發KVO,可能在特殊情況導致錯誤(比如忘記了 retain或者release,或者如果實例變量的內存管理發生了變化)。除了第一種方法,或者直接使用self.count = zero;

不要在初始化和dealloc中使用訪問器方法

不應該使用setget方法在initdealloc。應該使用_直接訪問成員變量進行初始化和dealloc。如下:

- init {
    self = [super init];
    if (self) {
        _count = [[NSNumber alloc] initWithInteger:0];
    }
    return self;
}
// 由于Counter類具有對象實例變量,因此還必須實現dealloc方法。
// 它應該通過向任何實例變量發送一個釋放消息來放棄它的所有權,最終它應該調用super的實現
- (void)dealloc {
    [_count release];
    [super dealloc];
}

使用弱引用來避免循環引用

  • retain對象,實際是對對象的強引用(strong reference),一個對象在所有強引用都沒有被釋放之前,不能釋放對象。因此,如果有兩個對象互相持有對方或者間接互相引用,會導致循環引用。這時候就需要弱引用對方來打破這個循環。

如父親強引用兒子,兒子強引用孫子,那么倒過來孫子只能弱引用兒子,兒子也只能弱引用父親。Cocoa建立了一個約定,副對象應該強引用子對象,并且子對象應該只對父對象弱引用。
Cocoa中常見的例子包括代理方法delegatedata source,observer,target等等

必須小心將消息發送到持有只是一個弱引用的對象。當發送消息給一個被dealloc的弱引用對象時,你的應用程序會崩潰(這是在MRC時期的代理delegate會出現,因為當時對代理弱引用的修飾符是assign,assign弱引用并不會在對象dealloc時,把對象置為nil。而ARC時代使用weak則會在對象dealloc時置為nil)。

避免正在使用的對象被釋放

  • Cocoa的所有權策略規定接收的對象通常在整個調用方法的范圍內保證有效。還應該是在當前方法范圍內,而不必擔心它被釋放。對象的getter方法返回一個緩存的實例變量或者一個計算的值,這不重要,重要的是,對象在需要的使用時還是有效的。
  • 有兩類例外情況:
  • 當一個對象從基本的集合類刪除時
heisenObject = [array objectAtIndex:n];
[array removeObjectAtIndex:n];
// heisenObject 現在可能無效
  • n從集合array刪除時也會向n發送release(而不是autorelease)消息。如果array集合時被刪除n對象的唯一擁有者,被移除的對象n是立即被釋放的。heisenObject并沒有對n進行retain,所以當narray刪除時同時被釋放。

正確的做法

heisenObject = [[array objectAtIndex:n] retain];
[array removeObjectAtIndex:n];
// Use heisenObject...
[heisenObject release];
  • 當一個父對象被釋放時
id parent = <#create a parent object#>;
// ...
heisenObject = [parent child] ;
[parent release]; // Or, for example: self.parent = nil;
// heisenObject 現在可能無效
  • 在某些情況下,從另一個對象獲取的對象,然后直接或者間接的釋放負對象。如果釋放父對象導致它被釋放,并且父對象是子對象唯一所有者,那么子對象heisenObject將被同一時間釋放。所以正確的做法還是子對象heisenObject獲取的時候先retain一次。

Collections類擁有它們所包含的對象所有權

  • 添加一個對象到一個collection中,如(數組、字典、集合)時,collection會得到該對象所有權。當對象從collection刪除或者collection自己被釋放時,collection將釋放它擁有的所有權。
NSMutableArray *array = <#Get a mutable array#>;
NSUInteger i;
// ...
for (i = 0; i < 10; i++) {
    NSNumber *allocedNumber = [[NSNumber alloc] initWithInteger:i];
    [array addObject:allocedNumber];
    [allocedNumber release];
}

通過引用計數實現所有所有權策略

  • 所有圈策略是通過引用計數實現的,通常retain方法后被稱為retain count。每個對象都有一個引用計數。

  • 當你創建一個對象,它的引用計數為1

  • 當你給對象發送retain消息,引用計數+1

  • 當你給對象發送release消息,引用計數-1

  • 當你給對象發送一個autorelease消息,它的引用計數器將在當前的自動釋放池結束后-1

  • 當對象的引用計數為0時將被釋放

# ARC時代

概要

iOS5后出現了ARC。那么ARC是什么呢?
自動引用計數ARC是一種編譯器的功能,為Objective-C對象提供了自動化的內存管理。
ARC不需要開發者考慮保留或者釋放的操作,就是不用自己手動retainreleaseautorelease(??開心),讓開發者可以專注寫有趣的代碼。
當然ARC依然是基于引用計數管理內存。

ARC 強制新規則

ARC相對于MRC強制加了一些新的規則。

  • 你不能主動調用dealloc、或者調用retain,release, retainCount,autorelease就是這些都不用你寫了。也不能@selector(retain), @selector(release)這樣子調用。
  • 你可以實現一個dealloc方法,如果你需要管理資源而不是釋放實例變量(比如解除監聽、釋放引用、socket close等等)。在重寫dealloc后需要[super dealloc](在手動管理引用計數時才需要)。
  • 仍然可以使用CFRetainCFRelease等其它對象。
  • 你不能使用NSAllocateObject或者NSDeallocateObject
  • 你不能使用C結構體,可以創建一個Objective-C類去管理數據而不是一個結構體。
  • idvoid沒有轉換關系,你必須使用cast特殊方式,以便在作為函數參數傳遞的Objective-C對象和Core Foundation類型之間進行轉換。
  • 你不能使用NSAutoreleasePool,使用@autoreleasepool
  • 沒必要使用NSZone

ARC 使用新修飾符

  • __strong 強引用,用來保證對象不會被釋放。
  • __weak弱引用 釋放時會置為nil
  • __unsafe_unretained弱引用 可能不安全,因為釋放時不置為nil
  • __autoreleasing對象被注冊到autorelease pool中方法在返回時自動釋放。

內存泄漏

ARC還是基于引用計數的管理機制所以依然會出現循環引用。

block使用中出現循環引用

  • 常見的有情況在block使用中出現循環引用
// 情況一
self.myBlock = ^{
self.objc = ...;
};
// 情況二
Dog *dog = [[Dog alloc] init];
dog.myBlock = ^{
  // do something
};
self.dog = dog;
  • 解決方法
__weak typeof (self) weakSelf = self;
self.myBlock = ^{
weakSelf.objc = ...;
};
  • 那么如果block內使用了self這個時候如果某一個時刻self被釋放就會導致出現問題。
  • 解決方法
__weak typeof (self) weakSelf = self;
self.myBlock = ^{
__strong typeof(self) strongSelf = weakSelf;
strongSelf.objc1 = ...;
strongSelf.objc2 = ...;
strongSelf.objc3 = ...;
};
  • 使用__weak打破循環引用。__strong用來避免在使用self過程中self被釋放,__strongblock后會調用objc_release(obj)釋放對象。
id __strong obj = [[NSObject alloc] init];

// clang 編譯后
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);

兩次調用objc_msgSend并在變量作用域結束時調用objc_release釋放對象,不會出現循環引用問題。

NSTimer循環引用

為什么NSTimer會導致循環引用呢?

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti 
           target:(id)aTarget 
           selector:(SEL)aSelector 
           userInfo:(nullable id)userInfo 
           repeats:(BOOL)yesOrNo;
           
           
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti 
                  target:(id)aTarget 
                  selector:(SEL)aSelector 
                  userInfo:(nullable id)userInfo 
                  repeats:(BOOL)yesOrNo;
  • 主要是因為NSRunloop運行循環保持了對NSTimer的強引用,并且NSTimertarger也使用了強引用。

  • 來自文檔NSTimer

Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
target
The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to this object until it (the timer) is invalidated.

舉個??:


@interface ViewController ()<viewControllerDelegate>

@property (strong, nonatomic) NSTimer *timer;

@end

 - (void)viewDidLoad
 {
     [super viewDidLoad];
     self.timer = [NSTimer scheduledTimerWithTimeInterval:1
                                               target:self 
                                             selector:@selector(onTimeOut:) 
                                             userInfo:nil 
                                              repeats:NO];
 }
  • 這里控制器強引用了timer,而timer也強引用了控制器,這個時候就是循環引用了,引用關系如下圖:
retain cycle
  • 那么如果控制器對timer使用了weak呢?
    使用weak是打破了循環引用,但是run loop還是強引用著timer,timer又強引用著控制器,所以還是會導致內存泄漏。引用關系如下圖:
leak

如果我們把timer加入主線程的runloop,主線程中的runloop生命周期只有主線程結束才會銷毀,所以我們不主動調用[timer invalidate],runloop會一直持有timertimer又持有控制器,那么就一直不會釋放控制器。

  • 解決方法:手動調用[timer invalidate]來解除持有關系,釋放內存。可能會想到在dealloc方法中來手動調用,但是因為timer持有控制器,所以控制器的dealloc方法永遠不會調用,因為dealloc是在控制器要被釋放前調用的。在Timer Programming Topics中有特別說明。所以一般我們可以在下面這些方法中手動調用[timer invalidate]然后置為nil
- (void)viewWillDisappear:(BOOL)animated; // Called when the view is dismissed, covered or otherwise hidden. Default does nothing
- (void)viewDidDisappear:(BOOL)animated;  // Called after the view was dismissed, covered or otherwise hidden. Default does nothing

A timer maintains a strong reference to its target. This means that as long as a timer remains valid, its target will not be deallocated. As a corollary, this means that it does not make sense for a timer’s target to try to invalidate the timer in its dealloc method—the dealloc method will not be invoked as long as the timer is valid.

# 參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容