iOS內存管理機制


這世上,沒有誰活得比誰容易,只是有人在呼天搶地,有人在默默努力。

  隨着科技的發展,移動設備的內存越來越大,設備的運行速度也越來越快,但是相對於整個應用市場上成千上萬的應用容量來說,還是及其有限的。因此,每一個應用所能占用的內存是有限制的。這一專題就是來探討系統中的內存是如何分配的。

image

一. 計算機的基本知識

1.1)硬件內存區分

  我們的手機、電腦、或者智能設備都有隨機存儲器RAM(運行內存/主存)和只讀存儲器ROM(相當於計算機中的硬盤)。RAM是內部存儲,ROM是外部存儲。我們的CPU直接訪問的是RAM,如果想訪問外部存儲,則數據須先從ROM中將數據調度到RAM中才能被CPU訪問。也就是說CPU不能直接從內存卡等硬盤里面讀取數據。

1.2)RAM和ROM的特點和區別

  • RAM:運行內存,CPU可以直接訪問,訪問速度快,價格高。不能夠掉電存儲,斷電會失去數據,不穩定;
  • ROM:存儲型內存,CPU不可以直接訪問,訪問速度慢,價格低。可以掉電存儲,穩定;

1.3)RAM和ROM的協同工作

  由於RAM不支持掉電存儲,所以App程序一般存儲在ROM中。手機里面使用的ROM基本都是Nand Flash(閃存),CPU是不能直接訪問的,而是需要文件系統/驅動程序(嵌入式中的EMC)將其讀到RAM里面,CPU才可以訪問。另外,RAM的速度也比Nand Flash快。

二. 內存分區

  說到內存分區,內存即指的是RAM,可以分為5個區。所有的進程(執行中的程序)都必須占用一定數量的內存(RAM),它或許是用來存放從磁盤(ROM)載入的程序代碼,或是存放取自用戶輸入的數據等等。不過進程對這些內存的管理方式因內存用途不一而不盡相同,有些內存是事先靜態分配和統一回收的,而有些卻是按需要動態分配和回收的。

2.1)代碼區

  代碼段是用來存放可執行文件的操作指令(存放函數的二進制代碼),也就是說它是可執行程序在內存中的鏡像。代碼段需要防止在運行時被非法修改,所以只允許讀取操作,而不允許寫入操作。它是不可寫的。

2.2)常量區

  常量存儲區,這是一塊比較特殊的存儲區,里面存放的是數字/字符常量。編譯時分配,APP結束時由系統釋放。

2.3)全局(靜態)區

  編譯時分配,APP結束時由系統釋放。全局變量和靜態變量的存儲是放在這一塊區域,程序退出后自動釋放。

  • 數據區(全局初始化區),數據段用來存放程序中已經初始化的全局變量和靜態變量。
  • BSS區(全局未初始化區),BSS段包含了程序中未初始化的全局變量和未初始化的靜態變量。

2.4)堆(heap)區

  堆(FIFO)是由程序開發者分配和釋放,地址是從低到高分配。用於存放進程運行中被動態分配的內存段,它大小並不固定,可動態擴張或縮減。當進程調用alloc等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張);當利用release釋放內存時,被釋放的內存從堆中被剔除(堆被縮減),因為我們現在iOS基本都使用ARC來管理對象,所以不用我們程序員來管理,但是我們要知道這個對象存儲的位置。

2.5)棧(stack)區

  棧(LIFO)是由系統自動分配並釋放,地址從高到低分配。用於存放程序臨時創建的局部變量,存放函數的參數值,局部變量等。也就是說我們函數括弧“{}”中定義的變量(但不包括static聲明的變量,static意味這在數據段中存放變量)。

  另外在函數被調用時,其參數也會被壓入發起調用的進程棧中,並且待到調用結束后,函數的返回值也會被存放回棧中。由於棧的先進后出(LIFO)特點,所以棧特別方便用來保存/恢復調用現場。從這個意義上將我們可以把棧看成一個臨時數據寄存、交換的內存區。函數跳轉時現場保護(寄存器值保存於恢復),這些系統都會幫我們自動實現,無需我們干預。所以大量的局部變量,深遞歸,函數循環調用都可能耗盡棧內存而造成程序崩潰 。

