OC對(duì)象原理探究之a(chǎn)lloc探索

一、對(duì)象的指針地址和內(nèi)存

先看下面的代碼:

    HPerson * p1 = [HPerson alloc];
    HPerson * p2 = [p1 init];
    HPerson * p3 = [p1 init];
    
    NSLog(@"%@-%p", p1, p1);
    NSLog(@"%@-%p", p2, p2);
    NSLog(@"%@-%p", p3, p3);

請(qǐng)問這3者的會(huì)有什么不同嗎韩脏?

執(zhí)行代碼就會(huì)發(fā)現(xiàn)p1该面,p2,p3打印的內(nèi)存地址是完全一樣的:

ia_100000026.png

所以在這個(gè)過程我們可以得出一個(gè)結(jié)論:

1奈附、alloc讓對(duì)象有了內(nèi)存空間,有了指針指向煮剧。

2斥滤、init后內(nèi)存沒有變化,證明init沒有對(duì)指針做什么操作勉盅。

再來看下面的代碼:

    HPerson * p1 = [HPerson alloc];
    HPerson * p2 = [p1 init];
    HPerson * p3 = [p1 init];
    
    NSLog(@"%@-%p-%p", p1, p1, &p1);
    NSLog(@"%@-%p-%p", p2, p2, &p2);
    NSLog(@"%@-%p-%p", p3, p3, &p3);

執(zhí)行:

ia_200000001.png

發(fā)現(xiàn)3個(gè)不同的指針地址指向了同一塊內(nèi)存空間佑颇,而且是3個(gè)連續(xù)的指針地址。

那么alloc是怎么分配內(nèi)存空間的呢草娜?init真的什么都沒有做嗎挑胸?

二、探索底層的三種方法

如果我們直接進(jìn)行代碼跳轉(zhuǎn):

ia_200000002.png
ia_200000003.png

就會(huì)進(jìn)入到NSObjectalloc方法:

ia_200000004.png

發(fā)現(xiàn)找不到方法的實(shí)現(xiàn)宰闰!

為什么沒有實(shí)現(xiàn)呢茬贵?我們應(yīng)該如何去查看alloc方法的實(shí)行呢?

方法一:斷點(diǎn)調(diào)試

我們先在alloc處打上斷點(diǎn):

ia_200000005.png

然后按住ctrl點(diǎn)擊step into

ia_200000006.png

即可看到议蟆,alloc方法調(diào)用的是objc_alloc方法:

ia_200000007.png

然后我們打上符號(hào)斷點(diǎn):

ia_200000008.png
ia_200000009.png

再點(diǎn)擊繼續(xù)執(zhí)行程序:

ia_200000010.png

然后我們就會(huì)發(fā)現(xiàn)objc_alloc來源于libobjc.A.dylib闷沥,即objc動(dòng)態(tài)庫(kù)底層方法:

ia_200000011.png

方法二:利用匯編一步一步跟進(jìn)

debug選擇欄打開進(jìn)入?yún)R編:

ia_200000012.png

依舊在alloc處打上斷點(diǎn):

ia_200000013.png

運(yùn)行程序就會(huì)進(jìn)入到匯編頁(yè)面:

ia_200000014.png

在該頁(yè)面就會(huì)發(fā)現(xiàn)是調(diào)用了objc_alloc方法,然后用符號(hào)斷點(diǎn)去查看該方法使用的哪個(gè)動(dòng)態(tài)庫(kù)咐容。

方法三:通過已知方法進(jìn)行符號(hào)斷點(diǎn)

當(dāng)程序停在我們的斷點(diǎn)處時(shí):

ia_200000015.png

添加已知方法的符號(hào)斷點(diǎn):

ia_200000016.png

然后進(jìn)入:

ia_200000017.png

就會(huì)發(fā)現(xiàn)alloc是來自libobjc.A.dylib動(dòng)態(tài)庫(kù):

ia_200000018.png

以上就是3中基本的探索底層的方式,還有反匯編蚂维、lldb戳粒、堆棧等等。

三虫啥、匯編結(jié)合源碼調(diào)試分析

1蔚约、下載源碼

我們已經(jīng)定位到了alloc是在libobjc這個(gè)動(dòng)態(tài)庫(kù)里面,接下來就是進(jìn)入源碼進(jìn)行調(diào)試涂籽。

先在蘋果開源網(wǎng)站下載源碼苹祟,或者在蘋果的源代碼目錄進(jìn)行下載,源代碼目錄更加方便评雌。

以源代碼目錄為例:

進(jìn)入網(wǎng)站后搜索objc

ia_200000019.png

找到objc4树枫,點(diǎn)進(jìn)去:

ia_200000020.png

最新的objc4-824.tar.gz有問題,無法下載景东,所以使用的是objc4-818.2.tar.gz砂轻。

2、查看源碼

打開下載的objc4-818.2斤吐,搜索alloc {:

ia_200000021.png

發(fā)現(xiàn)alloc方法里面調(diào)用的是_objc_rootAlloc方法搔涝。

再點(diǎn)_objc_rootAlloc方法進(jìn)去:

// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

發(fā)現(xiàn)_objc_rootAlloc方法里面是callAlloc方法厨喂。

再點(diǎn)callAlloc方法進(jìn)去:

// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

    // No shortcuts available.
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

這里就是alloc的核心方法。

__OBJC2__指的是2.0版本庄呈,現(xiàn)在用的都是2.0版本蜕煌。

那么到了這里,是執(zhí)行_objc_rootAllocWithZone呢诬留?還是執(zhí)行objc_msgSend呢幌绍?

3、匯編調(diào)試

我們可以通過匯編來查看故响,回到最開始的代碼傀广,在alloc處打上斷點(diǎn),于此再加上_objc_rootAlloc的符號(hào)斷點(diǎn):

ia_200000022.png

運(yùn)行:

ia_200000023.png

運(yùn)行后確實(shí)進(jìn)入了_objc_rootAlloc方法彩届,但是進(jìn)入?yún)s是HPerson的父類NSObject_objc_rootAlloc方法伪冰,我們應(yīng)該進(jìn)入的HPerson的方法才對(duì)!

所以需要先取消_objc_rootAlloc的斷點(diǎn)重新運(yùn)行樟蠕,當(dāng)斷到了HPersonalloc方法時(shí)贮聂,再加上_objc_rootAlloc的斷點(diǎn):

ia_200000024.png

斷住后,發(fā)現(xiàn)先執(zhí)行_objc_rootAllocWithZone方法寨辩,再執(zhí)行objc_msgSend方法吓懈。

4、源碼調(diào)試

先按照iOS_objc4-756.2 最新源碼編譯調(diào)試配置下載下來的objc4-818.2靡狞。

objc源碼里面創(chuàng)建HObjectBuild這個(gè)targets耻警,并創(chuàng)建HPerson這個(gè)類:

ia_200000025.png

main.m里面打上斷點(diǎn),并運(yùn)行程序:

ia_200000026.png

進(jìn)入上面查看到的_objc_rootAllocWithZone方法里:

NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

再進(jìn)入到_class_createInstanceFromZone方法里:

/***********************************************************************
* class_createInstance
* fixme
* Locking: none
*
* Note: this function has been carefully written so that the fastpath
* takes no branch.
**********************************************************************/
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;

    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

_class_createInstanceFromZone方法內(nèi)甸怕,發(fā)現(xiàn)返回的是obj這個(gè)對(duì)象甘穿,所以我們?cè)?code>obj這里打上斷點(diǎn):

ia_200000027.png

繼續(xù)執(zhí)行程序:

ia_200000028.png

發(fā)現(xiàn)obj已經(jīng)有地址了,因?yàn)楫?dāng)前內(nèi)存并未使用過梢杭,是臟內(nèi)存地址温兼!

點(diǎn)擊step over,到calloc方法武契,內(nèi)存地址沒有發(fā)生變化:

ia_200000029.png

在點(diǎn)擊step over募判,執(zhí)行完calloc方法后,發(fā)現(xiàn)obj的內(nèi)存地址不一樣了:

ia_200000030.png

說明在calloc方法中對(duì)obj進(jìn)行了內(nèi)存地址賦值咒唆。

在打印中發(fā)現(xiàn)obj對(duì)象的類型是id届垫,這是因?yàn)檫@個(gè)地址還沒有綁定到我們的HPerson這類里面去,關(guān)聯(lián)類的是isa钧排!

繼續(xù)執(zhí)行敦腔,過了initInstanceIsa方法后再打印發(fā)現(xiàn)obj已經(jīng)關(guān)聯(lián)了HPerson類:

ia_200000031.png

