Class 內(nèi)部結(jié)構(gòu)

isa, cache, bits

通過前面一篇從 MachO 加載到對象創(chuàng)建! 可以了解到:
  • 在 alloc 的時候, 系統(tǒng)會開辟一片內(nèi)存空間, 最終以指針的形式返回(對象).
    而后便會初始化(對象的) isa ==> initIsa(cls), 具體可以根據(jù)源碼編譯跟進(jìn)調(diào)試, 作為前面文章一個補(bǔ)充:

如果只看 Class 可跳過這一步.

編譯 objc-750

首先從官方資源網(wǎng)下載 objc 的源碼, 打開 objc.xcodeproj 文件然后進(jìn)行編譯 :

報錯1:

The i386 architecture is deprecated. 
You should update your ARCHS build setting to \
remove the i386 architecture. 
(in target 'objc')

'i8386' 架構(gòu)被廢棄, 你應(yīng)該更新你的 ARCHS 編譯設(shè)置項, 刪除 'i8386' 架構(gòu).
來到 target objcbuild setting, 搜索 ARCHS, 會發(fā)現(xiàn)報錯提示的 'i386' 以及 'x86_64', 刪除 'i386' 再編譯 :

報錯2:

The i386 architecture is deprecated. 
You should update your ARCHS build setting to \
remove the i386 architecture. 
(in target 'objc-trampolines')

同報錯1, 將 target objc-trampolinesbuild setting下 'i386' 刪除再編譯 (注: 這里可以將 objc-trampolinesobjc-simulator 兩個 target 刪除, 因為一般用不到) :

報錯3

'sys/reason.h' file not found

這里需要將頭文件 (注:需要下載多個依賴庫, 其中包含想要的頭文件, 比如 reason.h./xnu-4903.221.2/bsd/sys/reason.h 中, 所以需要下載該庫以獲取頭文件) 包含到任一新建文件夾下, 并將系統(tǒng)頭文件路徑 system header search paths 設(shè)置為該文件夾目錄, 具體可參照:
最新Runtime源碼objc4-750編譯

注: 此處并不需要依次建立文件夾, 直接放到工程目錄下, 把包含的頭文件路徑換成頭文件(即: <sys/reason.h> 修改為 <reason.h>) 也可以, 只要 system header search paths + import 頭文件的絕對路徑能找到就可以.

報錯4

Use of undeclared identifier 'CRGetCrashLogMessage'

查看依賴 CrashReporterClient.h 頭文件, 發(fā)現(xiàn)此處用了宏定義:

#ifdef LIBC_NO_LIBCRASHREPORTERCLIENT

/* Fake the CrashReporterClient API */
#define CRGetCrashLogMessage() 0
#define CRSetCrashLogMessage(x) /* nothing */

#else /* !LIBC_NO_LIBCRASHREPORTERCLIENT */

所以需要在 Build SettingsPreprocessor Macros(預(yù)處理宏)類目中加入 LIBC_NO_LIBCRASHREPORTERCLIENT 環(huán)境變量來使方法生效, 繼續(xù)編譯:

報錯5

clang:-1: linker command failed with exit code 1 

這個報錯很多人摸不著頭腦, 因為沒有報錯信息, 只知道是 link 時報錯, 這里有個技巧:
就是查看編譯日志 (快捷鍵 command 9), 這里記錄詳細(xì)的報錯信息:

ld: can't open order file: 
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform
/Developer/SDKs/MacOSX10.14.sdk/AppleInternal/OrderFiles/libobjc.order
clang: error: linker command failed with exit code 1

打不開 order file 文件 (因為找不到), 別擔(dān)心, 只需要換個路徑就可以了, 在工程目錄的 other 文件夾下同樣有一份這個文件, 同樣修改 Build Settings 設(shè)置, 搜索 order file, 即可看到原來是查找 AppleInternal目錄下的文件, 替換為工程目錄下的文件路徑繼續(xù)編譯:

報錯6

clang:-1: linker command failed with exit code 1

同樣是 clang 錯誤, 這下知道怎么看詳細(xì)的報錯信息了, 那么:

ld: library not found for -lCrashReporterClient
clang: error: linker command failed with exit code 1

