題型復(fù)現(xiàn)
iOS是怎么管理內(nèi)存的?
都有哪些與內(nèi)存管理有關(guān)的關(guān)鍵字?有何異同?
什么是ARC?MRC?區(qū)別?怎么進(jìn)行混編?
CF對(duì)象又該怎么辦呢?
Cocoa和CF框架間對(duì)象的轉(zhuǎn)換過程內(nèi)存所有權(quán)是怎么過渡的?
歷史沿革
任何一門語言都會(huì)創(chuàng)造變量,而這些變量時(shí)存儲(chǔ)在內(nèi)存空間的。計(jì)算機(jī)
(或移動(dòng)設(shè)備)的內(nèi)存是有限的,因此我們需要對(duì)內(nèi)存的使用做相應(yīng)的管理,在適當(dāng)?shù)臅r(shí)機(jī)分配內(nèi)存和釋放內(nèi)存。
在iOS5之前,iOS采用的是MRC(Manual Reference Counting),就是內(nèi)存的分配和釋放(或者說對(duì)象的生命周期)都是由我們程序猿手動(dòng)管理的。這就需要我們務(wù)必在,適當(dāng)?shù)臅r(shí)機(jī)去釋放已經(jīng)沒有用的對(duì)象來增加可用內(nèi)存空間。
上述對(duì)程序猿來說無疑是很麻煩的,稍有不慎就會(huì)造成一定的內(nèi)存問題。因此,Apple特地推出了ARC(Auto Reference Counting),就是對(duì)象的內(nèi)存分配和釋放不需要我們?nèi)プ隽恕4蟛糠智闆r下,我們只管創(chuàng)建就好,系統(tǒng)會(huì)為我們自動(dòng)地釋放已經(jīng)不需要的對(duì)象。
Reference Count
無論是MRC還是ARC,都有一個(gè)Reference Count的概念。那么,他是什么意思呢?
Reference Count,引用計(jì)數(shù),是每個(gè)對(duì)象都有的一個(gè)屬性。他代表當(dāng)前引用并持有這個(gè)對(duì)象的指針數(shù)量。只要他不為零說明當(dāng)前對(duì)象還有用,如果某一時(shí)刻他的值為零說明系統(tǒng)已經(jīng)不需要這個(gè)對(duì)象。這個(gè)時(shí)候就需要對(duì)這個(gè)對(duì)象釋放內(nèi)存空間。
貌似有點(diǎn)抽象哈,我們舉個(gè)簡單的??。
假設(shè)某組織成員名額是有限的,當(dāng)只要有組織內(nèi)的人支持你表示你有1票,只要有1票就可以出現(xiàn)在組織內(nèi)部。當(dāng)支持你的人多了1個(gè),你的票數(shù)就會(huì)相應(yīng)加1,反之亦然。當(dāng)你票數(shù)為0時(shí),你就必須離開這個(gè)組織,把名額讓給其他的人。
我們復(fù)盤這個(gè)例子,來看看他和內(nèi)存管理中引用計(jì)數(shù)的對(duì)應(yīng)關(guān)系:
- 某組織:當(dāng)前程序系統(tǒng)
- 成員名額:內(nèi)存空間
- 成員:當(dāng)前程序的各個(gè)對(duì)象
- 支持某成員的成員:引用(持有)某成員
- 票數(shù):引用計(jì)數(shù)
- 票數(shù)0->1:對(duì)象創(chuàng)建出現(xiàn)在程序中
- 票數(shù)1->0:對(duì)象無用應(yīng)該得到釋放
MRC
首先來看MRC,從上面也能得知內(nèi)存管理的本質(zhì)就是引用計(jì)數(shù)的改變。在MRC中有以下接口可以引起引用計(jì)數(shù)的改變。
| 相關(guān)操作 | 相關(guān)API | ReferenceCount變化 |
|---|---|---|
| 生成 | alloc/new | +1 |
| 持有 | retain | +1 |
| 拷貝 | copy/mutableCopy | +1 |
| 釋放 | release | -1 |
| 銷毀 | dealloc | == 0時(shí)執(zhí)行 |
MRC呢就是需要我們?cè)谶m當(dāng)?shù)臅r(shí)機(jī)去持有或釋放對(duì)象(執(zhí)行retain和release語句),典型的代表就是setter和getter方法的寫法。
//setter
-(void)setObject:(id) object {
if (_object != object) {
//前提是strong等表示強(qiáng)持有的對(duì)象類型
//先釋放舊值
[_object release];
//再持有新值
//非字符串對(duì)象
[object retain];
//字符串對(duì)象
//[object copy]; //或者 [object mutableCopy];
_object = object;
}
}
//getter
- (void)getObject {
//發(fā)生外部對(duì)象持有h該屬性對(duì)象
//非字符串對(duì)象
[_object retain];
//字符串對(duì)象
//[obj copy]; //或者 [obj mutableCopy];
return _object;
}
ARC
ARC本質(zhì)上還是MRC,只不過之前"Manual"是程序猿自己在做,現(xiàn)在這項(xiàng)工作交給了編譯器。編譯器會(huì)根據(jù)語義分析,在適當(dāng)?shù)臅r(shí)機(jī)為程序加上retain和release等語句。
還是拿setter和getter方法作為??:
//setter
-(void)setObject:(id) object {
if (_object != object) {
_object = object;
}
}
//getter
- (void)getObject {
//發(fā)生外部對(duì)象持有h該屬性對(duì)象
//非字符串對(duì)象
[_object retain];
//字符串對(duì)象
//[obj copy]; //或者 [obj mutableCopy];
return _object;
}
從我從事iOS開發(fā)以來,iOS就已經(jīng)全面過渡到了ARC時(shí)期。大部分OC對(duì)象的生命周期管理都不需要程序猿來做了,這對(duì)程序猿來說無疑是一件喜事。
屬性修飾符
我們?cè)诙x屬性時(shí)往往會(huì)有一個(gè)關(guān)鍵字來聲明該屬性與所屬對(duì)象的持有關(guān)系,而這種持有關(guān)系的保持又是靠所有權(quán)修飾符決定的。每一個(gè)屬性修飾福都有其對(duì)應(yīng)的所有權(quán)修飾符。
OC中主要涉及的修飾符及其對(duì)應(yīng)關(guān)系和作用如下:
| 屬性修飾符 | 對(duì)應(yīng)所有權(quán)修飾符 | ReferenceCount變化 |
|---|---|---|
| strong | __strong | +1 |
| copy | __strong | +1 |
| retain | __strong | +1 |
| __unsafe_unretained | __unsafe_unretained | +0 |
| assign | __unsafe_unretained | +0 |
| weak | __weak | +0 |
__strong
__strong表示強(qiáng)引用。當(dāng)對(duì)__strong修飾的屬性進(jìn)行賦值某個(gè)對(duì)象時(shí),該對(duì)象引用計(jì)數(shù)會(huì)+1。對(duì)應(yīng)的屬性修飾符主要有三個(gè):
strong 對(duì)象類型默認(rèn)的屬性修飾符,如果沒有對(duì)應(yīng)關(guān)鍵字聲明,說明默認(rèn)使用他。
retain與strong關(guān)鍵字基本相同。唯一的區(qū)別是在MRC下對(duì)block的修飾,strong等同于copy(會(huì)形成__NSMallocBlock),retain則相當(dāng)于assign(會(huì)形成__NSStackBlock)。
copy與strong類似,主要用于子類擁有可變類型的不可變對(duì)象的修飾上,保護(hù)其類型安全性。其他還有類如block等特殊類型的修飾。
首先,我們來看retain和strong的區(qū)別。我們都知道一般都會(huì)使用copy修飾block(原理以及block分類暫且略過后面會(huì)有專門章節(jié)來分析),現(xiàn)在假設(shè)有這樣一段代碼:
@property(nonatomic, strong)void(^strongBlock)();
@property(nonatomic, retain)void(^retainBlock)();
//...
NSInteger s = 520;
_strongBlock = ^(void) {
NSLog(@"i am strong block.I catch a int:%ld", s);
};
NSLog(@"_strongBlock Type:%@", [_strongBlock class]);
_retainBlock = ^(void) {
NSLog(@"i am retain block.I catch a int:%ld", s);
};
NSLog(@"_retainBlock Type:%@", [_retainBlock class]);
首先,我們?cè)贏RC環(huán)境下運(yùn)行。發(fā)現(xiàn)block類型相同,說明ARC下strong和retain修飾block是等效的,等同于copy。

接著,我們?cè)僭贛RC下(不會(huì)切換MRC的請(qǐng)自行百度)運(yùn)行。發(fā)現(xiàn)retain修飾的block是棧類型的block,說明沒有進(jìn)行堆區(qū)拷貝(暫且知道后續(xù)會(huì)講原理)。

接下來,我們?cè)倏纯碿opy的特殊之處。我們平時(shí)大部分時(shí)候使用它修飾NSString類型,但是我們有木有想過為什么不用strong呢?他們同樣是屬于__strong陣營。Talk is cheap, show you the code!
@property(nonatomic, strong)NSString *strongStr;
@property(nonatomic, copy)NSString *copyingStr;
NSMutableString *mStr = [[NSMutableString alloc] initWithString:@"hello!!!"];
self.strongStr = mStr;
self.copyingStr = mStr;
[mStr appendString:@"HAHAHA"];
NSLog(@"_strongStr :%@", self.strongStr);
NSLog(@"_copyingStr :%@", self.copyingStr);
看下輸出結(jié)果,我們發(fā)現(xiàn)copy修飾的字符串并沒有隨之改變。而strong修飾的字符串竟然意外的隨mStr的改變而改變了,我們?cè)陂_發(fā)中并不希望我們的變量這樣突然秘密改變。

其實(shí),我們?cè)诖蛴∫幌滤麄兊牡刂肪蜁?huì)有新的發(fā)現(xiàn)。copy修飾的字符串與strong修飾的相比,和mStr的地址并不相同。這個(gè)有涉及到copy修飾符內(nèi)部會(huì)代用copy方法作深淺拷貝(暫作了解后續(xù)會(huì)深入分析)。

綜上,為了保護(hù)NSString的數(shù)據(jù)安全性,我們需要使用copy修飾符而不是strong!這里可以進(jìn)行舉一反三的思考,我們不難發(fā)現(xiàn)NSMutableString是
NSString的子類,而且這種不安全發(fā)生在前者賦值給后者的時(shí)候。這就很容易聯(lián)想到NSArray,NSDictionary兩個(gè)類,其實(shí)他們也是需要用copy修飾的。關(guān)于他們的例子,讀者可以自己寫個(gè)Demo驗(yàn)證一下。
__unsafe_unretained
__unsafe_unretained表示弱引用,在屬性賦值的過程中并不影響賦值對(duì)象的引用計(jì)數(shù)。
- __unsafe_unretained 修飾對(duì)象
- assign 修飾諸如int,NSIngter,double等基本數(shù)據(jù)類型
__weak
__weak也表示弱引用,他是iOS5.0開始引入的一個(gè)所有權(quán)描述符。那么,既然有了__unsafe_unretained為什么還需要__weak呢?其實(shí)他是為了解決__unsafe_unretained有時(shí)會(huì)引起的crash問題的。__unsafe_unretained修飾的對(duì)象被釋放后指針并不置nil,這就使得再次訪問他就會(huì)發(fā)生crash。哈哈,是不很快猜到了__weak的特點(diǎn),沒錯(cuò)就是這一點(diǎn):
- 修飾的對(duì)象被銷毀后,其指針會(huì)置nil
那么,同樣有了weak是不是就不需要__unsafe_unretained了呢?答案顯然是否定的。原因是除了對(duì)象類型還有基礎(chǔ)數(shù)據(jù)類型呢,assign屬于__unsafe_unretained陣營。
上面說到__weak比較消耗性能,那么他底層原理是怎么樣的呢?有興趣的可以參看我之前寫過的一篇筆記: 《直搗黃龍一探iOS weak實(shí)現(xiàn)原理》。了解其原理就會(huì)發(fā)現(xiàn),__weak還是比較消耗性能的,不過這一點(diǎn)與可能引起的crash相比不足為道。
__bridge
我們知道雖然有了ARC,但是他只對(duì)Cocoa框架的對(duì)象管用。我們往往在開發(fā)中還會(huì)用到CoreFoundation框架,這可是面向C的接口。因此這里還是需要我們用類似MRC那一套進(jìn)行手動(dòng)內(nèi)存管理。
- CFReatin 同Cocoa中的retain
- CFRelease 同Cocoa中的release
有的時(shí)候Cocoa里字符串類型為NSString,CoreFoundation為string類型。因此,在兩者協(xié)同開發(fā)時(shí)某個(gè)類型從一個(gè)框架進(jìn)入另一個(gè)框架需要做內(nèi)存所有權(quán)的交接。
- __bridge 只做類型轉(zhuǎn)換,并不包括所有權(quán)的交接
- __bridge_retained 類型轉(zhuǎn)換+所有權(quán)交接,Cocoa --> CoreFoundation
- __bridge_transfer 類型轉(zhuǎn)換+所有權(quán)交接, CoreFoundation --> Cocoa
直接上代碼。
//cocoa 字符串
NSString *cocoaString = @"I am cocoa string.";
//轉(zhuǎn)換成CF類型需要做內(nèi)存所有權(quán)的轉(zhuǎn)移。這樣即使cocoaStr釋放,cfStr也不會(huì)受到影響
CFStringRef cfStr = (__bridge_retained CFStringRef)cocoaString;
//或者使用CFBridgingRetain函數(shù)
CFStringRef cfStr1 = CFBridgingRetain(cocoaString);
//使用cfStr
//...
//CF下使用完cfStr需要手動(dòng)釋放
CFRelease(cfStr);
//CF字符串
CFStringRef cfString = CFSTR("I am a CF string.");
//轉(zhuǎn)換成cocoa字符串要作所有權(quán)轉(zhuǎn)移。實(shí)際是對(duì)CF下的cfString做了一個(gè)release操作
NSString *cocoaStr = (__bridge_transfer NSString *)cfString;
//或者使用CFBridgingRelease函數(shù)
CFStringRef cocoaStr1 = CFBridgingRelease(cfString);//CF下使用必有一個(gè)retain與之對(duì)應(yīng)
思考題
ARC給我們帶來了很大的遍歷。但這樣就一定不會(huì)出現(xiàn)內(nèi)存相關(guān)的問題嗎?
--------------------------------------------------------------2019夜

不是所有的總結(jié)與記錄都有收獲
但毫無疑問
我們會(huì)在總結(jié)中慢慢成長
那些不經(jīng)意間的知識(shí)點(diǎn)將逐漸扎根
揮之不去
你的理解
亦會(huì)愈來愈深刻
這才是記錄的意義
-----------------------------非著名八線互聯(lián)網(wǎng)九流程序猿 chaors