iOS 內(nèi)存管理

# 前言

反復(fù)地復(fù)習(xí)iOS基礎(chǔ)知識(shí)和原理,打磨知識(shí)體系是非常重要的,本篇就是重新溫習(xí)iOS的內(nèi)存管理。

內(nèi)存管理是管理對(duì)象生命周期,在對(duì)象不需要時(shí)進(jìn)行內(nèi)存釋放的編程規(guī)范。

# 目錄

  • 前言

  • 目錄

  • MRC時(shí)代

    • 概要
    • Memory Management Policy 內(nèi)存管理策略
    • Practical Memory Management 實(shí)際內(nèi)存管理
    • 內(nèi)存管理實(shí)踐
      • 使用訪問(wèn)器方法使內(nèi)存管理更輕松
      • 使用訪問(wèn)器方法設(shè)置屬性
      • 不要在初始化和dealloc中使用訪問(wèn)器方法
      • 使用弱引用來(lái)避免循環(huán)引用
      • 避免正在使用的對(duì)象被釋放
      • Collections類擁有它們所包含的對(duì)象所有權(quán)
      • 通過(guò)引用計(jì)數(shù)實(shí)現(xiàn)所有所有權(quán)策略
  • ARC時(shí)代

    • 概要
    • ARC 強(qiáng)制新規(guī)則
    • 內(nèi)存泄漏
    • block使用中出現(xiàn)循環(huán)引用
    • NSTimer循環(huán)引用
  • 參考資料

# MRC時(shí)代

概要

Objective-C內(nèi)存管理使用使用引用計(jì)數(shù)(Reference Counting)來(lái)管理內(nèi)存。

在OS X 10.8以后也不再使用垃圾回收機(jī)制,iOS則從來(lái)都沒(méi)有支持垃圾回收機(jī)制。

當(dāng)create或者copy對(duì)象時(shí),會(huì)計(jì)數(shù)為1,其他對(duì)象需要retain時(shí),會(huì)增加引用計(jì)數(shù)。持有對(duì)象的所有者也可以放棄所有權(quán),放棄所有權(quán)時(shí)減少計(jì)數(shù),當(dāng)計(jì)數(shù)為0時(shí)就會(huì)釋放對(duì)象。
如圖:

memory_management 圖片來(lái)自官方文檔

Memory Management Policy 內(nèi)存管理策略

  • 通過(guò)分配內(nèi)存或copy來(lái)創(chuàng)建任何對(duì)象
  • 使用方法 alloc, allocWithZone:, copy, copyWithZone:, mutableCopy , mutableCopyWithZone:創(chuàng)建對(duì)象
  • 通過(guò)retain來(lái)獲取不是自己創(chuàng)建對(duì)象的所有權(quán)。以下兩種情況使用retain:
  1. accessor method或者init method方法獲取所需要的對(duì)象所有權(quán)為屬性property
  2. 需要操作對(duì)象時(shí),避免對(duì)象被釋放而導(dǎo)致錯(cuò)誤,需要retain持有對(duì)象。
  • 發(fā)送release, autorelease消息來(lái)釋放不需要的對(duì)象。
  • 不要不是你創(chuàng)建的對(duì)象和沒(méi)有所有權(quán)的對(duì)象發(fā)送release消息。

Practical Memory Management 實(shí)際內(nèi)存管理

  • Autorelease pools
  • 向?qū)ο蟀l(fā)送autorelease消息,會(huì)將對(duì)象標(biāo)記為延遲釋放,當(dāng)對(duì)象超出當(dāng)前作用域時(shí),釋放對(duì)象。
  • AppKit frameworksUIKit frameworks在事件循環(huán)的每個(gè)周期開始時(shí),在主線程上創(chuàng)建一個(gè)自動(dòng)釋放池,并在此次時(shí)間循環(huán)結(jié)束時(shí),釋放它,從而釋放在處理時(shí)生成的所有自動(dòng)釋放的對(duì)象。因此,通常不需要自己創(chuàng)建autoreleasePool,當(dāng)然,以下情況你需要自己創(chuàng)建和銷毀autoreleasePool
  1. 如果你編寫的代碼不是基于UI framework的程序,如command-line tool命令行工具。
  2. 如果你需要寫一個(gè)循環(huán),創(chuàng)建許多臨時(shí)對(duì)象,如讀入大量的銅像同時(shí)改變圖片尺寸,圖像讀入到NSData對(duì)象,并從中生成UIImage對(duì)象,改變?cè)搶?duì)象尺寸生成新的UIImage對(duì)象。
  3. 如果你創(chuàng)建一個(gè)長(zhǎng)期存在線程并且可能產(chǎn)生大量的autorelease對(duì)象。

autoreleasePool推薦使用以下方法:

@autoreleasepool {
 //do something
}
  • dealloc
    當(dāng)NSObject對(duì)象的引用計(jì)數(shù)為0時(shí),銷毀該對(duì)象前會(huì)調(diào)用dealloc方法,用來(lái)釋放該對(duì)象擁有的所有資源,包裹實(shí)例變量指向的對(duì)象。
    例子:
 // 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.
不要直接調(diào)用另一個(gè)對(duì)象的dealloc方法。你必須在類使用結(jié)束時(shí)調(diào)用父類的實(shí)現(xiàn)。你不應(yīng)該把系統(tǒng)資源與對(duì)象的生命周期綁定。
因?yàn)檫M(jìn)程的內(nèi)存退出時(shí),對(duì)象可能無(wú)法發(fā)送dealloc消息,該方法的內(nèi)存被自動(dòng)退出清零,所以讓操作系統(tǒng)清理資源比調(diào)用所有的內(nèi)存管理方法更有效。

內(nèi)存管理實(shí)踐

使用訪問(wèn)器方法使內(nèi)存管理更輕松

如果類有一個(gè)屬性是一個(gè)對(duì)象,你必須確保使用該對(duì)象時(shí),它不會(huì)被釋放。因此在設(shè)置時(shí),必須聲明對(duì)象的所有權(quán)。還必須保證持有這些對(duì)象所有權(quán)的放棄。

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

Counter類有一個(gè)屬性是NSNumber對(duì)象,屬性聲明了setget兩個(gè)訪問(wèn)器方法,在get中就是返回synthesized實(shí)例變量,所以沒(méi)必要retain或者release

- (NSNumber *)count {
 return _count;
}

set方法:

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

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

使用訪問(wèn)器方法設(shè)置屬性

// 方法一
- (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;
}

方法二沒(méi)有對(duì)count屬性賦新值時(shí)沒(méi)有使用set訪問(wèn)方法,也不會(huì)觸發(fā)KVO,可能在特殊情況導(dǎo)致錯(cuò)誤(比如忘記了 retain或者release,或者如果實(shí)例變量的內(nèi)存管理發(fā)生了變化)。除了第一種方法,或者直接使用self.count = zero;。

不要在初始化和dealloc中使用訪問(wèn)器方法

不應(yīng)該使用setget方法在initdealloc。應(yīng)該使用_直接訪問(wèn)成員變量進(jìn)行初始化和dealloc。如下:

- init {
    self = [super init];
    if (self) {
        _count = [[NSNumber alloc] initWithInteger:0];
    }
    return self;
}
// 由于Counter類具有對(duì)象實(shí)例變量,因此還必須實(shí)現(xiàn)dealloc方法。
// 它應(yīng)該通過(guò)向任何實(shí)例變量發(fā)送一個(gè)釋放消息來(lái)放棄它的所有權(quán),最終它應(yīng)該調(diào)用super的實(shí)現(xiàn)
- (void)dealloc {
    [_count release];
    [super dealloc];
}

使用弱引用來(lái)避免循環(huán)引用

  • retain對(duì)象,實(shí)際是對(duì)對(duì)象的強(qiáng)引用(strong reference),一個(gè)對(duì)象在所有強(qiáng)引用都沒(méi)有被釋放之前,不能釋放對(duì)象。因此,如果有兩個(gè)對(duì)象互相持有對(duì)方或者間接互相引用,會(huì)導(dǎo)致循環(huán)引用。這時(shí)候就需要弱引用對(duì)方來(lái)打破這個(gè)循環(huán)。

如父親強(qiáng)引用兒子,兒子強(qiáng)引用孫子,那么倒過(guò)來(lái)孫子只能弱引用兒子,兒子也只能弱引用父親。Cocoa建立了一個(gè)約定,副對(duì)象應(yīng)該強(qiáng)引用子對(duì)象,并且子對(duì)象應(yīng)該只對(duì)父對(duì)象弱引用。
Cocoa中常見的例子包括代理方法delegate,data source,observer,target等等

必須小心將消息發(fā)送到持有只是一個(gè)弱引用的對(duì)象。當(dāng)發(fā)送消息給一個(gè)被dealloc的弱引用對(duì)象時(shí),你的應(yīng)用程序會(huì)崩潰(這是在MRC時(shí)期的代理delegate會(huì)出現(xiàn),因?yàn)楫?dāng)時(shí)對(duì)代理弱引用的修飾符是assign,assign弱引用并不會(huì)在對(duì)象dealloc時(shí),把對(duì)象置為nil。而ARC時(shí)代使用weak則會(huì)在對(duì)象dealloc時(shí)置為nil)。

避免正在使用的對(duì)象被釋放

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

正確的做法

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

Collections類擁有它們所包含的對(duì)象所有權(quán)

  • 添加一個(gè)對(duì)象到一個(gè)collection中,如(數(shù)組、字典、集合)時(shí),collection會(huì)得到該對(duì)象所有權(quán)。當(dāng)對(duì)象從collection刪除或者collection自己被釋放時(shí),collection將釋放它擁有的所有權(quán)。
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];
}