2.6)內存分區總結

  上述幾種內存區域中常量區、數據段、BSS和堆通常是被連續存儲的,內存位置上是連續的,而代碼段和棧往往會被獨立存放。

  棧是向低地址擴展的數據結構,是一塊連續內存的區域。堆是向高地址擴展的數據結構,是不連續的內存區域。有人會問堆和棧會不會碰到一起,他們之間間隔很大,絕少有機會能碰到一起,況且堆是鏈表方式存儲。

int age = 27; //全局初始化區(數據區)
NSString *name; //全局未初始化區(BSS區)
static NSString *sName = @"Dely";//全局(靜態初始化)區(數據區)

@implementation ViewController 
- (void)viewDidLoad {  
		[super viewDidLoad];  
    Int tmpAge;//棧  
    NSString * tmpName = @"Dely";//棧  
    NSString * number = @"123456"; //123456\0在常量區,number在棧上。  
    NSMutableArray * array = [NSMutableArray arrayWithCapacity:1];//分配而來的8字節的區域就在堆中,array在棧中,指向堆區的地址。  
    NSInteger total = [self getTotalNumber:1 number2:1];
}

// 當ViewDidLoad代碼塊一過,tmpAge、tmpName 、number 、* array指針都會被系統編譯器自動回收。而OC
// 對象不會被系統回收,因為它存放在堆里面,堆里面的內存是動態存儲的,所以需要程序員手動回收內存。 

-(NSInteger)getTotalNumber:(NSInteger)number1 number2:(NSInteger)number2{  
    return number1 + number2;//number1和number2 棧區
}

@end 

三. 堆和棧的區別

3.1)申請方式和回收方式

  • 棧區(stack):由編譯器自動分配和釋放。

  • 堆區(heap):由程序開發者分配和釋放。

3.2)申請后的系統響應

  • 棧:每一個函數在執行的時候都會向操作系統索要資源,棧區就是函數運行時的內存,棧區中的變量由編譯器負責分配和釋放,內存隨着函數的運行分配,隨着函數的結束而釋放,由系統自動完成。

  注意:只要棧的剩余空間大於所申請空間,系統將為程序提供內存,否則將報異常提示棧溢出。

  • 堆:首先應該知道操作系統有一個記錄空閑內存地址的鏈表。當系統收到程序的申請時,會遍歷該鏈表,尋找第一個空間大於所申請空間的堆結點,然后將該結點從空閑結點鏈表中刪除,並將該結點的空間分配給程序。由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多余的那部分重新放入空閑鏈表中

3.3)申請大小的限制

  • 棧:棧是向低地址擴展的數據結構,是一塊連續的內存的區域。是棧頂的地址和棧的最大容量是系統預先規定好的,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數 ) ,如果申請的空間超過棧的剩余空間時,將提示overflow。因此,能從棧獲得的空間較小。

  • 堆:堆是向高地址擴展的數據結構,是不連續的內存區域。這是由於系統是用鏈表來存儲的空閑內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。

3.4)申請效率的比較

  • 棧區(stack):由系統自動分配,速度較快。但程序員是無法控制的。

  • 堆區(heap):是由alloc分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便。

3.5)分配方式的比較

  • 棧區(stack):有2種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的,比如局部變量的分配。動態分配由alloc函數進行分配,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器進行釋放,無需我們手工實現。

  • 堆區(heap):堆都是動態分配的,沒有靜態分配的堆。

3.6)分配效率的比較

  • 棧區(stack):棧是操作系統提供的數據結構,計算機會在底層對棧提供支持:分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高。

  • 堆區(heap):堆則是C/C++函數庫提供的,它的機制是很復雜的,例如為了分配一塊內存,庫函數會按照一定的算法(具體的算法可以參考數據結構/操作系統)在堆內存中搜索可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由於內存碎片太多),就有可能調用系統功能去增加程序數據段的內存空間,這樣就有機會分到足夠大小的內存,然后進行返回。顯然,堆的效率比棧要低得多。

