Class 内部结构

isa, cache, bits

通过前面一篇从 MachO 加载到对象创建! 可以了解到:
  • 在 alloc 的时候, 系统会开辟一片内存空间, 最终以指针的形式返回(对象).
    而后便会初始化(对象的) isa ==> initIsa(cls), 具体可以根据源码编译跟进调试, 作为前面文章一个补充:

如果只看 Class 可跳过这一步.

编译 objc-750

首先从官方资源网下载 objc 的源码, 打开 objc.xcodeproj 文件然后进行编译 :

报错1:

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

'i8386' 架构被废弃, 你应该更新你的 ARCHS 编译设置项, 删除 'i8386' 架构.
来到 target objcbuild setting, 搜索 ARCHS, 会发现报错提示的 '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 中, 所以需要下载该库以获取头文件) 包含到任一新建文件夹下, 并将系统头文件路径 system header search paths 设置为该文件夹目录, 具体可参照:
最新Runtime源码objc4-750编译

注: 此处并不需要依次建立文件夹, 直接放到工程目录下, 把包含的头文件路径换成头文件(即: <sys/reason.h> 修改为 <reason.h>) 也可以, 只要 system header search paths + import 头文件的绝对路径能找到就可以.

报错4

Use of undeclared identifier 'CRGetCrashLogMessage'

查看依赖 CrashReporterClient.h 头文件, 发现此处用了宏定义:

#ifdef LIBC_NO_LIBCRASHREPORTERCLIENT

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

#else /* !LIBC_NO_LIBCRASHREPORTERCLIENT */

所以需要在 Build SettingsPreprocessor Macros(预处理宏)类目中加入 LIBC_NO_LIBCRASHREPORTERCLIENT 环境变量来使方法生效, 继续编译:

报错5

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

这个报错很多人摸不着头脑, 因为没有报错信息, 只知道是 link 时报错, 这里有个技巧:
就是查看编译日志 (快捷键 command 9), 这里记录详细的报错信息:

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 文件 (因为找不到), 别担心, 只需要换个路径就可以了, 在工程目录的 other 文件夹下同样有一份这个文件, 同样修改 Build Settings 设置, 搜索 order file, 即可看到原来是查找 AppleInternal目录下的文件, 替换为工程目录下的文件路径继续编译:

报错6

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

同样是 clang 错误, 这下知道怎么看详细的报错信息了, 那么:

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

报错找不到该库文件, 由于我们前面设置过 LIBC_NO_LIBCRASHREPORTERCLIENT 环境变量, 所以这个 -lCrashReporterClient 是不需要的, 由于报错是 linker command, 所以要到 Build Settings 里面搜索 linking, 可以看见 Other link flags 里面有设置这样的 flag (注: 也可以直接搜索 lCrashReporterClient 关键字, 搜索结果会把包含所有 lCrashReporterClient flag 的类目找出来) , 删除该 flag 即可.
还是继续编译:

报错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++" 命令, 明显的还有执行的命令(如下图):

image.png

原来是在执行 script 时报错, 那么就来到 build phases 中查看执行的 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"

脚本旨在调用 clang++ 命令, 所以把 macosx.internal 改为 macosx, 使用系统自带的 clang 命令进行编译,
再编译:

报错8

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

同样, 查看编译日志:


image.png

由于是在 InstallAPI 时报错, 所以可以在 build setting 中查询关键字 InstallAPI, 直接将 Supports Text-Based InstallAPI 设置为 NO (注: 这里也可以将 InstallAPI flags 中对应的 flag 删除以消除编译错误, 需要相继删除几个).

编译成功
到这里, 编译已经成功了, 那么接下来可以新建测试项目了:
此时应注意, 编译环境只是在 mac 下, 所以新建 Target 时只能选择 macOS 下的Application 相关项 (Cocoa App, Game, Command Line Tool 等), 然后添加对刚刚配置好的 objc 库依赖 (如果需要的话).

