OC對(duì)象原理(一)— alloc流程探索

研究OC底層原理面褐,就應(yīng)該從最基本和最熟悉的開始,那就是對(duì)象的創(chuàng)建alloc底層實(shí)現(xiàn)鞠苟。本文就我自己探索和學(xué)習(xí)到的alloc實(shí)現(xiàn)進(jìn)行總結(jié)全跨,有問題請(qǐng)指出,大家一起交流探索成肘。

幾種源碼探索方法(建議真機(jī)環(huán)境)

以上方法前三種方法可以探索出簡(jiǎn)單的流程方法順序双霍,只有下載源碼才能看到流程方法的具體實(shí)現(xiàn)吼驶。探索起來雖說比較枯燥無味,但是只要堅(jiān)持下去店煞,對(duì)于自己技術(shù)幫助還是很大的蟹演,當(dāng)你探索出結(jié)果也會(huì)有很大的成就感。

alloc流程源碼探索

alloc打斷點(diǎn)

流程

第一步
+ (id)alloc {
    return _objc_rootAlloc(self);
}
第二步
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
第三步
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    //cls為空
    if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__
    //看下邊官方注釋 -->判斷是否自定義alloc/allocWithZone顷蟀,并且不是繼承于NSObject/NSProxy
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        // No alloc/allocWithZone implementation. Go straight to the allocator.
        // fixme store hasCustomAWZ in the non-meta class and 
        // add it to canAllocFast's summary
        
        //canAllocFast通過點(diǎn)進(jìn)去可以得出在arm64系統(tǒng)下酒请,canAllocFast一直返回false
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            //第三步之后,進(jìn)入這里
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            //此處打斷點(diǎn)鸣个,輸出一下obj羞反,發(fā)現(xiàn)是LGTeacher
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif

    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}
  • hasCustomAWZ()是用來判斷是否有alloc/allocWithZoneimp,以及是否有自定義allocWithZone囤萤。
  • canAllocFast()通過點(diǎn)進(jìn)去可以得出在arm64系統(tǒng)下昼窗,一直返回false。
  • hasCxxDtor()是否有c++析構(gòu)函數(shù)涛舍。
    if (slowpath(!obj)) return callBadAllocHandler(cls);打斷點(diǎn)澄惊,輸出obj對(duì)象,說明追蹤正確,如下圖:
    obj對(duì)象
第四步
id 
class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}
第五步
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    //cls為空
    if (!cls) return nil;

    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    //判斷class or superclass 是否有 .cxx_construct 方法實(shí)現(xiàn)
    bool hasCxxCtor = cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    //判斷是否需要優(yōu)化的isa
    bool fast = cls->canAllocNonpointer();
    //instanceSize計(jì)算需要為對(duì)象開辟的空間
    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (!zone  &&  fast) {
        //第四步之后掸驱,進(jìn)入這里
       //calloc是開辟內(nèi)存空間
        obj = (id)calloc(1, size);
        if (!obj) return nil;
       //isa初始化
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;

        // Use raw pointer isa on the assumption that they might be 
        // doing something weird with the zone or RR.
        //isa初始化
        obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }

    return obj;
}
  • canAllocNonpointer()判斷是否需要優(yōu)化的isa肛搬。
  • instanceSize(extraBytes)計(jì)算需要開辟的空間大小,extraBytes等于0毕贼。
  • calloc->malloc_zone_calloc開辟內(nèi)存空間温赔。