四. 內存分配的引入

4.1)什么行為會增加App的內存占用?

① 創建一個OC對象。

② 定義一個變量。

③ 調用一個函數和方法。

4.2)內存管理范圍

  任何繼承了NSObject的對象,其它非對象類型不需要管理。簡單來說,只有OC對象需要內存管理,非OC對象類型不需要內存管理,比如基本數據類型。

4.3)內存管理原因

  由內存管理范圍,我們是不是就有疑問了?為什么OC對象需要進行內存管理,而其它非對象類型比如基本數據類型就不需要進行內存管理了呢?只有OC對象才需要進行內存管理的本質原因是什么?

  由於移動設備或者PC設備的內存大小是有限制,所以任何應用都需要進行內存管理。

  因為OC的對象在內存中是以堆(heap)的方式分配空間的,而堆(heap)是由程序開發者釋放的,就是release。也就是說OC對象是存儲在堆(heap)里面的,堆內存需要程序開發者手動回收。而非OC對象一般存放在棧里面,棧內存會被系統自動回收。堆內存是動態分配的,所以也需要程序開發者手動添加內存,回收內存。

五. Objective-C內存管理

5.1)Objective-C內存管理相關術語

  什么是內存管理?是指軟件運行時對計算機內存資源的分配和使用的技術。其最主要的目的是如何高效,快速的分配,並且在適當的時候釋放和回收內存資源。

  引用計數:OC中每個對象都有一個與之對應的整數,叫引用計數。Objective-C的內存管理本質是通過引用計數實現的。

  MRC(manual reference counting):即手動引用計數,在iOS5之前內存是由開發者自己手動管理的,寫完代碼需要合理插入retainrelease,保證內存不會泄露,程序可以正常運行。

  ARC(automatic reference counting):即自動引用計數,2011年WWDC大會iOS5提出了自動引用計數(ARC),內存的管理由系統進行接管,開發者只需要關注業務邏輯實現,大大提高了開發效率。

5.2)什么是引用計數?

5.2.1)引用計數解釋

  引用計數是計算機編程語言中的一種內存管理技術,是指將資源(可以是對象、內存或磁盤空間等等)的被引用次數保存起來,當被引用次數變為零時就將其釋放的過程。使用引用計數技術可以實現自動資源管理的目的。同時引用計數還可以指使用引用計數技術回收未使用資源的垃圾回收算法。

  當創建一個對象的實例並在堆上申請內存時,對象的引用計數就為1,在其他對象中需要持有這個對象時,就需要把該對象的引用計數加1,需要釋放一個對象時,就將該對象的引用計數減1,直至對象的引用計數為0,對象的內存會被立刻釋放。

  在iOS5之前,iOS開發的內存管理是手動處理引用計數,在合適的地方使引用計數+1、-1,直到減為0,內存釋放。現在的iOS開發內存管理使用的是ARC,自動管理引用計數,會根據引用計數自動監視對象的生存周期,實現方式是在編譯時期自動在已有代碼中插入合適的內存管理代碼以及在 Runtime 做一些優化。

5.2.2)文藝解釋

  當這個這個世界上最后一個人都忘記你時,就迎來了終極死亡。類比於引用計數,就是每有一個人記得你時你的引用計數加1,每有一個人忘記你時,你的引用計數減1,當所有人都忘記你時,你就消失了,也就是從內存中釋放了。

  如果再深一層,包含我們后面要介紹的ARC中的強引用和弱引用的話,那這個記住的含義就不一樣了。強引用就是你摯愛的親人,朋友等對你比較重要的人記得你,你的引用計數才加1。

  而弱引用就是那種路人,一面之緣的人,他們只是對你有一個印象,他們記得你是沒有用的,你的引用計數不會加1。當你摯愛的人都忘記你時,你的引用計數歸零,你就從這個世界上消失了,而這些路人只是感覺到自己記憶中忽然少了些什么而已。

5.2.3)代碼測試

  我們創建一個工程,在Build Phases里設置AppDelegate的Compiler Flags-fno-objc-arc來開啟手動管理引用計數的模式。

