runtime-閑聊內(nèi)存管理

前言

ARC作為一個(gè)老生常談的話題宰缤,基本被網(wǎng)上的各種博客說盡了琐鲁。但是前段時(shí)間朋友通過某些手段對YYModel進(jìn)行了優(yōu)化菠齿,提高了大概1/3左右的效率,在觀賞過他改進(jìn)的源碼之后我又重新看了一遍ARC相關(guān)的實(shí)現(xiàn)源碼熙宇,主要體現(xiàn)ARC機(jī)制的幾個(gè)方法分別是retain鳖擒、release以及dealloc,主要與strongweak兩者相關(guān)

ARC的內(nèi)存管理

來看看一段ARC環(huán)境下的代碼
- (void)viewDidLoad {
NSArray * titles = @[@"title1", @"title2"];
}
在編譯期間烫止,代碼就會變成這樣:

- (void)viewDidLoad {
    NSArray * titles = @[@"title1", @"title2"];
    [titles retain];
    ///  .......
    [titles release];
}

簡單來說就是ARC在代碼編譯階段蒋荚,會自動在代碼的上下文中成對插入retain以及release,保證引用計(jì)數(shù)能夠正確管理內(nèi)存馆蠕。如果對象不是強(qiáng)引用類型圆裕,那么ARC的處理也會進(jìn)行相應(yīng)的改變


下面會分別說明在這幾個(gè)與引用計(jì)數(shù)相關(guān)的方法調(diào)用中發(fā)生了什么

retain

強(qiáng)引用有retainstrong以及__strong三種修飾荆几,默認(rèn)情況下吓妆,所有的類對象會自動被標(biāo)識為__strong強(qiáng)引用對象,強(qiáng)引用對象會在上下文插入retain以及release調(diào)用吨铸,從runtime源碼處可以下載到對應(yīng)調(diào)用的源代碼行拢。在retain調(diào)用的過程中,總共涉及到了四次調(diào)用:

  • id _objc_rootRetain(id obj)
    對傳入對象進(jìn)行非空斷言诞吱,然后調(diào)用對象的rootRetain()方法
  • id objc_object::rootRetain()
    斷言非GC環(huán)境舟奠,如果對象是TaggedPointer指針竭缝,不做處理。TaggedPointer是蘋果推出的一套優(yōu)化方案沼瘫,具體可以參考深入了解Tagged Pointer一文
  • id objc_object::sidetable_retain()
    增加引用計(jì)數(shù)抬纸,具體往下看
  • id objc_object::sidetable_retain_slow(SideTable& table)
    增加引用計(jì)數(shù),具體往下看

在上面的幾步中最重要的步驟就是最后兩部的增加引用計(jì)數(shù)耿戚,在NSObject.mm中可以看到函數(shù)的實(shí)現(xiàn)湿故。這里筆者剔除了部分不相關(guān)的代碼:

#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING      (1UL<<1)
#define SIDE_TABLE_RC_ONE            (1UL<<2)
#define SIDE_TABLE_RC_PINNED         (1UL<<(WORD_BITS-1))

typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
}

id objc_object::sidetable_retain()
{
    // 獲取對象的table對象
    SideTable& table = SideTables()[this];

    if (table.trylock()) {

        // 獲取 引用計(jì)數(shù)的引用
        size_t& refcntStorage = table.refcnts[this];
        if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
            // 如果引用計(jì)數(shù)未越界,則引用計(jì)數(shù)增加
            refcntStorage += SIDE_TABLE_RC_ONE;
        }
        table.unlock();
        return (id)this;
    }
    return sidetable_retain_slow(table);
}
  • SideTable這個(gè)類包含著一個(gè)自旋鎖slock來防止操作時(shí)可能出現(xiàn)的多線程讀取問題膜蛔、一個(gè)弱引用表weak_table以及引用計(jì)數(shù)表refcnts坛猪。另外還提供一個(gè)方法傳入對象地址來尋找對應(yīng)的SideTable對象

  • RefcountMap對象通過散列表的結(jié)構(gòu)存儲了對象持有者的地址以及引用計(jì)數(shù),這樣一來皂股,即便對象對應(yīng)的內(nèi)存出現(xiàn)錯誤墅茉,例如Zombie異常,也能定位到對象的地址信息

  • 每次retain后以后引用計(jì)數(shù)的值實(shí)際上增加了(1 << 2) == 4而不是我們所知的1呜呐,這是由于引用計(jì)數(shù)的后兩位分別被弱引用以及析構(gòu)狀態(tài)兩個(gè)標(biāo)識位占領(lǐng)就斤,而第一位用來表示計(jì)數(shù)是否越界。

