ios基礎篇內存管理

前言

最近有時間把ios的基礎知識整理一下主穗,淺談一下對ios內存管理機制的理解,以前也只是會用腮敌,知其然但不知其所以然竖瘾。本文的ARC專指ObjectiveC的ARC鸣剪,不會設計到其他語言的內存管理组底,在此需要有一點的ios的內存管理機制的理解。

什么是ARC?

ARC的全稱是Auto Reference Counting也就是自動引用計數(shù)筐骇。

Objective C的引用計數(shù)理解起來很容易债鸡,當一個對象被持有的時候計數(shù)加一,不再被持有的時候引用計數(shù)減一铛纬,當引用計數(shù)為零的時候厌均,說明這個對象已經無用了,則將其釋放告唆。

引用計數(shù)分為兩種:

  • 手動引用計數(shù)(MRC)
  • 自動引用計數(shù)(ARC)

內存管理的思考方式:

  • 自己生成的對象棺弊,自己持有
  • 非自己生成的對象晶密,自己也能持有
  • 不再需要自己持有對象時釋放
  • 非自己持有的對象無法釋放

(1) 自己生成的對象,自己持有

在iOS內存管理中有四個關鍵字模她,alloc稻艰、new、copy侈净、mutableCopy尊勿,自身使用這些關鍵字產生對象,那么自身就持有了對象

// 使用了alloc分配了內存,obj指向了對象畜侦,該對象本身引用計數(shù)為1,不需要retain
 id obj = [[NSObject alloc] init]; 
// 使用了new分配了內存,objc指向了對象元扔,該對象本身引用計數(shù)為1,不需要retain
 id obj = [NSObject new];

(2) 非自己生成的對象夏伊,自己也能持有

// NSMutableArray通過類方法array產生了對象(并沒有使用alloc摇展、new吻氧、copy溺忧、mutableCopt來產生對象),因此該對象不屬于obj自身產生的
 // 因此,需要使用retain方法讓對象計數(shù)器+1,從而obj可以持有該對象(盡管該對象不是他產生的) 
id obj = [NSMutableArray array];
 [obj retain];

(3) 不再需要自己持有對象時釋放

id obj = [NSMutableArray array];  
[obj retain];
// 當obj不在需要持有的對象盯孙,那么鲁森,obj應該發(fā)送release消息
[obj release];

(4) 無法釋放非自己持有的對象

// 1. 釋放一個已經釋放的對象 
id obj = [[NSObject alloc] init];
 // 已經釋放對象 
[obj release]; 
// 釋放了對象還進行釋放
 [obj release]; // 
//2. 釋放一個不屬于自己的對象 
id obj1 = [obj object]; 
// obj1沒有進行retain操作而進行release操作,使得obj持有對象釋放振惰,造成了野指針錯誤 
[obj1 release];

如上為iOS進行內存管理的四種思考方式(記住不論是ARC還是MRC都遵循該思考方式歌溉,只是ARC時代這些工作讓編譯器做了)

上述是我很好理解ios內存管理的的方法,在此我們做一下小的對比
在iOS開發(fā)早期骑晶,編寫代碼是采用MRC的

// MRC代碼
NSObject * obj = [[NSObject alloc] init]; //引用計數(shù)為1
//不需要的時候
[obj release] //引用計數(shù)減1
//持有這個對象
[obj retain] //引用計數(shù)加1
//放到AutoReleasePool
[obj autorelease]//在auto release pool釋放的時候痛垛,引用計數(shù)減1

雖說這種方式提供了面向對象的內存管理接口,但是開發(fā)者不得不花大量的時間在內存管理上桶蛔,并且容易出現(xiàn)內存泄漏或者release一個已被提前釋放的對象匙头,導致crash。

再后來仔雷,Apple對ios/Mac OS開發(fā)引入了ARC蹂析。使用ARC,開發(fā)者不再需要手動的retain/release/autorelease. 編譯器會自動插入對應的代碼碟婆,再結合Objective C的runtime电抚,實現(xiàn)自動引用計數(shù)。
比如如下ARC代碼:

NSObject * obj;
{
    obj = [[NSObject alloc] init]; //引用計數(shù)為1
}
NSLog(@"%@",obj);

等同于如下MRC代碼

NSObject * obj;
{
    obj = [[NSObject alloc] init]; //引用計數(shù)為1
    [obj relrease]
}
NSLog(@"%@",obj);

在Objective C中竖共,有三種類型是ARC適用的:

  • block
  • objective 對象蝙叛,id, Class, NSError*等
  • 由attribute((NSObject))標記的類型。
  • 像double *,CFStringRef等不是ARC適用的公给,仍然需要手動管理內存借帘。
  • 以CF開頭的(Core Foundation)的對象往往需要手動管理內存锻煌。

屬性所有權

我們在看看ARC中常見的所有權關鍵字,
  • assign對應關鍵字__unsafe_unretained, 顧名思義姻蚓,就是指向的對象被釋放的時候宋梧,仍然指向之前的地址,容易引起野指針狰挡。
  • copy對應關鍵字__strong,只不過在賦值的時候捂龄,調用copy方法。
  • retain對應__strong
  • strong對應__strong
  • unsafe_unretained對應__unsafe_unretained
  • weak對應__weak加叁。
    其中倦沧,__weak和__strong是本文要講解的核心內容。

ARC的內部實現(xiàn)

ARC背后的引用計數(shù)主要依賴于這三個方法:

  • retain 增加引用計數(shù)
  • release 降低引用計數(shù)它匕,引用計數(shù)為0的時候展融,釋放對象。
  • autorelease 在當前的auto release pool結束后豫柬,降低引用計數(shù)告希。

在Cocoa Touch中,NSObject協(xié)議中定義了這三個方法烧给,由于Cocoa Touch中燕偶,絕大部分類都繼承自NSObject(NSObject類本身實現(xiàn)了NSObject協(xié)議),所以可以“免費”獲得NSObject提供的運行時和ARC管理方法础嫡,這就是為什么適用OC開發(fā)iOS的時候指么,你的類要繼承自NSObject。

既然ARC是引用計數(shù)榴鼎,那么對應一個對象伯诬,內存中必然會有一個地方來存儲這個對象的引用計數(shù)。iOS的Runtime是開源的巫财,如果你有興趣可以下載全部的代碼盗似,我們通過源代碼一探究竟。
我們從retain入手

- (id)retain {
    return ((id)self)->rootRetain();
}
inline id objc_object::rootRetain()
{
    if (isTaggedPointer()) return (id)this;
    return sidetable_retain();
}

所以說翁涤,本質上retain就是調用sidetable_retain桥言,再看看sitetable_retain的實現(xiàn):

id objc_object::sidetable_retain()
{
    //獲取table
    SideTable& table = SideTables()[this];
    //加鎖
    table.lock();
    //獲取引用計數(shù)
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
         //增加引用計數(shù)
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    //解鎖
    table.unlock();
    return (id)this;
}

到這里,retain如何實現(xiàn)就很清楚了葵礼,通過SideTable這個數(shù)據(jù)結構來存儲引用計數(shù)号阿。我們看看這個數(shù)據(jù)結構的實現(xiàn):

typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
     //省略其他實現(xiàn)...
};

可以看到,這個數(shù)據(jù)結構就是存儲了一個自旋鎖鸳粉,一個引用計數(shù)map扔涧。這個引用計數(shù)的map以對象的地址作為key,引用計數(shù)作為value。到這里枯夜,引用計數(shù)的底層實現(xiàn)我們就很清楚了.

存在全局的map弯汰,這個map以地址作為key,引用計數(shù)的值作為value

再來看看release的實現(xiàn):

 SideTable& table = SideTables()[this];
    bool do_dealloc = false;
    table.lock();
    //找到對應地址的
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) { //找不到的話湖雹,執(zhí)行dellloc
        do_dealloc = true;
        table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
    } else if (it->second < SIDE_TABLE_DEALLOCATING) {//引用計數(shù)小于閾值咏闪,dealloc
        do_dealloc = true;
        it->second |= SIDE_TABLE_DEALLOCATING;
    } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
    //引用計數(shù)減去1
        it->second -= SIDE_TABLE_RC_ONE;
    }
    table.unlock();
    if (do_dealloc  &&  performDealloc) {
        //執(zhí)行dealloc
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return do_dealloc;

release的到這里也比較清楚了:查找map,對引用計數(shù)減1摔吏,如果引用計數(shù)小于閾值鸽嫂,則調用SEL_dealloc

Autorelease pool

上文提到了,autorelease方法的作用是把對象放到autorelease pool中征讲,到pool drain的時候据某,會釋放池中的對象。舉個例子

 __weak NSObject * obj;
    NSObject * temp = [[NSObject alloc] init];
    obj = temp;
    NSLog(@"%@",obj); //非空

放到auto release pool中诗箍,

 __weak NSObject * obj;
    @autoreleasepool {
        NSObject * temp = [[NSObject alloc] init];
        obj = temp;
    }
    NSLog(@"%@",obj); //null

可以看到癣籽,放到自動釋放池的對象是在超出自動釋放池作用域后立即釋放的。事實上在iOS 程序啟動之后滤祖,主線程會啟動一個Runloop筷狼,這個Runloop在每一次循環(huán)是被自動釋放池包裹的,在合適的時候對池子進行清空氨距。

對于Cocoa框架來說桑逝,提供了兩種方式來把對象顯式的放入AutoReleasePool.

  • NSAutoreleasePool(只能在MRC下使用)
  • @autoreleasepool {}代碼塊(ARC和MRC下均可以使用)

那么AutoRelease pool又是如何實現(xiàn)的呢?

我們先從autorelease方法源碼入手

//autorelease方法
- (id)autorelease {
    return ((id)self)->rootAutorelease();
}

//rootAutorelease 方法
inline id objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;

    //檢查是否可以優(yōu)化
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
    //放到auto release pool中俏让。
    return rootAutorelease2();
}

// rootAutorelease2
id objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}

可以看到,把一個對象放到auto release pool中茬暇,是調用了AutoreleasePoolPage::autorelease這個方法首昔。

我們繼續(xù)查看對應的實現(xiàn):

public: static inline id autorelease(id obj)
    {
        assert(obj);
        assert(!obj->isTaggedPointer());
        id *dest __unused = autoreleaseFast(obj);
        assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
        return obj;
    }

static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {
            return autoreleaseFullPage(obj, page);
        } else {
            return autoreleaseNoPage(obj);
        }
    }
id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }

到這里,autorelease方法的實現(xiàn)就比較清楚了糙俗,

autorelease方法會把對象存儲到AutoreleasePoolPage的鏈表里勒奇。等到auto release pool被釋放的時候,把鏈表內存儲的對象刪除巧骚。所以赊颠,AutoreleasePoolPage就是自動釋放池的內部實現(xiàn)。

__weak與__strong

用過block的同學一定寫過類似的代碼:

__weak typeSelf(self) weakSelf = self;

[object fetchSomeFromRemote:^{
    __strong typeSelf(weakSelf) strongSelf = weakSelf;
    //從這里開始用strongSelf
}];

那么劈彪,為什么要這么用呢竣蹦?原因是:

  • block會捕獲外部變量,用weakSelf保證self不會被block被捕獲沧奴,防止引起循環(huán)引用或者不必要的額外生命周期痘括。
  • 用strongSelf則保證在block的執(zhí)行過程中,對象不會被釋放掉。

首先__strong和__weak都是關鍵字纲菌,是給編譯器理解的挠日。為了理解其原理,我們需要查看它們編譯后的代碼翰舌,使用XCode嚣潜,我們可以容易的獲得一個文件的匯編代碼。

比如椅贱,對于Test.m文件郑原,當源代碼如下時:
 #import "Test.h"

 @implementation Test

- (void)testFunction{
    {
        __strong NSObject * temp = [[NSObject alloc] init];
    }
}

@end

轉換后的匯編代碼如下:

 .loc    2 15 37 prologue_end    ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:37
    ldr     x9, [x9]
    ldr     x1, [x8]
    mov  x0, x9
    bl  _objc_msgSend
    adrp    x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGE
    add x8, x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGEOFF
    .loc    2 15 36 is_stmt 0       ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:36
    ldr     x1, [x8]
    .loc    2 15 36 discriminator 1 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:36
    bl  _objc_msgSend
    mov x8, #0
    add x9, sp, #8              ; =8
    .loc    2 15 29                 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:29
    str x0, [sp, #8]
Ltmp4:
    .loc    2 16 5 is_stmt 1        ; /Users/hl/Desktop/OCTest/OCTest/Test.m:16:5
    mov  x0, x9
    mov  x1, x8
    bl  _objc_storeStrong
    .loc    2 17 1                  ; /Users/hl/Desktop/OCTest/OCTest/Test.m:17:1
    ldp x29, x30, [sp, #32]     ; 8-byte Folded Reload
    add sp, sp, #48             ; =48
    ret
Ltmp5:

即使我們不懂匯編,也能很輕易的獲取到調用順序如下

_objc_msgSend // alloc
_objc_msgSend // init
_objc_storeStrong // 強引用

在結合Runtime的源碼夜涕,我們看看最關鍵的objc_storeStrong的實現(xiàn)

void objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

id objc_retain(id obj) { return [obj retain]; }
void objc_release(id obj) { [obj release]; }

我們再來看看__weak. 將Test.m修改成為如下代碼犯犁,同樣我們分析其匯編實現(xiàn)

 .loc    2 15 35 prologue_end    ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:35
    ldr     x9, [x9]
    ldr     x1, [x8]
    mov  x0, x9
    bl  _objc_msgSend
    adrp    x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGE
    add x8, x8, L_OBJC_SELECTOR_REFERENCES_.2@PAGEOFF
    .loc    2 15 34 is_stmt 0       ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:34
    ldr     x1, [x8]
    .loc    2 15 34 discriminator 1 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:34
    bl  _objc_msgSend
    add x8, sp, #24             ; =24
    .loc    2 15 27                 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27
    mov  x1, x0
    .loc    2 15 27 discriminator 2 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27
    str x0, [sp, #16]           ; 8-byte Folded Spill
    mov  x0, x8
    bl  _objc_initWeak
    .loc    2 15 27                 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27
    ldr x1, [sp, #16]           ; 8-byte Folded Reload
    .loc    2 15 27 discriminator 3 ; /Users/hl/Desktop/OCTest/OCTest/Test.m:15:27
    str x0, [sp, #8]            ; 8-byte Folded Spill
    mov  x0, x1
    bl  _objc_release
    add x8, sp, #24  
    Ltmp4:
    .loc    2 16 5 is_stmt 1        ; /Users/hl/Desktop/OCTest/OCTest/Test.m:16:5
    mov  x0, x8
    bl  _objc_destroyWeak
    .loc    2 17 1                  ; /Users/hl/Desktop/OCTest/OCTest/Test.m:17:1
    ldp x29, x30, [sp, #48]     ; 8-byte Folded Reload
    add sp, sp, #64             ; =64
    ret

可以看到,__weak本身實現(xiàn)的核心就是以下兩個方法

  • _objc_initWeak
  • _objc_destroyWeak
    我們通過Runtime的源碼分析這兩個方法的實現(xiàn):
id objc_initWeak(id *location, id newObj)
{
    //省略....
    return storeWeak<false/*old*/, true/*new*/, true/*crash*/>
        (location, (objc_object*)newObj);
}
void objc_destroyWeak(id *location)
{
    (void)storeWeak<true/*old*/, false/*new*/, false/*crash*/>
        (location, nil);
}

所以女器,本質上都是調用了storeWeak函數(shù)酸役,這個函數(shù)內容較多,主要做了以下事情

  • 獲取存儲weak對象的map驾胆,這個map的key是對象的地址涣澡,value是weak引用的地址。
  • 當對象被釋放的時候丧诺,根據(jù)對象的地址可以找到對應的weak引用的地址入桂,將其置為nil即可。

這就是在weak背后的神秘的手驳阎。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末抗愁,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子呵晚,更是在濱河造成了極大的恐慌蜘腌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件饵隙,死亡現(xiàn)場離奇詭異撮珠,居然都是意外死亡,警方通過查閱死者的電腦和手機金矛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門芯急,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人驶俊,你說我怎么就攤上這事娶耍。” “怎么了废睦?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵伺绽,是天一觀的道長。 經常有香客問我,道長奈应,這世上最難降的妖魔是什么澜掩? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮杖挣,結果婚禮上肩榕,老公的妹妹穿的比我還像新娘。我一直安慰自己惩妇,他們只是感情好株汉,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著歌殃,像睡著了一般乔妈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上氓皱,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天路召,我揣著相機與錄音,去河邊找鬼波材。 笑死股淡,一個胖子當著我的面吹牛,可吹牛的內容都是我干的廷区。 我是一名探鬼主播唯灵,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼隙轻!你這毒婦竟也來了埠帕?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤大脉,失蹤者是張志新(化名)和其女友劉穎搞监,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體镰矿,經...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年俘种,在試婚紗的時候發(fā)現(xiàn)自己被綠了秤标。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡宙刘,死狀恐怖苍姜,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情悬包,我是刑警寧澤衙猪,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響垫释,放射性物質發(fā)生泄漏丝格。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一棵譬、第九天 我趴在偏房一處隱蔽的房頂上張望显蝌。 院中可真熱鬧,春花似錦订咸、人聲如沸曼尊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽骆撇。三九已至,卻和暖如春父叙,著一層夾襖步出監(jiān)牢的瞬間神郊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工高每, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留屿岂,地道東北人。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓鲸匿,卻偏偏與公主長得像爷怀,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子带欢,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內容

  • 內存管理 簡述OC中內存管理機制运授。與retain配對使用的方法是dealloc還是release,為什么乔煞?需要與a...
    丶逐漸閱讀 1,948評論 1 16
  • 29.理解引用計數(shù) Objective-C語言使用引用計數(shù)來管理內存吁朦,也就是說,每個對象都有個可以遞增或遞減的計數(shù)...
    Code_Ninja閱讀 1,470評論 1 3
  • 1.1 什么是自動引用計數(shù) 概念:在 LLVM 編譯器中設置 ARC(Automaitc Reference Co...
    __silhouette閱讀 5,082評論 1 17
  • 概述 在iOS中開發(fā)中,我們或多或少都聽說過內存管理空骚。iOS的內存管理一般指的是OC對象的內存管理纺讲,因為OC對象分...
    DamonMok閱讀 3,984評論 2 20
  • 春風暖了一季 夏雨遲遲未來 光辜負了白晝 夜尷尬了守候 天變換著顏色 地只管著堅守 水稀釋了憂愁 冰凍結了挽留 天...
    安靜的復蘇918閱讀 181評論 1 2