報錯找不到該庫文件, 由于我們前面設(shè)置過 LIBC_NO_LIBCRASHREPORTERCLIENT 環(huán)境變量, 所以這個 -lCrashReporterClient 是不需要的, 由于報錯是 linker command, 所以要到 Build Settings 里面搜索 linking, 可以看見 Other link flags 里面有設(shè)置這樣的 flag (注: 也可以直接搜索 lCrashReporterClient 關(guān)鍵字, 搜索結(jié)果會把包含所有 lCrashReporterClient flag 的類目找出來) , 刪除該 flag 即可.
還是繼續(xù)編譯:

報錯7

/xcodebuild:-1: SDK "macosx.internal" cannot be located.
/xcrun:-1: unable to find utility "clang++", 
not a developer tool or in PATH

看著 xcodebuildxcrun 報錯, 又是一臉懵, 其實很簡單, 還是查看編譯日志:

+ /usr/bin/xcrun -sdk macosx.internal clang++ \
-Wall -mmacosx-version-min=10.12 
-arch x86_64 -std=c++11 
PATH
xcodebuild: error: SDK "macosx.internal" cannot be located.
xcrun: error: unable to find utility "clang++", \
not a developer tool or in PATH

xcodebuild: error: "macosx.internal" SDK 找不到, 找不到 "clang++" 命令, 明顯的還有執(zhí)行的命令(如下圖):

image.png

原來是在執(zhí)行 script 時報錯, 那么就來到 build phases 中查看執(zhí)行的 script 信息:

set -x
/usr/bin/xcrun -sdk macosx.internal clang++ -Wall \
-mmacosx-version-min=10.12 
-arch x86_64 -std=c++11 "${SRCROOT}/markgc.cpp" -o 
"${BUILT_PRODUCTS_DIR}/markgc"
"${BUILT_PRODUCTS_DIR}/markgc" "${BUILT_PRODUCTS_DIR}/libobjc.A.dylib"

腳本旨在調(diào)用 clang++ 命令, 所以把 macosx.internal 改為 macosx, 使用系統(tǒng)自帶的 clang 命令進(jìn)行編譯,
再編譯:

報錯8

no such public header file: '/tmp/objc.dst/usr/include/objc/ObjectiveC\
.apinotes'

同樣, 查看編譯日志:


image.png

由于是在 InstallAPI 時報錯, 所以可以在 build setting 中查詢關(guān)鍵字 InstallAPI, 直接將 Supports Text-Based InstallAPI 設(shè)置為 NO (注: 這里也可以將 InstallAPI flags 中對應(yīng)的 flag 刪除以消除編譯錯誤, 需要相繼刪除幾個).

編譯成功
到這里, 編譯已經(jīng)成功了, 那么接下來可以新建測試項目了:
此時應(yīng)注意, 編譯環(huán)境只是在 mac 下, 所以新建 Target 時只能選擇 macOS 下的Application 相關(guān)項 (Cocoa App, Game, Command Line Tool 等), 然后添加對剛剛配置好的 objc 庫依賴 (如果需要的話).

Class

有源碼文件可見:
Class 定義為 typedef struct objc_class *Class;, 是一個指向 objc_class 的結(jié)構(gòu)體指針;

objc_class

包含類的 isa, 父類, 緩存等信息:

struct objc_class : objc_object {
    // Class ISA; 繼承自 objc_object
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

依次:

isa

在類實現(xiàn)方法中 static Class realizeClass(Class cls), 在設(shè)置好 cls->superclass 后便會進(jìn)行 isa 初始化 cls->initClassIsa(metacls)(文章開頭已給出鏈接, 不作贅述):
最終調(diào)用:

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) {

    if (!nonpointer) {
        isa.cls = cls;
    } else {
        isa_t newisa(0);
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
        isa = newisa;
    }
}

isa 簡單初始化并賦值 cls 的信息, 那么 isa 到底是什么?

解析 isa (附 superClass):
調(diào)用如下函數(shù)(方法):

// isa 流程圖驗證
int isaTest() {
    WXPerson *person = [[WXPerson alloc] init];
    Class cls = person.class; // 類對象
    Class cls4 = object_getClass(cls); // 元類
    Class cls5 = object_getClass(cls4); // 根元類
    NSLog(@"%@ -- %p 對象", person, person);
    NSLog(@"%@ -- %p 類對象", cls,cls);
    NSLog(@"%@ -- %p 元類", cls4, cls4);
    NSLog(@"%@ -- %p 根元類", cls5, cls5); // 根元類
    NSLog(@"%@ -- %p 對象父類", person.superclass, person.superclass);
    NSLog(@"%@ -- %p 類對象父類",[cls superclass] , [cls superclass]);
    NSLog(@"%@ -- %p 元類父類", [cls4 superclass], [cls4 superclass]);
    NSLog(@"%@ -- %p 根元類父類", [cls5 superclass], [cls5 superclass]);
    NSLog(@"%@ -- %p 根元類的isa", object_getClass(cls5), 
                                  object_getClass(cls5));
    
    NSLog(@"%@ -- %p NSObject 父類", [[[NSObject alloc] init] superclass],
                                    [[[NSObject alloc] init] superclass]);
                                    
    NSLog(@"%@ -- %p NSObject isa", object_getClass([NSObject class]),
                                    object_getClass([NSObject class]));
    
    return 0;
}

