iOS 內(nèi)存管理

本文中的源代碼來源:需要下載Runtime的源碼,官方的工程需要經(jīng)過大量調(diào)試才能使用握巢。這里有處理好的objc4-756.2工程晕鹊,以下都是基于處理好的objc4-756.2工程說明的。

一暴浦、內(nèi)存布局

  • 棧(stack):由編譯器自動分配溅话、釋放、存儲函數(shù)的參數(shù)值肉渴、返回值和局部變量公荧,在程序運行過程中實時分配和釋放,由操作系統(tǒng)自動管理同规,無須程序員手動管理循狰。棧區(qū)由高地址向低地址增長。
  • 堆(heap): 存放通過 alloc 等分配的內(nèi)存塊券勺,空間的申請和釋放由程序員控制绪钥。iOS 中的對象一般都會在堆區(qū)開辟空間,使用引用計數(shù)的機制去管理對象的內(nèi)存关炼。
  • bbs: 存放未初始化的全局變量和靜態(tài)變量程腹。
  • data:已初始化的全局變量、靜態(tài)變量儒拂。
  • text: 存放 CPU 執(zhí)行的機器指令寸潦,代碼區(qū)是可共享色鸳,并且是只讀的。

對于 iOS 對象的內(nèi)存管理方案主要有三種:TaggedPointer见转、NONPOINTER_ISA命雀、散列表

二、TaggedPointer

void main() {
    ClassA *a = [[ClassA alloc]init];
}

在執(zhí)行上面代碼的時候一般情況下系統(tǒng)會做以下事情:

  1. 在棧區(qū)開辟一個空間存放對象 a 的指針斩箫。
  2. 在堆區(qū)開辟一個空間存放對象 a 本身吏砂,并通過 a 的指針來訪問 a 的內(nèi)存。
  3. 存放在堆區(qū)的對象 a 需要在合適的時機釋放乘客。

iOS 系統(tǒng)對對象的管理一般就是上述的情況狐血,系統(tǒng)的引用計數(shù)管理(MRC、ARC)的基礎就是基于以上過程易核。

但是這種機制就是否是完美無缺的呢匈织?或者說是否在一些特殊情況下?lián)碛幸环N更加高效的方式去對對象的內(nèi)存進行管理呢?蘋果提供了一種名為 TaggedPointer 的內(nèi)存管理技術耸成。

痛點

系統(tǒng)在運行過程中會產(chǎn)生許多輕量級的對象报亩,如果這些對象都要在堆上為其分配內(nèi)存,維護它的引用計數(shù)井氢,管理它的生命周期,無疑會給程序增加額外的邏輯岳链、占用更多空間花竞,造成效率的損失。比如系統(tǒng)只想在 NSNumber 中存放一個 1掸哑,但是又要用到 NSNumber 的方法约急,不能簡單的使用 int 類型。如果這時系統(tǒng)使用常規(guī)的對象內(nèi)存管理機制苗分,就需要在棧區(qū)開辟一個空間厌蔽,堆區(qū)開辟一個空間,還要管理對象的引用計數(shù)摔癣,這無疑有些得不償失奴饮。就好像有的人總會想的一個問題:為何我要為 6 位數(shù)的密碼來保護兩位數(shù)的存款?

系統(tǒng)的做法(以前的做法)

蘋果發(fā)現(xiàn)對于 64 為的 CPU择浊,它的指針大小也是 64 位戴卜,而 64 位可以做什么呢,如果存放一個正整數(shù)它最大可以存放 2^64 的數(shù)值琢岩,那么通常情況下對于一個整數(shù)的存放投剥,這個指針所占用的空間完全夠用。這樣系統(tǒng)就可以將一些輕量級對象的值放到指針中担孔。這樣系統(tǒng)就無須在堆區(qū)開辟內(nèi)存江锨,也更無須考慮對象的釋放問題了吃警。這大大降低了系統(tǒng)的內(nèi)存空間和對象的管理成本。

但是有一些問題啄育,就是畢竟很多重對象不能用 TaggedPointer 技術的汤徽,系統(tǒng)需要識別哪個指針是指向堆區(qū)的,哪個指針是使用 TaggedPointer 技術的灸撰。還有就是在指針中還有存放關于類的信息谒府,不然光有值,確不能調(diào)用方法浮毯。

首先計算機對于對象的存儲是有一定規(guī)律的完疫,為了使對象在內(nèi)存中對齊,對象的地址總是指針大小的整數(shù)倍债蓝,通常是 16 倍壳鹤,這意味著啥?16的二級制是 1 0000饰迹,就是說一個正常對象的指針后四位都是 0芳誓,TaggedPointer 的方式就是將使用該技術的對象最后一位置位 1,這樣就可以區(qū)分正常指針和 TaggedPointer 指針了啊鸭。蘋果又使用了接下來的三位去存放關于類的信息锹淌,這樣系統(tǒng)就知道了這里存放的是什么類型的數(shù)據(jù)了。剩下的 60 為就是真正用來存放對象值的空間了赠制,如果存放正整數(shù)可以存放 2^60 之多赂摆,適用于大部分情況,如果值所占空間過大钟些,系統(tǒng)會重新在堆區(qū)開辟空間烟号,對對象進行操作和管理。

但是這個方式似乎棄用了政恍,蘋果現(xiàn)在的 TaggedPointer 技術似乎更加復雜,至少我是無法完全解析鹤树,先看以下代碼

    NSMutableString *muStr = [NSMutableString stringWithString:@"1"];
    for (int i=0; i<20; i++) {
        NSNumber *number = @([muStr longLongValue]);
        NSLog(@"%s, %p, %@", class_getName(number.class), number, number);
        [muStr appendFormat:@"1"];
    }
    
    NSString *baseStr = @"abcdefghijklmn";
    for (int i=0; i<baseStr.length; i++) {
        NSString *str = [[[baseStr substringToIndex:i+1] mutableCopy] copy];
        NSLog(@"%s, %p, %@", class_getName(str.class), str, str);
    }