由于引用計(jì)數(shù)可能存在越界情況(SIDE_TABLE_RC_PINNED位的值為1)蘑辑,因此散列表refcnts中應(yīng)該存儲了多個(gè)引用計(jì)數(shù)洋机,sidetable_retainCount()函數(shù)也證明了這一點(diǎn):

#define SIDE_TABLE_RC_SHIFT 2
uintptr_t objc_object::sidetable_retainCount()
{
    SideTable& table = SideTables()[this];
    size_t refcnt_result = 1;

    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    table.unlock();
    return refcnt_result;
}

引用計(jì)數(shù)總是返回1 + 計(jì)數(shù)表總計(jì)這個(gè)數(shù)值,這也是為什么經(jīng)常性的當(dāng)對象被釋放后以躯,我們獲取retainCount的值總不能為0槐秧。至于函數(shù)sidetable_retain_slow的實(shí)現(xiàn)和sidetable_retain幾乎一樣啄踊,就不再介紹了

release

release調(diào)用有著跟retain類似的四次調(diào)用忧设,前兩次調(diào)用的作用一樣,因此這里只放上引用計(jì)數(shù)減少的函數(shù)代碼:

uintptr_t objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.indexed);
#endif
    SideTable& table = SideTables()[this];

    bool do_dealloc = false;

    if (table.trylock()) {
        RefcountMap::iterator it = table.refcnts.find(this);
        if (it == table.refcnts.end()) {
            do_dealloc = true;
            table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
        } else if (it->second < SIDE_TABLE_DEALLOCATING) {
            // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
            do_dealloc = true;
            it->second |= SIDE_TABLE_DEALLOCATING;
        } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
            it->second -= SIDE_TABLE_RC_ONE;
        }
        table.unlock();
        if (do_dealloc  &&  performDealloc) {
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
        }
        return do_dealloc;
    }

    return sidetable_release_slow(table, performDealloc);
}

release中決定對象是否會被dealloc有兩個(gè)主要的判斷

  • 如果引用計(jì)數(shù)為計(jì)數(shù)表中的最后一個(gè)颠通,標(biāo)記對象為正在析構(gòu)狀態(tài)址晕,然后執(zhí)行完成后發(fā)送SEL_dealloc消息釋放對象
  • 即便計(jì)數(shù)表的值為零,sidetable_retainCount函數(shù)照樣會返回1的值顿锰。這時(shí)計(jì)數(shù)小于宏定義SIDE_TABLE_DEALLOCATING == 1谨垃,就不進(jìn)行減少計(jì)數(shù)的操作,直接標(biāo)記對象正在析構(gòu)

看到release的代碼就會發(fā)現(xiàn)在上面代碼中宏定義SIDE_TABLE_DEALLOCATING體現(xiàn)出了蘋果這個(gè)心機(jī)婊的用心之深硼控。通常而言刘陶,即便引用計(jì)數(shù)只有8位的占用,在剔除了首位越界標(biāo)記以及后兩位后牢撼,其最大取值為2^5-1 == 31位匙隔。通常來說,如果不是項(xiàng)目中block不加限制的引用熏版,是很難達(dá)到這么多的引用量的纷责。因此占用了SIDE_TABLE_DEALLOCATING位不僅減少了額外占用的標(biāo)記變量內(nèi)存捍掺,還能以作為引用計(jì)數(shù)是否歸零的判斷

weak

最開始的時(shí)候沒打算講weak這個(gè)修飾,不過因?yàn)?code>dealloc方法本身涉及到了弱引用對象置空的操作再膳,以及retain過程中的對象也跟weak有關(guān)系的情況下挺勿,簡單的說說weak的操作

bool objc_object::sidetable_isWeaklyReferenced()
{
    bool result = false;

    SideTable& table = SideTables()[this];
    table.lock();

    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        result = it->second & SIDE_TABLE_WEAKLY_REFERENCED;
    }

    table.unlock();

    return result;
}

weakstrong共用一套引用計(jì)數(shù)設(shè)計(jì),因此兩者的賦值操作都要設(shè)置計(jì)數(shù)表喂柒,只是weak修飾的對象的引用計(jì)數(shù)對象會被設(shè)置SIDE_TABLE_WEAKLY_REFERENCED位不瓶,并且不參與sidetable_retainCount函數(shù)中的計(jì)數(shù)計(jì)算而已

void objc_object::sidetable_setWeaklyReferenced_nolock()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.indexed);
#endif

    SideTable& table = SideTables()[this];

    table.refcnts[this] |= SIDE_TABLE_WEAKLY_REFERENCED;
}

另一個(gè)弱引用設(shè)置方法,相比上一個(gè)方法去掉了自旋鎖加鎖操作

dealloc

dealloc是重量級的方法之一胳喷,不過由于函數(shù)內(nèi)部調(diào)用層次過多湃番,這里不多闡述。實(shí)現(xiàn)代碼在objc-object.h798行吭露,可以自行到官網(wǎng)下載源碼后研讀

__unsafe_unretained

其實(shí)寫了這么多吠撮,終于把本文的主角給講出來了。在iOS5的時(shí)候讲竿,蘋果正式推出了ARC機(jī)制泥兰,伴隨的是上面的weakstrong等新修飾符题禀,當(dāng)然還有一個(gè)不常用的__unsafe_unretained

  • weak
    修飾的對象在指向的內(nèi)存被釋放后會被自動置為nil
  • strong
    持有指向的對象鞋诗,會讓引用計(jì)數(shù)+1
  • __unsafe_unretained
    不引用指向的對象。但在對象內(nèi)存被釋放掉后迈嘹,依舊指向內(nèi)存地址削彬,等同于assign,但是只能修飾對象

在機(jī)器上保證應(yīng)用能保持在55幀以上的速率會讓應(yīng)用看起來如絲綢般順滑秀仲,但是稍有不慎融痛,稍微降到50~55之間都有很大的可能展現(xiàn)出卡頓的現(xiàn)象。這里不談及圖像渲染神僵、數(shù)據(jù)大量處理等耳聞能詳?shù)男阅軔汗硌闼ⅲf說Model所造成的損耗。

如前面所說的保礼,在ARC環(huán)境下沛励,對象的默認(rèn)修飾為strong,這意味著這么一段代碼:

@protocol RegExpCheck

@property (nonatomic, copy) NSString * regExp;

- (BOOL)validRegExp;

@end

- (BOOL)valid: (NSArray<id<RegExpCheck>> *)params {
    for (id<RegExpCheck> item in params) {
        if (![item validRegExp]) { return NO; }
    }
    return YES;
}

把這段代碼改為編譯期間插入retainrelease方法后的代碼如下:

- (BOOL)valid: (NSArray<id<RegExpCheck>> *)params {
    for (id<RegExpCheck> item in params) {
        [item retain];
        if (![item validRegExp]) { 
            [item release];
            return NO;
        }
        [item release];
    }
    return YES;
}

遍歷操作在項(xiàng)目中出現(xiàn)的概率絕對排的上前列炮障,那么上面這個(gè)方法在調(diào)用期間會調(diào)用params.countretainrelease函數(shù)目派。通常來說,每一個(gè)對象的遍歷次數(shù)越多胁赢,這些函數(shù)調(diào)用的損耗就越大企蹭。如果換做__unsafe_unretained修飾對象,那么這部分的調(diào)用損耗就被節(jié)省下來,這也是筆者朋友改進(jìn)的手段

尾話

首先要承認(rèn)练对,相比起其他性能惡鬼改進(jìn)的優(yōu)化遍蟋,使用__unsafe_unretained帶來的收益幾乎微乎其微,因此筆者并不是很推薦用這種高成本低回報(bào)的方式優(yōu)化項(xiàng)目螟凭,起碼在性能惡鬼大頭解決之前不推薦虚青,但是去學(xué)習(xí)內(nèi)存管理底層的知識可以幫助我們站在更高的地方看待開發(fā)。

ps:在朋友的堅(jiān)持下螺男,可恥的取消了代碼鏈接

上一篇:消息機(jī)制
下一篇:分類為什么不生成setter和getter

轉(zhuǎn)載請注明本文作者和地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末棒厘,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子下隧,更是在濱河造成了極大的恐慌奢人,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件淆院,死亡現(xiàn)場離奇詭異何乎,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)土辩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門支救,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人拷淘,你說我怎么就攤上這事各墨。” “怎么了启涯?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵贬堵,是天一觀的道長。 經(jīng)常有香客問我结洼,道長黎做,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任补君,我火速辦了婚禮引几,結(jié)果婚禮上昧互,老公的妹妹穿的比我還像新娘挽铁。我一直安慰自己,他們只是感情好敞掘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布叽掘。 她就那樣靜靜地躺著,像睡著了一般玖雁。 火紅的嫁衣襯著肌膚如雪更扁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機(jī)與錄音浓镜,去河邊找鬼溃列。 笑死,一個(gè)胖子當(dāng)著我的面吹牛膛薛,可吹牛的內(nèi)容都是我干的听隐。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼哄啄,長吁一口氣:“原來是場噩夢啊……” “哼雅任!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起咨跌,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤沪么,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后锌半,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體禽车,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年刊殉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了哭当。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡冗澈,死狀恐怖钦勘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情亚亲,我是刑警寧澤彻采,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站捌归,受9級特大地震影響肛响,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜惜索,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一特笋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧巾兆,春花似錦猎物、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至圃伶,卻和暖如春堤如,著一層夾襖步出監(jiān)牢的瞬間蒲列,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工搀罢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蝗岖,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓榔至,卻偏偏與公主長得像剪侮,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子洛退,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355

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