iOS-內(nèi)存管理分析(上)

前言

我們知道內(nèi)存管理在任何一門編程語言中都有極其重要的地位误阻,即然極其重要行拢,也就意味著有難點(diǎn),今天我們就來剖析iOS的內(nèi)存管理相關(guān)的知識(shí)痕囱。

1 內(nèi)存五大區(qū)

內(nèi)核區(qū)田轧,用戶區(qū)。
用戶內(nèi)存五大區(qū):堆鞍恢、棧傻粘、bss(未初始化數(shù)據(jù))、data(已初始化數(shù)據(jù))帮掉、text(代碼段)弦悉。

  • 棧區(qū):局部變量,方法參數(shù)旭寿,函數(shù)警绩,內(nèi)存地址一般為:0x7開頭
  • 堆區(qū):通過alloc分配的對象崇败,block copy盅称,內(nèi)存地址一般為:0x6開頭
  • BSS段:未初始化的全局變量肩祥,靜態(tài)變量,內(nèi)存地址一般為:0x1開頭
  • 數(shù)據(jù)段: 初始化的全局變量缩膝,靜態(tài)變量混狠,內(nèi)存地址一般為:0x1開頭
  • text:程序代碼,加載到內(nèi)存中

棧區(qū)的內(nèi)存是通過sp寄存器來定位疾层。
棧區(qū)的速度要比堆區(qū)速度快

2 內(nèi)存管理方案

內(nèi)存管理方案一般有:ARC/MRC
在ARC/MRC比較常見有taggedPointer将饺、NONPOINTER_ISA散列表(引用計(jì)數(shù)表痛黎,弱引用表)
我們就來分析下這些內(nèi)存管理方案予弧。

2.1 taggedPointer底層分析

TaggedPointer:小對象-NSNumber,NSDate
TaggedPointer 是一個(gè)指針湖饱,會(huì)比普通的指針多了Tagged掖蛤,針對小對象,NSNumber井厌,NSDate蚓庭,NSString,UIColor仅仆,我們存這些對象的時(shí)候器赞,其實(shí)用不到64位,為了節(jié)省內(nèi)存空間墓拜,使用其部分位港柜,小對象=指針 + 存儲(chǔ)的內(nèi)容。

2.1.1 x86-64下的taggedPointer結(jié)構(gòu)

我們看以下代碼咳榜,來分析taggedPointer在x86上的結(jié)構(gòu)
代碼如下

uintptr_t
ro_objc_decodeTaggedPointer(id ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

- (void)indexPathDemo{
   NSString *str = [NSString stringWithFormat:@"ro"];
    NSLog(@"%p-%@-%@",str,str,str.class);
    NSLog(@"%p-%@ -%@- 0x%lx",str,str,str.class,ro_objc_decodeTaggedPointer(str));
}

運(yùn)行項(xiàng)目潘懊,如圖


1

我們知道堆區(qū)是0x7開頭,棧是0x6開頭贿衍,全局0x1開頭授舟,那么這個(gè)地址很奇怪,我們來分析下贸辈。
我們在objc源碼中搜下taggedPointer释树,如下所示

// payload = (decoded_obj << payload_lshift) >> payload_rshift 

payload就是有效負(fù)載,經(jīng)過位運(yùn)算擎淤,加密和解密的過程奢啥。
我們分析一下decoded_obj這個(gè),源碼中嘴拢,搜索decoded桩盲,找到如下代碼

static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    uintptr_t value = _objc_decodeTaggedPointer_noPermute(ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;

    value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
    value |= _objc_obfuscatedTagToBasicTag(basicTag) << _OBJC_TAG_INDEX_SHIFT;
#endif
    return value;
}

_objc_decodeTaggedPointer_noPermute的源碼如下

static inline uintptr_t
_objc_decodeTaggedPointer_noPermute(const void * _Nullable ptr)
{
    uintptr_t value = (uintptr_t)ptr;
#if OBJC_SPLIT_TAGGED_POINTERS
    if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
        return value;
#endif
    return value ^ objc_debug_taggedpointer_obfuscator;
}

value ^ objc_debug_taggedpointer_obfuscator;這里進(jìn)行異或運(yùn)行,混淆席吴。
我們再找下objc_debug_taggedpointer_obfuscator這個(gè)赌结,

/***********************************************************************
* initializeTaggedPointerObfuscator
* Initialize objc_debug_taggedpointer_obfuscator with randomness.
*
* The tagged pointer obfuscator is intended to make it more difficult
* for an attacker to construct a particular object as a tagged pointer,
* in the presence of a buffer overflow or other write control over some
* memory. The obfuscator is XORed with the tagged pointers when setting
* or retrieving payload values. They are filled with randomness on first
* use.
**********************************************************************/
static void
initializeTaggedPointerObfuscator(void)
{
    if (!DisableTaggedPointerObfuscation) {
        // 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;

#if OBJC_SPLIT_TAGGED_POINTERS
        // The obfuscator doesn't apply to any of the extended tag mask or the no-obfuscation bit.
        objc_debug_taggedpointer_obfuscator &= ~(_OBJC_TAG_EXT_MASK | _OBJC_TAG_NO_OBFUSCATION_MASK);

        // Shuffle the first seven entries of the tag permutator.
        int max = 7;
        for (int i = max - 1; i >= 0; i--) {
            int target = arc4random_uniform(i + 1);
            swap(objc_debug_tag60_permutations[i],
                 objc_debug_tag60_permutations[target]);
        }
#endif
    } else {
        // Set the obfuscator to zero for apps linked against older SDKs,
        // in case they're relying on the tagged pointer representation.
        objc_debug_taggedpointer_obfuscator = 0;
    }
}

initializeTaggedPointerObfuscator這里初始化

arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                       sizeof(objc_debug_taggedpointer_obfuscator));
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;

生成一個(gè)隨機(jī)數(shù)捞蛋,在整個(gè)內(nèi)存里是一個(gè)encode狀態(tài),它的真正地址是需要decode柬姚。
通過兩個(gè)異或(相同為0拟杉,不同為1)運(yùn)算,就可以得到真實(shí)的值量承。

uintptr_t
ro_objc_decodeTaggedPointer(id ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}  

所以這里我們通過這樣的異或運(yùn)算就可以得到真實(shí)的值搬设。
運(yùn)行項(xiàng)目,如圖

2

0xa00000000006f722就是真實(shí)的指針撕捍,我們解析下這個(gè)地址拿穴。
3

  • 第1位的1代表是taggedPointer類型
    我們再來看下面一張圖,如


    4

    114對應(yīng)o,111對應(yīng)r忧风,也就是我們的字符串贞言。
    我們再改下代碼,如

 NSString *str = [NSString stringWithFormat:@"ro"];
    NSLog(@"%p-%@-%@",str,str,str.class);
    NSLog(@"%p-%@ -%@- 0x%lx",str,str,str.class,ro_objc_decodeTaggedPointer(str));
    
    NSString *str1 = [NSString stringWithFormat:@"r"];

    NSLog(@"%p-%@ -%@- 0x%lx",str1,str1,str1.class,ro_objc_decodeTaggedPointer(str1));

    NSString *str2 = [NSString stringWithFormat:@"robert"];
    NSLog(@"%p-%@ -%@- 0x%lx",str2,str2,str2.class,ro_objc_decodeTaggedPointer(str2));
//
    NSNumber *number = @6;
    NSLog(@"%p-%@ -%@- 0x%lx",number,number,number.class,ro_objc_decodeTaggedPointer(number));

我們看下結(jié)果阀蒂,如圖

5

0xa對應(yīng)的是NSString,0xb對應(yīng)的是NSNumber该窗,我們來看下objc_tag

{
    // 60-bit payloads
    OBJC_TAG_NSAtom            = 0, 
    OBJC_TAG_1                 = 1, 
    OBJC_TAG_NSString          = 2, 
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6,

    // 60-bit reserved
    OBJC_TAG_RESERVED_7        = 7, 

    // 52-bit payloads
    OBJC_TAG_Photos_1          = 8,
    OBJC_TAG_Photos_2          = 9,
    OBJC_TAG_Photos_3          = 10,
    OBJC_TAG_Photos_4          = 11,
    OBJC_TAG_XPC_1             = 12,
    OBJC_TAG_XPC_2             = 13,
    OBJC_TAG_XPC_3             = 14,
    OBJC_TAG_XPC_4             = 15,
    OBJC_TAG_NSColor           = 16,
    OBJC_TAG_UIColor           = 17,
    OBJC_TAG_CGColor           = 18,
    OBJC_TAG_NSIndexSet        = 19,
    OBJC_TAG_NSMethodSignature = 20,
    OBJC_TAG_UTTypeRecord      = 21,

    // When using the split tagged pointer representation
    // (OBJC_SPLIT_TAGGED_POINTERS), this is the first tag where
    // the tag and payload are unobfuscated. All tags from here to
    // OBJC_TAG_Last52BitPayload are unobfuscated. The shared cache
    // builder is able to construct these as long as the low bit is
    // not set (i.e. even-numbered tags).
    OBJC_TAG_FirstUnobfuscatedSplitTag = 136, // 128 + 8, first ext tag with high bit set

    OBJC_TAG_Constant_CFString = 136,

    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263,

    OBJC_TAG_RESERVED_264      = 264
};

這里就不再一一驗(yàn)證了。

2.1.1 arm64下的taggedPointer

我們再來分析下真機(jī)下的效果蚤霞,相對于模擬器酗失,要復(fù)雜一些,代碼如下

#define kc_OBJC_TAG_INDEX_MASK 0x7UL
#define kc_OBJC_TAG_INDEX_SHIFT 0

extern uint8_t objc_debug_tag60_permutations[8];

uintptr_t ro_objc_obfuscatedTagToBasicTag(uintptr_t tag) {
    for (unsigned i = 0; i < 7; i++)
        if (objc_debug_tag60_permutations[i] == tag)
            return i;
    return 7;
}

uintptr_t
ro_objc_decodeTaggedPointer(id ptr)
{
    uintptr_t value = (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
    uintptr_t basicTag = (value >> kc_OBJC_TAG_INDEX_SHIFT) & kc_OBJC_TAG_INDEX_MASK;

    value &= ~(kc_OBJC_TAG_INDEX_MASK << kc_OBJC_TAG_INDEX_SHIFT);
    value |= ro_objc_obfuscatedTagToBasicTag(basicTag) << kc_OBJC_TAG_INDEX_SHIFT;

    return value;
}


static inline uintptr_t ro_objc_basicTagToObfuscatedTag(uintptr_t tag) {
    return objc_debug_tag60_permutations[tag];
}

void *
ro_objc_encodeTaggedPointer(uintptr_t ptr)
{
    uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);

    uintptr_t basicTag = (value >> kc_OBJC_TAG_INDEX_SHIFT) & kc_OBJC_TAG_INDEX_MASK;
    uintptr_t permutedTag = ro_objc_basicTagToObfuscatedTag(basicTag);
    value &= ~(kc_OBJC_TAG_INDEX_MASK << kc_OBJC_TAG_INDEX_SHIFT);
    value |= permutedTag << kc_OBJC_TAG_INDEX_SHIFT;
    return (void *)value;
}

真機(jī)跟模擬器有不一樣的地方昧绣。

  • 第1位是標(biāo)記taggedPointer
  • 類型是存儲(chǔ)低三位 char 0规肴,short 1,int 2夜畴,3 long拖刃,4 float,5 double贪绘。
  • 再住走四位兑牡,存儲(chǔ)的是數(shù)據(jù)的長度
  • 再往后是數(shù)據(jù)的內(nèi)容

由于真機(jī)出現(xiàn)問題,暫時(shí)無法詳細(xì)寫出驗(yàn)證流程税灌,與模擬器原理相似均函。

2.1.1 taggedPointer面試題分析

//案例1
- (void)taggedPointerDemo {
  
    self.queue = dispatch_queue_create("com.robert.ccom", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"robert"];
             NSLog(@"%@",self.nameStr);
        });
    }
}

//案例2,連續(xù)點(diǎn)擊
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    for (int i = 0; i<100000; i++) {
        dispatch_async(self.queue, ^{
         self.nameStr = [NSString stringWithFormat:@"3333333fdsdsfsafsafasfasfasfsafafafafda"];
            NSLog(@"%@",self.nameStr);
        });
    }
}

以上代碼的運(yùn)行會(huì)不會(huì)產(chǎn)生崩潰菱涤?
答案是肯定的苞也。