所以在initInstanceIsa方法內(nèi)讓isa關(guān)聯(lián)了HPerson以及c++的方法和函數(shù)!

接下來就是返回obj恨溜,所有的alloc方法流程已經(jīng)走完了符衔!

四找前、字節(jié)對(duì)齊

重新運(yùn)行,在_class_createInstanceFromZone方法的

size = cls->instanceSize(extraBytes);

打上斷點(diǎn):

ia_400000033.png

發(fā)現(xiàn)需要額外增加的字節(jié)為0判族。

然后進(jìn)入到instanceSize方法:

inline size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }

        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;  
        return size;
    }

return處打上斷點(diǎn)躺盛,繼續(xù):

ia_400000034.png

發(fā)現(xiàn)進(jìn)入到第一個(gè)return,說明有緩存形帮。

然后進(jìn)入fastInstanceSize方法:

size_t fastInstanceSize(size_t extra) const
    {
        ASSERT(hasFastInstanceSize(extra));

        if (__builtin_constant_p(extra) && extra == 0) {
            return _flags & FAST_CACHE_ALLOC_MASK16;
        } else {
            size_t size = _flags & FAST_CACHE_ALLOC_MASK;
            // remove the FAST_CACHE_ALLOC_DELTA16 that was added
            // by setFastInstanceSize
            return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
        }
    }

return處打上斷點(diǎn)槽惫,繼續(xù):

ia_400000035.png

走的是第二個(gè)return,而且size16辩撑。

那么這個(gè)數(shù)據(jù)怎么來的呢界斜?

如果沒有緩存的話,就會(huì)走alignedInstanceSize方法合冀,然后返回字節(jié)對(duì)齊的內(nèi)存大懈鬓薄:

ia_400000036.png

那么,一個(gè)對(duì)象的內(nèi)存大小由什么來確定呢君躺?

只有成員變量決定峭判!

我們進(jìn)入unalignedInstanceSize方法:

// May be unaligned depending on class's ivars.
    uint32_t unalignedInstanceSize() const {
        ASSERT(isRealized());
        return data()->ro()->instanceSize;
    }

發(fā)現(xiàn)大小是有instanceSize來決定的,即實(shí)例變量的大凶亟小林螃!

depending on class's ivars.-->取決于類的ivars。

并且是編譯完成的干凈內(nèi)存的大邪称疗认!

所以是依賴于成員變量的大小砌滞!

當(dāng)前的HPerson對(duì)象沒有成員變量侮邀,所以這里返回的大小是8

ia_400000037.png

為什么是8呢?因?yàn)?code>NSObject有一個(gè)成員變量isa

ia_400000038.png

為什么isa的大小是8呢贝润?

因?yàn)?code>Class是一個(gè)結(jié)構(gòu)體isa是一個(gè)結(jié)構(gòu)體指針铝宵,所以是8字節(jié)打掘!

我們可以在源碼里搜索objc_class

ia_400000039.png

發(fā)現(xiàn)Class是一個(gè)結(jié)構(gòu)體指針類型!

而且objc_class繼承于objc_object鹏秋,即萬物皆對(duì)象尊蚁!即類也是一個(gè)對(duì)象!

返回大小8字節(jié)后侣夷,如果小于16字節(jié)則等于16

ia_400000040.png

所以得出16字節(jié)横朋!

如果大于16呢?

就會(huì)進(jìn)行字節(jié)對(duì)齊百拓!

進(jìn)入到alignedInstanceSize方法:

// Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
    }

里面有word_align方法:

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

這個(gè)就是字節(jié)對(duì)齊算法琴锭!

點(diǎn)進(jìn)WORD_MASK

#ifdef __LP64__
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL
#   define WORD_BITS 64
#else
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif

發(fā)現(xiàn)WORD_MASK等于7晰甚!

按照(x + WORD_MASK) & ~WORD_MASK這個(gè)算法,x8

  (x + WORD_MASK) & ~WORD_MASK
= (8 + 7) & ~7
= 15 & ~7

轉(zhuǎn)為二進(jìn)制:
15 為:0000 1111
7  為:0000 0111
~7 為:1111 1000

所以:
  15 & ~7
= 0000 1111 & 1111 1000
= 0000 1000

轉(zhuǎn)為十進(jìn)制為:8

但是為9的時(shí)候則為16决帖!

所以這是一個(gè)以8字節(jié)對(duì)齊厕九,向上取8的整數(shù)的方法!

為什么是8的倍數(shù)呢地回?

因?yàn)樽畲缶褪?code>指針扁远,8字節(jié)!同時(shí)也是為了空間換時(shí)間刻像,方便內(nèi)存讀瘸┞颉!

五细睡、對(duì)象的內(nèi)存空間

先給HPerson添加對(duì)象:

ia_400000041.png

打上斷點(diǎn)谷羞,運(yùn)行,然后在lldb中輸入x p纹冤,顯示對(duì)象p的內(nèi)存分布:

ia_400000042.png

0x108f079c0是對(duì)象p的內(nèi)存首地址洒宝,接下來就是對(duì)象p的內(nèi)存。

iOS端為小端模式萌京,所有需要倒著讀妊愀琛:

ia_400000043.png

內(nèi)存打印出來是什么呢?

isa知残!

為什么沒有打印出isa呢靠瞎?

因?yàn)橐?code>&ISA_MASK:

# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#   else
#     define ISA_MASK        0x0000000ffffffff8ULL
#   endif

這里使用的是模擬器,所以&0x0000000ffffffff8:

ia_400000044.png

現(xiàn)在就正確的打印出isa了求妹!

后面的0則為對(duì)象的屬性的存儲(chǔ)空間乏盐!

進(jìn)入debug查看一下內(nèi)存:

ia_400000045.png

輸入內(nèi)存首地址:

ia_400000046.png

發(fā)現(xiàn)即使沒有給屬性賦值,依舊會(huì)開辟內(nèi)存制恍!

給屬性賦值后再運(yùn)行:

ia_400000047.png

x/5gx為格式化輸出父能!

如果把height改為BOOL類型:

ia_400000048.png

就會(huì)發(fā)現(xiàn)ageheight放在了一起!

這是蘋果的底層對(duì)內(nèi)存進(jìn)行了優(yōu)化净神,即內(nèi)存對(duì)齊何吝!

六、alloc簡(jiǎn)略流程圖

alloc流程圖.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鹃唯,一起剝皮案震驚了整個(gè)濱河市爱榕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌坡慌,老刑警劉巖黔酥,帶你破解...
    沈念sama閱讀 218,607評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡跪者,警方通過查閱死者的電腦和手機(jī)棵帽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來坑夯,“玉大人岖寞,你說我怎么就攤上這事」耱冢” “怎么了仗谆?”我有些...
    開封第一講書人閱讀 164,960評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)淑履。 經(jīng)常有香客問我隶垮,道長(zhǎng),這世上最難降的妖魔是什么秘噪? 我笑而不...
    開封第一講書人閱讀 58,750評(píng)論 1 294
  • 正文 為了忘掉前任狸吞,我火速辦了婚禮,結(jié)果婚禮上指煎,老公的妹妹穿的比我還像新娘蹋偏。我一直安慰自己,他們只是感情好至壤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,764評(píng)論 6 392
  • 文/花漫 我一把揭開白布威始。 她就那樣靜靜地躺著,像睡著了一般像街。 火紅的嫁衣襯著肌膚如雪黎棠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,604評(píng)論 1 305
  • 那天镰绎,我揣著相機(jī)與錄音脓斩,去河邊找鬼。 笑死畴栖,一個(gè)胖子當(dāng)著我的面吹牛随静,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播吗讶,決...
    沈念sama閱讀 40,347評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼挪挤,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了关翎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,253評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤鸠信,失蹤者是張志新(化名)和其女友劉穎纵寝,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,702評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡爽茴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,893評(píng)論 3 336
  • 正文 我和宋清朗相戀三年葬凳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片室奏。...
    茶點(diǎn)故事閱讀 40,015評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡火焰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出胧沫,到底是詐尸還是另有隱情昌简,我是刑警寧澤,帶...
    沈念sama閱讀 35,734評(píng)論 5 346
  • 正文 年R本政府宣布绒怨,位于F島的核電站纯赎,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏南蹂。R本人自食惡果不足惜犬金,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,352評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望六剥。 院中可真熱鬧晚顷,春花似錦、人聲如沸疗疟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)秃嗜。三九已至权均,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間锅锨,已是汗流浹背叽赊。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留必搞,地道東北人必指。 一個(gè)月前我還...
    沈念sama閱讀 48,216評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像恕洲,于是被迫代替她去往敵國(guó)和親塔橡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,969評(píng)論 2 355

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