第五章 內存管理(EffectiveObjective-C)

1 理解引用計數

每個對象有個可增可將的計數器,ARC實際上也是一種引用計數機制,

  • 引用計數工作原理
    OC對象有個"保留計數"(retain count),或者"引用計數"(reference count).
    NSObject協議聲明了三個方法
    用于操作計數器,以遞增或遞減其值
    Retain 遞增保留計數
    release 遞減保留計數
    autorelease 待稍后清理"自動釋放池"(autorelease pool)時,再遞減保留計數.
    retainCount 查看保留計數的方法

為避免在不經意間使用了無效對象,一般調用完release之后都會清空指針,這就能保證不會出現肯呢個指向無效對象的指針,這種指針通常稱為"懸掛指針".

 NSMutableArray *array = [NSMutableArray array];
 NSNumber *number = @110;
 [array addObject:number];  
 [number release];
 number = nil;
  • 屬性存取方法中的內存管理
- (void)setFood:(Food *)food{
     if(food != _food){
        [food retain];
         [_food release];
         _food = food; 
     }
}
  • 自動釋放池
    在OC的引用計數中.調用release會立刻遞減對象的標流計數,然而有時候可以不調用它,改為調用autorelease,此方法會在稍后遞減計數,通常是在下一次"循環事件"(event loop)時遞減.

  • 保留環
    呈環狀相互引用的多個對象,這將導致內存泄漏,因為循環中的對象其保留技術不會將為0.對于循環中的每個對象來說,至少還有另外一個對象引用著它.通常采用弱引用來解決此問題

2 以ARC簡化引用計數

ARC下,我們不能直接調用retain,release,autorelease,dealloc等方法.實際上,ARC在調用這些方法時,并不通過普通的OC消息派發機制,而是直接調用其底層C語言版本.因為保留及釋放操作要頻繁執行,直接調用底層函數能節省很多CPU周期,比如,ARC會調用與retain等價的底層objc_retain.

  • 有ARC之后,程序員無需擔心內存管理問題,使用ARC來編程,省去了類中的許多"樣板代碼"
  • ARC管理對象生命期的辦法基本上就是:在合適的地方插入"保留"與"釋放操作".
    在ARC環境下,變量的內存管理語意可以通過修飾符指明,而原來需要手工執行"保留"與"釋放操作".
  • 由方法所返回的對象,其內存管理語意總是通過方法名來體現,ARC將此確定為開發者必須遵守的規定
  • ARC只負責管理OC對象,尤其要注意:CoreFoundation對象(純C的API生成的)不歸ARC管理,開發者必須適時調用CFRetain/CFRelease

3 在dealloc方法中只是否引用并解除監聽

在dealloc方法中一般:

  • 釋放CoreFoundation對象,取消原來訂閱的"鍵值觀察"(KVO),注銷(unregister)NSNotificationCenter給此對象訂閱的某種通知,不要做其他事情
- (void)dealloc{
    CFRealease(coreFoundationObject);
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
  • 如對象持有文件描述符等系統資源, 在這種情況下如果非要等到系統調用dealloc方法時才釋放,那么保留這些稀缺資源的時間就有些過長了. 那么應該專門編寫一個方法來釋放此種資源, 這樣的類要和其他使用者約定: 用完資源后必須調用close方法.
  • 執行異步任務的方法不應在dealloc里調用, 只能在正常狀態下執行的哪些方法 也不應在dealloc里調用.因為此時對象已處于正在回收的狀態了, 運行期已經改動了對象內部數據結構
  • 在dealloc里 不要調用屬性的存取方法, 這種做法可能會令運行期系統的狀態完全失調,從而導致莫明錯誤

4 編寫"異常安全代碼"時留意內存管理問題

  • 捕獲異常時,一定要注意將try塊內所創立的對象清理干凈
    在MRC中,將釋放操作移到@finally(不管有沒有異常都會執行)
  • 在默認情況下,ARC不生成安全處理異常所需要的清理代碼,開啟編譯器標識后(-fobjc-arc-exceptions),可生成這種代碼.不過會導致應用程序變大,而且會降低運行效率.
    默認不開啟的原因是,OC中只有程序終止才拋出異常,如果程序終止了,是否有內存泄漏都無關緊要了.
    另外如果處于Objective-C++模式下,也會自動打開編譯器標識(-fobjc-arc-exceptions)

5 以弱引用避免保留環

對象圖里常會出現一種情況,就是幾個對象都以某種方式相互引用,從而形成"環"(cycle).由于OC內存管理模型使用.引用計數架構,所以這種情況通常會泄漏內存.
避免保留環的最佳方式就是弱引用,這種引用經常用來表示"非擁有關系"(nonowning relationship).將屬性聲明為weak

6 以"自動釋放池塊"降低內存峰值

OC對象的生命期取決于引用計數,在OC的引用計數架構中,有一項特性叫做"自動釋放池"(autorelease pool).
釋放對象有2種方式:

  • 調用release方法,使其保留計數立即遞減
  • 調用autorelease方法,將其加入"自動釋放池"中,自動釋放池用于存放哪些需要在稍后某個時刻釋放的對象
    清空(drain)自動釋放池時,系統會向其中的對象發送release方法

創建自動釋放池的語法

@autoreleasepool{
 //...
}

然而,一般情況下無須擔心自動釋放池的創建問題,iOS程序運行在CocoaTouch環境下. 系統會自動創建一些線程
比如說主線程,或者GCD機制中的線程, 這些線程默認都有自動釋放池,每次執行"事件循環"(event loop)時,就會
將其清空.因此不需要自己來創建"自動釋放池塊"

當從數據庫中多出許多對象的時候,代碼可能這么寫

NSArray *dataArray = [[DatabaseManager sharedInstance] getSportDataWithYear:currentDate.year andMonth:currentDate.month andDay:currentDate.day];
for (NSData *data in dataArray) {
       DBSportData *sportData = [[DBSportData alloc] initWithData:data];
       [sportArray addObject:sportData];
 }

在DBSportData初始化函數中,可能會再創建出一些臨時對象,若記錄有很多條,則內存中也會有很多不必要的臨時對象.他們都需要等待for循環完畢.自動釋放池執行下一次時間循環的時候才會清空.
我們可以增加一個自動釋放池即可解決此問題.

NSArray *dataArray = [[DatabaseManager sharedInstance] getSportDataWithYear:currentDate.year andMonth:currentDate.month andDay:currentDate.day];
for (NSData *data in dataArray) {
  @autoreleasepool{
   DBSportData *sportData = [[DBSportData alloc] initWithData:data];
   [sportArray addObject:sportData];
 }
}

自動釋放池機制就像"棧"(stack)一樣,系統創建好自動釋放池之后,就將其推入棧中,而清空自動釋放池,則相當于將其從棧中彈出. 在對象上執行自動釋放操作,就等于將其放入棧頂的哪個池中

在ARC之前,有一種老式的寫法,NSAutoreleasePool對象

//create a pool
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//do  something
//drain at the end
[pool drain];

7 用"僵尸對象"調試內存管理問題

向已回收的對象發送消息是不安全的,這么做有時可以,有時又不行.具體可心與否,完全取決于對象所占內存有沒有為其他內容所覆寫,而這塊內存有沒有移做他用,又無法確定,以此,應用程序只是偶爾崩潰
Cocoa提供了"僵尸對象"(Zombie Object).啟用這項調試功能之后,運行期系統會把已經回收的實例轉換成特殊的"僵尸對象",而不會真正回收他們,這種對象所在的核心內存無法重用,因此不能遭到覆寫僵尸對象收到消息后,會拋出異常

通過Xcode開啟僵尸對象方法


xcode.png
#import <objc/runtime.h>
@implementation Person
void PrintClassInfo(id obj){
   Class clas = object_getClass(obj); 
   Class superCls = object_getClass([obj superclass]); 
   NSLog(@"====%s:%s====",class_getName(clas),class_getName(superCls));
}
int main(){
  Person *person = [[Person alloc] init];
  NSLog(@"Before release");
  PrintClassInfo(person); 
  [person release];
  NSLog(@"Before release");
  PrintClassInfo(person);
}
@end

控制器會打印

Before release
===Person:NSObject===
Before release
===_NSZombie_Person:nil===

對象所屬的類已由Person變為_NSZombie_Person
系統會修改對象的isa指針,另其指向特殊的僵尸類,從而使該對象變為僵尸對象,僵尸類能夠響應所有的選擇器,響應方式為:打印一條包含消息內容及其接受者的消息,然后終止應用程序

8 不要使用retainCount

  • 對象的保留計數看似有用,實則不然,因為任何給定時間點上的"絕對保留計數"(absoulte retain count)都無法反映
    對象生命周期的全貌
  • 引入ARC之后,retainCount方法就正式廢棄了,在ARC下調用該方法會導致編譯器報錯
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 29.理解引用計數 Objective-C語言使用引用計數來管理內存,也就是說,每個對象都有個可以遞增或遞減的計數...
    Code_Ninja閱讀 1,532評論 1 3
  • 內存管理 簡述OC中內存管理機制。與retain配對使用的方法是dealloc還是release,為什么?需要與a...
    丶逐漸閱讀 2,008評論 1 16
  • 內存管理是程序在運行時分配內存、使用內存,并在程序完成時釋放內存的過程。在Objective-C中,也被看作是在眾...
    蹲瓜閱讀 3,188評論 1 8
  • 內存管理 ARC處理原理 ARC是Objective-C編譯器的特性,而不是運行時特性或者垃圾回收機制,ARC所做...
    b485c88ab697閱讀 11,242評論 3 47
  • iOS內存管理 概述 什么是內存管理 應用程序內存管理是在程序運行時分配內存(比如創建一個對象,會增加內存占用)與...
    蚊香醬閱讀 5,762評論 8 119