size_t instanceSize(size_t extraBytes) {
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        //對(duì)象內(nèi)存地址至少分配16字節(jié)
//一個(gè)對(duì)象內(nèi)存都會(huì)分配16字節(jié),實(shí)際在64位系統(tǒng)下鬼癣,只使用了8字節(jié)陶贼;32位系統(tǒng)下,只使用了4字節(jié)
        if (size < 16) size = 16;
        return size;
    }
    // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }
    static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
    }
    uint32_t unalignedInstanceSize() {
        assert(isRealized());
        return data()->ro->instanceSize;
    }
  • if (size < 16) size = 16;對(duì)象內(nèi)存地址至少分配16字節(jié)待秃。一個(gè)對(duì)象內(nèi)存都會(huì)分配16字節(jié)骇窍,實(shí)際在64位系統(tǒng)下,只使用了8字節(jié)锥余;32位系統(tǒng)下,只使用了4字節(jié)痢掠。
  • 此處word_align()方法用到了一個(gè)內(nèi)存的8字節(jié)對(duì)齊計(jì)算驱犹,保證返回的size大小是8的倍數(shù)。WORD_MASK在arm64系統(tǒng)下等于7足画。假如傳入word_align()的x = 10雄驹,計(jì)算過程:

x = 10 ; x + WORD_MASK = 17
0000 0111 //WORD_MASK二進(jìn)制(4 + 2 + 1)

1111 1000 //~WORD_MASK二進(jìn)制
0001 0001 //17的二進(jìn)制:(16 + 1)
0001 0000 //(x + WORD_MASK) & ~WORD_MASK 二進(jìn)制(16)
所以return 16淹辞; 8的倍數(shù)

calloc開辟內(nèi)存空間

這里探索需要用到源碼libmalloc源碼医舆,打開源碼直接調(diào)用calloc方法,得出如下結(jié)果象缀,接下來就一步一步探索下去蔬将,看看如何得出此結(jié)果。

calloc開辟空間結(jié)果
calloc方法

void *
calloc(size_t num_items, size_t size)
{
    void *retval;
    retval = malloc_zone_calloc(default_zone, num_items, size);
    if (retval == NULL) {
        errno = ENOMEM;
    }
    return retval;
} 

進(jìn)入malloc_zone_calloc方法

void *
malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
    //malloc追蹤央星,DBG_FUNC_START  開始
    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);

    void *ptr;
    //malloc檢查
    if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
        internal_check();
    }
    //調(diào)用calloc
    ptr = zone->calloc(zone, num_items, size);
    
    //malloc 記錄器  三種類型
    if (malloc_logger) {
        malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone, (uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
    }
    //malloc追蹤霞怀,DBG_FUNC_END  結(jié)束
    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
    return ptr;
}

通過這段代碼可以看出系統(tǒng)在這里開啟了malloc追蹤、malloc監(jiān)測(cè)莉给、malloc記錄(看我注釋)毙石。真正執(zhí)行開辟內(nèi)存的是ptr = zone->calloc(zone, num_items, size);這句代碼。zone通過->調(diào)用calloc颓遏,那么和屬性一樣我們可以打印zone->calloc徐矩,結(jié)果如下:

default_zone_calloc
我的探索過程就是全局搜索default_zone_calloc,找到default_zone_calloc的實(shí)現(xiàn)叁幢,如下:

static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
    zone = runtime_default_zone();
    return zone->calloc(zone, num_items, size);
}

又碰到了zone->calloc(頭大)滤灯,打斷點(diǎn)正好還能走到這里,說明探索的過程是正確的。沒辦法接著打印zone->calloc力喷,得到nano_calloc刽漂,搜到nano_calloc的實(shí)現(xiàn)如下:

static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
    size_t total_bytes;
    if (calloc_get_size(num_items, size, 0, &total_bytes)) {
        //異常情況
        return NULL;
    }
    if (total_bytes <= NANO_MAX_SIZE) {
        void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
        if (p) {
            return p;
        } else {
            /* FALLTHROUGH to helper zone */
            //異常情況-->失敗會(huì)走h(yuǎn)elper zone
        }
    }
    malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
    return zone->calloc(zone, 1, total_bytes);
}

可以看出沒有異常情況,會(huì)進(jìn)入_nano_malloc_check_clear方法弟孟,打斷點(diǎn)進(jìn)入贝咙。

static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
    MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);

    void *ptr;
    size_t slot_key;
    //加鹽處理,size在此處使用
    size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
    mag_index_t mag_index = nano_mag_index(nanozone);

    nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);

    ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
    if (ptr) {
        //都是一些異常情況處理拂募,代碼沒有粘貼
    } else {
                //正常情況
        ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
    }

    if (cleared_requested && ptr) {
        memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
    }
    return ptr;
}

