iOS-底層原理 :內(nèi)存管理(一)TaggedPointer/retain/release/dealloc/retainCount

本文主要是分析內(nèi)存管理中的內(nèi)存管理方案,以及retainretainCountrelease棉浸、dealloc的底層源碼分析

ARC & MRC

iOS中的內(nèi)存管理方案三热,大致可以分為兩類:MRC(手動內(nèi)存管理)和ARC(自動內(nèi)存管理)

MRC

  • MRC時代,系統(tǒng)是通過對象的引用計數(shù)來判斷一個是否銷毀,有以下規(guī)則

    • 對象被創(chuàng)建時引用計數(shù)都為1

    • 當(dāng)對象被其他指針引用時,需要手動調(diào)用[objc retain]有缆,使對象的引用計數(shù)+1

    • 當(dāng)指針變量不再使用對象時袖外,需要手動調(diào)用[objc release]釋放對象,使對象的引用計數(shù)-1

    • 當(dāng)一個對象的引用計數(shù)為0時熔酷,系統(tǒng)就會銷毀這個對象

  • 所以翼抠,在MRC模式下,必須遵守:誰創(chuàng)建,誰釋放,誰引用累颂,誰管理
    ARC

  • ARC模式是在WWDC2011和iOS5引入的自動管理機制朱监,即自動引用計數(shù)。是編譯器的一種特性。其規(guī)則與MRC一致逾冬,區(qū)別在于匹厘,ARC模式下不需要手動retain牛隅、release、autorelease欢嘿。編譯器會在適當(dāng)?shù)奈恢貌迦雛elease和autorelease掐隐。

內(nèi)存布局

我們在iOS-底層原理 24:內(nèi)存五大區(qū)文章中,介紹了內(nèi)存的五大區(qū)慷妙。其實除了內(nèi)存區(qū)架馋,還有內(nèi)核區(qū)保留區(qū),以4GB手機為例勘纯,如下所示,系統(tǒng)將其中的3GB給了五大區(qū)+保留區(qū),剩余的1GB給內(nèi)核區(qū)使用

image
  • 內(nèi)核區(qū):系統(tǒng)用來進行內(nèi)核處理操作的區(qū)域

  • 五大區(qū):這里不再作說明,具體請參考上面的鏈接

  • 保留區(qū):預(yù)留給系統(tǒng)處理nil等

這里有個疑問,為什么五大區(qū)的最后內(nèi)存地址是從0x00400000開始的。其主要原因是0x00000000表示nil帽衙,不能直接用nil表示一個段谴垫,所以單獨給了一段內(nèi)存用于處理nil等情況

內(nèi)存布局相關(guān)面試題

面試題1:全局變量和局部變量在內(nèi)存中是否有區(qū)別?如果有,是什么區(qū)別?

  • 有區(qū)別

  • 全局變量保存在內(nèi)存的全局存儲區(qū)(即bss+data段),占用靜態(tài)的存儲單元

  • 局部變量保存在中狈醉,只有在所在函數(shù)被調(diào)用時才動態(tài)的為變量分配存儲單元

面試題2:Block中可以修改全局變量班巩,全局靜態(tài)變量,局部靜態(tài)變量,局部變量嗎?

  • 可以修改全局變量,全局靜態(tài)變量玲躯,因為全局變量 和 靜態(tài)全局變量是全局的朽缴,作用域很廣

  • 可以修改局部靜態(tài)變量誓斥,不可以修改局部斌量

    • 局部靜態(tài)變量(static修飾的) 和 局部變量成畦,被block從外面捕獲舀武,成為 __main_block_impl_0這個結(jié)構(gòu)體的成員變量

    • 局部變量是以值方式傳遞到block的構(gòu)造函數(shù)中的寻馏,只會捕獲block中會用到的變量核偿,由于只捕獲了變量的值轰绵,并非內(nèi)存地址,所以在block內(nèi)部不能改變局部變量的值

    • 局部靜態(tài)變量是以指針形式蝗羊,被block捕獲的藏澳,由于捕獲的是指針,所以可以修改局部靜態(tài)變量的值

  • ARC環(huán)境下耀找,一旦使用__block修飾并在block中修改,就會觸發(fā)copy,block就會從棧區(qū)copy到堆區(qū)审洞,此時的block是堆區(qū)block

  • ARC模式下,Block中引用id類型的數(shù)據(jù),無論有沒有__block修飾,都會retain,對于基礎(chǔ)數(shù)據(jù)類型抛人,沒有__block就無法修改變量值酷鸦;如果有__block修飾,也是在底層修改__Block_byref_a_0結(jié)構(gòu)體徊哑,將其內(nèi)部的forwarding指針指向copy后的地址,來達到值的修改

內(nèi)存管理方案

內(nèi)存管理方案除了前文提及的MRCARC,還有以下三種

  • Tagged Pointer:專門用來處理小對象,例如NSNumber坞嘀、NSDate、小NSString等

  • Nonpointer_isa:非指針類型的isa饰抒,主要是用來優(yōu)化64位地址躲查,這個在iOS-底層原理 07:isa與類關(guān)聯(lián)的原理一文中典唇,已經(jīng)介紹了

  • SideTables散列表蟆豫,在散列表中主要有兩個表芍锚,分別是引用計數(shù)表弱引用表

這里主要著重介紹Tagged PointerSideTables蔓榄,我們通過一個面試題來引入Tagged Pointer

面試題

以下代碼會有什么問題并炮?

//*********代碼1*********
- (void)taggedPointerDemo {
  self.queue = dispatch_queue_create("com.cjl.cn", DISPATCH_QUEUE_CONCURRENT);

    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"CJL"];  // alloc 堆 iOS優(yōu)化 - taggedpointer
             NSLog(@"%@",self.nameStr);
        });
    }
}

//*********代碼2*********
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"來了");
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"CJL_越努力,越幸運H笥!Tァ!"];
            NSLog(@"%@",self.nameStr);
        });
    }
}

運行以上代碼壹若,發(fā)現(xiàn)taggedPointerDemo單獨運行沒有問題嗅钻,當(dāng)觸發(fā)touchesBegan方法后。程序會崩潰店展,崩潰的原因是多條線程同時對一個對象進行釋放养篓,導(dǎo)致了 過渡釋放所以崩潰。其根本原因是因為nameStr在底層的類型不一致導(dǎo)致的赂蕴,我們可以通過調(diào)試看出

image
  • taggedPointerDemo方法中的nameStr類型是 NSTaggedPointerString柳弄,存儲在常量區(qū)。因為nameStralloc分配時在堆區(qū)概说,由于較小碧注,所以經(jīng)過xcode中iOS的優(yōu)化,成了NSTaggedPointerString類型糖赔,存儲在常量區(qū)

  • touchesBegan方法中的nameStr類型是 NSCFString類型萍丐,存儲在堆上

NSString的內(nèi)存管理

我們可以通過NSString初始化的兩種方式,來測試NSString的內(nèi)存管理

  • 通過 WithString + @""方式初始化

  • 通過 WithFormat方式初始化

#define KLog(_c) NSLog(@"%@ -- %p -- %@",_c,_c,[_c class]);

- (void)testNSString{
    //初始化方式一:通過 WithString + @""方式
    NSString *s1 = @"1";
    NSString *s2 = [[NSString alloc] initWithString:@"222"];
    NSString *s3 = [NSString stringWithString:@"33"];

    KLog(s1);
    KLog(s2);
    KLog(s3);

    //初始化方式二:通過 WithFormat
    //字符串長度在9以內(nèi)
    NSString *s4 = [NSString stringWithFormat:@"123456789"];
    NSString *s5 = [[NSString alloc] initWithFormat:@"123456789"];

    //字符串長度大于9
    NSString *s6 = [NSString stringWithFormat:@"1234567890"];
    NSString *s7 = [[NSString alloc] initWithFormat:@"1234567890"];

    KLog(s4);
    KLog(s5);
    KLog(s6);
    KLog(s7);
}

以下是運行的結(jié)果

image

所以放典,從上面可以總結(jié)出逝变,NSString的內(nèi)存管理主要分為3種

  • __NSCFConstantString:字符串常量,是一種編譯時常量奋构,retainCount值很大壳影,對其操作,不會引起引用計數(shù)變化弥臼,存儲在字符串常量區(qū)

  • __NSCFString:是在運行時創(chuàng)建的NSString子類宴咧,創(chuàng)建后引用計數(shù)會加1,存儲在堆上

  • NSTaggedPointerString:標(biāo)簽指針径缅,是蘋果在64位環(huán)境下對NSString悠汽、NSNumber等對象做的優(yōu)化箱吕。對于NSString對象來說

    • 當(dāng)字符串是由數(shù)字、英文字母組合且長度小于等于9時柿冲,會自動成為NSTaggedPointerString類型茬高,存儲在常量區(qū)

    • 當(dāng)有中文或者其他特殊符號時,會直接成為__NSCFString類型假抄,存儲在堆區(qū)

Tagged Pointer 小對象

由一個NSString的面試題怎栽,引出了Tagged Pointer,為了探索小對象的引用計數(shù)處理宿饱,所以我們需要進入objc源碼中查看retain熏瞄、release源碼 中對 Tagged Pointer小對象的處理

小對象的引用計數(shù)處理分析

  • 查看setProperty -> reallySetProperty源碼,其中是對新值retain谬以,舊值release

    image
  • 進入objc_retain强饮、objc_release源碼,在這里都判斷是否是小對象,如果是小對象为黎,則不會進行retain或者release邮丰,會直接返回。因此可以得出一個結(jié)論:如果對象是小對象铭乾,不會進行retain 和 release

//****************objc_retain****************
__attribute__((aligned(16), flatten, noinline))
id 
objc_retain(id obj)
{
    if (!obj) return obj;
    //判斷是否是小對象剪廉,如果是,則直接返回對象
    if (obj->isTaggedPointer()) return obj;
    //如果不是小對象炕檩,則retain
    return obj->retain();
}

//****************objc_release****************
__attribute__((aligned(16), flatten, noinline))
void 
objc_release(id obj)
{
    if (!obj) return;
    //如果是小對象斗蒋,則直接返回
    if (obj->isTaggedPointer()) return;
    //如果不是小對象,則release
    return obj->release();
}

小對象的地址分析

繼續(xù)以NSString為例笛质,對于NSString來說

  • 一般的NSString對象指針泉沾,都是string值 + 指針地址,兩者是分開的

  • 對于Tagged Pointer指針妇押,其指針+值跷究,都能在小對象中體現(xiàn)。所以Tagged Pointer 既包含指針舆吮,也包含值

在之前的文章講類的加載時,其中的_read_images源碼有一個方法對小對象進行了處理队贱,即initializeTaggedPointerObfuscator方法

  • 進入_read_images -> initializeTaggedPointerObfuscator源碼實現(xiàn)
static void
initializeTaggedPointerObfuscator(void)
{

    if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
        // Set the obfuscator to zero for apps linked against older SDKs,
        // in case they're relying on the tagged pointer representation.
        DisableTaggedPointerObfuscation) {
        objc_debug_taggedpointer_obfuscator = 0;
    }
    //在iOS14之后色冀,對小對象進行了混淆,通過與操作+_OBJC_TAG_MASK混淆
    else {
        // Pull random data into the variable, then shift away all non-payload bits.
        arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                       sizeof(objc_debug_taggedpointer_obfuscator));
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
    }
}

在實現(xiàn)中柱嫌,我們可以看出锋恬,在iOS14之后,Tagged Pointer采用了混淆處理编丘,如下所示

image
  • 我們可以在源碼中通過objc_debug_taggedpointer_obfuscator查找taggedPointer的編碼解碼与学,來查看底層是如何混淆處理的
//編碼
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
//編碼
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;

