1. 程式人生 > >高效能iOS應用開發-記憶體管理

高效能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只進行值複製而沒有任何實質性的檢查。