通過(guò)引用計(jì)數(shù)實(shí)現(xiàn)所有所有權(quán)策略

  • 所有圈策略是通過(guò)引用計(jì)數(shù)實(shí)現(xiàn)的,通常retain方法后被稱為retain count。每個(gè)對(duì)象都有一個(gè)引用計(jì)數(shù)。

  • 當(dāng)你創(chuàng)建一個(gè)對(duì)象,它的引用計(jì)數(shù)為1

  • 當(dāng)你給對(duì)象發(fā)送retain消息,引用計(jì)數(shù)+1

  • 當(dāng)你給對(duì)象發(fā)送release消息,引用計(jì)數(shù)-1

  • 當(dāng)你給對(duì)象發(fā)送一個(gè)autorelease消息,它的引用計(jì)數(shù)器將在當(dāng)前的自動(dòng)釋放池結(jié)束后-1

  • 當(dāng)對(duì)象的引用計(jì)數(shù)為0時(shí)將被釋放

# ARC時(shí)代

概要

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

ARC 強(qiáng)制新規(guī)則

ARC相對(duì)于MRC強(qiáng)制加了一些新的規(guī)則。

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

ARC 使用新修飾符

  • __strong 強(qiáng)引用,用來(lái)保證對(duì)象不會(huì)被釋放。
  • __weak弱引用 釋放時(shí)會(huì)置為nil
  • __unsafe_unretained弱引用 可能不安全,因?yàn)獒尫艜r(shí)不置為nil
  • __autoreleasing對(duì)象被注冊(cè)到autorelease pool中方法在返回時(shí)自動(dòng)釋放。

內(nèi)存泄漏

ARC還是基于引用計(jì)數(shù)的管理機(jī)制所以依然會(huì)出現(xiàn)循環(huán)引用。

block使用中出現(xiàn)循環(huán)引用

  • 常見的有情況在block使用中出現(xiàn)循環(huán)引用
// 情況一
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內(nèi)使用了self這個(gè)時(shí)候如果某一個(gè)時(shí)刻self被釋放就會(huì)導(dǎo)致出現(xiàn)問(wèn)題。
  • 解決方法
__weak typeof (self) weakSelf = self;
self.myBlock = ^{
__strong typeof(self) strongSelf = weakSelf;
strongSelf.objc1 = ...;
strongSelf.objc2 = ...;
strongSelf.objc3 = ...;
};
  • 使用__weak打破循環(huán)引用。__strong用來(lái)避免在使用self過(guò)程中self被釋放,__strongblock后會(huì)調(diào)用objc_release(obj)釋放對(duì)象。
id __strong obj = [[NSObject alloc] init];

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

兩次調(diào)用objc_msgSend并在變量作用域結(jié)束時(shí)調(diào)用objc_release釋放對(duì)象,不會(huì)出現(xiàn)循環(huán)引用問(wèn)題。

NSTimer循環(huán)引用

為什么NSTimer會(huì)導(dǎo)致循環(huán)引用呢?

+ (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;
  • 主要是因?yàn)?code>NSRunloop運(yùn)行循環(huán)保持了對(duì)NSTimer的強(qiáng)引用,并且NSTimertarger也使用了強(qiáng)引用。

  • 來(lái)自文檔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.

舉個(gè)??:


@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];
 }
  • 這里控制器強(qiáng)引用了timer,而timer也強(qiáng)引用了控制器,這個(gè)時(shí)候就是循環(huán)引用了,引用關(guān)系如下圖:
retain cycle
  • 那么如果控制器對(duì)timer使用了weak呢?
    使用weak是打破了循環(huán)引用,但是run loop還是強(qiáng)引用著timer,timer又強(qiáng)引用著控制器,所以還是會(huì)導(dǎo)致內(nèi)存泄漏。引用關(guān)系如下圖:
leak

如果我們把timer加入主線程的runloop,主線程中的runloop生命周期只有主線程結(jié)束才會(huì)銷毀,所以我們不主動(dòng)調(diào)用[timer invalidate],runloop會(huì)一直持有timer,timer又持有控制器,那么就一直不會(huì)釋放控制器。

  • 解決方法:手動(dòng)調(diào)用[timer invalidate]來(lái)解除持有關(guān)系,釋放內(nèi)存??赡軙?huì)想到在dealloc方法中來(lái)手動(dòng)調(diào)用,但是因?yàn)?code>timer持有控制器,所以控制器的dealloc方法永遠(yuǎn)不會(huì)調(diào)用,因?yàn)?code>dealloc是在控制器要被釋放前調(diào)用的。在Timer Programming Topics中有特別說(shuō)明。所以一般我們可以在下面這些方法中手動(dòng)調(diào)用[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.

# 參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容