- (void)noObjcArc {
    NSObject *object = [[NSObject alloc] init];
    NSLog(@"引用計數=%lu 對象內存(堆)=%p object指針內存地址(棧)=%p", (unsigned long)[object retainCount], object, &object);

    self.property = object;
    NSLog(@"引用計數=%lu 對象內存(堆)=%p object指針內存地址(棧)=%p property指針內存地址=%p", (unsigned long)[object retainCount], object, &object, &_property);
    
    [object release];
    NSLog(@"引用計數=%lu 對象內存(堆)=%p object指針內存地址(棧)=%p property指針內存地址=%p", (unsigned long)[object retainCount], object, &object, &_property);
}

​ 輸出結果:

引用計數=1 對象內存(堆)=0x600001dc84d0 object指針內存地址(棧)=0x7ffeeceb0808
引用計數=2 對象內存(堆)=0x600001dc84d0 object指針內存地址(棧)=0x7ffeeceb0808 property指針內存地址=0x600001fe9378
引用計數=1 對象內存(堆)=0x600001dc84d0 object指針內存地址(棧)=0x7ffeeceb0808 property指針內存地址=0x600001fe9378

  我們看到object持有對象引用計數+1為1,然后self.property又持有了對象,引用計數再+1為2,然后我們主動釋放object,引用計數-1變為1。我們能看到[object release]釋放后指向對象的指針仍就被保留在object這個變量中,只是對象的引用計數-1了而已。

  對應的內存上的分配如下圖所示:

image

5.3)自動釋放池

5.3.1)AutoreleasePool的原理

  自動釋放池是OC中的一種內存自動回收機制,它可以延遲加入AutoreleasePool中的變量release的時機,即當我們創建了一個對象,並把他加入到了自動釋放池中時,他不會立即被釋放,會等到一次runloop結束或者作用域超出{}或者超出[pool release]之后再被釋放。

  系統有一個現成的自動內存管理池,他會隨着每一個mainRunloop的結束而釋放其中的對像;自動釋放池也可以手動創建,他可以讓pool中的對象在執行完代碼后馬上被釋放,可以起到優化內存,防止內存溢出的效果(如視頻針圖片的切換時、創建大量臨時對象時等)。

  autorelease 只是一個標記,表明會延遲自動釋放,當一個autorelease對象超出自己的作用域后,會被添加到離他最近的autorelease pool中,當pool開始傾倒的時候,會向池里面所有的對象發送一次release方法,釋放pool中所有的對象。

5.3.2)自動釋放池的創建和銷毀

  • MRC環境下:

    // MRC下 AutoreleasePool
    // 創建一個自動釋放池
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    // do something
    id obj = [[NSMutableArray alloc] init];
    // 調autorelease方法將對象加入到自動釋放池
    [obj autorelease];
    // 手動釋放自動釋放池執行完這行代碼是,自動釋放池會對加入他中的對象做一次release操作
    [pool release];
    // 自動釋放池銷毀時機:[pool release]代碼執行完后.
    
  • ARC環境下

    @autoreleasepool {
        // 在這個{}之內的變量默認被添加到自動釋放池
        id obj = [[NSMutableArray alloc] init];
    } // 出了這個括號,p被釋放
    

5.3.3)自動釋放池的使用場景

  • 如果你寫了一個創建了很多臨時變量的循環。你可能在下次循環之前使用一個自動釋放池來釋放那些變量。在循環中使用自動釋放池來減少應用程序的峰值內存占用。

    for (int i = 0; i < 1000; ++i) {
        @autoreleasepool{
            NSString *str = @"Hello World";
            NSLog(@"%@", str);
        }
    }
    
    //在循環中創建了大量的臨時對象NSString,在方法沒有走完的情況下,每次創建的對象是不會釋放的,所以我們用自動釋放池,在每次循環開始的時候把臨時變量NSString放到池里,等每次循環結束的時候傾倒池子,從而每次釋放釋放NSString臨時變量。
    
  • 如果你創建了一個二級線程。你必須創建你的自動釋放池在線程一開始執行的時候,否則,你的應用將會內存泄漏。

