高效能iOS應用開發-記憶體管理
iOS裝置中某個應用記憶體使用超過單個程序上的限制,會被系統終止使用。
記憶體問題常出現在重複的記憶體釋放
和迴圈引用
的情況。
記憶體消耗
記憶體消耗
指的是應用消耗的RAM
。iOS的虛擬記憶體模型並不包含交換記憶體,意味著不會被用來分頁記憶體。
應用中記憶體消耗分為兩部分:棧大小
和堆大小
。
棧大小
應用中新執行緒都有專用的棧空間,該空間由保留的記憶體和初始提交的記憶體組成。棧可以線上程存在期間自由使用。執行緒的最大空間很小,這就決定了以下限制:
- 可被遞迴呼叫的最大方法數的限制,每個方法都有自己的棧幀或者可以看這個連結,消耗整體的棧空間。main中呼叫func1,func1中呼叫func2,這就存在三個棧幀,每個棧幀都會消耗一定位元組的記憶體。
main() { //第一個棧幀
func1(); //第二個棧幀
}
func1() {
func2(); //在func1()上增加一個棧幀。
}
- 一個方法中最多可以使用的變數個數的限制,所有的變數都會載入方法的棧幀中,並消耗一定棧空間。
- 檢視層級中可以嵌入的最大檢視深度的限制,渲染符合檢視,整個檢視層級樹種遞迴呼叫layoutSubviews和drawRect方法。若層級過深,會導致棧溢位。
堆大小
每個程序的所有執行緒共享同一個堆。應用並不能控制分配給它的堆。只有作業系統才能管理堆。
通過類建立的物件相關資料都存放在堆中。類可能包含屬性或值型別的例項變數(iVars,基本資料型別),如int、char或struct。因為物件是在堆內建立的,所以他們只消耗記憶體。
使用NSString、載入圖片、建立或使用JSON/XML資料、使用檢視等都會消耗大量的堆記憶體。需要關注平均值和峰值記憶體使用的最小化。
當物件被建立並賦值時,資料可能會從棧複製到堆。類似的,當值僅在內部使用時,也可能會被從堆複製到棧。
雖然沒有強制規定,但記憶體最好不要超過80%~85%,要給系統核心留下足夠記憶體。**不要忽視didReceiveMemoryWarning
訊號。
記憶體管理模型
管理模型基於持有關係(引用計數?retain or release?)的概念。如果一個物件正處於被持有狀態,那它佔用的記憶體就不能被回收。
當一個物件創建於某個方法內部十,那麼方法就持有該物件。如果這個方法返回,則呼叫者聲稱建立了持有關係。這個值可以付給其他變數,對應的變數同樣會聲稱建立了持有關係。
一旦與某個物件相關的任務全部完成,就是放棄了持有關係。這一過程中沒有轉移持有關係,而是分別增加或減少了持有者的數量。當持有者的數量降為零時,物件會被釋放,相關記憶體會被回收。這種持有關係計數通常被正式成為引用計數
手動引用計數(MRC)
。現如今的應用大都使用自動引用計數(ARC)
。
引用計數基本結構
NSString *message = @"a"; //引用計數為1
NSString *messageRetained = [message retain]; //引用計數為2
[messageRetained release]; //引用計數為1
[message release]; //引用計數為0
NSLog(@"%@", message); //此時值已經為未定義狀態,但還是能取得相同的值,因為對應的記憶體還沒被回收或重置。
方法中的引用計數
//一個Person類的部分
- (NSString *)address {
NSString *result = [[NSString alloc] initWithFormat:@"%@", self.city]; //首次建立為1
return result;
}
- (void)showPerson:(Persson *)p {
NSString *paddress = [p address]; //引用計數仍未1
NSLog(@"%@", paddress); //不變
[paddress release]; //引用計數為0
}
自動釋放物件
自動釋放物件讓你能夠放棄一個物件的持有關係,但是延後對它的銷燬。當方法中建立一個物件並需要將其返回時,自動釋放就顯得十分有用。
- (NSString *)address{
NSString *result = [[[NSString alloc] initWithFortmat:@"%@", self.city] autorelease];
//[result release]; //3物件在返回之前釋放,返回引用無效。
return result;
}
規則:
- 持有的物件是alloc方法生成並返回的;
- 確保沒有記憶體洩露,必須在失去引用之前放棄關係;
- 如果使用release,那麼物件釋放將發生在返回之前,因而方法將返回無效的引用;
- autorelease表明想放棄持有關係,但同時發放的呼叫者允許物件被釋放之前使用物件。
當建立一個物件並將其從非
alloc
方法返回時,應使用autorelease。
自動釋放池塊(@autoreleasepool)
自動釋放池塊
是允許你放棄對一個物件的持有關係、但可避免它立即被回收的一個工具。當從方法返回物件時,這個功能非常有用。
它(自動釋放池塊)
能確保在塊內建立的物件會在塊完成時被回收(用完了就被回收)。這在建立了多個物件的場景中非常有用。本地的塊可以用來今早的釋放其中的物件,從而使記憶體用量保持在較低水平。
自動釋放池塊用@autoreleasepool
表示。
//例子
int main(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
塊中收到autorelease
訊息的所有物件都會在autoreleasepool
塊結束時收到release訊息。更加重要的是,每個autorelease呼叫都會發送一個release訊息。這意味著如果一個物件收到了不知一次的autorelease訊息,那它也會多次收到release訊息。這樣能保證物件引用技術下降到使用autoreleasepool塊之前的值。如果計數為0,則物件將被回收,從而保持較低的記憶體使用率。
autoreleasepool
可以巢狀。另外autorelease物件在autoreleasepool塊內執行,能確保autorelease物件被釋放,從而防止應用記憶體洩露的情況。
AppKit和UIKit框架將事件–迴圈的迭代放入autoreleasepool塊中。因此,通常不需要自己在建立autoreleasepool塊。
使用autoreleasepool塊的場景:
- 當建立了很多臨時物件的迴圈時。迴圈中使用autoreleasepool塊可以為每個迭代釋放記憶體,可以大大降低記憶體的需求。
- 當建立了一個執行緒時。主執行緒用自己的autoreleasepool塊,然而對自定義執行緒,必須自己建立autoreleasepool。
-(void)threadStart {
@autoreleasepool{
//新執行緒的程式碼
}
}
//其他地方
{
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadStart) object:nil];
[thread start];
}
自動引用計數
出現的原因:持續跟蹤retain、release和autorelease不容易。2011年提出的ARC。Objective-C同時支援ARC和MRC,Swift不支援MRC。
ARC
是編譯器特性。評估了物件在程式碼中的生命週期,並在編譯時自動注入合適的記憶體管理呼叫(ARC記憶體管理呼叫發生在編譯時)。編譯器還會生成合適的dealloc方法。
優勢:節約了開發時間,降低了開發難度,減少了程式碼量。
禁用ARC,需要進入 Targets->Build Phases->Compile Sources,選擇必須禁用 ARC的檔案,然後新增編譯器標記 -fno-objc-arc。
- 不能實現或者呼叫retain、release、autorelease或retainCount方法。這一限制不僅針對物件,對選擇器(
@selector()
)同樣有效。因此,[obj release]
或@selector(retain)
是編譯時的錯誤。 - 可以實現
dealloc
方法,但是不能主動呼叫。不僅不能呼叫其他物件的dealloc方法,也不能呼叫其超類。如:[super dealloc]
是編譯時的錯誤。 - 不能呼叫NSAllocateObject和NSDeallocateObject方法。應使用alloc方法建立物件,執行時負責回收物件。
- 不能再C語言結構體內使用物件指標?
- 不能再id型別和void*型別之間自動轉換。需要做顯示轉換。
- 不能使用NSAutoreleasePool,要替換使用autoreleasepool塊。
- 不能使用NSZone記憶體區域。
- 屬性的訪問器名稱不能以new開頭,確保與MRC的互操作性。
- ARC和MRC仍然可以混合使用。(new開頭的屬性會報錯)
arc屬性名稱不能以new開頭,但開啟訪問器可以
//won't work
@property NSString *newTitle;
//Works
@property (getter=getNewTitle) NSString *newTitle;
引用型別
ARC帶來了新的引用型別:弱引用
。支援的型別包括:
- 強引用。預設引用型別,被強引用指向不會被釋放。強引用會對引用計數加1,從而擴充套件物件的生命週期。
- 弱引用。不會增加引用計數,因而不會擴充套件物件的生命週期。
變數限定符
ARC
為變數提供了四種生命週期限定符。
- __strong,這是預設的限定符,無需顯示引入。只要有強引用指向,物件就會長時間駐留在記憶體 中。可以將 __strong 理解為 retain 呼叫的 ARC 版本。
- __weak,這表明引用不會保持被引用物件的存活。當沒有強引用指向物件時,弱引用會被置為 nil。可將 __weak 看作是 assign 操作符的 ARC 版本,只是物件被回收時,__weak 具有安全性——指標將自動被設定為 nil。
- __unsafe_unretained,與 __weak 類似,只是當沒有強引用指向物件時,__unsafe_unretained 不會被置為 nil。 可將其看作 assign 操作符的 ARC 版本。
- __autoreleasing,__autoreleasing用於由引用使用id *傳遞的訊息引數。它預期了autorelease方法會 在傳遞引數的方法中被呼叫。
//TypeName * qualifier variable;
Person * __strong p1 = [[Person alloc] init]; //1
Person * __weak p2 = [[Person alloc] init]; //2
Person * __unsafe_unretained p3 = [[Person alloc] init]; //3
Person * __autoreleasing p4 = [[Person alloc] init]; //4
1.建立後引用計數為1,並且物件在p1引用期間不會被回收。
2.建立物件後飲用計數為0,物件會被立即釋放,且p2將置為nil。
3.建立後計數變為1,物件會被立即釋放,但p3不會被設定為nil。
4.建立後計數為1,當方法返回時物件會被立即釋放。
屬性限定符
屬性宣告有六個持有關係限定符:
- strong,預設符,指定了__strong關係;
- weak,指定了__weak關係;
- assign,在新版本中,assign的含義發生了變化。在ARC之前,assign是預設的持有關係限定符。在啟用ARC之後,assign表示了__unsafe_unretained關係;
- copy,暗指了__strong關係,暗示了setter中的複製語句的常規行為;
- retain,指定了__strong關係;
- unsafe_unretained,指定了__unsafe_unretained關係。
因為assign和unsafe_unretained只進行值複製而沒有任何實質性的檢查。