通過實現(xiàn)彤悔,我們可以得知,在編碼和解碼部分索守,經(jīng)過了兩層異或晕窑,其目的是得到小對象自己,例如以 1010 0001為例卵佛,假設(shè)mask0101 1000

    1010 0001 
   ^0101 1000 mask(編碼)
    1111 1001
   ^0101 1000 mask(解碼)
    1010 0001

  • 所以在外界杨赤,為了獲取小對象的真實地址,我們可以將解碼的源碼拷貝到外面截汪,將NSString混淆部分進行解碼疾牲,如下所示

    image

    觀察解碼后的小對象地址,其中的62表示bASCII碼衙解,再以NSNumber為例阳柔,同樣可以看出,1就是我們實際的值

    image

到這里蚓峦,我們驗證了小對象指針地址中確實存儲了值舌剂,那么小對象地址高位其中的0xa、0xb又是什么含義呢枫匾?

//NSString
0xa000000000000621

//NSNumber
0xb000000000000012
0xb000000000000025

  • 需要去源碼中查看_objc_isTaggedPointer源碼架诞,主要是通過保留最高位的值(即64位的值),判斷是否等于_OBJC_TAG_MASK(即2^63),來判斷是否是小對象
static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    //等價于 ptr & 1左移63干茉,即2^63谴忧,相當(dāng)于除了64位,其他位都為0角虫,即只是保留了最高位的值
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

所以0xa沾谓、0xb主要是用于判斷是否是小對象taggedpointer,即判斷條件戳鹅,判斷第64位上是否為1(taggedpointer指針地址即表示指針地址均驶,也表示值)

  • 0xa 轉(zhuǎn)換成二進制為 1 010(64為為1,63~61后三位表示 tagType類型 - 2)枫虏,表示NSString類型

  • 0xb 轉(zhuǎn)換為二進制為 1 011(64為為1妇穴,63~61后三位表示 tagType類型 - 3),表示NSNumber類型隶债,這里需要注意一點腾它,如果NSNumber的值是-1,其地址中的值是用補碼表示的

這里可以通過_objc_makeTaggedPointer方法的參數(shù)tag類型objc_tag_index_t進入其枚舉死讹,其中 2表示NSString瞒滴,3表示NSNumber

image
  • 同理,我們可以定義一個NSDate對象,來驗證其tagType是否為6妓忍。通過打印結(jié)果虏两,其地址高位是0xe,轉(zhuǎn)換為二進制為1 110世剖,排除64位的1定罢,剩余的3位正好轉(zhuǎn)換為十進制是6,符合上面的枚舉值

    image

Tagged Pointer 總結(jié)

  • Tagged Pointer小對象類型(用于存儲NSNumber搁廓、NSDate引颈、小NSString),小對象指針不再是簡單的地址境蜕,而是地址 + 值蝙场,即真正的值,所以粱年,實際上它不再是一個對象了售滤,它只是一個披著對象皮的普通變量而以。所以可以直接進行讀取台诗。優(yōu)點是占用空間小 節(jié)省內(nèi)存

  • Tagged Pointer小對象 不會進入retain 和 release完箩,而是直接返回了兄淫,意味著不需要ARC進行管理万皿,所以可以直接被系統(tǒng)自主的釋放和回收

  • Tagged Pointer內(nèi)存并不存儲在堆中褐桌,而是在常量區(qū)中扬蕊,也不需要malloc和free,所以可以直接讀取伸眶,相比存儲在堆區(qū)的數(shù)據(jù)讀取肛冶,效率上快了3倍左右览徒。創(chuàng)建的效率相比堆區(qū)快了近100倍左右

  • 所以事哭,綜合來說漫雷,taggedPointer的內(nèi)存管理方案,比常規(guī)的內(nèi)存管理鳍咱,要快很多

  • Tagged Pointer的64位地址中降盹,前4位代表類型,后4位主要適用于系統(tǒng)做一些處理谤辜,中間56位用于存儲值

  • 優(yōu)化內(nèi)存建議:對于NSString來說蓄坏,當(dāng)字符串較小時,建議直接通過@""初始化丑念,因為存儲在常量區(qū)涡戳,可以直接進行讀取。會比WithFormat初始化方式更加快速

SideTables 散列表

當(dāng)引用計數(shù)存儲到一定值是渠欺,并不會再存儲到Nonpointer_isa的位域的extra_rc中妹蔽,而是會存儲到SideTables 散列表中

下面我們就來繼續(xù)探索引用計數(shù)retain的底層實現(xiàn)

retain 源碼分析

  • 進入objc_retain -> retain -> rootRetain源碼實現(xiàn)椎眯,主要有以下幾部分邏輯:
    • 【第一步】判斷是否為Nonpointer_isa

    • 【第二步】操作引用計數(shù)

      • 1挠将、如果不是Nonpointer_isa胳岂,則直接操作SideTables散列表,此時的散列表并不是只有一張舔稀,而是有很多張(后續(xù)會分析乳丰,為什么需要多張)

      • 2、判斷是否正在釋放内贮,如果正在釋放产园,則執(zhí)行dealloc流程

      • 3、執(zhí)行extra_rc+1夜郁,即引用計數(shù)+1操作什燕,并給一個引用計數(shù)的狀態(tài)標(biāo)識carry,用于表示extra_rc是否滿了

      • 4竞端、如果carray的狀態(tài)表示extra_rc的引用計數(shù)滿了屎即,此時需要操作散列表,即 將滿狀態(tài)的一半拿出來存到extra_rc事富,另一半存在 散列表的rc_half技俐。這么做的原因是因為如果都存儲在散列表,每次對散列表操作都需要開解鎖统台,操作耗時雕擂,消耗性能大,這么對半分操作的目的在于提高性能

ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;
    //為什么有isa贱勃?因為需要對引用計數(shù)+1井赌,即retain+1,而引用計數(shù)存儲在isa的bits中募寨,需要進行新舊isa的替換
    isa_t oldisa;
    isa_t newisa;
    //重點
    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        //判斷是否為nonpointer isa
        if (slowpath(!newisa.nonpointer)) {
            //如果不是 nonpointer isa族展,直接操作散列表sidetable
            ClearExclusive(&isa.bits);
            if (rawISA()->isMetaClass()) return (id)this;
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        //dealloc源碼
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }

        uintptr_t carry;
        //執(zhí)行引用計數(shù)+1操作,即對bits中的 1ULL<<45(arm64) 即extra_rc拔鹰,用于該對象存儲引用計數(shù)值
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
        //判斷extra_rc是否滿了仪缸,carry是標(biāo)識符
        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            //如果extra_rc滿了,則直接將滿狀態(tài)的一半拿出來存到extra_rc
            newisa.extra_rc = RC_HALF;
            //給一個標(biāo)識符為YES列肢,表示需要存儲到散列表
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        //將另一半存在散列表的rc_half中恰画,即滿狀態(tài)下是8位,一半就是1左移7位瓷马,即除以2
        //這么操作的目的在于提高性能拴还,因為如果都存在散列表中,當(dāng)需要release-1時欧聘,需要去訪問散列表片林,每次都需要開解鎖,比較消耗性能。extra_rc存儲一半的話费封,可以直接操作extra_rc即可焕妙,不需要操作散列表。性能會提高很多
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}

問題1:散列表為什么在內(nèi)存有多張弓摘?最多能夠多少張焚鹊?

  • 如果散列表只有一張表,意味著全局所有的對象都會存儲在一張表中韧献,都會進行開鎖解鎖(鎖是鎖整個表的讀寫)末患。當(dāng)開鎖時,由于所有數(shù)據(jù)都在一張表锤窑,則意味著數(shù)據(jù)不安全

  • 如果每個對象都開一個表璧针,會耗費性能,所以也不能有無數(shù)個表

  • 散列表的類型是SideTable渊啰,有如下定義

struct SideTable {
    spinlock_t slock;//開/解鎖
    RefcountMap refcnts;//引用計數(shù)表
    weak_table_t weak_table;//弱引用表

    ....
}

  • 通過查看sidetable_unlock方法定位SideTables陈莽,其內(nèi)部是通過SideTablesMap的get方法獲取。而SideTablesMap是通過StripedMap<SideTable>定義的
void 
objc_object::sidetable_unlock()
{
    //SideTables散列表并不只是一張虽抄,而是很多張走搁,與關(guān)聯(lián)對象表類似
    SideTable& table = SideTables()[this];
    table.unlock();
}
??
static StripedMap<SideTable>& SideTables() {
    return SideTablesMap.get();
}
??
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;

從而進入StripedMap的定義,從這里可以看出迈窟,同一時間私植,真機中散列表最多只能有8張

image

問題2:為什么在用散列表,而不用數(shù)組车酣、鏈表曲稼?

  • 數(shù)組:特點在于查詢方便(即通過下標(biāo)訪問),增刪比較麻煩(類似于之前講過的methodList湖员,通過memcopy贫悄、memmove增刪,非常麻煩)娘摔,所以數(shù)據(jù)的特性是讀取快窄坦,存儲不方便

  • 鏈表:特點在于增刪方便,查詢慢(需要從頭節(jié)點開始遍歷查詢)凳寺,所以鏈表的特性是存儲快鸭津,讀取慢

  • 散列表本質(zhì)就是一張哈希表,哈希表集合了數(shù)組和鏈表的長處肠缨,增刪改查都比較方便逆趋,例如拉鏈哈希表(在之前鎖的文章中,講過的tls的存儲結(jié)構(gòu)就是拉鏈形式的)晒奕,是最常用的闻书,如下所示

    image

    可以從SideTables -> StripedMap -> indexForPointer中驗證是通過哈希函數(shù)計算哈希下標(biāo) 以及sideTables為什么可以使用[]的原因

    image

所以名斟,綜上所述,retain的底層流程如下所示

image

總結(jié):retain 完整回答

  • retain在底層首先會判斷是否是 Nonpointer isa魄眉,如果不是蒸眠,則直接操作散列表 進行+1操作

  • 如果是Nonpointer isa,還需要判斷是否正在釋放杆融,如果正在釋放,則執(zhí)行dealloc流程霜运,釋放弱引用表和引用技術(shù)表脾歇,最后free釋放對象內(nèi)存

  • 如果不是正在釋放,則對Nonpointer isa進行常規(guī)的引用計數(shù)+1.這里需要注意一點的是淘捡,extra_rc在真機上只有8位用于存儲引用計數(shù)的值藕各,當(dāng)存儲滿了時,需要借助散列表用于存儲焦除。需要將滿了的extra_rc對半分激况,一半(即2^7)存儲在散列表中。另一半還是存儲在extra_rc中膘魄,用于常規(guī)的引用計數(shù)的+1或者-1操作乌逐,然后再返回

release 源碼分析

分析了retain的底層實現(xiàn),下面來分析release的底層實現(xiàn)

  • 通過setProperty -> reallySetProperty -> objc_release -> release -> rootRelease -> rootRelease順序创葡,進入rootRelease源碼浙踢,其操作與retain 相反
    • 判斷是否是Nonpointer isa,如果不是灿渴,則直接對散列表進行-1操作

    • 如果是Nonpointer isa洛波,則對extra_rc中的引用計數(shù)值進行-1操作,并存儲此時的extra_rc狀態(tài)到carry

    • 如果此時的狀態(tài)carray為0骚露,則走到underflow流程

    • underflow流程有以下幾步:

      • 判斷散列表是否存儲了一半的引用計數(shù)

      • 如果是蹬挤,則從散列表取出存儲的一半引用計數(shù),進行-1操作棘幸,然后存儲到extra_rc

      • 如果此時extra_rc沒有值焰扳,散列表中也是空的,則直接進行析構(gòu)误续,即dealloc操作蓝翰,屬于自動觸發(fā)

ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    if (isTaggedPointer()) return false;

    bool sideTableLocked = false;

    isa_t oldisa;
    isa_t newisa;

 retry:
    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        //判斷是否是Nonpointer isa
        if (slowpath(!newisa.nonpointer)) {
            //如果不是,則直接操作散列表-1
            ClearExclusive(&isa.bits);
            if (rawISA()->isMetaClass()) return false;
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        uintptr_t carry;
        //進行引用計數(shù)-1操作女嘲,即extra_rc-1
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        //如果此時extra_rc的值為0了畜份,則走到underflow
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));

    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;

 underflow:
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate

    // abandon newisa to undo the decrement
    newisa = oldisa;
    //判斷散列表中是否存儲了一半的引用計數(shù)
    if (slowpath(newisa.has_sidetable_rc)) {
        if (!handleUnderflow) {
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }

        // Transfer retain count from side table to inline storage.

        if (!sideTableLocked) {
            ClearExclusive(&isa.bits);
            sidetable_lock();
            sideTableLocked = true;
            // Need to start over to avoid a race against 
            // the nonpointer -> raw pointer transition.
            goto retry;
        }

        // Try to remove some retain counts from the side table.
        //從散列表中取出存儲的一半引用計數(shù)
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

        // To avoid races, has_sidetable_rc must remain set 
        // even if the side table count is now zero.

        if (borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            //進行-1操作,然后存儲到extra_rc中
            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            if (!stored) {
                // Inline update failed. 
                // Try it again right now. This prevents livelock on LL/SC 
                // architectures where the side table access itself may have 
                // dropped the reservation.
                isa_t oldisa2 = LoadExclusive(&isa.bits);
                isa_t newisa2 = oldisa2;
                if (newisa2.nonpointer) {
                    uintptr_t overflow;
                    newisa2.bits = 
                        addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
                    if (!overflow) {
                        stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, 
                                                       newisa2.bits);
                    }
                }
            }

            if (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                sidetable_addExtraRC_nolock(borrowed);
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            // This decrement cannot be the deallocating decrement - the side 
            // table lock and has_sidetable_rc bit ensure that if everyone 
            // else tried to -release while we worked, the last one would block.
            sidetable_unlock();
            return false;
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }
    //此時extra_rc中值為0欣尼,散列表中也是空的爆雹,則直接進行析構(gòu)停蕉,即自動觸發(fā)dealloc流程
    // Really deallocate.
    //觸發(fā)dealloc的時機
    if (slowpath(newisa.deallocating)) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return overrelease_error();
        // does not actually return
    }
    newisa.deallocating = true;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

    if (performDealloc) {
        //發(fā)送一個dealloc消息
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return true;
}

所以,綜上所述钙态,release的底層流程如下圖所示

image

dealloc 源碼分析

retainrelease的底層實現(xiàn)中慧起,都提及了dealloc析構(gòu)函數(shù),下面來分析dealloc的底層的實現(xiàn)

  • 進入dealloc -> _objc_rootDealloc -> rootDealloc源碼實現(xiàn)册倒,主要有兩件事:
    • 根據(jù)條件判斷是否有isa蚓挤、cxx、關(guān)聯(lián)對象驻子、弱引用表灿意、引用計數(shù)表,如果沒有崇呵,則直接free釋放內(nèi)存
    • 如果有缤剧,則進入object_dispose方法
inline void
objc_object::rootDealloc()
{
    //對象要釋放,需要做哪些事情域慷?
    //1荒辕、isa - cxx - 關(guān)聯(lián)對象 - 弱引用表 - 引用計數(shù)表
    //2、free
    if (isTaggedPointer()) return;  // fixme necessary?

    //如果沒有這些犹褒,則直接free
    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        //如果有
        object_dispose((id)this);
    }
}

  • 進入object_dispose源碼抵窒,其目的有以下幾個
    • 銷毀實例,主要有以下操作

      • 調(diào)用c++析構(gòu)函數(shù)

      • 刪除關(guān)聯(lián)引用

      • 釋放散列表

      • 清空弱引用表

    • free釋放內(nèi)存

id 
object_dispose(id obj)
{
    if (!obj) return nil;
    //銷毀實例而不會釋放內(nèi)存
    objc_destructInstance(obj);
    //釋放內(nèi)存
    free(obj);

    return nil;
}
??
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        //調(diào)用C ++析構(gòu)函數(shù)
        if (cxx) object_cxxDestruct(obj);
        //刪除關(guān)聯(lián)引用
        if (assoc) _object_remove_assocations(obj);
        //釋放
        obj->clearDeallocating();
    }

    return obj;
}
??
inline void 
objc_object::clearDeallocating()
{
    //判斷是否為nonpointer isa
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        //如果不是叠骑,則直接釋放散列表
        sidetable_clearDeallocating();
    }
    //如果是估脆,清空弱引用表 + 散列表
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}
??
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        //清空弱引用表
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        //清空引用計數(shù)
        table.refcnts.erase(this);
    }
    table.unlock();
}

所以,綜上所述座云,dealloc底層的流程圖如圖所示

image

所以疙赠,到目前為止,從最開始的alloc底層分析(見iOS-底層原理 02:alloc & init & new 源碼分析)-> retain -> release -> dealloc就全部串聯(lián)起來了

retainCount 源碼分析

引用計數(shù)的分析通過一個面試題來說明

面試題:alloc創(chuàng)建的對象的引用計數(shù)為多少朦拖?

  • 定義如下代碼圃阳,打印其引用計數(shù)
NSObject *objc = [NSObject alloc];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));

打印結(jié)果如下

image
  • 進入retainCount -> _objc_rootRetainCount -> rootRetainCount源碼,其實現(xiàn)如下
- (NSUInteger)retainCount {
    return _objc_rootRetainCount(self);
}
??
uintptr_t
_objc_rootRetainCount(id obj)
{
    ASSERT(obj);

    return obj->rootRetainCount();
}
??
inline uintptr_t 
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    //如果是nonpointer isa璧帝,才有引用計數(shù)的下層處理
    if (bits.nonpointer) {
        //alloc創(chuàng)建的對象引用計數(shù)為0捍岳,包括sideTable,所以對于alloc來說,是 0+1=1睬隶,這也是為什么通過retaincount獲取的引用計數(shù)為1的原因
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }
    //如果不是,則正常返回
    sidetable_unlock();
    return sidetable_retainCount();
}

在這里我們可以通過源碼斷點調(diào)試银萍,來查看此時的extra_rc的值贴唇,結(jié)果如下

image

答案:綜上所述搀绣,alloc創(chuàng)建的對象實際的引用計數(shù)為0,其引用計數(shù)打印結(jié)果為1戳气,是因為在底層rootRetainCount方法中链患,引用計數(shù)默認(rèn)+1了,但是這里只有對引用計數(shù)的讀取操作瓶您,是沒有寫入操作的麻捻,簡單來說就是:為了防止alloc創(chuàng)建的對象被釋放(引用計數(shù)為0會被釋放),所以在編譯階段呀袱,程序底層默認(rèn)進行了+1操作贸毕。實際上在extra_rc中的引用計數(shù)仍然為0

總結(jié)

  • alloc創(chuàng)建的對象沒有retain和release

  • alloc創(chuàng)建對象的引用計數(shù)為0,會在編譯時期压鉴,程序默認(rèn)加1,所以讀取引用計數(shù)時為1

強應(yīng)用(強持有)

假設(shè)此時有兩個界面A锻拘、B油吭,從A push 到B界面,在B界面中有如下定時器代碼署拟。當(dāng)從B pop回到A界面[圖片上傳中...(E70D3F5D-8815-4138-BFDD-017B1BFCE0E7.png-6861f8-1609331145410-0)]
時婉宰,發(fā)現(xiàn)定時器沒有停止,其方法仍然在執(zhí)行推穷,為什么?

self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

其主要原因是B界面沒有釋放蟹腾,即沒有執(zhí)行dealloc方法,導(dǎo)致timer也無法停止和釋放

解決方式一

  • 重寫didMoveToParentViewController方法
- (void)didMoveToParentViewController:(UIViewController *)parent{
    // 無論push 進來 還是 pop 出去 正常跑
    // 就算繼續(xù)push 到下一層 pop 回去還是繼續(xù)
    if (parent == nil) {
       [self.timer invalidate];
        self.timer = nil;
        NSLog(@"timer 走了");
    }
}

解決方式二

  • 定義timer時炉爆,采用閉包的形式,因此不需要指定target
- (void)blockTimer{
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timer fire - %@",timer);
    }];
}

現(xiàn)在,我們從底層來深入研究艺晴,為什么B界面有了timer之后然评,導(dǎo)致B界面釋放不掉,即不會走到dealloc方法亿眠。我們可以通過官方文檔查看timerWithTimeInterval:target:selector:userInfo:repeats:方法中對target的描述

image

從文檔中可以看出,timer對傳入的target具有強持有竟趾,即timer持有self。由于timer是定義在B界面中犀勒,所以self也持有timer,因此 self -> timer -> self構(gòu)成了循環(huán)引用

iOS-底層原理 30:Block底層原理文章中铸本,針對循環(huán)應(yīng)用提供了幾種解決方式。我們我們嘗試通過__weak弱引用來解決锡足,代碼修改如下

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

我們再次運行程序,進行push-pop跳轉(zhuǎn)沐批。發(fā)現(xiàn)問題還是存在先馆,即定時器方法仍然在執(zhí)行,并沒有執(zhí)行B的dealloc方法仿野,為什么呢?

  • 我們使用__weak雖然打破了 self -> timer -> self之前的循環(huán)引用球涛,即引用鏈變成了self -> timer -> weakSelf -> self酿秸。但是在這里我們的分析并不全面肝箱,此時還有一個Runloop對timer的強持有,因為Runloop生命周期B界面更長骏融,所以導(dǎo)致了timer無法釋放,同時也導(dǎo)致了B界面的self也無法釋放误趴。所以枣申,最初引用鏈應(yīng)該是這樣的

    image

    加上weakSelf之后泊窘,變成了這樣

    image

weakSelf 與 self

對于weakSelfself瓜贾,主要有以下兩個疑問

  • 1、weakSelf會對引用計數(shù)進行+1操作嗎龟劲?

  • 2、weakSelfself 的指針地址相同嗎蚕愤,是指向同一片內(nèi)存嗎污呼?

  • 帶著疑問籍凝,我們在weakSelf前后打印self的引用計數(shù)

NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
__weak typeof(self) weakSelf = self;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));

運行結(jié)果如下,發(fā)現(xiàn)前后self的引用計數(shù)都是8

image

因此可以得出一個結(jié)論:weakSelf沒有對內(nèi)存進行+1操作

  • 繼續(xù)打印weakSelfself對象,以及指針地址
po weakSelf
po self

po &weakSelf
po &self

結(jié)果如下

image

從打印結(jié)果可以看出囤攀,當(dāng)前self取地址 和 weakSelf取地址的值是不一樣的漓骚。意味著有兩個指針地址噩斟,指向的是同一片內(nèi)存空間,即weakSelf 和 self 的內(nèi)存地址是不一樣,都指向同一片內(nèi)存空間

image
  • 從上面打印可以看出,此時timer捕獲的是<LGTimerViewController: 0x7f890741f5b0>淆九,是一個對象拧抖,所以無法通過weakSelf來解決強持有擦盾。即引用鏈關(guān)系為:NSRunLoop -> timer -> weakSelf(<LGTimerViewController: 0x7f890741f5b0>)徒仓。所以RunLoop對整個 對象的空間有強持有喂走,runloop沒停,timer 和 weakSelf是無法釋放的

  • 而我們在Block原理中提及的block的循環(huán)引用,與timer的是有區(qū)別的睡汹。通過block底層原理的方法__Block_object_assign可知帮孔,block捕獲的是 對象的指針地址,即weakself 是 臨時變量的指針地址不撑,跟self沒有關(guān)系文兢,因為weakSelf是新的地址空間。所以此時的weakSelf相當(dāng)于中間值焕檬。其引用關(guān)系鏈為self -> block -> weakSelf(臨時變量的指針地址)姆坚,可以通過地址拿到指針

所以在這里击喂,我們需要區(qū)別下blocktimer循環(huán)引用的模型

  • timer模型self -> timer -> weakSelf -> self,當(dāng)前的timer捕獲的是B界面的內(nèi)存铲敛,即vc對象的內(nèi)存,即weakSelf表示的是vc對象

  • Block模型self -> block -> weakSelf -> self寺枉,當(dāng)前的block捕獲的是指針地址避归,即weakSelf表示的是指向self的臨時變量的指針地址

解決 強引用(強持有)

以下幾種方法的思路均是:依賴中介者模式奸柬,打破強持有蹲蒲,其中推薦思路四

思路一:pop時在其他方法中銷毀timer

根據(jù)前面的解釋表锻,我們知道由于Runloop對timer的強持有拷肌,導(dǎo)致了Runloop間接的強持有了self(因為timer中捕獲的是vc對象)拴清。所以導(dǎo)致dealloc方法無法執(zhí)行。需要查看在pop時蛛株,是否還有其他方法可以銷毀timer。這個方法就是didMoveToParentViewController

  • didMoveToParentViewController方法东涡,是用于當(dāng)一個視圖控制器中添加或者移除viewController后,必須調(diào)用的方法择诈。目的是為了告訴iOS,已經(jīng)完成添加/刪除子控制器的操作顶掉。

  • 在B界面中重寫didMoveToParentViewController方法

- (void)didMoveToParentViewController:(UIViewController *)parent{
    // 無論push 進來 還是 pop 出去 正常跑
    // 就算繼續(xù)push 到下一層 pop 回去還是繼續(xù)
    if (parent == nil) {
       [self.timer invalidate];
        self.timer = nil;
        NSLog(@"timer 走了");
    }
}

思路二:中介者模式,即不使用self,依賴于其他對象

在timer模式中巷嚣,我們重點關(guān)注的是fireHome能執(zhí)行蜕煌,并不關(guān)心timer捕獲的target是誰,由于這里不方便使用self(因為會有強持有問題)绿贞,所以可以將target換成其他對象因块,例如將target換成NSObject對象,將fireHome交給target執(zhí)行

  • 將timer的target 由self改成objc
//**********1籍铁、定義其他對象**********
@property (nonatomic, strong) id            target;

//**********1涡上、修改target**********
self.target = [[NSObject alloc] init];
class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:");
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(fireHome) userInfo:nil repeats:YES];

//**********3、imp**********
void fireHomeObjc(id obj){
    NSLog(@"%s -- %@",__func__,obj);
}

運行結(jié)果如下

image

運行發(fā)現(xiàn)執(zhí)行dealloc之后寨辩,timer還是會繼續(xù)執(zhí)行吓懈。原因是解決了中介者的釋放,但是沒有解決中介者的回收靡狞,即self.target的回收耻警。所以這種方式有缺陷

可以通過在dealloc方法中,取消定時器來解決甸怕,代碼如下

- (void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%s",__func__);
}

運行結(jié)果如下甘穿,發(fā)現(xiàn)pop之后,timer釋放梢杭,從而中介者也會進行回收釋放

image

思路三:自定義封裝timer

這種方式是根據(jù)思路二的原理温兼,自定義封裝timer,其步驟如下

  • 自定義timerWapper
    • 在初始化方法中武契,定義一個timer募判,其target是自己。即timerWapper中的timer咒唆,一直監(jiān)聽自己届垫,判斷selector,此時的selector已交給了傳入的target(即vc對象)全释,此時有一個方法fireHomeWapper装处,在方法中,判斷target是否存在

      • 如果target存在浸船,則需要讓vc知道妄迁,即向傳入的target發(fā)送selector消息寝蹈,并將此時的timer參數(shù)也一并傳入,所以vc就可以得知fireHome方法登淘,就這事這種方式定時器方法能夠執(zhí)行的原因

      • 如果target不存在箫老,已經(jīng)釋放了,則釋放當(dāng)前的timerWrapper形帮,即打破了RunLoop對timeWrapper的強持有 (timeWrapper <-×- RunLoop

    • 自定義cjl_invalidate方法中釋放timer槽惫。這個方法在vc的dealloc方法中調(diào)用周叮,即vc釋放,從而導(dǎo)致timerWapper釋放辩撑,打破了vctimeWrapper的的強持有( vc -×-> timeWrapper

//*********** .h文件 ***********
@interface CJLTimerWapper : NSObject

- (instancetype)cjl_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- (void)cjl_invalidate;

@end

//*********** .m文件 ***********
#import "CJLTimerWapper.h"
#import <objc/message.h>

@interface CJLTimerWapper ()

@property(nonatomic, weak) id target;
@property(nonatomic, assign) SEL aSelector;
@property(nonatomic, strong) NSTimer *timer;

@end

@implementation CJLTimerWapper

- (instancetype)cjl_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
    if (self == [super init]) {
        //傳入vc
        self.target = aTarget;
        //傳入的定時器方法
        self.aSelector = aSelector;

        if ([self.target respondsToSelector:self.aSelector]) {
            Method method = class_getInstanceMethod([self.target class], aSelector);
            const char *type = method_getTypeEncoding(method);
            //給timerWapper添加方法
            class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);

            //啟動一個timer,target是self仿耽,即監(jiān)聽自己
            self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
        }
    }
    return self;
}

//一直跑runloop
void fireHomeWapper(CJLTimerWapper *wapper){
    //判斷target是否存在
    if (wapper.target) {
        //如果存在則需要讓vc知道合冀,即向傳入的target發(fā)送selector消息,并將此時的timer參數(shù)也一并傳入项贺,所以vc就可以得知`fireHome`方法君躺,就這事這種方式定時器方法能夠執(zhí)行的原因
        //objc_msgSend發(fā)送消息,執(zhí)行定時器方法
        void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
         lg_msgSend((__bridge void *)(wapper.target), wapper.aSelector,wapper.timer);
    }else{
        //如果target不存在开缎,已經(jīng)釋放了棕叫,則釋放當(dāng)前的timerWrapper
        [wapper.timer invalidate];
        wapper.timer = nil;
    }
}

//在vc的dealloc方法中調(diào)用,通過vc釋放奕删,從而讓timer釋放
- (void)cjl_invalidate{
    [self.timer invalidate];
    self.timer = nil;
}

- (void)dealloc
{
    NSLog(@"%s",__func__);
}

@end

  • timerWapper的使用
//定義
self.timerWapper = [[CJLTimerWapper alloc] cjl_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];

//釋放
- (void)dealloc{
     [self.timerWapper cjl_invalidate];
}

運行結(jié)果如下

image

這種方式看起來比較繁瑣俺泣,步驟很多,而且針對timerWapper完残,需要不斷的添加method伏钠,需要進行一系列的處理。

思路四:利用NSProxy虛基類的子類

下面來介紹一種timer強引用最常用的處理方式:NSProxy子類

可以通過NSProxy虛基類谨设,可以交給其子類實現(xiàn)熟掂,NSProxy的介紹在iOS-底層原理 30:Block底層原理已經(jīng)介紹過了,這里不再重復(fù)

  • 首先定義一個繼承自NSProxy的子類
//************NSProxy子類************
@interface CJLProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end

@interface CJLProxy()
@property (nonatomic, weak) id object;
@end

@implementation CJLProxy
+ (instancetype)proxyWithTransformObject:(id)object{
    CJLProxy *proxy = [CJLProxy alloc];
    proxy.object = object;
    return proxy;
}
-(id)forwardingTargetForSelector:(SEL)aSelector {
    return self.object;
}

  • timer中的target傳入NSProxy子類對象扎拣,即timer持有NSProxy子類對象
//************解決timer強持有問題************
self.proxy = [CJLProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];

//在dealloc中將timer正常釋放
- (void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
}

這樣做的主要目的是將強引用的注意力轉(zhuǎn)移成了消息轉(zhuǎn)發(fā)赴肚。虛基類只負(fù)責(zé)消息轉(zhuǎn)發(fā),即使用NSProxy作為中間代理二蓝、中間者

這里有個疑問誉券,定義的proxy對象,在dealloc釋放時侣夷,還存在嗎横朋?

  • proxy對象會正常釋放,因為vc正常釋放了百拓,所以可以釋放其持有者琴锭,即timer和proxy晰甚,timer的釋放也打破了runLoop對proxy的強持有。完美的達到了兩層釋放决帖,即 vc -×-> proxy <-×- runloop厕九,解釋如下:
    • vc釋放,導(dǎo)致了proxy的釋放

    • dealloc方法中地回,timer進行了釋放扁远,所以runloop強引用也釋放了

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市刻像,隨后出現(xiàn)的幾起案子畅买,更是在濱河造成了極大的恐慌,老刑警劉巖细睡,帶你破解...
    沈念sama閱讀 210,835評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谷羞,死亡現(xiàn)場離奇詭異著觉,居然都是意外死亡炊豪,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,900評論 2 383
  • 文/潘曉璐 我一進店門舵稠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蠢壹,“玉大人嗓违,你說我怎么就攤上這事⊥济常” “怎么了蹂季?”我有些...
    開封第一講書人閱讀 156,481評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長求妹。 經(jīng)常有香客問我乏盐,道長,這世上最難降的妖魔是什么制恍? 我笑而不...
    開封第一講書人閱讀 56,303評論 1 282
  • 正文 為了忘掉前任父能,我火速辦了婚禮,結(jié)果婚禮上净神,老公的妹妹穿的比我還像新娘何吝。我一直安慰自己,他們只是感情好鹃唯,可當(dāng)我...
    茶點故事閱讀 65,375評論 5 384
  • 文/花漫 我一把揭開白布爱榕。 她就那樣靜靜地躺著,像睡著了一般坡慌。 火紅的嫁衣襯著肌膚如雪黔酥。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,729評論 1 289
  • 那天,我揣著相機與錄音跪者,去河邊找鬼棵帽。 笑死,一個胖子當(dāng)著我的面吹牛渣玲,可吹牛的內(nèi)容都是我干的逗概。 我是一名探鬼主播,決...
    沈念sama閱讀 38,877評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼忘衍,長吁一口氣:“原來是場噩夢啊……” “哼逾苫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起枚钓,我...
    開封第一講書人閱讀 37,633評論 0 266
  • 序言:老撾萬榮一對情侶失蹤铅搓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后秘噪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體狸吞,經(jīng)...
    沈念sama閱讀 44,088評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡勉耀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,443評論 2 326
  • 正文 我和宋清朗相戀三年指煎,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片便斥。...
    茶點故事閱讀 38,563評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡至壤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出枢纠,到底是詐尸還是另有隱情像街,我是刑警寧澤,帶...
    沈念sama閱讀 34,251評論 4 328
  • 正文 年R本政府宣布晋渺,位于F島的核電站镰绎,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏木西。R本人自食惡果不足惜畴栖,卻給世界環(huán)境...
    茶點故事閱讀 39,827評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望八千。 院中可真熱鬧吗讶,春花似錦、人聲如沸恋捆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,712評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽沸停。三九已至膜毁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瘟滨。 一陣腳步聲響...
    開封第一講書人閱讀 31,943評論 1 264
  • 我被黑心中介騙來泰國打工葬凳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人室奏。 一個月前我還...
    沈念sama閱讀 46,240評論 2 360
  • 正文 我出身青樓火焰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親胧沫。 傳聞我的和親對象是個殘疾皇子昌简,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,435評論 2 348

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