到了此處確實(shí)不知該如何進(jìn)行下去(一臉懵逼)庭猩。我們既然要探索size是如何開辟到內(nèi)存的,那我們就跟蹤size陈症,進(jìn)入segregated_size_to_fit方法蔼水。(我不會(huì)告訴你這都是老師教的)

static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
    size_t k, slot_bytes;

    if (0 == size) {
        size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
    }
    //40 + 16 - 1  >> 4
    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
    // << 4
    slot_bytes = k << SHIFT_NANO_QUANTUM;                           // multiply by power of two quanta size
    *pKey = k - 1;                                                  // Zero-based!

    return slot_bytes;
}

k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
slot_bytes = k << SHIFT_NANO_QUANTUM;

這句代碼翻譯出來就是

k = (40 + 16 - 1)>>4 //右移4位
slot_bytes = 55 << 4 //再左移4位

我們?cè)囍?jì)算一下。

k = 40 + 16 - 1 = 55
0011 0111 //k的二進(jìn)制
右移4位录肯,空的補(bǔ)0(>>4)
0000 0011
左移4位趴腋,空的補(bǔ)0(<<4)
0011 0000 //正好等于48

這里其實(shí)運(yùn)用了16字節(jié)內(nèi)存對(duì)齊,和上邊的8字節(jié)對(duì)齊有著異曲同工之妙(把~WORD_MASK換成>>3论咏、<<3也可以實(shí)現(xiàn)8字節(jié)對(duì)齊)优炬。上邊的8字節(jié)對(duì)齊是為了保證每個(gè)對(duì)象占用8字節(jié)內(nèi)存(64位),16字節(jié)對(duì)齊則是保證了每個(gè)對(duì)象開辟了16字節(jié)的內(nèi)存厅贪。關(guān)于對(duì)象占用內(nèi)存和開辟內(nèi)存的關(guān)系可以看下 Cocoi老師的文章蠢护。

進(jìn)行到這里我們大致也就驗(yàn)證出來了,那些沒看懂可以以后慢慢探索养涮,現(xiàn)在我們至少了解了對(duì)象占用內(nèi)存和開辟內(nèi)存蘋果底層都是怎么實(shí)現(xiàn)的葵硕。

objc_alloc的探索

其實(shí)使用探索方法1和3的時(shí)候,我們都看到了objc_alloc這個(gè)方法贯吓,那這個(gè)方法到底是干什么的呢懈凹。
全局搜索objc_alloc,可以找到兩個(gè)方法悄谐。

// Calls [cls alloc].
id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

// Calls [cls allocWithZone:nil].
id 
objc_allocWithZone(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, true/*allocWithZone*/);
}

可以看出objc_alloc也會(huì)調(diào)用callAlloc蘸劈,只不過和_objc_rootAlloc調(diào)用傳入的參數(shù)不同。在objc_alloc打斷點(diǎn)調(diào)試得出objc_alloc_objc_rootAlloc之前執(zhí)行尊沸,進(jìn)入callAlloc執(zhí)行[cls alloc]威沫,會(huì)調(diào)起alloc。然后才是上邊總結(jié)的流程洼专。

那么objc_alloc做了什么事情棒掠?全局搜索objc_alloc,可以看到一個(gè)調(diào)用objc_alloc的地方屁商,方法如下:

