你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> [編寫高質量iOS代碼的52個有效方法](八)內存管理(下)

[編寫高質量iOS代碼的52個有效方法](八)內存管理(下)

編輯:IOS開發綜合

先睹為快

33.以弱引用避免保留環

34.以自動釋放池塊降低內存峰值

35.用僵屍對象調試內存管理問題

36.不要使用retainCount

 

第33條:以弱引用避免保留環

對象圖裡經常會出現一種情況,就是幾個對象都以某種方式相互引用,從而形成環。這種情況通常會洩漏內存,因為最後沒有別的東西會引用環中的對象。而環裡的對象會因為相互間的引用而繼續存活,不被系統回收。

#import 

@class EOCClassA;
@class EOCClassB;

@interface EOCClassA : NSObject
@property (nonatomic, strong) EOCClassB *other;
@end

@interface EOCClassB : NSObject
@property (nonatomic, strong) EOCClassA *other;
@end

本段代碼中就可能出現了保留環,如果把EOCClassA實例的other屬性設置為了某個EOCClassB實例,而又把EOCClassB實例的other屬性設置成了這個EOCClassA實例。那麼兩個對象就會相互引用,出現保留環。

這裡寫圖片描述

避免保留環的最佳方式就是弱引用。這種引用經常用來表示非擁有關系。將屬性聲明為unsafe_unretained或weak即可。

#import 

@class EOCClassA;
@class EOCClassB;

@interface EOCClassA : NSObject
@property (nonatomic, strong) EOCClassB *other;
@end

@interface EOCClassB : NSObject
@property (nonatomic, weak) EOCClassA *other;
@end

修改之後,EOCClassB實例就不能再通過other屬性來擁有EOCClassA實例了。weak與unsafe_unretained的區別在於,系統把屬性回收後,weak屬性會自動設置為nil,而unsafe_unretained屬性仍然指向那個已經回收的實例,這樣可能會不安全。不過無論如何,只要所在對象已經被系統回收後,都不應該繼續使用弱引用。

第34條:以自動釋放池塊降低內存峰值

在執行循環體時,一般會持續有新對象創建出來,並加入自動釋放池中。這種對象都要等到循環執行完才會釋放。這樣一來,在執行循環時,應用程序所占內存量會持續上漲,而等到所有臨時對象都釋放後,內存用量又會突然下降。

NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [NSMutableArray new];
for(NSDictionary *record in databaseRecords){
    EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
    [people addObject:person];
}

這種情況不甚理想,尤其是循環長度無法預知時,再創建出一些臨時的EOCPerson對象,它們本該提早回收的。增加一個自動釋放池即可解決問題,把循環內的代碼包裹在自動釋放池塊中,那麼循環體中自動釋放的對象就會在這個池,而不是線程的主池裡:

NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [NSMutableArray new];
for(NSDictionary *record in databaseRecords){
    @autoreleasepool{
        EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
        [people addObject:person];
    }
}

加上自動循環池之後,就會降低應用程序在執行循環時的內存峰值。因為系統會在塊的末尾將臨時對象回收掉。如果循環的內存用量不高,則盡量不建立額外的自動循環池,因為自動釋放池塊還是存在開銷(雖然不大)。

在ARC出現之前一般使用NSAutoreleasePool對象,這樣可以不用每次循環都清空池,通常用來創建偶爾需要清空的池:

NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [NSMutableArray new];
int i = 0;

// 創建自動釋放池會被推入棧中,在對象上執行autorelease等於將其放到棧頂的自動釋放池中。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
for(NSDictionary *record in databaseRecords){
    EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
    [people addObject:person];

    // 每執行10次循環,清空一次自動釋放池
    if (++i == 10){
        [pool drain];
        i = 0;;
    }
}
// 結束循環後,再次清空自動釋放池
[pool drain];

第35條:用僵屍對象調試內存管理問題

向已回收的對象發送消息是不安全的。這麼做是否可行完全取決於對象所占內存有沒有為其他內容所覆寫。Cocoa提供了僵屍對象這個方便的功能。啟用這項調試功能後,運行期系統會把所有已經回收的實例轉化為特殊的僵屍對象,而不會真正回收它們。僵屍對象收到消息後,會拋出異常,其中准確說明了發送過來的消息,並描述回收之前的那個對象。僵屍對象是調試內存管理的最佳方式。

在Xcode中打啟用僵屍對象:點擊下圖中左上角標注的位置選擇 Edit Scheme,再選擇run中的Diagnostics分頁,勾選Enabled Zombine Objects選項

這裡寫圖片描述

下面代碼就演示普通對象轉換為僵屍對象的過程
注意:采用的是手動計數,在Build Settings中將Objective-C Automatic Reference Counting設為NO即可不用ARC。

#import 
#import 

@interface EOCClass : NSObject
@end

@implementation EOCClass
@end

void PrintClassInfo(id obj){
    Class cls = object_getClass(obj);
    Class superCls = class_getSuperclass(cls);
    NSLog(@"=== %s : %s ===", class_getName(cls), class_getName(superCls));
}

int main(int argc, const char * argv[]) {
    EOCClass *obj = [[EOCClass alloc] init];
    NSLog(@"Before release:");
    PrintClassInfo(obj);

    [obj release];
    NSLog(@"After release");
    PrintClassInfo(obj);
    return 0;
}

運行結果:

2016-07-27 14:47:31.096 MRR Orders[89086:765092] Before release:
2016-07-27 14:47:31.097 MRR Orders[89086:765092] === EOCClass : NSObject ===
2016-07-27 14:47:31.097 MRR Orders[89086:765092] After release
2016-07-27 14:47:31.097 MRR Orders[89086:765092] === _NSZombie_EOCClass : nil ===

對象所屬的類已經由EOCClass變為NSZombie_EOCClass。這個類是代碼中沒有定義的,在運行期生成的。編譯器首次遇到EOCClass類對象要變成僵屍對象時,就會在類名前加上_NSZombie前綴生成對應的僵屍類。

僵屍類只是充當一個標記,它的作用會在消息轉發過程中體現出來。當執行到完整轉發時,“forwarding”函數會檢查對象所屬的類名,若名稱前綴為NSZombie,表明消息接收者是僵屍對象,需要特殊處理,此時會打印一條消息,其中指明僵屍對象收到的消息及原來所屬的類(僵屍類去掉前綴),然後應用程序終止。

在之前代碼末尾加上一句代碼向僵屍對象發送消息:

int main(int argc, const char * argv[]) {
    EOCClass *obj = [[EOCClass alloc] init];
    NSLog(@"Before release:");
    PrintClassInfo(obj);

    [obj release];
    NSLog(@"After release");
    PrintClassInfo(obj);

    // 向僵屍對象發送消息
    [obj description];
    return 0;
}

運行結果

2016-07-27 15:02:32.822 MRR Orders[89855:774958] Before release:
2016-07-27 15:02:32.823 MRR Orders[89855:774958] === EOCClass : NSObject ===
2016-07-27 15:02:32.823 MRR Orders[89855:774958] After release
2016-07-27 15:02:32.823 MRR Orders[89855:774958] === _NSZombie_EOCClass : nil ===
2016-07-27 15:02:32.823 MRR Orders[89855:774958] *** -[EOCClass description]: message sent to deallocated instance 0x1006002e0

可以看到僵屍對象原來所屬的類,收到的選擇器以及對應的指針值都打印出來了。

第36條:不要使用retainCount

Objective-C通過引用計數來管理內存,每個對象都有一個計數器,其值表明還有多少個其他對象想令此對象繼續存活。NSObject協議中定義了下列方法,用於查詢對象當前的保留計數:

- (NSUInteger)retainCount

ARC中已經廢棄此方法了,非ARC環境仍然可用,但是不應該用。
首要原因在於:它所返回的保留計數只是某個給定時間點上的值,並未考慮稍後清空自動釋放池,因此未必能真是反應實際的保留計數。

while([object reatinCount]){
    [object release];
}

這種寫法的錯誤在於,它沒有考慮後續的自動釋放操作,假如對象在自動釋放池中,稍後系統清空池子還要再釋放對象一次,引起程序崩潰。而且reatinCount可能永遠不返回0,因為有時系統會優化對象的釋放行為,在保留計數還是1的時候就把它回收了。如果對象已經回收了,循環還在進行,也會導致程序崩潰。

reatinCount返回的保留計數具體值也不一定有用

NSString *string = @"Some string";
NSLog(@"string retainCount = %lu",[string retainCount]);

NSNumber *numberI = @1;
NSLog(@"numberI retainCount = %lu",[numberI retainCount]);

NSNumber *numberF = @3.14f;
NSLog(@"numberF retainCount = %lu",[numberF retainCount]);

運行結果:

2016-07-27 15:16:59.776 MRR Orders[90612:784462] string retainCount = 18446744073709551615
2016-07-27 15:16:59.777 MRR Orders[90612:784462] numberI retainCount = 9223372036854775807
2016-07-27 15:16:59.777 MRR Orders[90612:784462] numberF retainCount = 1

第一個對象的保留計數是2的64次方減1,第二個是2的63次方減一.由於二者都是單例對象,所以其保留計數都很大。系統會盡可能把NSString實現成單例對象,NSNumber也類似,它使用了一種叫做標簽指針的概念來標注特定類型的數值,將有關信息都存放在指針值裡。由於浮點數沒有此優化,所以保留計數為1。

對於單例對象來說,保留計數永遠不會變,保留及釋放都是空操作。

由於對象可能出在自動釋放池中,其保留計數未必如想象般精確,而且其他程序庫也可能自行保留或釋放對象,者都會擾亂計數的具體取值。所以任何情況下都不要使用retainCount。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved