iOS探索 alloc流程

我是好寶寶發表於2019-12-22

歡迎閱讀iOS探索系列(按序閱讀食用效果更加)

寫在前面

OC作為一門萬物皆物件的語言,那麼對於物件建立、開闢記憶體的瞭解必不可少,本文就將探索一下alloc在底層的具體步驟

官方原始碼

Cooci司機objc4-756.2除錯方案(Xcode11暫時無法斷點進原始碼)

一、探索方向

在原始碼中,我們可以通過Command+單擊/右擊->Jump to Defintion的方式進入alloc呼叫方法,脫離了原始碼我們又該如何知道它呼叫了什麼底層方法呢?

在物件建立的程式碼處下個斷點,等執行到斷點處使用以下方法:

  • Control+Step into
  • 符號斷點
  • 選單欄Debug->Debug Workflow->Always Show Disassembly(始終顯示彙編程式碼)

這三種方法都能得出呼叫了objc_alloc方法

二、開始探索

//
//  main.m
//  FXTest
//
//  Created by mac on 2019/12/19.
//

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "FXPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSObject *object1 = [NSObject alloc];
        FXPerson *object2 = [FXPerson alloc];
    }
    return 0;
}
複製程式碼

不出意料,各位都能來到如下方法

+ (id)alloc {
    return _objc_rootAlloc(self);
}
複製程式碼
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
複製程式碼

但是接下來的原始碼就會讓你頭暈目眩,不想看了

// Call [cls alloc] or [cls allocWithZone:nil], with appropriate 
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__
    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
        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 {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif

    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}
複製程式碼

本來看原始碼就枯燥,還有這麼多if-else邏輯岔路口,就會有很多人關閉了Xcode

看啥不好看原始碼,是嫌自己頭髮太旺盛嗎?

別急,我這裡已經幫你掉過頭發了(捋過思路了)

三、alloc原始碼流程

iOS探索 alloc流程

1.坑——objc_alloc、alloc傻傻分不清楚

這個坑無傷大雅,瞭解即可;可以簡單理解為OC物件alloc->alloc

不知道你有沒有發現奇怪的一點,第二節探索方向中明明呼叫的是底層objc_alloc方法,為什麼在建立物件處跟進原始碼會來到alloc方法呢?

函式呼叫棧也略有問題

iOS探索 alloc流程

這個問題還與Xcode版本有關,Xcode11->objc_alloc,Xcode10->alloc,實在令人百思不得其解

對於這個問題,目前的說法是原始碼開源得不夠充分。以下這段程式碼雖然未呼叫到,但其邏輯也是耐人尋味。 大致猜測是這個方法等於交換單次的Method Swizzling(既然官方不開源,說明無關痛癢)

第一處:註釋告訴你會調起[cls alloc]

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

另一處:如果出問題就fixMessageRef(下斷點不會被呼叫)

iOS探索 alloc流程

2.alloc、_objc_rootAlloc方法

前面提及過的兩個方法

+ (id)alloc {
    return _objc_rootAlloc(self);
}
複製程式碼
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
複製程式碼

3.callAlloc方法

// Call [cls alloc] or [cls allocWithZone:nil], with appropriate 
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__
    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
        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 {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif

    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}
複製程式碼

①if (slowpath(checkNil && !cls))判斷

fastpath(x)表示x很可能不為0,希望編譯器進行優化;slowpath(x)表示x很可能為0,希望編譯器進行優化——這裡表示cls大概率是有值的,編譯器可以不用每次都讀取 return nil 指令

②if (fastpath(!cls->ISA()->hasCustomAWZ()))判斷

hasCustomAWZ實際意義是hasCustomAllocWithZone——這裡表示有沒有alloc / allocWithZone的實現(只有不是繼承NSObject/NSProxy的類才為true)

③if (fastpath(cls->canAllocFast()))判斷

內部呼叫了bits.canAllocFast預設為false

④id obj = class_createInstance(cls, 0)

內部呼叫了_class_createInstanceFromZone(cls, extraBytes, nil)

這裡有個id obj,嘗試著控制檯列印一下

iOS探索 alloc流程

我們已經找到了我們想要的結果,接下來我們探索下_class_createInstanceFromZone方法是怎麼將obj建立出來的

4._class_createInstanceFromZone方法

/***********************************************************************
* class_createInstance
* fixme
* Locking: none
**********************************************************************/

static __attribute__((always_inline)) 
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    if (!cls) return nil;

    assert(cls->isRealized());

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

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

    id obj;
    if (!zone  &&  fast) {
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        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.
        obj->initIsa(cls);
    }

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

    return obj;
}
複製程式碼

①hasCxxCtor()

hasCxxCtor()是判斷當前class或者superclass是否有.cxx_construct 構造方法的實現

②hasCxxDtor()

hasCxxDtor()是判斷判斷當前class或者superclass是否有.cxx_destruct 析構方法的實現

③canAllocNonpointer()

anAllocNonpointer()是具體標記某個類是否支援優化的isa

④instanceSize()

instanceSize()獲取類的大小(傳入額外位元組的大小)

已知zone=false,fast=true,則(!zone && fast)=true

⑤calloc()

用來動態開闢記憶體,沒有具體實現程式碼,接下來的文章會講到malloc原始碼

⑥initInstanceIsa()

內部呼叫initIsa(cls, true, hasCxxDtor)初始化isa

這一步已經完成了初始化isa並開闢記憶體空間,那我們來看看instanceSize做了什麼

5.位元組對齊

#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

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

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

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

size_t instanceSize(size_t extraBytes) {
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
}
複製程式碼

下面按呼叫順序講解

①size_t instanceSize(size_t extraBytes)

前面講過——獲取類的大小

②alignedInstanceSize()

獲取類所需要的記憶體大小

③unalignedInstanceSize()

data()->ro->instanceSize就是獲取這個類所有屬性記憶體的大小。這裡只有繼承NSObject的一個屬性isa——返回8位元組

④word_align

顧名思義,位元組對齊——64位系統下,物件大小採用8位元組對齊

⑤if (size < 16) size = 16

CoreFoundation需要所有物件之和至少是16位元組

6.位元組對齊演算法

假如: x = 9,已知WORD_MASK = 7

 x + WORD_MASK = 9 + 7 = 16
 WORD_MASK 二進位制 :0000 0111 = 7 (4+2+1)
 ~WORD_MASK : 1111 1000
 16二進位制為  : 0001 0000
  
 1111 1000
 0001 0000
---------------
 0001 0000 = 16

 所以 x = 16    也就是 8的倍數對齊,即 8 位元組對齊
複製程式碼

總結:物件大小為16位元組,必定是8的倍數

這裡有個疑問:為什麼要使用8位元組對齊演算法呢?

簡單畫了個示意圖,上邊是緊緊挨著,下面是8位元組為一格。如果cpu存資料的時候緊緊挨著,讀取的時候要不斷變化讀取長度,所以這時候就採用了空間換時間的做法

那為什麼是8位元組?不是4位元組或是16位元組?

——因為記憶體中8位元組的指標比較多

iOS探索 alloc流程

7.alloc實際流程圖

iOS探索 alloc流程
instanceSize計算記憶體大小——量房子

calloc申請開闢記憶體——造房子

initInstanceIsa指標關聯物件——房子寫下名字

四、init & new

init什麼也不做,就是給開發者使用工廠設計模式提供一個介面

// Replaced by CF (throws an NSException)
+ (id)init {
    return (id)self;
}

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

id
_objc_rootInit(id obj)
{
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
}
複製程式碼

補充:關於子類中if (self = [super init])為什麼要這麼寫——子類先繼承父類的屬性,再判斷是否為空,如若為空沒必要進行一系列操作了直接返回nil

new等於先呼叫alloc,再init

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}
複製程式碼

寫在後面

研究原始碼必然是枯燥的,但是面對原始碼不用害怕,一步步把它拆分開來研究,多利用官方給的註釋/Github大神的註釋,慢慢也就啃下來了。

看別人學習津津有味,不如自己上手實際玩一下會更有收穫;只看不練永遠學不會,或許你學的正是別人錯誤的理論呢

相關文章