5.4)MRC手動管理引用計數

  MRC內存管理原則,誰申請,誰釋放,遇到alloc/copy/retain等都需要添加release或autorelease。

5.4.1)對象操作

  在MRC中增加的引用計數都是需要自己手動釋放的,所以我們需要知道哪些方式會引起引用計數發生變化,如下表所示:

對象操作 OC中對應的方法(消息) 引用計數的變化
生成並持有對象 alloc/new/copy/mutableCopy等 +1
持有對象 retain +1
釋放對象 release -1
廢棄對象 dealloc -

5.4.2)四個法則

  • 自己生成的對象,自己持有;

    // 1.自己生成並持有該對象
    NSObject *obj1 = [[NSObject alloc] init];
    NSObject *obj2 = [NSObject new];
    
  • 非自己生成的對象,自己也能持有;

    // 2.持有非自己生成的對象
    id obj3 = [NSArray array]; // 非自己生成的對象,且該對象存在,但自己不持有
    [obj3 retain]; // 自己持有對象
    
  • 不在需要自己持有對象的時候,釋放;

    // 3.不在需要自己持有對象的時候,釋放
    NSObject *obj4 = [[NSObject alloc] init]; // 此時持有對象
    [obj4 release]; // 釋放對象
    // 注意:指向對象的指針仍就被保留在obj4這個變量中,但對象已經釋放,不可訪問
    
  • 非自己持有的對象無需釋放;

    // 4.非自己持有的對象無法釋放
    id obj = [NSArray array]; // 非自己生成的對象,且該對象存在,但自己不持有
    [obj release]; // ~~~此時將運行時crash 或編譯器報error~~~ 非 ARC 下,調用該方法會導致編譯器報 issues。此操作的行為是未定義的,可能會導致運行時 crash 或者其它未知行為
    

5.4.3 非自己生成的對象,且該對象存在,但自己不持有

  其中關於非自己生成的對象,且該對象存在,但自己不持有是如何實現的呢?這個特性是使用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實現的。

5.5)ARC自動管理引用計數

5.5.1)ARC介紹

  ARC其實也是基於引用計數,只是編譯器在編譯時期自動在已有代碼中插入合適的內存管理代碼(包括 retain、release、copy、autorelease、autoreleasepool)以及在 Runtime 做一些優化。

  現在的iOS開發基本都是基於ARC的,所以開發人員大部分情況都是不需要考慮內存管理的,因為編譯器已經幫你做了。為什么說是大部分呢,因為底層的 Core Foundation 對象由於不在 ARC 的管理下,所以需要自己維護這些對象的引用計數。

  還有就算循環引起的互相之間強引用,引用計數永遠不會減到0,所以需要自己主動斷開循環引用,使引用計數能夠減少。

5.5.2)所有權修飾符

  Objective-C編程中為了處理對象,可將變量類型定義為id類型或各種對象類型。 ARC中id類型和對象類其類型必須附加所有權修飾符。

  其中有以下4種所有權修飾符(聲明變量修飾符):

  • __strong:強引用,持有所指向對象的所有權,無修飾符情況下的默認值。如需強制釋放,可置nil。

  • __weak:弱引用,不持有所指向對象的所有權,引用指向的對象內存被回收之后,引用本身會置nil,避免野指針。

  • __unsafe_unretaied:這個修飾符主要是為了在ARC剛發布時兼容iOS4以及版本更低的系統,因為這些版本沒有弱引用機制。

  • __autoreleasing:自動釋放對象的引用,一般用於傳遞參數。

    所有權修飾符和聲明屬性的修飾符對應關系如下所示:

  • strong對應的所有權類型是 __strong

  • copy 對應的所有權類型是 __strong

  • retain 對應的所有權類型是 __strong

  • weak對應的所有權類型是 __weak

  • unsafe_unretained對應的所有權類型是__unsafe_unretained

  • assign 對應的所有權類型是 __unsafe_unretained

  關系對照圖:

image

關於屬性修飾符詳細參考文章:https://www.cnblogs.com/hubert-style/p/15045430.html

