iOS 內(nèi)存管理

總結(jié)自 美團(tuán)點(diǎn)評(píng)技術(shù)沙龍 Online 第4期

內(nèi)存管理的幾種方式

  • 顯式內(nèi)存釋放(C:-free盏檐、C++:-delete)
    • 內(nèi)存可能被提前釋放,(懸停指針 dangling pointer)
    • 內(nèi)存永遠(yuǎn)無(wú)法釋放(內(nèi)存泄露)
  • 基于引用計(jì)數(shù)(smart pointer、Objective-C)
  • 垃圾回收(Java、C#)

MRC(Mannul Reference Counting)

也叫 MRR(Mannul Retain-Release)

  • 嚴(yán)格遵守引用計(jì)數(shù)規(guī)則,保持 retain 和 release 之間的平衡
  • 對(duì)象創(chuàng)建后音同,retain count 為1
  • retain count 為正词爬,對(duì)象繼續(xù)存或
  • retain count 降為0秃嗜,對(duì)象被銷毀
規(guī)則:
  • 自己生成的對(duì)象,自己持有(alloc顿膨,allocWithZone:锅锨,copy,copyWithZone:恋沃,mutableCopy必搞,mutableCopyWithZone)
  • 非自己生成的對(duì)象,自己也可以持有(retain)
  • 不需要自己持有的對(duì)象囊咏,必須釋放它(release恕洲,autorelease)
  • 非自己持有的對(duì)象無(wú)法釋放
MRC 循環(huán)引用(retain cycle)

對(duì)象間直接或間接相互持有引用,自己持有自己也算梅割。
解決方法:

  • assign 修飾屬性霜第,需要的地方手動(dòng)置為 nil
  • block 中的循環(huán)引用,使用 __block 修飾對(duì)象
MRC 釋放時(shí)機(jī)的問(wèn)題

有時(shí)在創(chuàng)建一個(gè)對(duì)象之后户辞,無(wú)法確定在什么時(shí)候釋放泌类,比如一個(gè)方法要返回一個(gè)對(duì)象,那么在方法內(nèi)產(chǎn)生的 retain count 要在什么時(shí)候 release 是未知的底燎。

- (NSObject *)object
{
  NSObject *o = [[NSObject alloc] init]; // 生成對(duì)象刃榨,retain count +1
  //方法執(zhí)行完畢后要減去上面的 retain count,但是又不能在這里釋放
  //[o release]; 
  return o;
}

針對(duì)這個(gè)問(wèn)題双仍,OC 引入了 Autorelease枢希。

Autorelease

延遲釋放對(duì)象。
方法:

  • 手動(dòng)調(diào)用 autorelease 方法
  • 工廠方法返回值是一般都是 autorelease 的
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString *string;
char* cString = "Hello";
string = [NSString stringWithUTF8String:cString];
NSObject *obj = [[[NSObject alloc] init] autorelease];
[pool release];

autorelease 對(duì)象不會(huì)立即釋放朱沃,會(huì)被注冊(cè)到 autoreleasepool 中苞轿,在 pool 釋放時(shí),pool 中的對(duì)象會(huì)自動(dòng)調(diào)用 release。

在 iOS 應(yīng)用程序的 main 函數(shù)中呕屎,由系統(tǒng)創(chuàng)建了一個(gè) autoreleasepool让簿,在主線程的 Runloop 結(jié)束時(shí)會(huì)自動(dòng)釋放 autoreleasepool。

好處:減少內(nèi)存峰值

for (int i = 0; i < 100; i++) {
      NSError *error;
      NSString *fileContents = [NSString stringWithContentsOfURL:urlArray[i] encoding:NSUTF8StringEncoding error:&error];
}

由于工廠方法生成的對(duì)象一般都是 autorelease 的秀睛,所以這里創(chuàng)建的100個(gè)都不會(huì)立即被釋放尔当,會(huì)等到 autoreleasepool 釋放時(shí)才會(huì)釋放,這樣的話蹂安,會(huì)真用很多內(nèi)存椭迎,造成內(nèi)存峰值飆升。
解決方法是將循環(huán)體的代碼放到 autoreleasepool 中田盈。

for (int i = 0; i < 100; i++) {
      NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
      NSError *error;
      NSString *fileContents = [NSString stringWithContentsOfURL:urlArray[i] encoding:NSUTF8StringEncoding error:&error];
      [pool release];
}

這樣畜号,每次有工廠方法生成的 autorelease 對(duì)象都會(huì)被注冊(cè)到我們自己創(chuàng)建的 autreleasepool 中,在每次循環(huán)結(jié)束時(shí)釋放掉 autoreleasepool允瞧,同時(shí)也會(huì)是放掉 pool 中的 autorelease 對(duì)象简软。

相關(guān)點(diǎn):

  • Run loop 會(huì)在每次 loop 到尾部時(shí)銷毀 Autorelease pool。
  • GCD 的 dispatched blocks 會(huì)在一個(gè) Autorelease Pool 的上下文中執(zhí)行述暂,這個(gè) Autorelease Pool 不時(shí)的就被銷毀了(依賴于實(shí)現(xiàn)細(xì)節(jié))痹升。NSOperationQueue 也類似。
  • 其他線程則會(huì)各自對(duì)自己的 Autorelease Pool 的生命周期負(fù)責(zé)畦韭。

MRC 缺點(diǎn)

  • 忘記釋放疼蛾,內(nèi)存泄露
  • 提前釋放,懸停指針

為了解決這些問(wèn)題艺配,Apple 在 iOS4.3 之后引入 ARC察郁。iOS5.0 之后引入 weak


ARC (Automatic Reference Counting)

ARC 是 OBjective-C 編譯器的特性转唉,而不是運(yùn)行時(shí)特性或者垃圾回收機(jī)制皮钠。ARC 所做的只不過(guò)是在代碼編譯時(shí),由編譯子幫你自動(dòng)在合適的位置插入 retain/release 或 autorelease酝掩,所以本質(zhì)和 MRC 是一樣的鳞芙。

ARC 中可以 retain 的對(duì)象指針
  • block 指針
  • Objective-C 的對(duì)象指針
  • __attribute__((NSObject)) 標(biāo)記的對(duì)象
編譯器會(huì)在調(diào)用如下方法(方法族)時(shí)插入 retain

alloc、new期虾、copy原朝、mutableCopy、init

ARC 環(huán)境下的關(guān)鍵字
  • __strong
    持有對(duì)象
  • __weak
    不持有對(duì)象镶苞,對(duì)象沒(méi)有強(qiáng)引用時(shí)會(huì)置為 nil
  • __unsafe_unretained
    不持有對(duì)象喳坠,對(duì)象沒(méi)有強(qiáng)引用時(shí)不會(huì)置為 nil
    可以在 C 結(jié)構(gòu)體里使用 OC 對(duì)象
struct x {
        // 這里如果使用__strong 或 __weak,編譯器會(huì)報(bào)錯(cuò)
        NSString __unsafe_unretained *s;
        int x;
}
  • __autoreleaseing
    表明傳引用的參數(shù)(id *)在返回時(shí)是 autorelease 的
    效果等同 MRC 下的 autorelease 方法
for (int i = 0; i < 100; i++) {
        @autoreleasepool {
            id __autoreleaseing obj = [[NSObject alloc] init];
            NSError *error;
            // 實(shí)際上在 ARC 環(huán)境下茂蚓,工廠方法已經(jīng)不在返回 autorelease 對(duì)象了壕鹉。
            NSString *fileContents = [NSString stringWithContentsOfURL:urlArray[i] encoding:NSUTF8StringEncoding error:&error];
        }
}
ARC 循環(huán)引用
  • property: weak
  • block:__weak剃幌,@weakify & @strongify

Core Foundation 的內(nèi)存管理

相關(guān)方法

CFRetain、CFRelease晾浴、CFAutorelease负乡、CCGetRetainCount

規(guī)則
  • The Create - Rule
    含有"Create"、"Copy"的方法創(chuàng)建的對(duì)象脊凰,需要手動(dòng)釋放抖棘。
CFTimeZoneRef CFTimeZoneCreateWithTimeIntervalFromGMT(CFAllocator allocator, CFTimeInterval ti);
CFDictionaryRef CFTimeZoneCopyAbbreviationDictionary(void);
CFBundleRef CFBundleCreate(CFAllocator allocator, CFURLRef bundleURL);
  • The Get - Rule
    不含有則不需要手動(dòng)釋放
CFStringRef CFAttributedStringGetString(CFAttributedStringRef aStr);
  • Toll - Free Bridging
    • __bridge:只是聲明類型轉(zhuǎn)變,內(nèi)存管理所有權(quán)不變狸涌;
    • __bridge_retained:指針類型轉(zhuǎn)變切省,同時(shí)內(nèi)存管理所有權(quán)由 Objective-C 交由 Core Foundation 處理,即由 ARC 變?yōu)?MRC帕胆;
    • __bridge_transfer:指針類型轉(zhuǎn)變朝捆,同事內(nèi)存管理所有權(quán)由 Core Foundation 交由 Objective-C 處理,即 MRC 變?yōu)?ARC懒豹;