這里直接貼下 log :

2019-04-03 11:36 [51997:6246754] <WXPerson: 0x100f4bd20> --0x100f4bd20 對象
2019-04-03 11:36 [51997:6246754] WXPerson -- 0x1000026d8 類對象        
2019-04-03 11:36 [51997:6246754] WXPerson -- 0x1000026b0 元類          
2019-04-03 11:36 [51997:6246754] NSObject -- 0x100b170f0 根元類        
2019-04-03 11:36 [51997:6246754] WXHuman  -- 0x100002688 對象父類       
2019-04-03 11:36 [51997:6246754] WXHuman  -- 0x100002688 類對象父類     
2019-04-03 11:36 [51997:6246754] WXHuman  -- 0x100002660 元類父類       
2019-04-03 11:36 [51997:6246754] NSObject -- 0x100b17140 根元類父類     
2019-04-03 11:36 [51997:6246754] NSObject -- 0x100b170f0 根元類的isa    
2019-04-03 11:36 [51997:6246754] (null)   -- 0x0         NSObject 父類  
2019-04-03 11:36 [51997:6246754] NSObject -- 0x100b170f0 NSObject isa

這里在貼一張根據(jù)打印地址畫的一張 isa 走向圖 (地址對號入座)

image.png

這里引入一個虛擬類 元類, 元類地址可以通過 objc_getMetaClass 得到.

元類 :

通過實例對象(person)的 class 方法可以查看對象所屬類(Person), 根據(jù)對象的 isa 可知, 類仍然是一個對象, 只是這個對象是相對于元類而言:

對象 --> 類 == 類對象 --> 元類

元類什么時候初始化 ?
運(yùn)用 objc/runtime 的接口 objc_allocateClassPair 動態(tài)創(chuàng)建類, 通過源碼可以看到, 該接口在創(chuàng)建類的時候, 定義了兩個 Class 對象(結(jié)構(gòu)體) clsmeta, 然后對兩個類對象作空間開辟操作:

Class objc_allocateClassPair(Class superclass, 
                        const char *name, 
                            size_t extraBytes)
{
    Class cls, meta;

    mutex_locker_t lock(runtimeLock);

    // Fail if the class name is in use.
    // Fail if the superclass isn't kosher.
    if (getClass(name) || !verifySuperclass(superclass, true/*rootOK*/)) {
        return nil;
    }

    // Allocate new classes.
    // 僅僅只開辟空間, 開辟一個 objc_class 結(jié)構(gòu)體大小的空間作為返回.
    // _calloc_class(sizeof(objc_class) + extraBytes);
    cls  = alloc_class_for_subclass(superclass, extraBytes);
    meta = alloc_class_for_subclass(superclass, extraBytes);

    // fixme mangle the name if it looks swift-y?
    objc_initializeClassPair_internal(superclass, name, cls, meta);

    return cls;
}

其次會調(diào)用到 objc_initializeClassPair_internal 函數(shù):

