前言
什么是內存管理?是指軟件運行時對計算機內存資源的分配和使用的技術。其最主要的目的是如何高效,快速的分配,并且在適當的時候釋放和回收內存資源。
我們本篇學習的就是iOS開發中是如何對內存進行管理的。其中有部分章節是從前人的文章中搬運過來整理而成,這些文章里已經對部分知識點解釋的很清楚明了了,我也沒有更好的表達方式,所以站在巨人的肩膀上,我只是一個整理者加了部分自己的理解。
內存分配
首先既然我們需要對內存進行管理,就需要知道內存是怎么分配的,是分配在哪里的?
在iOS中數據是存在在堆和棧中的,然而我們的內存管理管理的是堆上的內存,棧上的內存并不需要我們管理。
- 非OC對象(基礎數據類型)存儲在棧上
- OC對象存儲在堆上
如下面一段代碼:
int a = 10; //棧
int b = 20; //棧
Car *c = [[Car alloc] init];
在內存中的表現形式如下:
引用計數
引用計數解釋
引用計數是計算機編程語言中的一種內存管理技術,是指將資源(可以是對象、內存或磁盤空間等等)的被引用次數保存起來,當被引用次數變為零時就將其釋放的過程。使用引用計數技術可以實現自動資源管理的目的。同時引用計數還可以指使用引用計數技術回收未使用資源的垃圾回收算法。
當創建一個對象的實例并在堆上申請內存時,對象的引用計數就為1,在其他對象中需要持有這個對象時,就需要把該對象的引用計數加1,需要釋放一個對象時,就將該對象的引用計數減1,直至對象的引用計數為0,對象的內存會被立刻釋放。
在遙遠的以前,iOS開發的內存管理是手動處理引用計數,在合適的地方使引用計數-1,直到減為0,內存釋放。現在的iOS開發內存管理使用的是ARC,自動管理引用計數,會根據引用計數自動監視對象的生存周期,實現方式是在編譯時期自動在已有代碼中插入合適的內存管理代碼以及在 Runtime 做一些優化。
文藝的解釋
記得在《尋夢環游記》里對于一個人的死亡是這樣定義的:當這個這個世界上最后一個人都忘記你時,就迎來了終極死亡。類比于引用計數,就是每有一個人記得你時你的引用計數加1,每有一個人忘記你時,你的引用計數減1,當所有人都忘記你時,你就消失了,也就是從內存中釋放了。
如果再深一層,包含我們后面要介紹的ARC中的強引用和弱引用的話,那這個記住的含義就不一樣了。強引用就是你摯愛的親人,朋友等對你比較重要的人記得你,你的引用計數才加1。
而弱引用就是那種路人,一面之緣的人,他們只是對你有一個印象,他們記得你是沒有用的,你的引用計數不會加1。當你摯愛的人都忘記你時,你的引用計數歸零,你就從這個世界上消失了,而這些路人只是感覺到自己記憶中忽然少了些什么而已。
代碼測試
我們創建一個工程,在Build Phases
里設置AppDelegate的Compiler Flags
為-fno-objc-arc
來開啟手動管理引用計數的模式。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSObject *object = [[NSObject alloc] init];
NSLog(@"\n 引用計數 = %lu \n 對象內存 = %p \n object指針內存地址 = %x", (unsigned long)[object retainCount], object, &object);
self.property = object;
NSLog(@"\n 引用計數 = %lu \n 對象內存 = %p \n object指針內存地址 = %x \n property指針內存地址 = %x", (unsigned long)[object retainCount], object, &object, &_property);
[object release];
NSLog(@"\n 引用計數 = %lu \n 對象內存 = %p \n object指針內存地址 = %x \n property指針內存地址 = %x", (unsigned long)[object retainCount], object, &object, &_property);
return YES;
}
輸出:
2018-08-25 21:01:01.323677+0800 test[26304:9610044]
引用計數 = 1
對象內存 = 0x60000000e290
object指針內存地址 = ee0fee28
2018-08-25 21:01:01.323880+0800 test[26304:9610044]
引用計數 = 2
對象內存 = 0x60000000e290
object指針內存地址 = ee0fee28
property指針內存地址 = 301b8
2018-08-25 21:01:01.324088+0800 test[26304:9610044]
引用計數 = 1
對象內存 = 0x60000000e290
object指針內存地址 = ee0fee28
property指針內存地址 = 301b8
我們看到object
持有對象引用計數+1為1,然后self.property
又持有了對象,引用計數再+1為2,然后我們主動釋放object
,引用計數-1變為1。我們能看到[object release]
釋放后指向對象的指針仍就被保留在object這個變量中,只是對象的引用計數-1了而已。
對應的內存上的分配如下圖所示:
MRC手動管理引用計數
在MRC中增加的引用計數都是需要自己手動釋放的,所以我們需要知道哪些方式會引起引用計數+1;
對象操作 | OC中對應的方法 | 引用計數的變化 |
---|---|---|
生成并持有對象 | alloc/new/copy/mutableCopy等 | +1 |
持有對象 | retain | +1 |
釋放對象 | release | -1 |
廢棄對象 | dealloc | - |
四個法則
- 自己生成的對象,自己持有。
- 非自己生成的對象,自己也能持有。
- 不在需要自己持有對象的時候,釋放。
- 非自己持有的對象無需釋放。
/*
* 自己生成并持有該對象
*/
id obj0 = [[NSObeject alloc] init];
id obj1 = [NSObeject new];
/*
* 持有非自己生成的對象
*/
id obj = [NSArray array]; // 非自己生成的對象,且該對象存在,但自己不持有
[obj retain]; // 自己持有對象
/*
* 不在需要自己持有的對象的時候,釋放
*/
id obj = [[NSObeject alloc] init]; // 此時持有對象
[obj release]; // 釋放對象
/*
* 指向對象的指針仍就被保留在obj這個變量中
* 但對象已經釋放,不可訪問
*/
/*
* 非自己持有的對象無法釋放
*/
id obj = [NSArray array]; // 非自己生成的對象,且該對象存在,但自己不持有
[obj release]; // ~~~此時將運行時crash 或編譯器報error~~~ 非 ARC 下,調用該方法會導致編譯器報 issues。此操作的行為是未定義的,可能會導致運行時 crash 或者其它未知行為
非自己生成的對象,且該對象存在,但自己不持有
其中關于非自己生成的對象,且該對象存在,但自己不持有
是如何實現的呢?這個特性是使用autorelease來實現的,示例代碼如下:
- (id) getAObjNotRetain {
id obj = [[NSObject alloc] init]; // 自己持有對象
[obj autorelease]; // 取得的對象存在,但自己不持有該對象
return obj;
}
使用autorelease
方法可以使取得的對象存在,但自己不持有對象。autorelease 使得對象在超出生命周期后能正確的被釋放(通過調用release方法)。在調用 release 后,對象會被立即釋放,而調用 autorelease
后,對象不會被立即釋放,而是注冊到 autoreleasepool
中,經過一段時間后 pool結束,此時調用release方法,對象被釋放。
像[NSMutableArray array]
[NSArray array]
都可以取得誰都不持有的對象,這些方法都是通過autorelease
實現的。
ARC自動管理引用計數
ARC介紹
ARC其實也是基于引用計數,只是編譯器在編譯時期自動在已有代碼中插入合適的內存管理代碼(包括 retain、release、copy、autorelease、autoreleasepool)以及在 Runtime 做一些優化。
現在的iOS開發基本都是基于ARC的,所以開發人員大部分情況都是不需要考慮內存管理的,因為編譯器已經幫你做了。為什么說是大部分呢,因為底層的 Core Foundation
對象由于不在 ARC 的管理下,所以需要自己維護這些對象的引用計數。
還有就算循環引起情況就算由于互相之間強引用,引用計數永遠不會減到0,所以需要自己主動斷開循環引用,使引用計數能夠減少。
所有權修飾符
Objective-C編程中為了處理對象,可將變量類型定義為id類型或各種對象類型。 ARC中id類型和對象類其類型必須附加所有權修飾符。
其中有以下4種所有權修飾符:
- __strong
- __weak
- __unsafe_unretaied
- __autoreleasing
所有權修飾符和屬性的修飾符對應關系如下所示:
-
assign
對應的所有權類型是__unsafe_unretained
-
copy
對應的所有權類型是__strong
-
retain
對應的所有權類型是__strong
-
strong
對應的所有權類型是__strong
-
unsafe_unretained
對應的所有權類型是__unsafe_unretained
-
weak
對應的所有權類型是__weak
__strong
__strong
表示強引用,對應定義 property
時用到的 strong
。當對象沒有任何一個強引用指向它時,它才會被釋放。如果在聲明引用時不加修飾符,那么引用將默認是強引用。當需要釋放強引用指向的對象時,需要保證所有指向對象強引用置為 nil。__strong
修飾符是 id 類型和對象類型默認的所有權修飾符。
原理:
{
id __strong obj = [[NSObject alloc] init];
}
//編譯器的模擬代碼
id obj = objc_msgSend(NSObject,@selector(alloc));
objc_msgSend(obj,@selector(init));
// 出作用域的時候調用
objc_release(obj);
雖然ARC有效時不能使用release方法,但由此可知編譯器自動插入了release。
對象是通過除alloc、new、copy、multyCopy外方法產生的情況
{
id __strong obj = [NSMutableArray array];
}
結果與之前稍有不同:
//編譯器的模擬代碼
id obj = objc_msgSend(NSMutableArray,@selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);
objc_retainAutoreleasedReturnValue
函數主要用于優化程序的運行。它是用于持有(retain)對象的函數,它持有的對象應為返回注冊在autoreleasePool
中對象的方法,或是函數的返回值。像該源碼這樣,在調用array類方法之后,由編譯器插入該函數。
而這種objc_retainAutoreleasedReturnValue
函數是成對存在的,與之對應的函數是objc_autoreleaseReturnValue
。它用于array類方法返回對象的實現上。下面看看NSMutableArray
類的array
方法通過編譯器進行了怎樣的轉換:
+ (id)array
{
return [[NSMutableArray alloc] init];
}
//編譯器模擬代碼
+ (id)array
{
id obj = objc_msgSend(NSMutableArray,@selector(alloc));
objc_msgSend(obj,@selector(init));
// 代替我們調用了autorelease方法
return objc_autoreleaseReturnValue(obj);
}
我們可以看見調用了objc_autoreleaseReturnValue
函數且這個函數會返回注冊到自動釋放池的對象,但是,這個函數有個特點,它會查看調用方的命令執行列表,如果發現接下來會調用objc_retainAutoreleasedReturnValue
則不會將返回的對象注冊到autoreleasePool
中而僅僅返回一個對象。達到了一種最優效果。如下圖:
__weak
__weak
表示弱引用,對應定義 property
時用到的 weak。弱引用不會影響對象的釋放,而當對象被釋放時,所有指向它的弱引用都會自定被置為 nil,這樣可以防止野指針。使用__weak修飾的變量,即是使用注冊到autoreleasePool
中的對象。__weak
最常見的一個作用就是用來避免循環循環。需要注意的是,__weak
修飾符只能用于 iOS5 以上的版本,在 iOS4 及更低的版本中使用 __unsafe_unretained
修飾符來代替。
__weak 的幾個使用場景:
- 在 Delegate 關系中防止循環引用。
- 在 Block 中防止循環引用。
- 用來修飾指向由 Interface Builder 創建的控件。比如:@property (weak, nonatomic) IBOutlet UIButton *testButton;。
原理:
{
id __weak obj = [[NSObject alloc] init];
}
編譯器轉換后的代碼如下:
id obj;
id tmp = objc_msgSend(NSObject,@selector(alloc));
objc_msgSend(tmp,@selector(init));
objc_initweak(&obj,tmp);
objc_release(tmp);
objc_destroyWeak(&object);
對于__weak
內存管理也借助了類似于引用計數表的散列表,它通過對象的內存地址做為key,而對應的__weak
修飾符變量的地址作為value注冊到weak表中,在上述代碼中objc_initweak
就是完成這部分操作,而objc_destroyWeak
則是銷毀該對象對應的value。當指向的對象被銷毀時,會通過其內存地址,去weak表中查找對應的__weak
修飾符變量,將其從weak
表中刪除。所以,weak
在修飾只是讓weak
表增加了記錄沒有引起引用計數表的變化。
對象通過objc_release
釋放對象內存的動作如下:
- objc_release
- 因為引用計數為0所以執行dealloc
- _objc_rootDealloc
- objc_dispose
- objc_destructInstance
- objc_clear_deallocating
而在對象被廢棄時最后調用了objc_clear_deallocating
,該函數的動作如下:
- 從weak表中獲取已廢棄對象內存地址對應的所有記錄
- 將已廢棄對象內存地址對應的記錄中所有以weak修飾的變量都置為nil
- 從weak表刪除已廢棄對象內存地址對應的記錄
- 根據已廢棄對象內存地址從引用計數表中找到對應記錄刪除
- 據此可以解釋為什么對象被銷毀時對應的weak指針變量全部都置為nil,同時,也看出來銷毀weak步驟較多,如果大量使用weak的話會增加CPU的負荷。
還需要確認一點是:使用__weak修飾符的變量,即是使用注冊到autoreleasePool
中的對象。
{
id __weak obj1 = obj;
NSLog(@"obj2-%@",obj1);
}
編譯器轉換上述代碼如下:
id obj1;
objc_initweak(&obj1,obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@",tmp);
objc_destroyWeak(&obj1);
objc_loadWeakRetained
函數獲取附有__weak修飾符變量所引用的對象并retain
, objc_autorelease
函數將對象放入autoreleasePool中,據此當我們訪問weak修飾指針指向的對象時,實際上是訪問注冊到自動釋放池的對象。因此,如果大量使用weak的話,在我們去訪問weak修飾的對象時,會有大量對象注冊到自動釋放池,這會影響程序的性能。
解決方案:要訪問weak修飾的變量時,先將其賦給一個strong變量,然后進行訪問
為什么訪問weak修飾的對象就會訪問注冊到自動釋放池的對象呢?
因為weak不會引起對象的引用計數器變化,因此,該對象在運行過程中很有可能會被釋放。所以,需要將對象注冊到自動釋放池中并在autoreleasePool銷毀時釋放對象占用的內存。
__unsafe_unretained
ARC 是在 iOS5 引入的,而 __unsafe_unretained
這個修飾符主要是為了在ARC剛發布時兼容iOS4以及版本更低的系統,因為這些版本沒有弱引用機制。這個修飾符在定義property時對應的是unsafe_unretained
。__unsafe_unretained
修飾的指針純粹只是指向對象,沒有任何額外的操作,不會去持有對象使得對象的 retainCount +1。而在指向的對象被釋放時依然原原本本地指向原來的對象地址,不會被自動置為 nil,所以成為了野指針,非常不安全。
__unsafe_unretained
的應用場景:
在 ARC 環境下但是要兼容 iOS4.x 的版本,用__unsafe_unretained
替代 __weak 解決強循環循環的問題。
__autoreleasing
將對象賦值給附有__autoreleasing
修飾符的變量等同于MRC時調用對象的autorelease方法。
@autoeleasepool {
// 如果看了上面__strong的原理,就知道實際上對象已經注冊到自動釋放池里面了
id __autoreleasing obj = [[NSObject alloc] init];
}
編譯器轉換上述代碼如下:
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject,@selector(alloc));
objc_msgSend(obj,@selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
@autoreleasepool {
id __autoreleasing obj = [NSMutableArray array];
}
編譯器轉換上述代碼如下:
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSMutableArray,@selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
上面兩種方式,雖然第二種持有對象的方法從alloc方法變為了objc_retainAutoreleasedReturnValue
函數,都是通過objc_autorelease
,注冊到autoreleasePool
中。
循環引用
什么是循環引用?循環引用就是在兩個對象互相之間強引用了,引用計數都加1了,我們前面說過,只有當引用計數減為0時對象才釋放。但是這兩個的引用計數都依賴于對方,所以也就導致了永遠無法釋放。
最容易產生循環引用的兩種情況就是Delegate
和Block
。所以我們就引入了弱引用這種概念,即弱引用雖然持有對象,但是并不增加引用計數,這樣就避免了循環引用的產生。也就是我們上面所說的所有權修飾符__weak
的作用。關于原理在__weak
部分也有描述,簡單的描述就是每一個擁有弱引用的對象都有一張表來保存弱引用的指針地址,但是這個弱引用并不會使對象引用計數加1,所以當這個對象的引用計數變為0時,系統就通過這張表,找到所有的弱引用指針把它們都置成nil。
所以在ARC中做內存管理主要就是發現這些內存泄漏,關于內存泄漏Instrument為我們提供了 Allocations/Leaks 這樣的工具用來檢測。但是個人覺得還是很麻煩的,大部分時候內存泄漏并不會引起應用的崩潰或者報錯之類的,所以我們也不會每次主動的去查看當前代碼有沒有內存泄漏之類的。
這里有一個微信讀書團隊開源的工具MLeaksFinder,它可以在你程序運行期間,如果有內存泄漏就會彈出提示告訴你泄漏的地方。
具體原理如下:
我們知道,當一個 UIViewController 被 pop 或 dismiss 后,該 UIViewController 包括它的 view,view 的 subviews 等等將很快被釋放(除非你把它設計成單例,或者持有它的強引用,但一般很少這樣做)。于是,我們只需在一個 ViewController 被 pop 或 dismiss 一小段時間后,看看該 UIViewController,它的 view,view 的 subviews 等等是否還存在。
具體的方法是,為基類 NSObject 添加一個方法 -willDealloc 方法,該方法的作用是,先用一個弱指針指向 self,并在一小段時間(3秒)后,通過這個弱指針調用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接中斷言。
- (BOOL)willDealloc {
__weak id weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakSelf assertNotDealloc];
});
return YES;
}
- (void)assertNotDealloc {
NSAssert(NO, @“”);
}
這樣,當我們認為某個對象應該要被釋放了,在釋放前調用這個方法,如果3秒后它被釋放成功,weakSelf 就指向 nil,不會調用到 -assertNotDealloc 方法,也就不會中斷言,如果它沒被釋放(泄露了),-assertNotDealloc 就會被調用中斷言。這樣,當一個 UIViewController 被 pop 或 dismiss 時(我們認為它應該要被釋放了),我們遍歷該 UIViewController 上的所有 view,依次調 -willDealloc,若3秒后沒被釋放,就會中斷言。
Core Foundation 對象的內存管理
底層的 Core Foundation 對象,在創建時大多以 XxxCreateWithXxx 這樣的方式創建,例如:
// 創建一個 CFStringRef 對象
CFStringRef str= CFStringCreateWithCString(kCFAllocatorDefault, “hello world", kCFStringEncodingUTF8);
// 創建一個 CTFontRef 對象
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
對于這些對象的引用計數的修改,要相應的使用 CFRetain 和 CFRelease 方法。如下所示:
// 創建一個 CTFontRef 對象
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
// 引用計數加 1
CFRetain(fontRef);
// 引用計數減 1
CFRelease(fontRef);
對于 CFRetain
和 CFRelease
兩個方法,讀者可以直觀地認為,這與 Objective-C 對象的 retain
和 release
方法等價。
所以對于底層 Core Foundation
對象,我們只需要延續以前手工管理引用計數的辦法即可。
除此之外,還有另外一個問題需要解決。在 ARC 下,我們有時需要將一個 Core Foundation
對象轉換成一個 Objective-C
對象,這個時候我們需要告訴編譯器,轉換過程中的引用計數需要做如何的調整。這就引入了bridge
相關的關鍵字,以下是這些關鍵字的說明:
__bridge: 只做類型轉換,不修改相關對象的引用計數,原來的 Core Foundation 對象在不用時,需要調用 CFRelease 方法。
__bridge_retained:類型轉換后,將相關對象的引用計數加 1,原來的 Core Foundation 對象在不用時,需要調用 CFRelease 方法。
__bridge_transfer:類型轉換后,將該對象的引用計數交給 ARC 管理,Core Foundation 對象在不用時,不再需要調用 CFRelease 方法。