6
7

分析如下:

  • 案例1 taggedPointer類型的string
  • 案例2 是普通的的string
  • 案例2中會(huì)產(chǎn)生多線程的讀寫操作
  • 案例2中會(huì)把setter方法操作,對新值的retain和舊值的release粘秆,所以在某一個(gè)瞬間會(huì)釋放多次如迟,產(chǎn)生對野指針的操作
  • 案例2中NSCFString原因是因?yàn)閿?shù)據(jù)太長,taggedPointer針對小對象的,根本不受ARC的影響
    部分源碼如下
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return (id)this;

if (slowpath(isTaggedPointer())) return (id)this;在這里如果是小針對直接返回殷勘,release同樣的道理此再,部分源碼如下


ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return false;

  • 小對象的值直接存在指針里面,普通棧結(jié)構(gòu)的回收劳吠,根本不需在這里處理
  • 在objc_msgSend中引润,如果是小對象根本不會(huì)進(jìn)行慢速查找流程

2.2 NONPOINTER_ISA

taggedPointer對類型的處理
NONPOINTER_ISA:非指針型isa
他們都會(huì)對位置進(jìn)行處理巩趁,如下

  • nonpointer:表示是否對 isa 指針開啟指針優(yōu)化
    0:純isa指針痒玩,1:不?是類對象地址,isa 中包含了類信息、對象的引?計(jì)數(shù)等
  • has_assoc:關(guān)聯(lián)對象標(biāo)志位议慰,0沒有蠢古,1存在
  • has_cxx_dtor:該對象是否有 C++ 或者 Objc 的析構(gòu)器,如果有析構(gòu)函數(shù),則需要做析構(gòu)邏輯, 如果沒有,則可以更快的釋放對象
  • shiftcls:存儲(chǔ)類指針的值(相當(dāng)于taggedPointer的payload)。開啟指針優(yōu)化的情況下别凹,在arm64架構(gòu)中有33位?來存儲(chǔ)類指針草讶。
  • magic:?于調(diào)試器判斷當(dāng)前對象是真的對象還是沒有初始化的空間
  • weakly_referenced:志對象是否被指向或者曾經(jīng)指向?個(gè)ARC的弱變量,沒有弱引?的對象可以更快釋放炉菲。
  • deallocating:標(biāo)志對象是否正在釋放內(nèi)存
  • has_sidetable_rc:當(dāng)對象引?技術(shù)?于10時(shí)堕战,則需要借?該變量存儲(chǔ)進(jìn)位
  • extra_rc:當(dāng)表示該對象的引?計(jì)數(shù)值,實(shí)際上是引?計(jì)數(shù)值減1拍霜,例如嘱丢,如果對象的引?計(jì)數(shù)為10,那么extra_rc為9祠饺。如果引?計(jì)數(shù)?于10越驻,則需要使用到has_sidetable_rc。

3 retain及release分析

3.1 MAR&ARC概念

ARC是LLVM和Runtime配合的結(jié)果道偷。
ARC中禁??動(dòng)調(diào)?retain/release/retainCount/dealloc
ARC新加了weak缀旁、strong屬性關(guān)鍵字

3.2 retain的流程分析

我們在源碼中搜下rootRetain,如下源碼

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

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    oldisa = LoadExclusive(&isa.bits);

    if (variant == RRVariant::FastOrMsgSend) {
        // These checks are only meaningful for objc_retain()
        // They are here so that we avoid a re-load of the isa.
        if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
            ClearExclusive(&isa.bits);
            if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
                return swiftRetain.load(memory_order_relaxed)((id)this);
            }
            return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
        }
    }

    if (slowpath(!oldisa.nonpointer)) {
        // a Class is a Class forever, so we can perform this check once
        // outside of the CAS loop
        if (oldisa.getDecodedClass(false)->isMetaClass()) {
            ClearExclusive(&isa.bits);
            return (id)this;
        }
    }

    do {
        transcribeToSideTable = false;
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain(sideTableLocked);
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            if (slowpath(tryRetain)) {
                return nil;
            } else {
                return (id)this;
            }
        }
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (variant != RRVariant::Full) {
                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 (variant == RRVariant::Full) {
        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();
    } else {
        ASSERT(!transcribeToSideTable);
        ASSERT(!sideTableLocked);
    }

    return (id)this;
}