(1)__strong

  __strong 表示強引用,持有所指向對象的所有權,對應定義 property的修飾符時用到的 strong。當對象沒有任何一個強引用指向它時,它才會被釋放。如果在聲明引用時不加修飾符,那么引用將默認是強引用。當需要釋放強引用指向的對象時,需要保證所有指向對象強引用置為 nil。__strong 修飾符是 id 類型和對象類型默認的所有權修飾符。

  示例:我們常用的定時器

// 所有權修飾符 __strong
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(runTimer) userInfo:nil repeats:NO];

// 默認所有權,相當於
NSTimer * __strong timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(runTimer) userInfo:nil repeats:NO];

// 當不需要使用時,強制銷毀定時器
[timer invalidate];
timer = nil;

  原理解析1:對象通過alloc、new、copy、mutableCopy生成

{
    id __strong obj = [[NSObject alloc] init];
}
//編譯器的模擬代碼
id obj = objc_msgSend(NSObject,@selector(alloc));
objc_msgSend(obj,@selector(init));
// 出作用域的時候調用
objc_release(obj);

  雖然ARC有效時不能使用release方法,但由此可知編譯器自動插入了release。

  原理解析2:對象是通過除alloc、new、copy、mutableCopy外方法產生的情況:

{
    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中而僅僅返回一個對象。達到了一種最優效果。如下圖:

image

(2)__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銷毀時釋放對象占用的內存。

(3)__unsafe_unretained

  ARC 是在 iOS5 引入的,而 __unsafe_unretained 這個修飾符主要是為了在ARC剛發布時兼容iOS4以及版本更低的系統,因為這些版本沒有弱引用機制。這個修飾符在定義property時對應的是unsafe_unretained__unsafe_unretained 修飾的指針純粹只是指向對象,沒有任何額外的操作,不會去持有對象使得對象的 retainCount +1。而在指向的對象被釋放時依然原原本本地指向原來的對象地址,不會被自動置為 nil,所以成為了野指針,非常不安全。

  __unsafe_unretained的應用場景:在 ARC 環境下但是要兼容 iOS4.x 的版本,用__unsafe_unretained 替代 __weak 解決強循環循環的問題。

(4)__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中。

5.6)循環引用

  什么是循環引用?循環引用就是在兩個對象互相之間強引用了,引用計數都加1了,我們前面說過,只有當引用計數減為0時對象才釋放。但是這兩個的引用計數都依賴於對方,所以也就導致了永遠無法釋放。

  最容易產生循環引用的兩種情況就是DelegateBlock。所以我們就引入了弱引用這種概念,即弱引用雖然持有對象,但是並不增加引用計數,這樣就避免了循環引用的產生。也就是我們上面所說的所有權修飾符__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秒后沒被釋放,就會中斷言。

5.7)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);

  對於 CFRetainCFRelease 兩個方法,讀者可以直觀地認為,這與 Objective-C 對象的 retainrelease 方法等價。

  所以對於底層 Core Foundation 對象,我們只需要延續以前手工管理引用計數的辦法即可。

  除此之外,還有另外一個問題需要解決。在 ARC 下,我們有時需要將一個 Core Foundation 對象轉換成一個 Objective-C 對象,這個時候我們需要告訴編譯器,轉換過程中的引用計數需要做如何的調整。這就引入了bridge相關的關鍵字,以下是這些關鍵字的說明:

  • __bridge: 只做類型轉換,不修改相關對象的引用計數,原來的 Core Foundation 對象在不用時,需要調用 CFRelease 方法。
  • __bridge_retained:類型轉換后,將相關對象的引用計數加 1,原來的 Core Foundation 對象在不用時,需要調用 CFRelease 方法。
  • __bridge_transfer:類型轉換后,將該對象的引用計數交給 ARC 管理,Core Foundation 對象在不用時,不再需要調用 CFRelease 方法。

5.8)總結常見內存泄漏

(1)僵屍對象和野指針

  • 僵屍對象:內存被回收的對象;
  • 野指針:指向僵屍對象的指針叫野指針,向野指針發送消息會導致崩潰;

(2)循環引用;

(3)循環中對象占用內存大;

(3)無線循環;

(4)系統內存警告;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM