內存管理系列—OC的內存管理模式

1. 引言

經常面試的時候,會被問到「談談你對OC中內存管理的理解」,我的以爲應該從如下三個部分來回答,才比較全面程序員

本文主要介紹OC的內存管理的模式(機制)來分析。 面試

2.爲何須要對OC進行內存管理

  • 程序在運行的過程當中一般經過如下行爲,來增長程序的的內存佔用
    1. 建立一個OC對象
    2. 定義一個變量
    3. 調用一個函數或者方法
  • 移動設備分配給每一個App的內存有限,App運行中會建立大量對象, OC對象存儲在堆中,系統不會自動釋放堆中的內存,對象沒有及時釋放,就會佔用大量內存,系統會發出內存警告,對應用運行形成影響
  • 若是程序佔用內存過大,系統可能會強制關閉程序,形成程序崩潰、閃退現象,影響用戶體驗

因此,咱們須要對內存進行合理的分配內存、清除內存,回收那些不須要再使用的對象。從而保證程序的穩定性。數組

3. 哪些對象才須要咱們進行內存管理

任何繼承了NSObject的對象須要進行內存管理,而其餘非對象類型(int、char、float、double、struct、enum等) 不須要進行內存管理bash

這是由於:數據結構

  • 繼承了NSObject的對象的存儲在操做系統的堆裏邊。通常由程序員分配釋放,若程序員不釋放,程序結束時可能由OS回收,分配方式相似於鏈表
  • 非OC對象通常放在操做系統的棧裏面,由操做系統自動分配釋放,存放函數的參數值,局部變量的值等。其操做方式相似於數據結構中的棧(先進後出)

4. 內存管理機制

在OC中沒有垃圾回收機制,OC提供了一套機制來管理內存,即「引用計數」,每一個OC對象都有本身的引用計數器多線程

4.1 引用計數

  1. 【簡單定義】:引用計數簡單來講就是統計一塊內存的全部權, 當這塊內存被建立出來的時候(alloc、new或copy),它的引用計數從0增長到1,表示有一個對象或指針持有這塊內存,擁有這塊內存的全部權
  2. 【引用計數的增長】:若是這時候有另一個對象或指針指向這塊內存,那麼爲了表示這個後來的對象或指針對這塊內存的全部權,引用計數加1變爲2
  3. 【引用計數的較少】:以後如有一個對象或指針再也不指向這塊內存時,引用計數減1,表示這個對象或指針再也不擁有這塊內存的全部權
  4. 【內存的釋放】:當一塊內存的引用計數變爲0,表示沒有任何對象或指針持有這塊內存,系統便會馬上釋放掉這塊內存

5. 內存管理原則

《iOS與OS X多線程和內存管理》這本書說的是函數

  • 本身生成的對象,本身持有
  • 非本身生成的對象,本身也能持有
  • 不在須要本身持有的對象時釋放
  • 非本身持有的對象沒法釋放

可是我的以爲太繞了,簡單來講: 對於全部的對象而言,你只要記住Apple的官網上的內存管理三定律就能夠:oop

  • 一個對象能夠有一個或多個擁有者
  • 當它一個擁有者都沒有時,它就會被回收
  • 若是想保留一個對象不被回收,你就必需成爲它的擁有者(ownership)

6. 內存管理的3種模式

在開發時引用計數又分爲ARC(自動引用計數)和MRC(手動引用計數)。ARC的本質其實就是MRC,只不過是系統幫助開發者管理已建立的對象或內存空間,自動在系統認爲合適的時間和地點釋放掉已經失去做用的內存空間,原理是同樣的。佈局

而對於自動釋放池(Autorelease Pool能夠算是半自動的機制,因此這裏我單獨歸爲一類,不和MRC一塊兒分析。post

6.1 MRC手動引用計數

遵循誰申請、誰添加、誰釋放的原則。須要手動處理內存技術的增長和修改。將從如下幾個方面來深刻了解MRC種的內存管理。

6.1.1 內存管理原則

  1. 誰建立誰release:若是你經過alloc、new、copy或mutableCopy來建立一個對象,那麼必須調用release或autorelease
  2. 誰retain誰release: 只要你調用了retain,就必須調用一次release 上個demo感覺下
    運行環境:MRC模式
-(void)test{
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        NSLog(@"retainCount = %lu", [p retainCount]); // 1
        [p retain];// 只要給對象發送一個retain消息, 對象的引用計數器就會+1
        NSLog(@"retainCount = %lu", [p retainCount]); // 2
        // 經過指針變量p,給p指向的對象發送一條release消息
        // 只要對象接收到release消息, 引用計數器就會-1
        [p release];
        // 須要注意的是: release並不表明銷燬\回收對象, 僅僅是計數器-1
        NSLog(@"retainCount = %lu", [p retainCount]); // 1
        [p release]; // 0
        
    }
    NSLog(@"----自動釋放池已釋放------");
}
// 打印結果
2020-03-25 14:58:10.251949+0800 02-內存管理-MRC開發[13803:235592] retainCount = 1
2020-03-25 14:58:10.252081+0800 02-內存管理-MRC開發[13803:235592] retainCount = 2
2020-03-25 14:58:10.252147+0800 02-內存管理-MRC開發[13803:235592] retainCount = 1
2020-03-25 14:58:10.252239+0800 02-內存管理-MRC開發[13803:235592] Person被釋放了
2020-03-25 14:58:10.252332+0800 02-內存管理-MRC開發[13803:235592] ----自動釋放池已釋放------

複製代碼

當引用計數爲0的時候,person對象就被釋放了,Peson中得dealloc方法就會打印「Person被釋放了」****

6.1.2 引用計數關鍵字

在MRC中會引發引用計數變化的關鍵字有:alloc,retain,copy,release,autorelease。(strong關鍵字只用於ARC,做用等同於retain)

  • alloc:它的做用是開闢一塊新的內存空間,並使這塊內存的引用計數從0增長到1,
  • retain:只能由對象調用,它的做用是使這個對象的內存空間的引用計數加1,並不會新開闢一塊內存空間
  • copy:這個部分單獨抽出來說,涉及內容比較多。
  • release:它的做用是使對象的內存空間的引用計數減1,若引用計數變爲0則系統會馬上釋放掉這塊內存。若是引用計數爲0的基礎上再調用release,便會形成過分釋放,使內存崩潰;
  • autorelease:它的做用於release相似,但不是馬上減1,至關於一個延遲的release,一般用於方法返回值的釋放

6.1.3 屬性關鍵字

MRC中經常使用的屬性關鍵自主要是:assign、reatin、copy

  • assign
    若是在property後邊加上assign,系統就不會幫咱們生成set方法內存管理的代碼,僅僅只會生成普通的getter/setter方法,默認什麼都不寫就是assign
@property (nonatomic,assign) int val;
複製代碼
  • copy關鍵字
    系統會自動幫咱們生成getter/setter方法內存管理的代碼
@property (nonatomic,copy) NSString *name;
// copy修飾的屬性,內部setter方法的實現大概醬紫:
- (void)setName:(NSString *)name {
    if (_name != name) {
        [_name release];
        _name = [name copy];
    }
}
複製代碼
  • retain關鍵字
    系統會自動幫咱們生成getter/setter方法內存管理的代碼
@property (nonatomic,retain) NSString *name;
// retain修飾的屬性,內部setter方法的實現大概醬紫:
- (void)setName:(NSString *)name {
    if (_name != name) {
        [_name release];
        _name = [name retain];
    }
}
複製代碼

6.1.4 copy關鍵字重點解析

我門先來弄清楚一些概念:

  1. iOS提供了兩個拷貝方法:

    • copy 不可變拷貝,產生【不可變】副本(即使原本是可變的,拷貝出來的都是不可變的)
    • mutableCopy 可變拷貝,產生【可變】副本(即使原本是不可變的,拷貝出來的都是可變的)
  2. 拷貝的目的

    • 產生一個副本對象,跟源對象互不影響
    • 修改了源對象,不會影響副本對象
    • 修改了副本對象,不會影響源對象
  3. 淺拷貝和深拷貝

    • 淺拷貝:指針拷貝,【沒有】產生新對象,引用計數會加1(retainCount += 1)
    • 深拷貝:內容拷貝,【有】產生新對象,引用計數初始1(retainCount = 1)

思考🤔:對於copy,引用計數是否可能小於0?

下面來看幾個列子:

  • TaggedPointer的淺拷貝和深拷貝
void copyTest1) {   
    NSString *str1 = [[NSString alloc] initWithFormat:@"abc"]; // TaggedPointer
    NSString *str2 = str1.copy; // 淺拷貝 TaggedPointer
    NSMutableString *str3 = str1.mutableCopy; // 深拷貝 對象
    
    NSLog(@"str1 %@ --- %zd --- %p", str1, str1.retainCount, str1);
    NSLog(@"str2 %@ --- %zd --- %p", str2, str2.retainCount, str2);
    NSLog(@"str3 %@ --- %zd --- %p", str3, str3.retainCount, str3);
}
// 打印結果
2020-03-25 16:00:37.509103+0800 03-內存管理-copy[14479:285375] str1 abc --- -1 --- 0x8093262be428885
2020-03-25 16:00:37.509190+0800 03-內存管理-copy[14479:285375] str2 abc --- -1 --- 0x8093262be428885
2020-03-25 16:00:37.509270+0800 03-內存管理-copy[14479:285375] str3 abc --- 1 --- 0x1007029d0
複製代碼
void copyTest2() {
   NSString *str1 = @"ABC"; // 直接寫出來的,不是經過方法建立的字符串,編譯時會生成爲【字符串常量】
    NSLog(@"str1 %@ --- %zd --- %p", str1, str1.retainCount, str1);
    
    NSString *str2 = str1.copy; // 淺拷貝 字符串常量
    NSLog(@"str2 %@ --- %zd --- %p", str2, str2.retainCount, str2);
    
    NSString *str3 = [[NSString alloc] initWithFormat:@"efg"]; // TaggedPointer
    NSLog(@"str3 %@ --- %zd --- %p", str3, str3.retainCount, str3);
    
    NSMutableString *str4 = str3.mutableCopy; // 深拷貝 對象
    NSString *str5 = str4.copy; // 深拷貝 對象
    
    NSLog(@"str4 %@ --- %zd --- %p", str4, str4.retainCount, str4);
    NSLog(@"str5 %@ --- %zd --- %p", str5, str5.retainCount, str5);
    

    //打印結果
2020-03-25 16:12:07.188709+0800 03-內存管理-copy[14642:295736] str1 ABC --- -1 --- 0x1000030b0
2020-03-25 16:12:07.188866+0800 03-內存管理-copy[14642:295736] str2 ABC --- -1 --- 0x1000030b0
2020-03-25 16:12:07.189023+0800 03-內存管理-copy[14642:295736] str3 efg --- -1 --- 0xd9de73a142fc7bc1
2020-03-25 16:12:07.189245+0800 03-內存管理-copy[14642:295736] str4 efg --- 1 --- 0x10077d8d0
2020-03-25 16:12:07.189316+0800 03-內存管理-copy[14642:295736] str5 efg --- -1 --- 0xd9de73a142fc7bc1
}
複製代碼

總結

對於常量區的數據(字符串常量),TaggedPointer的引用計數一直都爲【-1】,TaggedPointer不是對象,是個指針。
複製代碼

思考🤔:對於copy,引用計數是否可能大於1,網上不少文章說copy不會改變引用計數?

void copyTest3() {
    NSString *str1 = [[NSString alloc] initWithFormat:@"老鄭的技術雜貨鋪"]; // 對象 str1.retainCount = 1
    NSString *str2 = str1.copy; // 淺拷貝 對象 str1.retainCount = 2
    NSMutableString *str3 = str1.mutableCopy; // 深拷貝 對象 str3.retainCount = 1
    
    NSLog(@"str1 %@ --- %zd --- %p", str1, str1.retainCount, str1);
    NSLog(@"str2 %@ --- %zd --- %p", str2, str2.retainCount, str2);
    NSLog(@"str3 %@ --- %zd --- %p", str3, str3.retainCount, str3);
}
//打印結果:
2020-03-25 16:18:45.498256+0800 03-內存管理-copy[14728:302195] str1 老鄭的技術雜貨鋪 --- 2 --- 0x1006059b0
2020-03-25 16:18:45.498313+0800 03-內存管理-copy[14728:302195] str2 老鄭的技術雜貨鋪 --- 2 --- 0x1006059b0
2020-03-25 16:18:45.498349+0800 03-內存管理-copy[14728:302195] str3 老鄭的技術雜貨鋪 --- 1 --- 0x100605820
複製代碼

總結

可見對不可變對象進行copy操做,引用計數會+1
複製代碼

6.2 ARC自動引用計數管理

在App編譯階段,由Xcode添加了內存管理的代碼,自動加入了 retain 、 release 後的代碼

6.2.1 內存管理原則

只要沒有強指針指向(沒有被強引用),對象就會被釋放。

  • 強指針
    • 默認全部對象的指針變量都是強指針
    • 被__strong修飾的指針
Person *person1 = [[Person alloc] init];
 __strong  Person *person2 = [[Person alloc] init];
複製代碼

相應的也有弱指針(弱引用),但不影響對象的釋放

  • 弱指針:被__weak修飾的指針
__weak  Person *p = [[Person alloc] init];
複製代碼

6.2.2 屬性關鍵字

  • strong : 用於OC對象,至關於MRC中的retain,是強引用
  • weak : 用於OC對象,至關於MRC中的assign,好比修飾delegate
  • assign : 用於基本數據類型,跟MRC中的assign同樣,也可修飾對象,但會存在野指針
  • copy : 主要用於修飾block、字符串、數組

6.2.3 ARC注意事項

  • 不容許調用release、retain、retainCount、autorelease方法
  • 重寫父類的dealloc方法時,不能再調用 [super dealloc];

6.2.4 ARC中對象的釋放

簡單看幾種狀況:

  • 局部變量釋放對象隨之被釋放
#import "Person.h"

@implementation Person
-(void)dealloc{
    NSLog(@"Person已釋放-----dealloc");
}
@end

複製代碼
//Test.m
-(void)test{
    @autoreleasepool {
        int a = 10; // 棧
        int b = 20; // 棧
        //p在棧上 Person對象(計數器==1) : 堆
        Person *p = [[Person alloc] init];
    }// 執行到這一行局部變量p釋放
    // 因爲沒有強指針指向對象, 因此對象也釋放
    NSLog(@"----自動釋放池已釋放------");
}
// 打印結果
2020-03-25 16:32:39.073845+0800 Test[14845:312200] Person已釋放-----dealloc
2020-03-25 16:32:39.073965+0800 Test[14845:312200] ----自動釋放池已釋放------
複製代碼
  • 清空指針對象隨之被釋放
-(void)test{
    @autoreleasepool {
       Person *p = [[Person alloc] init];
       p = nil; // 執行到這一行, 因爲沒有強指針指向對象, 因此對象被釋放
        NSLog(@"----當前還在函數做用域內------");
    }
    NSLog(@"----自動釋放池已釋放------");
}
// 打印結果
2020-03-25 16:39:12.924164+0800 Test[14915:317802] Person已釋放-----dealloc
2020-03-25 16:39:12.924311+0800 Test[14915:317802] ----當前還在函數做用域內------
2020-03-25 16:39:12.924446+0800 Test[14915:317802] ----自動釋放池已釋放------

複製代碼

6.3 自動釋放池(Autorelease Pool)

它不會像ARC或者MRC那樣在對象再也不會被使用時立刻被釋放,而是等到一個時機去釋放它,內存池的釋放操做分爲自動和手動。自動釋放受runloop機制影響

6.3.1 手動釋放

  • ARC下使用方式:

好比for循環,多是內存飆升,這個時候我門能夠手動釋放內存(@autoreleasepool)

// ARC
 NSMutableArray * arr = [NSMutableArray array];
    for (int i = 0; i < largeCount; i++) {
        @autoreleasepool {//開始表明建立自動釋放池 
            NSNumber * numTep = [NSNumber numberWithInt:i];
            [arr addObject:numTep];
        }//結束表明銷燬自動釋放池
    }
複製代碼
  • MRC下使用方式

方式一:

NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];
Person *p = [[[Person alloc] init] autorelease];
[autoreleasePool drain];
複製代碼

方式二:

@autoreleasepool
{ // 建立一個自動釋放池
        Person *p = [[Person new] autorelease];
        // 將代碼寫到這裏就放入了自動釋放池
} // 銷燬自動釋放池(會給池子中全部對象發送一條release消息)
複製代碼

6.3.2 自動釋放

這裏涉及到runloop,不作詳細的描述,會在runloop相關文章中在作深刻講解,有個大概瞭解便可

蘋果在主線程 RunLoop 裏註冊了兩個 Observer:

  1. 第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush() 建立自動釋放池。優先級最高,保證建立釋放池發生在其餘全部回調以前。
  2. 第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入睡眠) 和 Exit(即將退出Loop),
    • BeforeWaiting(準備進入睡眠)時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池並建立新池;
    • Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池子發生在其餘全部回調以後

6.3.3 autorelease關鍵字和釋放池(MRC)

當咱們再也不使用一個對象的時候應該將其空間釋放,可是有時候咱們不知道什麼時候應該將其釋放。爲了解決這個問題,Objective-C提供了autorelease方法,在MRC中才能使用。

  1. autorelease注意事項
    • autorelease是一種支持引用計數的內存管理方式,只要給對象發送一條autorelease消息,會將對象放到一個自動釋放池中,當自動釋放池被銷燬時,會對池子裏面的全部對象作一次release操做
    • autorelease方法會返回對象自己,且調用完autorelease方法後,對象的計數器不變
    • 並非放到自動釋放池代碼中,都會自動加入到自動釋放池
    • 在自動釋放池的外部發送autorelease 不會被加入到自動釋放池中
  • 錯誤寫法❌
//錯誤的寫法
@autoreleasepool {
}
// 沒有與之對應的自動釋放池, 只有在自動釋放池中調用autorelease纔會放到釋放池
Person *p = [[[Person alloc] init] autorelease];
[p run];

複製代碼
  • 正確寫法
// 正確寫法
@autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
 }

// 正確寫法
Person *p = [[Person alloc] init];
@autoreleasepool {
    [p autorelease];
}
複製代碼
  1. autorelease本質原理
    autorelease實際上只是把對release的調用延遲了,對於每個autorelease,系統只是把該對象放入了當前的autorelease pool中,當該pool被釋放時,該pool中的全部對象會被調用release。

6.3.4 小結

  • autorelease方法不會改變對象的引用計數器(銷燬時影響),只是將這個對象放到自動釋放池中;
  • OC中類庫的類方法通常都不須要手動釋放,由於內部已經調用了autorelease方法,如[NSDate date]
  • 自動釋放池是以棧的形式存在
  • 自動釋放池中不適宜放佔用內存比較大的對象

參考文章:

相關文章
相關標籤/搜索