程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> Objective-C中的內存管理

Objective-C中的內存管理

編輯:關於C語言

Objective-C中的內存管理


IOS開發中, 之前一直使用swift, 因此對於Objective-C的內存管理機制長期處於混亂的一知半解狀態. 今天終於看到一篇講得透徹的博客Objective-C內存管理教程和原理剖析, 感謝作者.
本文只是個人的一個學習摘要, 更詳細內容請參考原文.

基本的內存分配

OC的對象是在堆裡邊生成, 需要一個指針指向它.

ClassA *obj1 = [[ClassA alloc] init];

使用完之後不會自動銷毀, 需執行dealloc來銷毀, 否則會出現內存洩露.

[obj1 dealloc];

那麼看如下代碼:

ClassA *obj1 = [[ClassA alloc] init];
ClassA *obj2 = obj1;
[obj1 hello];
[obj1 dealloc];
// obj1, obj2都是指針, 指向同一對象. 而[obj1 dealloc]已經銷毀了該對象. 因此下邊的代碼無法執行, obj2是無效指針.
[obj2 hello];
[obj2 dealloc];

引用計數

為了避免出現上邊的無效指針, OC采用引用計數的方式. 引用計數加1的有:alloc, retain, strong, 減1的有release. 當對象的引用計數為0的時候, 會自動調用dealloc.

ClassA *obj1 = [[ClassA alloc] init]; // 計數為1
[obj1 release]; // 計數為0, 自動dealloc
ClassA *obj1 = [[ClassA alloc] init]; // 計數為1
ClassA *obj2 = obj1; // 計數為1, 指針賦值不會增加引用計數
[obj1 hello];
[obj1 release]; // 計數為0, 自動dealloc
// 因對象已被dealloc, 故以下代碼不會執行, obj2仍然是無效指針.
[obj2 hello];
[obj2 release];

那麼如何解決obj2的無效指針問題呢? 通過retain使得引用計數加1.

ClassA *obj1 = [[ClassA alloc] init]; // 計數為1
ClassA *obj2 = obj1; // 計數為1
[obj2 retain]; // 計數為2
[obj1 hello];
[obj1 release]; // 計數為1
[obj2 hello];
[obj2 release]; // 計數為0, 自動dealloc

所以, 引用計數的關鍵在於當對象的引用計數為0時, 會自動調用dealloc銷毀該對象. 指針賦值時, retain count不會自動增加, 需要手動retain.
另外, release一個對象之後, 應立即將指針清空(release一個空指針是合法的). 如:

ClassA *obj1 = [[ClassA alloc] init];
[obj1 release];
obj1 = nil;

autorelease

自動釋放池其實是NSAutoreleasePool, 可以自動釋放對象.

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

NSAutoreleasePool內部包含一個NSMutableArray, 用於保存聲明為autorelease的所有對象.

ClassA *obj1 = [[[ClassA alloc] init] autorelease]; // 引用計數為1, 把對象加到autorelease pool中

下邊看一下Apple的例子:
首先不用autorelease,

- (NSString *)fullName {
    NSString *string = [[NSString alloc] initWithFormat:@"%@ %@", self.firstName, self.lastName];
    return string;
}

則, 該string不能找到合適的時機釋放, 因此會出現內存洩露.
那麼, 采用autorelease之後,

- (NSString *)fullName {
    NSString *string = [[[NSString alloc] initWithFormat:@"%@ %@", self.firstName, self.lastName] autorelease];
    return string;
}

雖然, string不會立即釋放, 但autorelease會在其release的時機將其釋放.
或者采用更直接簡便的方式:

- (NSString *)fullName {
    NSString *string = [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
    return string;
}

NSAutoreleasePool自身在銷毀的時候, 會遍歷並試圖release其中的所有autorelease對象. 如果該對象的引用計數為1, 則將其引用計數減1, 銷毀該對象; 如果該對象的引用計數大於1, 則autorelease之後其引用計數仍大於0, 對象未銷毀, 出現內存洩露.
如在iOS工程的main.m文件中

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
   }
}

其實可以自行使用NSAutoreleasePool,

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
for (int i=0;i<100;i++) {
    for (int j=0;j<10000;j++) {
        // 產生autorelease對象
        [NSString stringWithFromat:@"1234567890"];
    }
}
[pool release];
return 0;

可以看出, 所有autorelease對象都只能在NSAutoreleasePool執行release的時候銷毀, 顯然不能很好地利用內存. 那麼可以使用內嵌的NSAutoreleasePool, 代碼優化如下:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
for (int i=0;i<100;i++) {
    NSAutoreleasePool *loopPool = [[NSAutoreleasePool alloc] init];
    for (int j=0;j<10000;j++) {
        // 產生autorelease對象
        [NSString stringWithFromat:@"1234567890"];
    }
    [loopPool release];
}
[pool release];
return 0;

這樣, 就可以做到及時地釋放內存.
因oc對象是動態內存, 沒有棧的概念, 所以不能像C那樣返回一個對象到棧, 只能用autorelease對象.
所以, NSAutoreleasePool的關鍵在於, 放置到其中的autorelease對象, 會在該pool release的時候自動銷毀(絕大多數情況下, 都是正常的引用計數為1的autorelease對象). 因此對象本身就沒必要調用release方法. 同時NSAutoreleasePool的release時機很重要.
不要濫用autorelease, 如對象的生命周期很清晰, 則結束使用後立即release. 過多等待autorelease對象浪費內存.

property中的關鍵字

property中的關鍵字其實全都是用於設置getter/setter的操作特性.

assign

assign相當於指針賦值, 不對引用計數進行操作, 如原對象不用了, 一定要將其設置為nil.
即: 調用setter方法時直接賦值, 不進行retain操作. 不改變引用計數.

copy

內容拷貝. 復制一個對象變成新的對象(新的內存地址), 新對象的引用計數為1, 原對象的引用計數不變. 遵循NSCopying協議.
mutableCopy: 也是復制一個對象. 新對象的引用計數為1.
即copy得到的均為一個獨立的對象.

retain

retain其實是指針拷貝(地址相同), 會增加對象的引用計數,而基本數據類型或CoreFoundation對象沒有引用計數。
把對象添加到數組中,引用計數將增加對象的引用次數+1.
即: 調用setter方法時, 先release舊值, 對賦予的新值進行引用計數+1. 二者的內存地址一樣.

strong

strong是強引用, 對象的引用計數+1, 如string1和string2都指向一個字符串, 則string1=nil, 而string2不變.
strong變量執行ARC計數, 不會自動釋放. 其死亡直接決定了所指向對象的死亡.
為了確保使用中的實例不會被銷毀,ARC 會跟蹤和計算每一個實例正在被多少屬性,常量和變量所引用。哪怕實例的引用數為1,ARC都不會銷毀這個實例。
為了使之成為可能,無論你將實例賦值給屬性,常量或者是變量,屬性,常量或者變量,都會對此實例創建強引用。之所以稱之為強引用,是因為它會將實例牢牢的保持住,只要強引用還在,實例是不允許被銷毀的。

weak

為了防止循環強引用,可采用弱引用和無主引用。這兩種允許循環引用中的一個實例引用另外一個實例而不保持強引用,這樣實例能夠相互引用而不產生循環強引用。
弱引用 :對於生命周期中會變為nil的實例采用。
無主引用:對於初始化賦值後再也不會變為nil的實例采用。
weak是弱引用, 對象的計數不變. 如string1和string2都指向一個字符串, 則string1=nil, 那麼string2也會變為nil.
因其沒有retain內存地址, 若其指向的地址對象一旦釋放, 則該指針會指向nil.
ARC空閒時釋放, 對象釋放時自動將指針置NULL. 不決定對象的存亡, 即使一個對象被持有無數個弱引用, 只要沒有強引用指向它, 則最後還是會清除
如兩個對象互相為對方的成員變量, 則一定不能同時retain, 否則dealloc函數形成死鎖, 兩個對象都無法釋放. weak可以用於防止野指針和死鎖, 避免循環引用.

__weak

__weak 聲明了可以自動nil化的弱引用.
注意: 代碼中使用self之類的有可能導致retain cycle, 所以一般用__weak的方式. 請看下邊的城市列表例子.
在CityListVC中, 通過block將city的值傳遞給MainVC.
在CityListVC.h中: 定義block塊對象

typedef void(^SelectedCity)(NSString *);
@property(copy,nonatomic) SelectedCity selectCity;
CityListVC.m中:
if (_selectCity) {
  _selectCity(cell.textLabel.text); // 這裡要通過block進行View之間的數據傳遞.
}

MainVC.h中:

typedef void(^SelectedCity)(NSString *);
@property(copy,nonatomic) SelectedCity selectCity;

MainVC.m中:

    self.vTracks.selectedCity = @"城市";
    __weak MainViewController *weakVC = self;
    _selectCity = ^(NSString *city){
        weakVC.cityLabel.text = ([city isEqualToString:@"全國"]) ? @"城市" : city;
        weakVC.vTracks.selectedCity = ([city isEqualToString:@"城市"] | [city isEqualToString:@"全國"]) ? nil : city;
        [weakVC.vTracks invokeRefresh];
    };

在block中, 使用__weak的方式聲明了一個weakVC, 弱引用的方式可以避免出現循環引用.

static

static可以視作類變量, 跟對象實例無關. 已經實例化的各個對象之間的該static變量不會相互影響, 但對其的改變會影響到class本身. 若再次實例化新的對象, 則該新對象就擁有最新的static類變量.

const

const, 常量不能修改. 超出作用域後會釋放. 即const對於對象生存期內是常量, 對於整個類而言可變.

extern

extern: 全局變量.

讀寫權限

readwrite, readonly: 控制成員變量的訪問權限.

原子操作

nonatomic: 非原子性訪問, 不加同步, 多線程並發訪問.
atomic 線程保護. 互斥鎖.

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved