高性能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。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章