我們的引用計(jì)數(shù)存在isa里面勺鸦,也就是在extra_rc中并巍,在這里引用計(jì)數(shù)存滿時(shí),會(huì)放到散列表中换途。
reatain流程:

  • if (slowpath(isTaggedPointer())) return (id)this;判斷是否是taggedPointer履澳,是的話直接返回
  • isa_t oldisa;為了處理extra_rc,下面的do while循環(huán)是要著重分析的怀跛,
  • if (slowpath(!newisa.nonpointer)) {
    ClearExclusive(&isa.bits);
    if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
    else return sidetable_retain(sideTableLocked);
    }
    這里判斷是不是nonpointer距贷,如果是,isa位的操作
  • 如果不是nonpointer吻谋,調(diào)用sidetable_retain這個(gè)函數(shù)忠蝗,
id
objc_object::sidetable_retain(bool locked)
{
#if SUPPORT_NONPOINTER_ISA
    ASSERT(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];
    
    if (!locked) table.lock();
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    table.unlock();

    return (id)this;
}

獲取SideTables的數(shù)據(jù),然后執(zhí)行+ 1<<2操作,也就是在原來的值上+2操作漓拾, 為什么是這樣的阁最,我們需要分析SideTables的結(jié)構(gòu)戒祠。
#define SIDE_TABLE_RC_PINNED (1UL<<(WORD_BITS-1)),WORD_BITS速种,如果是64位姜盈,左移1位,也就是63位配阵,2的二進(jìn)制是010馏颂,其它位置不變,2號(hào)位置+1操作棋傍,在2號(hào)位置存儲(chǔ)引用計(jì)數(shù)的值

  • 如果是nonpointer救拉,執(zhí)行這段代碼
  if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            if (slowpath(tryRetain)) {
                return nil;
            } else {
                return (id)this;
            }
        }

*判斷是不是在析構(gòu),如果在析構(gòu)就沒必要-1操作瘫拣,在多線程的情況下亿絮,已經(jīng)在釋放,還有可能-1麸拄。

  • 接著在這里
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

在addc中派昧,拿到bits,執(zhí)行++操作拢切,# define RC_ONE (1ULL<<45)蒂萎,uintptr_t extra_rc : 19,extra_rc是在最后19位失球,剛好是64位岖是,左移45位剛好是extra_rc,然后執(zhí)行++1操作实苞,uintptr_t extra_rc : 8定義豺撑,說明extra_rc是8位,2的8次方256位黔牵,如果carry加多了聪轿,這個(gè)時(shí)候,執(zhí)行以下代碼

 if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (variant != RRVariant::Full) {
                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;
        }

在散列表中存儲(chǔ)猾浦,這里rootRetain_overflow調(diào)用這個(gè)函數(shù)判斷所有超載陆错。
接著

 sideTableLocked = true;
 transcribeToSideTable = true;
 newisa.extra_rc = RC_HALF;
 newisa.has_sidetable_rc = true;

執(zhí)行這段代碼,打開鎖金赦,newisa.extra_rc = RC_HALF; #define RC_HALF (1ULL<<18)音瓷,左移18位,也就是移一半夹抗,把一半存在存在了extra_rc绳慎,另一半標(biāo)記為has_sidetable_rc,之后調(diào)用sidetable_addExtraRC_nolock這個(gè)函數(shù),加入到引用計(jì)數(shù)表中杏愤。
所以extra_rc存一半靡砌,散列表(引用計(jì)數(shù)表)存一半。

  • extra_rc存在isa中珊楼,直接獲取到通殃,散列表需要先查表,size_t& refcntStorage = table.refcnts[this];找到相關(guān)對象存儲(chǔ)的區(qū)域厕宗,再移一半画舌。
    通過散列表(引用計(jì)數(shù)表)增刪改查,同時(shí)加鎖解鎖媳瞪,是比較復(fù)雜骗炉,效率低照宝。
    那為什么extra_rc還只要存一半蛇受?。
  • 如果執(zhí)行release的--操作時(shí)厕鹃,如果全部存儲(chǔ)在散列表中兢仰,當(dāng)滿了的情況,去散列表(引用計(jì)數(shù)表)中去獲取剂碴,還是有問題把将,比如300,執(zhí)行-1操作忆矛,299察蹲,如果放在extra_rc是放不下的(extra_rc最值256),這樣操作比較耗費(fèi)性能催训。
  • 所以各存一半洽议,在extra_rc存一半128位,也足夠執(zhí)行--操作了漫拭。

3.3 release的流程分析

我們在objc源碼搜索亚兄,經(jīng)過搜索找到如下源碼

bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return false;

    bool sideTableLocked = false;

    isa_t newisa, oldisa;

    oldisa = LoadExclusive(&isa.bits);

    if (variant == RRVariant::FastOrMsgSend) {
        // These checks are only meaningful for objc_release()
        // They are here so that we avoid a re-load of the isa.
        if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
            ClearExclusive(&isa.bits);
            if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
                swiftRelease.load(memory_order_relaxed)((id)this);
                return true;
            }
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
            return true;
        }
    }

    if (slowpath(!oldisa.nonpointer)) {
        // a Class is a Class forever, so we can perform this check once
        // outside of the CAS loop
        if (oldisa.getDecodedClass(false)->isMetaClass()) {
            ClearExclusive(&isa.bits);
            return false;
        }
    }

retry:
    do {
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            return sidetable_release(sideTableLocked, performDealloc);
        }
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            return false;
        }

        // don't check newisa.fast_rr; we already called any RR overrides
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));

    if (slowpath(newisa.isDeallocating()))
        goto deallocate;

    if (variant == RRVariant::Full) {
        if (slowpath(sideTableLocked)) sidetable_unlock();
    } else {
        ASSERT(!sideTableLocked);
    }
    return false;

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

    // abandon newisa to undo the decrement
    newisa = oldisa;

    if (slowpath(newisa.has_sidetable_rc)) {
        if (variant != RRVariant::Full) {
            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.
            oldisa = LoadExclusive(&isa.bits);
            goto retry;
        }

        // Try to remove some retain counts from the side table.        
        auto borrow = sidetable_subExtraRC_nolock(RC_HALF);

        bool emptySideTable = borrow.remaining == 0; // we'll clear the side table if no refcounts remain there

        if (borrow.borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            bool didTransitionToDeallocating = false;
            newisa.extra_rc = borrow.borrowed - 1;  // redo the original decrement too
            newisa.has_sidetable_rc = !emptySideTable;

            bool stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);

            if (!stored && oldisa.nonpointer) {
                // 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.
                uintptr_t overflow;
                newisa.bits =
                    addc(oldisa.bits, RC_ONE * (borrow.borrowed-1), 0, &overflow);
                newisa.has_sidetable_rc = !emptySideTable;
                if (!overflow) {
                    stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
                    if (stored) {
                        didTransitionToDeallocating = newisa.isDeallocating();
                    }
                }
            }

            if (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                ClearExclusive(&isa.bits);
                sidetable_addExtraRC_nolock(borrow.borrowed);
                oldisa = LoadExclusive(&isa.bits);
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            if (emptySideTable)
                sidetable_clearExtraRC_nolock();

            if (!didTransitionToDeallocating) {
                if (slowpath(sideTableLocked)) sidetable_unlock();
                return false;
            }
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }

deallocate:
    // Really deallocate.

    ASSERT(newisa.isDeallocating());
    ASSERT(isa.isDeallocating());

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return true;
}
  • if (slowpath(isTaggedPointer())) return false;先判斷是不是TaggedPointer的話,直接返回不處理采驻。
  • 在do while中
 if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            return sidetable_release(sideTableLocked, performDealloc);
        }

判斷如果不是nonpointer就執(zhí)行sidetable_release這個(gè)函數(shù)审胚,對散列表執(zhí)行release操作,源碼如下

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

    bool do_dealloc = false;

    if (!locked) table.lock();
    auto it = table.refcnts.try_emplace(this, SIDE_TABLE_DEALLOCATING);
    auto &refcnt = it.first->second;
    if (it.second) {
        do_dealloc = true;
    } else if (refcnt < SIDE_TABLE_DEALLOCATING) {
        // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
        do_dealloc = true;
        refcnt |= SIDE_TABLE_DEALLOCATING;
    } else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
        refcnt -= SIDE_TABLE_RC_ONE;
    }
    table.unlock();
    if (do_dealloc  &&  performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return do_dealloc;
}
  • 接著