Class

有源码文件可见:
Class 定义为 typedef struct objc_class *Class;, 是一个指向 objc_class 的结构体指针;

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

在类实现方法中 static Class realizeClass(Class cls), 在设置好 cls->superclass 后便会进行 isa 初始化 cls->initClassIsa(metacls)(文章开头已给出链接, 不作赘述):
最终调用:

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):
调用如下函数(方法):

// 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

这里在贴一张根据打印地址画的一张 isa 走向图 (地址对号入座)

image.png

这里引入一个虚拟类 元类, 元类地址可以通过 objc_getMetaClass 得到.

元类 :

通过实例对象(person)的 class 方法可以查看对象所属类(Person), 根据对象的 isa 可知, 类仍然是一个对象, 只是这个对象是相对于元类而言:

对象 --> 类 == 类对象 --> 元类

元类什么时候初始化 ?
运用 objc/runtime 的接口 objc_allocateClassPair 动态创建类, 通过源码可以看到, 该接口在创建类的时候, 定义了两个 Class 对象(结构体) 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 结构体大小的空间作为返回.
    // _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;
}

其次会调用到 objc_initializeClassPair_internal 函数:

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;   // 根元类指向根类
        
        // 类关系 链表
        // NSObject->nil
        // 通过 mask 获取 bits.data() 将 _firstRealizedClass 设置为 cls
        // cls->data()->nextSiblingClass = _firstRealizedClass;
        // _firstRealizedClass = cls;
        addRootClass(cls);
        
        // NSObject(元类) nextSiblingClass -> NSObject firstSubclass
        // NSObject firstSubclass -> NSObject(元类)
        
        // 将子类的 next 指向父类 first
        // cls(父类)的 firstSubclass 设置为 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();
    
    // 内部递归 meta 类, 所以不需要写   addClassTableEntry(meta);
    addClassTableEntry(cls);
}

这样, 相信你应该知道元类到底是什么了:

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

isa 作用

那么 isa 的作用是什么呢?

  • 查找类的实现:
    在动态创建类时还需要调用另外一个接口: objc_registerClassPair(Class cls):
objc_registerClassPair(Class cls) {
    // 改变标记值 正在创建 -> 已创建
    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表中 :

// 添加类实现到 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 注册的回调) -> read_images 第一次的时候就会初始化, 在查找类实现的时候会通过 name(类名) 从该表中进行查找, 即 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);
}

因为是获取类的实现, 而获取 类实现 在 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 函数中会调用 getClass 接口去获取类的实现!

  • 方法查找:
    在调用类方法的时候, receiver 由编译器编译成 objc_getClass(类对象) 获取类的实现(通过汇编取 isa 找元类, 其实是找类对象的实现), 然后在元类中找方法的 IMP;
    而调用对象方法时, 则会在通过 isa 获取对象的 class(即类对象, 其实是找对象的实现) 中查找方法的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 的作用之一是查找类的实现(class_impl), 而且, 在调用方法的时候并没有区分类方法/对象方法, 只是通过 isa 去类对象中查找方法的 IMP.

cache

首先看下 cache_t 结构:


image.png

mask:缓存 bucket 的总数.
occupied:目前实际占用的缓存 bucket 的个数。
buckets:hash 表,用来缓存方法,bucket_t 类型,包含 key(sel 方法编号) 以及方法实现 IMP。

看该结构体提供方法就可以知道, cache_t 是以 hash 表的方式存储了方法的 IMP,
将 sel 方法编号转为 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;
}

在调用方法时, objc_msgSend 会先找 cache, 指的就是这里的方法缓存列表.


image.png

bits

bits 结构:


image.png

可以看到 bits 中 data() 主要存储了 方法/属性/协议 列表, 并且记录当前类的第一个子类, 以及下一个类, flags 标记当前类状态. version 标记当前类类型, 如:cls version = 0, meta version = 7,
其他只读属性保存在属性 ro 结构体中.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342