// NSString 轉(zhuǎn)為 CFString 芙盘,內(nèi)存管理所有權(quán)不變,依然是 ARC
CFStringRef s = (__bridge CFStringRef)[[NSString alloc] initWithFormat:@"Hello, %@!", name];
NSString *s1 = [[NSString alloc] initWithFormat:@"Hello, %@!", name];
// NSString 轉(zhuǎn)為 CFString歼捐,且內(nèi)存管理所有權(quán)改變何陆, ARC 變?yōu)?MRC
CFStringRef s2 = (__bridge_retained CFStringRef)s1;
// or CFStringRef s2 = (CFStringRef)CFBridgingRetain(s1);
...
// 手動(dòng)釋放
CFRelease(s2);
CFStringRef result = CFURLCreateStringByAddingPercentEscapes(...);
// CFString 轉(zhuǎn) NSString,內(nèi)存管理所有權(quán)改變豹储, MRC 變 ARC
NSStrig *s = (__bridge_transfer)result;
// or NSStrig *s = (NSString *)CFBridgingRelease(result);
// 雖然創(chuàng)建方法含有"Create",但是無(wú)需手動(dòng)釋放
return s;

思考

1淘这、為什么 weak 變量會(huì)自動(dòng)置為 nil 剥扣?

OC 的運(yùn)行時(shí)在運(yùn)行期會(huì)維護(hù)一個(gè) weak 表(hash 表),會(huì)以對(duì)象(被 weak 指針指向的對(duì)象)的地址 作為key铝穷,以 weak 對(duì)象地址作為value钠怯,一個(gè)key可以注冊(cè)多個(gè)value。
如:__weak typeof(self) self_weak = self;
以 self 的地址作為 key曙聂,self_weak 的地址作為 value晦炊;

一個(gè) weak 對(duì)象引用計(jì)數(shù)變?yōu)?后的執(zhí)行過(guò)程:

  • objc_release 此時(shí)引用計(jì)數(shù)變?yōu)?,執(zhí)行 dealloc
  • _objc_rootDealloc
  • object_dispose
  • objc_destructInstance
  • objc_clear_deallocating
    最后一個(gè)方法宁脊,會(huì)從 weak 表里獲取釋放對(duì)象的地址作為鍵值的所有記錄断国,將記錄里面 value 對(duì)應(yīng)的變量地址,賦值為 nil榆苞,然后刪除這條記錄稳衬。
2、AutoreleasePool 是怎么將 Autorelease 對(duì)象注冊(cè)到 pool 中和從 pool 中釋放坐漏?

注冊(cè)時(shí)會(huì)調(diào)用 push 方法薄疚,釋放時(shí)則會(huì)調(diào)用 pop 方法

void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}

AutoreleasePool 類內(nèi)部實(shí)現(xiàn)是一個(gè)雙向鏈表碧信。

class AutoreleasePoolPage {
  magic_t const magic;
  id *next;
  pthread_t const thread;
  AutoreleasePoolPage *const parent; 
  AutoreleasePoolPage *child;
  uint32_t const depth;
  uint32_t const hiwat;
}

AutoreleasePool 有一個(gè)哨兵對(duì)象(POOL_SENTINEL)用來(lái)標(biāo)記嵌套關(guān)系,當(dāng)其中的一個(gè)對(duì)象釋放后遇到哨兵對(duì)象街夭,就意味著當(dāng)前的 AutoreleasePool 已經(jīng)釋放完畢了砰碴,接下來(lái)繼續(xù)釋放的對(duì)象就是另外一個(gè) AutoreleasePool 中的對(duì)象了。

3板丽、為什么 nonatomic 屬性會(huì)導(dǎo)致 crash 衣式?

在給一個(gè)屬性賦值時(shí),運(yùn)行時(shí)執(zhí)行的代碼類似如下代碼:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic)
{
    id oldValue;
    id *slot = (id *) ((char *)self + offset);
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t & slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;
        slotlock.unlock();
    }
    objc_release(oldValue);
}

在最后會(huì)釋放舊值檐什,這時(shí)候就會(huì)存在問(wèn)題碴卧。在多線程情況下,nonatomic 屬性不是線程安全的乃正,所以會(huì)存在兩個(gè)線程同時(shí)修改屬性的情況住册,假設(shè)有兩個(gè)線程同時(shí)修改屬性,這兩個(gè)線程中的 oldValue 是指向同一塊內(nèi)存的瓮具,在線程1第一次釋放后荧飞,這塊內(nèi)存會(huì)被重新分配,此時(shí)線程2再次釋放名党,就會(huì)將新分配的變量釋放掉叹阔,之后若在別的地方用到了這個(gè)變量,就可能會(huì)發(fā)生 crash传睹。

4耳幢、Objective-C 里的數(shù)組,放進(jìn)去的元素都會(huì)被持有嗎欧啤?如果被數(shù)組持有睛藻,怎樣做一個(gè)不吃又元素的數(shù)組?
5邢隧、NSDictionary 的 key 和 value 的內(nèi)存語(yǔ)義是怎樣的店印?
6、在 ARC 環(huán)境下倒慧,函數(shù)的返回值已經(jīng)不再使用 Autorelease 機(jī)制了按摘,它是怎么實(shí)現(xiàn)的?但是如果調(diào)用方是 MRC 環(huán)境纫谅,ARC 下面的函數(shù)返回值會(huì)自動(dòng)啟動(dòng) Autorelease炫贤,這又是怎么實(shí)現(xiàn)的?
7系宜、Toll-Free Bridging 是怎么實(shí)現(xiàn)的照激?什么樣的 Core Foundation 和 Foundation 對(duì)象才能做到 Toll-Free Bridging?
8盹牧、如果一個(gè)對(duì)象持有一個(gè) block俩垃,在 block 里面使用到了對(duì)象的一個(gè)實(shí)例變量励幼,會(huì)形成循環(huán)引用嗎?如果以一個(gè)臨時(shí)變量引用實(shí)例變量口柳,在 block 中使用這個(gè)臨時(shí)變量又會(huì)怎樣苹粟?
9、對(duì)象的引用計(jì)數(shù)存在對(duì)象的內(nèi)存布局里還是全局的引用計(jì)數(shù)變里跃闹?為什么嵌削??jī)煞N方式各有什么優(yōu)缺點(diǎn)?

Q&A

  1. 靜態(tài)分析工具:Xcode望艺、sonar

相關(guān)參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末苛秕,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子找默,更是在濱河造成了極大的恐慌艇劫,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惩激,死亡現(xiàn)場(chǎng)離奇詭異店煞,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)风钻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門顷蟀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人骡技,你說(shuō)我怎么就攤上這事鸣个。” “怎么了哮兰?”我有些...
    開(kāi)封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵毛萌,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我喝滞,道長(zhǎng),這世上最難降的妖魔是什么膏秫? 我笑而不...
    開(kāi)封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任右遭,我火速辦了婚禮,結(jié)果婚禮上缤削,老公的妹妹穿的比我還像新娘窘哈。我一直安慰自己,他們只是感情好亭敢,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布滚婉。 她就那樣靜靜地躺著,像睡著了一般帅刀。 火紅的嫁衣襯著肌膚如雪让腹。 梳的紋絲不亂的頭發(fā)上远剩,一...
    開(kāi)封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音骇窍,去河邊找鬼瓜晤。 笑死,一個(gè)胖子當(dāng)著我的面吹牛腹纳,可吹牛的內(nèi)容都是我干的痢掠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼嘲恍,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼足画!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起佃牛,我...
    開(kāi)封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤淹辞,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后吁脱,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體桑涎,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年兼贡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了攻冷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡遍希,死狀恐怖等曼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情凿蒜,我是刑警寧澤禁谦,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站废封,受9級(jí)特大地震影響州泊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜漂洋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一遥皂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧刽漂,春花似錦演训、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春窟她,著一層夾襖步出監(jiān)牢的瞬間陈症,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工礁苗, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留爬凑,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓试伙,卻偏偏與公主長(zhǎng)得像嘁信,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子疏叨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Copyright ? 2017年ZaneWangWang. All rights reserved. 如果你看到...
    2897275c8a00閱讀 917評(píng)論 0 1
  • 貌似每個(gè)iOS開(kāi)發(fā)者都有一篇屬于自己的內(nèi)存管理潘靖,記錄了自己對(duì)內(nèi)存管理理解的深度以及廣度,所以我也來(lái)記錄一下我的理解...
    Bugfix閱讀 2,263評(píng)論 0 3
  • # 前言 反復(fù)地復(fù)習(xí)iOS基礎(chǔ)知識(shí)和原理蚤蔓,打磨知識(shí)體系是非常重要的卦溢,本篇就是重新溫習(xí)iOS的內(nèi)存管理。 內(nèi)存管理是...
    Vein_閱讀 799評(píng)論 0 2
  • 一秀又、MRC(手動(dòng)引用計(jì)數(shù)): 不像 java 有垃圾回收機(jī)制单寂,Objective-C 繼承于 C ,使用一套基于對(duì)...
    Lee堅(jiān)武閱讀 982評(píng)論 0 51
  • 今天的陽(yáng)光吐辙,格外的嬌嫩 讓我想起了宣决,煎的金燦燦的荷包蛋 撲面而來(lái)的,都是誘人的清香 今天的陽(yáng)光昏苏,也格外的溫暖 洗掉...
    傾花似雪閱讀 246評(píng)論 2 4