if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            return false;
        }

判斷是不是在析構(gòu)礼旅。

  • 如果是nonpointer膳叨,執(zhí)行以下代碼
 uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  //

執(zhí)行-1操作,如果

 if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;
        }

如果減多了痘系,跳轉(zhuǎn)到這里underflow菲嘴,如下

if (slowpath(newisa.has_sidetable_rc)) {
        if (variant != RRVariant::Full) {
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }

判斷是否有has_sidetable_rc,如果沒有跳轉(zhuǎn)到

 if (slowpath(sideTableLocked)) sidetable_unlock();

    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }

這時(shí)給當(dāng)前對象發(fā)送一個(gè)dealloc消息。

  • 如果有has_sidetable_rc临谱,執(zhí)行
 auto borrow = sidetable_subExtraRC_nolock(RC_HALF);

這里代碼璃俗,取一半,然后

newisa.extra_rc = borrow.borrowed - 1; 

執(zhí)行--操作悉默。

  • 在sidetable_subExtraRC_nolock函數(shù)中
objc_object::SidetableBorrow
objc_object::sidetable_subExtraRC_nolock(size_t delta_rc)
{
    ASSERT(isa.nonpointer);
    SideTable& table = SideTables()[this];

    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()  ||  it->second == 0) {
        // Side table retain count is zero. Can't borrow.
        return { 0, 0 };
    }
    size_t oldRefcnt = it->second;

    // isa-side bits should not be set here
    ASSERT((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
    ASSERT((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);

    size_t newRefcnt = oldRefcnt - (delta_rc << SIDE_TABLE_RC_SHIFT);
    ASSERT(oldRefcnt > newRefcnt);  // shouldn't underflow
    it->second = newRefcnt;
    return { delta_rc, newRefcnt >> SIDE_TABLE_RC_SHIFT };
}

散列中存一半城豁。

3.4 dealloc簡單基本流程概述

  • 根據(jù)當(dāng)前對象的狀態(tài)是否直接調(diào)用free()釋放
  • 是否在瞎了眼在C++的析構(gòu)函數(shù),移除這個(gè)對象的關(guān)聯(lián)屬性
  • 將指向該對象的弱引用指針置為nil
  • 從弱引用表中移除該對象的引用計(jì)數(shù)

總結(jié)

這次我們介紹了內(nèi)存的五大區(qū)抄课,taggedPointer唱星,retain,release的底層分析跟磨,希望些文章可以讓大家對iOS的內(nèi)存管理有一個(gè)新的認(rèn)識(shí)间聊,該文章略有粗糙,還望諒解抵拘。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末哎榴,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子僵蛛,更是在濱河造成了極大的恐慌尚蝌,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件充尉,死亡現(xiàn)場離奇詭異飘言,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)驼侠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門姿鸿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人倒源,你說我怎么就攤上這事苛预。” “怎么了相速?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵形庭,是天一觀的道長敷扫。 經(jīng)常有香客問我狈定,道長睦袖,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任旺隙,我火速辦了婚禮绒极,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蔬捷。我一直安慰自己垄提,他們只是感情好榔袋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著铡俐,像睡著了一般凰兑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上审丘,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天吏够,我揣著相機(jī)與錄音,去河邊找鬼滩报。 笑死锅知,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的脓钾。 我是一名探鬼主播售睹,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼可训!你這毒婦竟也來了昌妹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤沉噩,失蹤者是張志新(化名)和其女友劉穎捺宗,沒想到半個(gè)月后柱蟀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體川蒙,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年长已,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了畜眨。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡术瓮,死狀恐怖康聂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情胞四,我是刑警寧澤恬汁,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站辜伟,受9級(jí)特大地震影響氓侧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜导狡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一约巷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧旱捧,春花似錦独郎、人聲如沸踩麦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谓谦。三九已至,卻和暖如春贪婉,著一層夾襖步出監(jiān)牢的瞬間茁计,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來泰國打工谓松, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留星压,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓鬼譬,卻偏偏與公主長得像娜膘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子优质,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355