這是打印

我發(fā)現(xiàn)每次打印都不相同,實在沒什么規(guī)律岛蚤,但是也能夠確定哪些情況使用了 TaggedPointer 技術,正常對象的指針后四位都是0够掠,上面打印的是16進制的指針疯潭,那么最后一位是 0(代表2進制后四位為0) 的就是正常對象,最后一位不是 0 的就是使用了 TaggedPointer 技術的對象了逛犹。

三渗柿、NONPOINTER_ISA

對于不能使用 TaggedPointer 的情況霸奕,系統(tǒng)只能去堆區(qū)開辟空間了脯厨,我們知道任何一個 OC 對象都有一個 isa 指針指向?qū)ο蟮念惽觯箤ο罂梢哉{(diào)用其方法、屬性等合武。那么和 TaggedPointer 類似临梗,對象的 isa 指針也是有 64 位(64位系統(tǒng)下)的,而指針信息的存儲或許不需要那么多的位數(shù)稼跳,系統(tǒng)完全可以利用剩余的位數(shù)做一些什么盟庞。蘋果利用了這些位數(shù)存儲一些與內(nèi)存管理相關的內(nèi)容,使 isa 不僅僅是一個類對象的指針汤善,這種技術就是 NONPOINTER_ISA什猖。

下面看一下 isa 指針 isa_t 聯(lián)合體的定義和里面的 ISA_BITFIELD

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   define ISA_BITFIELD                                                      \
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t deallocating      : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

# else
#   error unknown architecture for packed isa
# endif

在第二段代碼中我們知道在 arm64 和 x86_64 架構下都定義了 ISA_BITFIELD,而其他情況是沒有的红淡,它們共同情況就是這兩種架構都是 64 位的不狮,32 為手機(模擬器)因為其指針能存儲的信息太少(32位)而沒有使用 NONPOINTER_ISA 的價值。我們主要看 arm64 架構下的情況在旱,因為這是真機使用的情況摇零。

位數(shù) 名字 作用 解釋
1 nonpointer 判斷該指針是否使用 NONPOINTER_ISA 技術 上小節(jié)得出正常指針后四位為 0,所以這指針最低位為 1 就可判斷是否使用 NONPOINTER_ISA 技術
1 has_assoc 判斷是否使用關聯(lián)對象技術 關聯(lián)對象需要一個全局的 manager 管理桶蝎,需要在對象釋放的時候移除
1 has_cxx_dtor 判斷是否使用 c++ 構析函數(shù)(.cxx_destruct) 有的話需要做一些處理
33 shiftcls 存放類對象的內(nèi)存地址信息 同前
6 magic 用于在調(diào)試時分辨對象是否未完成初始化 同前
1 weakly_referenced 是否有被弱引用指針引用過 弱引用的指針需要在對象釋放時將指針自動置位 nil
1 deallocating 判斷對象是否正在釋放 同前
1 has_sidetable_rc 是否需要使用散列表來存放對象的引用計數(shù) 后 19 位還有一個 extra_rc驻仅,是用來存放對象引用計數(shù)的,但是如果對象引用計數(shù)過大登渣,需要在外掛的散列表中查找對象的引用計數(shù)噪服,如果比較小,可以存放在 extra_rc 的 19 位中
19 extra_rc 存放對象的引用計數(shù) 在對象引用計數(shù)較小時使用

四胜茧、散列表

散列表中包含:自旋鎖(spinlock_t)粘优、引用計數(shù)表(RefcountMap),弱引用表(weak_table_t)
下面是散列表結構體的數(shù)據(jù)結構

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

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }

    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    void forceReset() { slock.forceReset(); }

    // Address-ordered lock discipline for a pair of side tables.

    template<HaveOld, HaveNew>
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template<HaveOld, HaveNew>
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

盡管在 NONPOINTER_ISA 中留出來 19 位存放它的引用計數(shù)敬飒,但是對象的引用計數(shù)可能超出 NONPOINTER_ISA 所能存儲的極限邪铲,而且在一些情況下并不能使用 NONPOINTER_ISA 技術,例如 32 位的手機无拗,或者類對象內(nèi)存地址信息超出 33 位時带到。這就要使用引用計數(shù)表來查找對象的引用計數(shù)。

還有在 NONPOINTER_ISA 下英染,系統(tǒng)只知道對象是否被弱引用揽惹,但并沒有體現(xiàn)有哪些弱引用。如果有弱引用四康,就要查找弱引用表來對對象的弱引用進行處理搪搏。

1、散列表的結構

系統(tǒng)如何獲取散列表闪金?系統(tǒng)使用的是 Sidetable()[obj] 函數(shù)疯溺,獲取 obj 的散列表,進而獲取里面的引用計數(shù)和弱引用信息哎垦。

static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}

根據(jù)上面的源代碼囱嫩,我們可以知道 SideTables() 得到的是一個 StripedMap<SideTable> 類型數(shù)據(jù),看名字應該是一個存放 SideTable 的 Map漏设。

下面是 StripedMap 部分源代碼

template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif

    struct PaddedT {
        T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];

    static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }
}

這部分代碼我們可以知道墨闲,系統(tǒng)應該是將 SideTable 存放在代碼中定義的 array 中,array 的大小 StripeCount 在 TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR 情況下是 8(iPhone 真機)郑口,其他情況是 64鸳碧。

indexForPointer 函數(shù)中可以看到返回的數(shù)組下標是對對象指針進行一番操作后對數(shù)組個數(shù) StripeCount 取的余數(shù)。

所有這個 StripedMap 中會存放 8 或 64 個 SideTable 元素犬性,存取是通過哈希算法計算出來的(不需要遍歷數(shù)組就可以直接根據(jù)對象的指針計算出數(shù)組的下標)瞻离。

但是獲取到 SideTable 只是第一步,我們最終的目的是要對表里面相應對象的引用計數(shù)和弱引用指針進行操作乒裆,在本節(jié)一開始的源代碼 SideTable 結構體中就有這兩方面的信息分別是 refcnts 和 weak_table 這兩個參數(shù)琐脏。下面我們具體看一下如何使用 refcnts 獲取對對象的引用計數(shù)及進行加減操作,如何使用 weak_table 找到對象的弱引用指針及進行添加刪除弱引用指針操作缸兔。

2、對引用計數(shù)表的操作

我們看看 SideTable 中的 RefcountMap吹艇,點進去后可以看到

// RefcountMap disguises its pointers because we 
// don't want the table to act as a root for `leaks`.
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;

它本質(zhì)是一個 DenseMap 類型惰蜜,也是通過哈希運算的方式通過對象的指針獲取表中的內(nèi)容,并進行操作受神。我們從注釋中可以看到抛猖,蘋果對對象的指針進行了偽裝,目的是不泄露內(nèi)存。

為了更好的理解對引用計數(shù)表的操作我們看一下源代碼财著,看系統(tǒng)如何通過散列表進行 retain 操作的(retain 操作不僅包括散列表的联四,還有 NONPOINTER_ISA 的,通過前面的學習不難理解撑教,這里先只貼出關于散列表的 retain 操作)

id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];
    
    table.lock();
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    table.unlock();

    return (id)this;
}

從這里我們可以看出朝墩,散列表的 retain 操作首先是經(jīng)過兩次哈希查找(一次是SideTables()[this] 是前面那個取余的算法 StripedMap 里的 indexForPointer 函數(shù),一個是table.refcnts[this]前面說的偽裝算法)找到引用計數(shù)伟姐,然后進行加 SIDE_TABLE_RC_ONE 的操作收苏。

看看SIDE_TABLE_RC_ONE的定義:#define SIDE_TABLE_RC_ONE (1UL<<2) // MSB-ward of deallocating bit 這個說明是兩位比特,每次加 4愤兵。引用計數(shù)不是每引用一次加 1 嗎?為什么會是 4?原因請看下圖:

refcnts 里面存的 size_t 的結構

我們可以看到函數(shù)的一開始做了一個斷言 assert(!isa.nonpointer); 這說明 isa 使用 NONPOINTER_ISA 的情況下會拋出異常弊知,這個函數(shù)是只有在不使用 NONPOINTER_ISA 技術的情況下才會調(diào)用纪铺,這時我們不能從 isa 指針中找到 weakly_referenced 和 deallocating 的值,所以會保存在引用計數(shù)表當中屹堰。這樣為了保證后兩位不被改變肛冶,每次引用計數(shù)的“加 1”操作實際上是加 4。

當然對于使用 NONPOINTER_ISA 技術的對象双藕,它的 retain 操作并不是那么簡單淑趾,因為這樣做完全放棄了 isa 指針中 19 位的 extra_rc。而且 weakly_referenced 和 deallocating 也重復寫入了忧陪,系統(tǒng)會有更好的方式去解決這種情況扣泊,我們稍后討論。

3嘶摊、對弱引用表的操作

從注釋來看延蟹,這是一個全局的弱引用表,將對象的 id 當做 key 值叶堆,weak_entry_t 結構體變量為 value 值阱飘,顯然這是一個通過哈希查找來確定對象存放弱引用指針集合的地方。weak_entry_t 里面就存儲著對象所有的 weak 指針虱颗。

/**
 * The global weak references table. Stores object ids as keys,
 * and weak_entry_t structs as their values.
 */
struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};
struct weak_entry_t {
    DisguisedPtr<objc_object> referent;
    union {
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };

    bool out_of_line() {
        return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
    }

    weak_entry_t& operator=(const weak_entry_t& other) {
        memcpy(this, &other, sizeof(other));
        return *this;
    }

    weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
        : referent(newReferent)
    {
        inline_referrers[0] = newReferrer;
        for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
            inline_referrers[i] = nil;
        }
    }
};

為了更好的理解系統(tǒng)對弱引用表的操作沥匈,我們探討一下 __weak 修飾的對象初始化的原理。

① __weak修飾對象的初始化原理
{
    NSObject *obj = [[NSObject alloc] init];
    id __weak obj1 = obj;
}

id __weak obj1 = obj 事實上是調(diào)用 NSObject.mm 中的 id objc_initWeak(id *location, id newObj) 函數(shù)忘渔。

/* @param location Address of __weak ptr. 
 * @param newObj Object ptr. 
 */
id
objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

objc_initWeak 有兩個參數(shù)高帖,第一個參數(shù)是 __weak 修飾的指針,第二個是賦值的對象畦粮,具體到第一部分的代碼就是:location 是 obj1 的指針散址,還沒有賦值的情況下指向 nil乖阵,newObj 是 obj 對象。這里面主要調(diào)用 storeWeak 函數(shù)预麸。<> 中的三個變量瞪浸,點進去后發(fā)現(xiàn)它們分別是 false、true吏祸、true对蒲。我們看一下 storeWeak 這個函數(shù)

template <HaveOld haveOld, HaveNew haveNew,
          CrashIfDeallocating crashIfDeallocating>
static id 
storeWeak(id *location, objc_object *newObj)
{
// 根據(jù)傳遞過來的情況參數(shù)分別是:
// location:__weak 指針
// newObj:obj 對象
// haveOld: 初始化的時候 false,第二次可能為 true
// haveNew: 賦值 nil 為 false犁罩,賦值對象為 true
    assert(haveOld  ||  haveNew);
    if (!haveNew) assert(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;

    // Acquire locks for old and new values.
    // Order by lock address to prevent lock ordering problems. 
    // Retry if the old value changes underneath us.
 retry:
    if (haveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (haveNew) {
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }

    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);

    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }

    // Prevent a deadlock between the weak reference machinery
    // and the +initialize machinery by ensuring that no 
    // weakly-referenced object has an un-+initialized isa.
    if (haveNew  &&  newObj) {
        Class cls = newObj->getIsa();
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        {
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            class_initialize(cls, (id)newObj);

            // If this class is finished with +initialize then we're good.
            // If this class is still running +initialize on this thread 
            // (i.e. +initialize called storeWeak on an instance of itself)
            // then we may proceed but it will appear initializing and 
            // not yet initialized to the check above.
            // Instead set previouslyInitializedClass to recognize it on retry.
            previouslyInitializedClass = cls;

            goto retry;
        }
    }

    // Clean up old value, if any.
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    // Assign new value, if any.
    if (haveNew) {
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating);
        // weak_register_no_lock returns nil if weak store should be rejected

        // Set is-weakly-referenced bit in refcount table.
        if (newObj  &&  !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }

        // Do not set *location anywhere else. That would introduce a race.
        *location = (id)newObj;
    }
    else {
        // No new value. The storage is not changed.
    }
    
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

    return (id)newObj;
}

根據(jù)傳遞過來的情況參數(shù)分別是:

  • location:__weak 指針
  • newObj:obj 對象
  • haveOld: 初始化的時候 false齐蔽,第二次可能為 true
  • haveNew: 賦值 nil 為 false,賦值對象為 true

這里定義了兩個表床估,舊表和新表(oldTable含滴、newTable),分別在 haveOld 和 haveNew 時使用 SideTables()[] 進行賦值丐巫。

由于多線程并發(fā)操作在為新舊表上鎖的時候谈况,location 的內(nèi)容可以已經(jīng)被修改(weak 指針指向其他內(nèi)存塊),需要判斷 if (haveOld && *location != oldObj)递胧,如果被修改要回到 retry 重新執(zhí)行碑韵。

if (haveNew && newObj) 這個分支是處理 newObj 沒有完成初始化的情況。下面是核心

看 haveOld 的分支缎脾,如果 weak 指針之前已經(jīng)指向了一個內(nèi)存塊祝闻,比如第二次為 location 賦值,haveOld 為 true遗菠,這時需要將 weak 指針從表中刪除(通過 weak_unregister_no_lock 函數(shù))联喘。也就是說當 weakObj = nil 代碼執(zhí)行時,系統(tǒng)會自動刪除弱引用表中的相應指針辙纬。也就是說在對象被釋放時豁遭,只需 weak_unregister_no_lock 所有弱引用指針就可以了。

上面已經(jīng)把表中的 weak 指針刪除了贺拣,如果 haveNew == true蓖谢,就需要再次添加回來,這里調(diào)用了 weak_register_no_lock 函數(shù)譬涡,我們看一看它的具體內(nèi)容闪幽,了解系統(tǒng)如何操作弱引用表。

/** 
 * @param weak_table The global weak table.
 * @param referent The object pointed to by the weak reference.
 * @param referrer The weak pointer address.
 */
id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, bool crashIfDeallocating)
{
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;

    if (!referent  ||  referent->isTaggedPointer()) return referent_id;

    // ensure that the referenced object is viable
    bool deallocating;
    if (!referent->ISA()->hasCustomRR()) {
        deallocating = referent->rootIsDeallocating();
    }
    else {
        BOOL (*allowsWeakReference)(objc_object *, SEL) = 
            (BOOL(*)(objc_object *, SEL))
            object_getMethodImplementation((id)referent, 
                                           SEL_allowsWeakReference);
        if ((IMP)allowsWeakReference == _objc_msgForward) {
            return nil;
        }
        deallocating =
            ! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
    }

    if (deallocating) {
        if (crashIfDeallocating) {
            _objc_fatal("Cannot form weak reference to instance (%p) of "
                        "class %s. It is possible that this object was "
                        "over-released, or is in the process of deallocation.",
                        (void*)referent, object_getClassName((id)referent));
        } else {
            return nil;
        }
    }

    // now remember it and where it is being stored
    weak_entry_t *entry;
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        append_referrer(entry, referrer);
    } 
    else {
        weak_entry_t new_entry(referent, referrer);
        weak_grow_maybe(weak_table);
        weak_entry_insert(weak_table, &new_entry);
    }

    // Do not set *referrer. objc_storeWeak() requires that the 
    // value not change.

    return referent_id;
}

我們看一下各參數(shù)的意義:

  • weak_table:對象所在的弱引用表
  • referent_id:新對象 obj
  • referrer_id:__weak 指針

我們直接看函數(shù)的后面 // now remember it and where it is being stored 注釋后面的內(nèi)容涡匀。

這個過程就是通過 weak_entry_for_referent 函數(shù)得到弱引用表中對象所有弱引用指針集合 entry,它的定義可以看前面的代碼渊跋,如果存在 entry腊嗡,就把 weak 指針添加到 entry 中(使用 append_referrer),不存在 entry 時拾酝,需要創(chuàng)建一個新的 entry燕少,第一個元素就是 weak 指針,然后使用 weak_entry_insert 插入弱引用表中蒿囤。

我們看一下 weak_entry_for_referent 函數(shù)客们,查看 entry 的查找是否為哈希運算

static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
    assert(referent);

    weak_entry_t *weak_entries = weak_table->weak_entries;

    if (!weak_entries) return nil;

    size_t begin = hash_pointer(referent) & weak_table->mask;
    size_t index = begin;
    size_t hash_displacement = 0;
    while (weak_table->weak_entries[index].referent != referent) {
        index = (index+1) & weak_table->mask;
        if (index == begin) bad_weak_table(weak_table->weak_entries);
        hash_displacement++;
        if (hash_displacement > weak_table->max_hash_displacement) {
            return nil;
        }
    }
    
    return &weak_table->weak_entries[index];
}

里面使用 index 來獲取數(shù)組 weak_entries 中的 entry,系統(tǒng)一開始通過新對象來獲取 index 可能的最小值材诽,然后通過循環(huán)遍歷來找到 具體的 entry 值底挫。可以說系統(tǒng)使用了哈希算法找到了 entry 位置的最小值脸侥,但是之后通過的是遍歷獲取 entry 具體的位置建邓。

4、對散列表結構的思考及自旋鎖的作用
散列表結構

我們看到系統(tǒng)無論是操作引用計數(shù)還是弱引用指針集都需要經(jīng)過兩次哈希查找睁枕,而對象的地址唯一官边,完全可以只需要一個 SideTable,這樣只需要一次查找就可以找到它們外遇,這樣做的效率似乎更高注簿,為何系統(tǒng)不這樣使用?

原因與系統(tǒng)的多線程并發(fā)有關跳仿,系統(tǒng)的每個線程都有可能訪問散列表中的數(shù)據(jù)诡渴,如果不對散列表進行保護,在多線程并發(fā)情況下容易造成數(shù)據(jù)錯亂菲语,故而 SideTable 使用了自旋鎖妄辩,即在某個線程訪問 SideTable 時,其他線程只能等待該線程訪問完才可以訪問谨究,如果系統(tǒng)只有一張 SideTable恩袱,那么會造成散列表數(shù)據(jù)的訪問總是在等待中。

不給每個對象都配一套自旋鎖的原因是自旋鎖本身會占用一定的系統(tǒng)資源胶哲,而且系統(tǒng)的線程數(shù)不可能無限畔塔,而對象的數(shù)量遠遠超過線程的數(shù)量。這也是 SideTable 只有 8 或 64 個的原因鸯屿。

五澈吨、引用計數(shù)及相應內(nèi)存管理方法

當一段代碼需要訪問某個對象時,該對象的引用計數(shù)加 1寄摆;當不再訪問時引用計數(shù)減 1谅辣,當引用計數(shù)為 0 時,系統(tǒng)回收對象所占內(nèi)存婶恼。
一般來說:

  • 當程序調(diào)用 alloc桑阶、new柏副、copy、mutableCopy 開頭的方法時該對象的引用計數(shù)加 1蚣录。
  • 調(diào)用 retain 方法時割择,該對象的引用計數(shù)加 1.
  • 調(diào)用 release 方法時,該對象引用計數(shù)減 1.

iOS 中提供如下引用計數(shù)方法
retain萎河、release荔泳、autorelease、retainCount虐杯、dealloc

1玛歌、MRC 和 ARC 的區(qū)別
  • MRC 是手動引用計數(shù),需要手動調(diào)用引用計數(shù)相關方法擎椰,ARC 是自動引用計數(shù)支子,系統(tǒng)在合適的時機自動調(diào)用引用計數(shù)相關方法,禁止手動調(diào)用引用計數(shù)相關方法确憨。

  • ARC 是編譯器和 Runtime 協(xié)作的結果译荞。

  • ARC 中新增 weak、strong 屬性關鍵字休弃。

2吞歼、retain

retain 是使對象引用計數(shù)加 1 的方法,前面已經(jīng)討論過存散列表情況下的 retain 操作塔猾,但是在使用 NONPOINTER_ISA 情況下篙骡,以及二者混合使用的情況還沒有討論,那么我們看一下 retain 的具體實現(xiàn)丈甸。

- (id)retain {
    return ((id)self)->rootRetain();
}

ALWAYS_INLINE id 
objc_object::rootRetain()
{
    return rootRetain(false, false);
}

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

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            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
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        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;
            newisa.extra_rc = RC_HALF;
            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.
        sidetable_addExtraRC_nolock(RC_HALF);
    }

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

以上三個函數(shù)是 retain 調(diào)用后的具體過程糯俗,我們主要看最后一個 rootRetain 函數(shù)。

首先判斷該對象是否使用 TaggedPointer 技術睦擂,非 TaggedPointer 對象才可繼續(xù)進行得湘。在 do while 循環(huán)中 if (slowpath(!newisa.nonpointer)),這是沒有 NONPOINTER_ISA 技術的對象情況顿仇,此時的情況就是上節(jié)討論的散列表 retain 操作淘正,調(diào)用了 sidetable_retain 函數(shù),然后返回掉臼闻。

然后是 if (slowpath(tryRetain && newisa.deallocating)) 的分支鸿吆,說明對象正在被釋放,此時將 SideTable 解鎖述呐,然后返回掉惩淳。

接下來就是使用 NONPOINTER_ISA 技術的對象了,它進行了 newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++ 操作乓搬∷祭纾看后面的注釋代虾,說明這是給 extra_rc 加 1 操作,這也驗證了前面說的抒倚,19 位 extra_rc(模擬器 8 位)是用來存儲引用計數(shù)的了褐着。

循環(huán)體中還有一個 if (slowpath(carry)) 的分支,這個分支是用來處理 extra_rc 溢出的情況托呕。我們只看最后三行代碼,首先 transcribeToSideTable = true频敛,這是告訴后面需要往散列表中寫數(shù)據(jù)了项郊,循環(huán)結束后第一個分支判斷就是基于這個數(shù)據(jù)。newisa.extra_rc = RC_HALF斟赚,是給 extra_rc 重新賦值着降。

# if __arm64__
#   define RC_HALF  (1ULL<<18)

# elif __x86_64__
#   define RC_HALF  (1ULL<<7)

# else
#   error unknown architecture for packed isa
# endif

我們看一下 RC_HALF 這個只,在 arm64 下是 218拗军,在 x86_64 下是 27任洞,正好是 extra_rc 最大值的一半。所以就是每次 extra_rc 達到最大值的時候发侵,就將 extra_rc 減少到一半交掏,后面一半的內(nèi)容累加到散列表中。

newisa.has_sidetable_rc = true 很好理解刃鳄,因為溢出就使用了散列表盅弛,所以為 true,這個變量也是存在 NONPOINTER_ISA 指針中的叔锐,前面小節(jié)(三)中總結的表中有這個變量挪鹏。

然后我們看溢出后怎樣把額外引用計數(shù)存儲在散列表中,就是循環(huán)體外 if (slowpath(transcribeToSideTable)) 分支愉烙,我們看一下里面的函數(shù) sidetable_addExtraRC_nolock 注釋告訴我們是要 Copy 另一半的 retain count 到 SideTable 中讨盒。

bool 
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
    assert(isa.nonpointer);
    SideTable& table = SideTables()[this];

    size_t& refcntStorage = table.refcnts[this];
    size_t oldRefcnt = refcntStorage;
    // isa-side bits should not be set here
    assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
    assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);

    if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;

    uintptr_t carry;
    size_t newRefcnt = 
        addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
    if (carry) {
        refcntStorage =
            SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
        return true;
    }
    else {
        refcntStorage = newRefcnt;
        return false;
    }
}

addc 函數(shù)的調(diào)用size_t newRefcnt = addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry) 很好的說明了將 delta_rc 添加到 oldRefcnt 上面。
下面通過一幅流程圖來更加清晰的了解一下其中的過程:

retain 流程圖.jpg

3步责、release

release 的操作就是和 retain 的操作相反返顺,同樣是先判斷 TaggedPointer,再判斷 NONPOINTER_ISA勺择,然后 extra_rc 減 1创南,減到 0,從 SideTable 中拿出 RC_HALF 的數(shù)據(jù)到 extra_rc省核,引用計數(shù)為 0 時稿辙,如果需要調(diào)用 dealloc 釋放。這里對 release 就不做詳細說明了气忠。

4邻储、autorelease 和 @autoreleasepool

autoreleasepool 本質(zhì)上是一個以棧為結點的雙向鏈表結構赋咽,結構如下圖:


autoreleasepool 數(shù)據(jù)結構

一個 AutoreleasePoolPage 擁有 4096 個字節(jié)用來存放加入 autoreleasepool 的對象等信息,當 AutoreleasePoolPage 填滿時吨娜,會自動創(chuàng)建一個 child 開辟新的空間脓匿。

AutoreleasePoolPage 主要有push、pop宦赠、autorelease 等方法陪毡。

@autoreleasepool {} 使用 clang 編譯后(clang -rewrite-objc main.m)main.cpp 文件中最后會出現(xiàn)如下代碼

{ __AtAutoreleasePool __autoreleasepool; 
       
}

全局搜索 __AtAutoreleasePool,可以找到如下結構體

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

也就是說 @autoreleasepool {} 本質(zhì)上是如下代碼

    {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();
        // autoreleasepool 里面的代碼
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }

objc_autoreleasePoolPushobjc_autoreleasePoolPop 就是類 AutoreleasePoolPagepush勾扭、pop 方法

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

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

NSObject *obj1 = obj; 這種代碼在 ARC 環(huán)境下會自動插入 autorelease毡琉,如下

NSObject *obj1 = obj;
[obj1 autorelease];

下面我用一個示例來說明一下 autoreleasepool 的工作原理

int main(int argc, const char * argv[]) {
    NSObject *obj = [NSObject new];
    @autoreleasepool {
        NSObject *obj1 = obj;
        @autoreleasepool {
            NSObject *obj2 = obj;
            // ......
            // 假設 objm 正好是第一個 AutoreleasePoolPage 的棧頂
            NSObject *objm = obj;
            NSObject *objm1 = obj;
            NSObject *objm2 = obj;
            @autoreleasepool {
                NSObject *objm3 = obj;
                // ......
                NSObject *objn = obj;
                
            }
        }
    }
    return 0;
}

main 函數(shù)中第一個 @autoreleasepool,開始會調(diào)用 push 方法

static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

我們正常開發(fā) iOS 的時候都是走的第二個分支妙色,即 dest = autoreleaseFast(POOL_BOUNDARY);桅滋,這是每個AutoreleasePoolPage 填滿后創(chuàng)建新的(第一個分支是每次 push 都新建 AutoreleasePoolPage,我們不做進一步了解)身辨,POOL_BOUNDARY 就是 nil丐谋,也就是每次 push 實際上就是調(diào)用 autoreleaseFast(nil)

    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);
        }
    }

autoreleaseFast 里面有三個分支煌珊,首先獲取當前的 page号俐,如果有 page 并且沒有被填滿,就 add 一個參數(shù)怪瓶,第一個 autoreleasepool 的 push 不會走這個分支萧落,因為此時還沒有創(chuàng)建 page。第二個分支是 page 被填滿的情況洗贰,這一次也不會走找岖,我們直接看第三個分支 autoreleaseNoPage

    id *autoreleaseNoPage(id obj)
    {
        敛滋。许布。。绎晃。蜜唾。。/ 前面還有代碼庶艾,不做了解
        // Install the first page.
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        
        // Push a boundary on behalf of the previously-placeholder'd pool.
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        
        // Push the requested object or pool.
        return page->add(obj);
    }

這個函數(shù)的功能就是新建一個 page袁余,然后加到 hotPage 中(加到雙向鏈表的最后一項,作為上一個 page 的 child)咱揍,然后調(diào)用 add 函數(shù)颖榜,具體到本次就是 add(nil)

    id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }

*next++ = obj; 意思是 *next = obj; next++;,所以 add 函數(shù)是把 obj 寫入 next 指針,然后指針進一位掩完,知道棧頂(達到 4096 字節(jié))

那么第一個 @autoreleasepool 的 push 作用是創(chuàng)建一個 AutoreleasePoolPage噪漾,并插入一個 nil,如下圖:

新建第一個AutoreleasePoolPage

之后的 NSObject *obj1 = obj; ARC 環(huán)境下自動插入 [obj1 autorelease]; 我們看看 autorelease 方法的實現(xiàn)

-(id) autorelease
{
    return _objc_rootAutorelease(self);
}
id
_objc_rootAutorelease(id obj)
{
    assert(obj);
    return obj->rootAutorelease();
}
inline id 
objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}
__attribute__((noinline,used))
id 
objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}

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;
    }

我們看上面的調(diào)用過程且蓬,最終調(diào)用 AutoreleasePoolPage 的 autorelease 方法欣硼。autorelease 方法中仍然調(diào)用 autoreleaseFast 方法,與 push 相同恶阴,但是本次插入不是 nil 而是 autorelease 的對象诈胜,本次就是 obj1。故而如下圖:

插入 obj1

我們看接下來第二個 @autoreleasepool 的 push冯事,然后從 obj2 到 objm 的插入(objm 正好為棧頂)耘斩,如下圖:

插入 objm

我們看當插入 objm1,的時候桅咆,第一個 AutoreleasePoolPage 已滿,看 autoreleaseFast 走第二個分支坞笙,即調(diào)用 autoreleaseFullPage

static __attribute__((noinline))
    id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
    {
        // The hot page is full. 
        // Step to the next non-full page, adding a new page if necessary.
        // Then add the object to that page.
        assert(page == hotPage());
        assert(page->full()  ||  DebugPoolAllocation);

        do {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

        setHotPage(page);
        return page->add(obj);
    }

這里有一個 do while 循環(huán)岩饼,本次會走一次,走 else 分支薛夜,新建一個 AutoreleasePoolPage籍茧,將當前 page 作為 parent 傳遞給 new page,然后 page 指針指向 new page梯澜。最后把 objm 加到 new page 上面寞冯。如下圖:

插入 objm1

之后一直到 objn


插入 objn

之后最里面的 @autoreleasepool 結束,調(diào)用 pop 方法晚伙。

 static inline void pop(void *token) 
    {
        AutoreleasePoolPage *page;
        id *stop;

        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            // Popping the top-level placeholder pool.
            if (hotPage()) {
                // Pool was used. Pop its contents normally.
                // Pool pages remain allocated for re-use as usual.
                pop(coldPage()->begin());
            } else {
                // Pool was never used. Clear the placeholder.
                setHotPage(nil);
            }
            return;
        }

        page = pageForPointer(token);
        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            if (stop == page->begin()  &&  !page->parent) {
                // Start of coldest page may correctly not be POOL_BOUNDARY:
                // 1. top-level pool is popped, leaving the cold page in place
                // 2. an object is autoreleased with no pool
            } else {
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                return badPop(token);
            }
        }

        if (PrintPoolHiwat) printHiwat();

        page->releaseUntil(stop);

        // memory: delete empty children
        if (DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
            // special case: delete everything for pop(top) 
            // when debugging missing autorelease pools
            page->kill();
            setHotPage(nil);
        } 
        else if (page->child) {
            // hysteresis: keep one empty child if page is more than half full
            if (page->lessThanHalfFull()) {
                page->child->kill();
            }
            else if (page->child->child) {
                page->child->child->kill();
            }
        }
    }

pop 方法中的參數(shù) token 是 @autoreleasepool push 方法的返回值吮龄,這是成對存在的,之前最后一個 @autoreleasepool 的返回值是插入 POOL_BOUNDARY(nil)的指針咆疗,所以參數(shù)告訴我們從哪個地址開始 pop漓帚。

stop = (id *)token; page->releaseUntil(stop);,這兩句就是具體的 pop 操作午磁,我們看看具體實現(xiàn)

void releaseUntil(id *stop) 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release 
            // autoreleased more objects
            AutoreleasePoolPage *page = hotPage();

            // fixme I think this `while` can be `if`, but I can't prove it
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            id obj = *--page->next;
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }

        setHotPage(this);

#if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            assert(page->empty());
        }
#endif
    }

我們先看 id obj = *--page->next; 拆解開后是 page->next --; id obj = *page->next; 外循環(huán)體中每次循環(huán) next-1尝抖,然后 objc_release(obj); 釋放一次,最終 next 會逼近 stop 最終退出循環(huán)迅皇,我們看最里面的 @autoreleasepool pop 會怎么樣昧辽?如圖:

最后一個 autoreleasepool pop

第二個 @autoreleasepool pop 會跨兩個 AutoreleasePoolPage,具體邏輯請看上面 pop 代碼中的內(nèi)循環(huán)登颓,如果 page->empty()(page 為空) page 就指向他的父節(jié)點繼續(xù)查找搅荞。pop 函數(shù)中releaseUntil后面的代碼也告訴我們,系統(tǒng)會清理掉當前 page 的 child。

第三個 @autoreleasepool 會釋放 obj1 和第一個 AutoreleasePoolPage取具,這里不多解釋脖隶。

至此關于自動釋放池的部分就說明到這里

5、運行時優(yōu)化(Thread Local Storage)

從上小節(jié)可知自動釋放池是一個雙向鏈表暇检,每個結點都是有 AutoreleasePoolPage 的對象組成产阱,它的結構比較復雜,開銷比較大块仆,對于這種現(xiàn)狀构蹬,系統(tǒng)提供了一種 Thread Local Storage 技術在 ARC 環(huán)境下對對象的自動釋放做了優(yōu)化,使其性能大大高于自動釋放池技術悔据。

對于一個工廠方法我們分別分析使用 @autoreleasepoolThread Local Storage 的情況

@autoreleasepool

//MRC
+ (instancetype)createObj {
id any = [[CustomClass alloc]init];
return [any autorelease];
}

CustomClass *obj = [CustomClass createObj];

類方法 + createObj 創(chuàng)建的對象不會在方法結束時被銷毀庄敛,而是在 autoreleasepool 執(zhí)行 pop 操作的時候釋放。這樣將對象放入自動釋放出科汗,和把對象從自動釋放出取出的過程消耗了一定的資源

ARC 中對 autorelease 進行的優(yōu)化 —— Thread Local Storage

//ARC
+ (instancetype)createObj {
    id tmp = [[self alloc]init];
    return objc_autoreleaseReturnValue(tmp);
}

id tmp = objc_retainAutoreleasedReturnValue([CustomClass createObj]);
CustomClass * obj = tmp;
objc_storeStrong(&obj, nil);//就是release

這里面主要使用了一下三個函數(shù) objc_autoreleaseReturnValue藻烤、objc_retainAutoreleasedReturnValue、objc_storeStrong

id 
objc_autoreleaseReturnValue(id obj)
{
    if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;

    return objc_autorelease(obj);
}

id
objc_retainAutoreleasedReturnValue(id obj)
{
    if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;

    return objc_retain(obj);
}

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

objc_storeStrong 是對對象的 release 操作头滔,不多做解釋怖亭。
我們看看另外兩個函數(shù)

objc_autoreleaseReturnValue、objc_retainAutoreleasedReturnValue 成對使用坤检,首先 objc_autoreleaseReturnValue 時如果 prepareOptimizedReturn(ReturnAtPlus1) == YES 代表需要進行優(yōu)化兴猩,那么直接返回 obj,不把它加入自動釋放池早歇,此時 obj 的引用計數(shù)為 1倾芝,不會被釋放。objc_retainAutoreleasedReturnValue 會經(jīng)過 (acceptOptimizedReturn() == ReturnAtPlus1 判斷箭跳,說明 obj 是經(jīng)過優(yōu)化的 obj晨另,這樣就不會進行 retain 操作,這樣引用計數(shù)還是 1衅码,最后經(jīng)過 objc_storeStrong 把 obj 釋放掉拯刁。

這樣使用 Thread Local Storage 技術就可以減少 obj 對象進入自動釋放池,從自動釋放池出來的操作逝段,又減少了一次 retain 操作和一次 release 操作從而大大降低了系統(tǒng)的消耗(如果是 @autoreleasepool 情況:工廠函數(shù)會 retain 一次垛玻,外界調(diào)用會 retain 一次,@autoreleasepool pop 時會釋放一次奶躯,調(diào)用結束會釋放一次)帚桩。

Thread Local Storage 優(yōu)化技術有使用條件,那就是工廠方法和調(diào)用方都支持 ARC嘹黔,因為只有這樣方法內(nèi)的objc_autoreleaseReturnValueobjc_retainAutoreleasedReturnValue才會配套使用.很多系統(tǒng)庫還可能是 MRC 實現(xiàn)的,這樣的系統(tǒng)類調(diào)用工廠方法生成的對象還是得進 @autoreleasepool账嚎。

這也是為何 ARC 模式下也要保留 @autoreleasepool 模式的原因之一莫瞬。

這也說明了只要在 ARC 環(huán)境下我們平常寫的代碼都會使用 Thread Local Storage 技術進行優(yōu)化,而不進入自動釋放池郭蕉。(著重強調(diào)疼邀,因為與直觀不符,我們平常寫的代碼一般不會進入 @autoreleasepool)

這里對 Thread Local Storage 技術只做簡單介紹召锈,對于平常理解和面試一般夠用旁振,如果想要更加詳細的理解請看 iOS 底層拾遺:autorelease 優(yōu)化

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末涨岁,一起剝皮案震驚了整個濱河市拐袜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌梢薪,老刑警劉巖蹬铺,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異秉撇,居然都是意外死亡甜攀,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門琐馆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赴邻,“玉大人,你說我怎么就攤上這事啡捶。” “怎么了奸焙?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵瞎暑,是天一觀的道長。 經(jīng)常有香客問我与帆,道長了赌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任玄糟,我火速辦了婚禮勿她,結果婚禮上,老公的妹妹穿的比我還像新娘阵翎。我一直安慰自己逢并,他們只是感情好,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布郭卫。 她就那樣靜靜地躺著砍聊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贰军。 梳的紋絲不亂的頭發(fā)上玻蝌,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機與錄音,去河邊找鬼俯树。 笑死帘腹,一個胖子當著我的面吹牛叭莫,可吹牛的內(nèi)容都是我干的冕广。 我是一名探鬼主播吧恃,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼徙缴,長吁一口氣:“原來是場噩夢啊……” “哼鄙才!你這毒婦竟也來了洪己?” 一聲冷哼從身側響起公条,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤血当,失蹤者是張志新(化名)和其女友劉穎翘贮,沒想到半個月后赊窥,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡狸页,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年锨能,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芍耘。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡址遇,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出斋竞,到底是詐尸還是另有隱情倔约,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布坝初,位于F島的核電站浸剩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏鳄袍。R本人自食惡果不足惜绢要,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望拗小。 院中可真熱鬧重罪,春花似錦、人聲如沸哀九。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽阅束。三九已至惨篱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間围俘,已是汗流浹背砸讳。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工琢融, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人簿寂。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓漾抬,卻偏偏與公主長得像,于是被迫代替她去往敵國和親常遂。 傳聞我的和親對象是個殘疾皇子纳令,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

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