static void 
fixupMessageRef(message_ref_t *msg)
{    
    msg->sel = sel_registerName((const char *)msg->sel);

    if (msg->imp == &objc_msgSend_fixup) { 
        if (msg->sel == SEL_alloc) {
            msg->imp = (IMP)&objc_alloc;
        } else if (msg->sel == SEL_allocWithZone) {
            msg->imp = (IMP)&objc_allocWithZone;
        } else if (msg->sel == SEL_retain) {
            msg->imp = (IMP)&objc_retain;
        } else if (msg->sel == SEL_release) {
            msg->imp = (IMP)&objc_release;
        } else if (msg->sel == SEL_autorelease) {
            msg->imp = (IMP)&objc_autorelease;
        } else {
            msg->imp = &objc_msgSend_fixedup;
            //下邊代碼省略

此處判斷sel如果是SEL_alloc(即alloc)就把objc_alloc綁定為他的IMP烟很。
我們接著尋找fixupMessageRef方法颈墅,結(jié)果如下:

fixupMessageRef

_read_images是系統(tǒng)讀取鏡像文件,就是dyld進(jìn)行符號(hào)綁定的時(shí)候雾袱,由此我們可以猜想msg->imp = (IMP)&objc_alloc;就是一個(gè)符號(hào)綁定的過程恤筛。那我們驗(yàn)證一下,編譯一下工程芹橡,把目錄Products下的可執(zhí)行文件用MachOView(鏈接:https://pan.baidu.com/s/10k5BFJueUpz_ccBS54DQHQ 密碼:eadw)打開毒坛,可以看到下圖:
符號(hào)綁定驗(yàn)證

至此也就驗(yàn)證了我們的猜想。

init&new探索

init

init流程方法

- (id)init {
    return _objc_rootInit(self);
}

id
_objc_rootInit(id obj)
{
    // In practice, it will be hard to rely on this function.(事實(shí)上林说,很難依靠此方法)
    // Many classes do not properly chain -init calls.(很多類并未實(shí)現(xiàn)此方法)
    return obj;
}

可以看出init源碼直接返回obj煎殷,其實(shí)init方法更多的是為了開發(fā)者自定義類使用的,在實(shí)際中我們自定義類都會(huì)重寫init腿箩,把自定義的內(nèi)容寫在init方法內(nèi)豪直,這個(gè)時(shí)候init才真正發(fā)揮了作用。創(chuàng)建對(duì)象時(shí)調(diào)用init珠移,是為了防止有自定義內(nèi)容弓乙。

new

new流程方法

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}

new方法是調(diào)用了callAlloc&init方法,其實(shí)和alloc&init是一樣的钧惧。

總結(jié)

alloc流程圖
alloc流程圖.jpg

努力就有收獲暇韧,想要在開發(fā)的路上走遠(yuǎn),就要不斷地更新自己的知識(shí)庫(kù)垢乙!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市语卤,隨后出現(xiàn)的幾起案子追逮,更是在濱河造成了極大的恐慌,老刑警劉巖粹舵,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钮孵,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡眼滤,警方通過查閱死者的電腦和手機(jī)巴席,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诅需,“玉大人漾唉,你說我怎么就攤上這事⊙咚” “怎么了赵刑?”我有些...
    開封第一講書人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)场刑。 經(jīng)常有香客問我般此,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任铐懊,我火速辦了婚禮邀桑,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘科乎。我一直安慰自己壁畸,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開白布喜喂。 她就那樣靜靜地躺著瓤摧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪玉吁。 梳的紋絲不亂的頭發(fā)上照弥,一...
    開封第一講書人閱讀 49,730評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音进副,去河邊找鬼这揣。 笑死,一個(gè)胖子當(dāng)著我的面吹牛影斑,可吹牛的內(nèi)容都是我干的给赞。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼矫户,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼片迅!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起皆辽,我...
    開封第一講書人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤柑蛇,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后驱闷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體耻台,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年空另,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了盆耽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡扼菠,死狀恐怖摄杂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情循榆,我是刑警寧澤匙姜,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站冯痢,受9級(jí)特大地震影響氮昧,放射性物質(zhì)發(fā)生泄漏框杜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一袖肥、第九天 我趴在偏房一處隱蔽的房頂上張望咪辱。 院中可真熱鬧,春花似錦椎组、人聲如沸油狂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽专筷。三九已至,卻和暖如春蒸苇,著一層夾襖步出監(jiān)牢的瞬間磷蛹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工溪烤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留味咳,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓檬嘀,卻偏偏與公主長(zhǎng)得像槽驶,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子鸳兽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

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