static void objc_initializeClassPair_internal(Class superclass, 
                                         const char *name, 
                                              Class cls, 
                                              Class meta)
{
    runtimeLock.assertLocked();

    class_ro_t *cls_ro_w, *meta_ro_w;
    
    // 開辟類/元類 data / ro
    cls->setData((class_rw_t *)calloc(sizeof(class_rw_t), 1));
    meta->setData((class_rw_t *)calloc(sizeof(class_rw_t), 1));
    cls_ro_w   = (class_ro_t *)calloc(sizeof(class_ro_t), 1);
    meta_ro_w  = (class_ro_t *)calloc(sizeof(class_ro_t), 1);
    cls->data()->ro = cls_ro_w;
    meta->data()->ro = meta_ro_w;

    // Set basic info

    // 查詢 RW_ FLAGS
    cls->data()->flags = RW_CONSTRUCTING | 
                         RW_COPIED_RO | 
                         RW_REALIZED | 
                         RW_REALIZING;
    meta->data()->flags = RW_CONSTRUCTING | 
                          RW_COPIED_RO | 
                          RW_REALIZED | 
                          RW_REALIZING;
    cls->data()->version = 0;
    meta->data()->version = 7;

    cls_ro_w->flags = 0;
    meta_ro_w->flags = RO_META;
    if (!superclass) {
        cls_ro_w->flags |= RO_ROOT;
        meta_ro_w->flags |= RO_ROOT;
    }
    if (superclass) {
        cls_ro_w->instanceStart = superclass->unalignedInstanceSize();
        meta_ro_w->instanceStart = 
                              superclass->ISA()->unalignedInstanceSize();
        cls->setInstanceSize(cls_ro_w->instanceStart);
        meta->setInstanceSize(meta_ro_w->instanceStart);
    } else {
        cls_ro_w->instanceStart = 0;
        meta_ro_w->instanceStart = (uint32_t)sizeof(objc_class);
        cls->setInstanceSize((uint32_t)sizeof(id));  // just an isa
        meta->setInstanceSize(meta_ro_w->instanceStart);
    }

    cls_ro_w->name = strdupIfMutable(name);
    meta_ro_w->name = strdupIfMutable(name);

    cls_ro_w->ivarLayout = &UnsetLayout;
    cls_ro_w->weakIvarLayout = &UnsetLayout;

    meta->chooseClassArrayIndex();
    cls->chooseClassArrayIndex();

// 這里是上面 isa 及 superclass 走位圖的根源!
    // Connect to superclasses and metaclasses
    // 初始化類對象的 isa 為 meta 類
    cls->initClassIsa(meta);
    if (superclass) {
        // 元類的 isa 指向 元類的元類(根元類)
        meta->initClassIsa(superclass->ISA()->ISA()); 
        cls->superclass = superclass;
        meta->superclass = superclass->ISA(); // 子元類的父類 是 父元類
        addSubclass(superclass, cls);
        addSubclass(superclass->ISA(), meta); // 父類的元類 是 子類元類的父類
    } else {
        meta->initClassIsa(meta); // 根元類指向本身
        cls->superclass = Nil;    // 根類指向 Nil
        meta->superclass = cls;   // 根元類指向根類
        
        // 類關(guān)系 鏈表
        // NSObject->nil
        // 通過 mask 獲取 bits.data() 將 _firstRealizedClass 設(shè)置為 cls
        // cls->data()->nextSiblingClass = _firstRealizedClass;
        // _firstRealizedClass = cls;
        addRootClass(cls);
        
        // NSObject(元類) nextSiblingClass -> NSObject firstSubclass
        // NSObject firstSubclass -> NSObject(元類)
        
        // 將子類的 next 指向父類 first
        // cls(父類)的 firstSubclass 設(shè)置為 meta(子類)
        // subcls->data()->nextSiblingClass = 
        //                            supercls->data()->firstSubclass;
        // supercls->data()->firstSubclass = subcls;
        // NSObject(meta) 的 父類是 NSObject, 即: 根元類的父類是 NSObject;
        addSubclass(cls, meta);
    }

    // 初始化 cache
    cls->cache.initializeToEmpty();
    meta->cache.initializeToEmpty();
    
    // 內(nèi)部遞歸 meta 類, 所以不需要寫   addClassTableEntry(meta);
    addClassTableEntry(cls);
}

這樣, 相信你應(yīng)該知道元類到底是什么了:

  • 元類也是 Class;
  • 子元類的 isa 是 父元類 的 元類, 即: 根元類;
  • 根元類的 isa 指向自己;
  • 根類的父類是 nil;
  • 子類的元類的父類是父類的元類;
  • 根元類的父類是根類;

isa 作用

那么 isa 的作用是什么呢?

  • 查找類的實現(xiàn):
    在動態(tài)創(chuàng)建類時還需要調(diào)用另外一個接口: objc_registerClassPair(Class cls):
objc_registerClassPair(Class cls) {
    // 改變標(biāo)記值 正在創(chuàng)建 -> 已創(chuàng)建
    cls->ISA()->changeInfo(RW_CONSTRUCTED, \
                           RW_CONSTRUCTING | RW_REALIZING);
    cls->changeInfo(RW_CONSTRUCTED, 
                           RW_CONSTRUCTING | RW_REALIZING);

    // Add to named class table.
    addNamedClass(cls, cls->data()->ro->name);
}

該接口會將注冊的 clskey-value 的形式添加到 gdb_objc_realized_classes表中 :

// 添加類實現(xiàn)到 hash 表中
static void addNamedClass(Class cls, 
                     const char *name, 
                          Class replacing = nil) {
    // 插入到 NXMapTable hash 表中, 以 name 作為 key, cls 作為 value 保存
    NXMapInsert(gdb_objc_realized_classes, name, cls);
}

gdb_objc_realized_classes 表在 load_images(dyld 注冊的回調(diào)) -> read_images 第一次的時候就會初始化, 在查找類實現(xiàn)的時候會通過 name(類名) 從該表中進(jìn)行查找, 即 getClass(const char *name):

static Class getClass(const char *name) {
    runtimeLock.assertLocked();
    // Try name as-is
    Class result = getClass_impl(name);
    if (result) return result;
}
static Class getClass_impl(const char *name)
{
    runtimeLock.assertLocked();
    // Try runtime-allocated table
    Class result = (Class)NXMapGet(gdb_objc_realized_classes, name);
    if (result) return result;

    // Try table from dyld shared cache
    return getPreoptimizedClass(name);
}

因為是獲取類的實現(xiàn), 而獲取 類實現(xiàn) 在 objc/runtime 中, 即: objc_getClass:

Class objc_getClass(const char *aClassName) {
    return look_up_class(aClassName, NO, YES);
}
look_up_class(const char *name, 
              bool includeUnconnected __attribute__((unused)), 
              bool includeClassHandler __attribute__((unused))) {
 
    Class result;
    bool unrealized;
    {
        mutex_locker_t lock(runtimeLock);
        result = getClass(name);
        unrealized = result  &&  !result->isRealized();
    }
    if (unrealized) {
        mutex_locker_t lock(runtimeLock);
        realizeClass(result);
    }
    return result;
}

可以看到, 在 look_up_class 函數(shù)中會調(diào)用 getClass 接口去獲取類的實現(xiàn)!

  • 方法查找:
    在調(diào)用類方法的時候, receiver 由編譯器編譯成 objc_getClass(類對象) 獲取類的實現(xiàn)(通過匯編取 isa 找元類, 其實是找類對象的實現(xiàn)), 然后在元類中找方法的 IMP;
    而調(diào)用對象方法時, 則會在通過 isa 獲取對象的 class(即類對象, 其實是找對象的實現(xiàn)) 中查找方法的IMP;
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov p16, $0         // optimistically set dst = src
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
    // isa in p16 is indexed
    adrp    x10, _objc_indexed_classes@PAGE
    add x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
#elif __LP64__
    // 64-bit packed isa
    and p16, $0, #ISA_MASK   
#else
    // 32-bit raw isa
    mov p16, $0
#endif
.endmacro

至此, 可以了解到, isa 的作用之一是查找類的實現(xiàn)(class_impl), 而且, 在調(diào)用方法的時候并沒有區(qū)分類方法/對象方法, 只是通過 isa 去類對象中查找方法的 IMP.

cache

首先看下 cache_t 結(jié)構(gòu):


image.png

mask:緩存 bucket 的總數(shù).
occupied:目前實際占用的緩存 bucket 的個數(shù)。
buckets:hash 表,用來緩存方法,bucket_t 類型,包含 key(sel 方法編號) 以及方法實現(xiàn) IMP。

看該結(jié)構(gòu)體提供方法就可以知道, cache_t 是以 hash 表的方式存儲了方法的 IMP,
將 sel 方法編號轉(zhuǎn)為 cache_key_t 類型即 uintptr_t 作為 key 存儲.

typedef uintptr_t cache_key_t;

cache_key_t getKey(SEL sel) 
{
    assert(sel);
    return (cache_key_t)sel;
}

在調(diào)用方法時, objc_msgSend 會先找 cache, 指的就是這里的方法緩存列表.


image.png

bits

bits 結(jié)構(gòu):


image.png

可以看到 bits 中 data() 主要存儲了 方法/屬性/協(xié)議 列表, 并且記錄當(dāng)前類的第一個子類, 以及下一個類, flags 標(biāo)記當(dāng)前類狀態(tài). version 標(biāo)記當(dāng)前類類型, 如:cls version = 0, meta version = 7,
其他只讀屬性保存在屬性 ro 結(jié)構(gòu)體中.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容