IOS底层原理之NSObject的结构

一、疑惑

在OC程序中,我们知道NSObject是“万物之源”,所有的类的都继承自NSObject,我们疑惑的是在OC的底层NSObject是什么样的?类的结构在OC底层是什么样的?我们在类中定义的属性、成员变量、方法、实现的协议等是以什么样的形式存在的?这篇文章我们将深入OC底层探究NSObject的结构。

二、OC底层的NSObject

1、clang命令获取main.m的C++代码

为了知道NSObject底层是什么样的,clang也许是一个选择。

Clang是一个由Apple主导编写,基于LLVM的C/C++/Objective-C编译器。如果你不知道clang,可以在这里找到你想要的。

在工程目录中的main.m文件目录下进入到终端,输入如下命令

clang -rewrite-objc main.m -o main.cpp

该命令会将main.m编译成C++的代码,但是不同平台支持的代码肯定是不一样的。

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件
如果需要链接其他框架,使用-framework参数。比如-framework UIKit

在终端输入命令以后,会生成一个main.cpp文件。打开main.cpp文件,直接将拉到最下面,我们会看到这样的一段代码。

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_q5_25qtkmds1pz_0k3brcv05ft00000gn_T_main_218cf4_mi_0,person);
    }
    return 0;
}

这段代码便是main函数的底层实现。我们有看到我们熟悉的Person,在这里我们关心的是Person的定义什么样的。

typedef struct objc_object Person;
typedef struct objc_class *Class;
struct objc_object {
    Class _Nonnull isa __attribute__((deprecated));
};

__OBJC_RW_DLLIMPORT struct objc_class *objc_getClass(const char *);//获取当前类
__OBJC_RW_DLLIMPORT struct objc_class *class_getSuperclass(struct objc_class *);//获取superClass
__OBJC_RW_DLLIMPORT struct objc_class *objc_getMetaClass(const char *);//获取元类

我们看到Person在底层其实是一个objc_object的结构体,objc_object内部有一个objc_class类型的isa,在前面的详细了解isa这一篇文章中我们已经分析了isa的走位关系。

  1. 实例对象的isa指向的是类;
  2. 类的isa指向的元类;
  3. 元类指向根元类;
  4. 根元类指向自己;
  5. NSObject的父类是nil,根元类的父类是NSObject。

这样分析起来我们有理由相信NSObject的OC底层实现是objc_object,objc_class是类的OC底层实现,而且两者之间应该还会存在这谋种关系。
我们再来看下在main.cpp中还能找到些什么熟悉的东西。

//方法
typedef struct objc_method *Method;
//成员变量
typedef struct objc_ivar *Ivar;
//category
typedef struct objc_category *Category;
//属性列表
typedef struct objc_property *objc_property_t;

2、Objc底层源码分析

我们从main.cpp找到的东西有限,而且代码过于庞大,分析起来很繁杂,下面我们从Objc源码开始分析。

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

// objc_class继承于objc_object,因此
// objc_class中也有isa结构体
struct objc_class : objc_object {
    // Class ISA; //8字节
    Class superclass;//8字节
    // 缓存的是指针和vtable,目的是加速方法的调用  cache占16字节
    cache_t cache;             // formerly cache pointer and vtable
    // class_data_bits_t 相当于是class_rw_t 指针加上rr/alloc标志
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    ...
};

这段代码是objc_object和objc_class的代码,我们看到objc_class是从objc_object中继承而来,
所以objc_class中也有isa结构体。objc_object和objc_class之间的关系如下图所示。


objc_object和objc_class关系图
  • isa指向元类的指针,如果你不知道什么是元类,可以看 Classes and Metaclasses这篇文章。
  • superclass 当前类的父类。
  • cache 用于缓存指针和vtable( formerly cache pointer and vtable)。

bits是什么?这里我们重点探索下bits。

struct class_data_bits_t {
    // 相当于 unsigned long bits; 占64位
    // bits实际上是一个地址(是一个对象的指针,可以指向class_ro_t,也可以指向class_rw_t)
    uintptr_t bits;
};

在objc_class结构体中关于class_data_bits_t的注释:class_rw_t * plus custom rr/alloc flags,意思是class_data_bits_t相当于class_rw_t * 加上rr/alloc标志。它提供了便捷的方式data()方法返回class_rw_t *指针。

 class_rw_t *data() {
     // 这里的bits就是class_data_bits_t bits;
     return bits.data();
 }

 class_rw_t* data() {
     // FAST_DATA_MASK的值是0x00007ffffffffff8UL
     // bits和FAST_DATA_MASK按位与,实际上就是取了bits中的[3,47]位
     return (class_rw_t *)(bits & FAST_DATA_MASK);
 }

这里将 bits 与 FAST_DATA_MASK 进行位运算,只取其中的 [3, 47] 位转换成 class_rw_t * 返回。


objc_class中的data()方法调用了class_data_bits_t 结构体中的 data() 方法,返回class_rw_t * 指针。

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    Class firstSubclass;
    Class nextSiblingClass;
    ...
};

这段是代码是class_rw_t结构体的代码,我们惊奇发现这里有methods(方法)、properties(属性)、protocols(协议)这些信息,那么我们所需要的类中的方法、属性、成员变量等信息是不是在这里存储的呢?下面我来验证下。

我们定义了一个Person类,在Person类中定义了属性name,age,成员变量hobby,实例方法sayHello,类方法eat,断点进入Debug,使用LLVM指令来查看内存信息。

在objc_class的结构中,isa占8字节,superclass占用8字节,cache占用16个字节,将cls的地址偏移32个字节即0x20便是bits的地址。

struct cache_t {
  struct bucket_t *_buckets;//8字节
  mask_t _mask;//4字节
  mask_t _occupied;//4字节
};
typedef uint32_t mask_t;

如上截图所示,在class_rw_t中如愿找到了我们定义的两个属性name和age,同样的也找到了sayHello方法以及name和age的getter/setter方法。但是成员变量hobby和类方法eat在class_rw_t并没有找到。

我们有注意到在class_rw_t结构中有const class_ro_t *ro这样一个东西,这是一个常量结构体指针,那么我们猜测成员变量和hobby是不是存放在ro中呢?来看一下class_ro_t的结构。

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout;
    const char * name;
    // 方法列表
    method_list_t * baseMethodList;
    // 协议列表
    protocol_list_t * baseProtocols;
    // 变量列表
    const ivar_list_t * ivars;
    const uint8_t * weakIvarLayout;
    // 属性列表
    property_list_t *baseProperties;
    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

果然在class_ro_t中有ivars这样一个东西,这里存放的就是成员变量,除此之外还有协议列表baseProtocols,属性列表baseProperties,方法列表baseMethodList。接下来我们利用LLVM来看下ro的内存信息吧。

如上图所示我们如愿找到了hobby,这就证明了成员变量是存放在class_ro_t中的,但是很遗憾的是类方法eat并没有找到。那么类方法会不会存在于元类中呢。

上图是拿到的元类来读取内存信息,我们可以找到类方法eat,证明了类方法是存在于元类中的。

上面的分析我们得出一下结论
1. 类的对象方法、属性、实现的协议,成员变量是存在类里面的,其中成员变量是存放在class_ro_t中,class_ro_t也会存放在编译期间确定的属性、方法以及遵循的协议。class_rw_t中也会存放类的属性、方法以及遵循的协议。
2. 类方法在元类中,对象方法在本类中。

通过上面的分析我们已然知道了类属性、方法、成员变量、遵守的协议等信息的存储位置,但是我们还不知道这些信息是怎么样存放进去的。

3、类的信息存储过程

通过前面的分析我们知道了,objc_class结构中的data()方法可以返回类的信息,那么我们便可以通过setData(class_rw_t *newData)这个方法追本溯源找到了setData的调用这realizeClass方法。

static Class realizeClass(Class cls)
{
    runtimeLock.assertLocked();

    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    if (!cls) return nil;
    // 如果类已经实现了,直接返回
    if (cls->isRealized()) return cls;
    assert(cls == remapClass(cls));
    // fixme verify class is not in an un-dlopened part of the shared cache?
    // 编译期间,cls->data指向的是class_ro_t结构体
    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        // rw结构体已经被初始化(正常不会执行到这里)
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // 正常的类都是执行到这里
        // Normal class. Allocate writeable class data.
        // 初始化class_rw_t结构体
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        // 赋值class_rw_t的class_ro_t,也就是ro
        rw->ro = ro;
        rw->flags = RW_REALIZED|RW_REALIZING;
        // cls->data 指向class_rw_t结构体
        cls->setData(rw);  
        ...
    };

我们看到其实最开始的时候cls->data是指向class_ro_t的,然后才会把class_ro_t设置到class_rw_t中。那么在realizeClass之前,在class_rw_t中一定拿不到类的相关信息的。
上面在realizeClass里面下的断点,只需要判断cls == 当前类的地址就好了,我这里的地址是0x100001490。



如上图所示我们在realizeClass里面下断点,这个时候在class_rw_t中并没有类的相关信息,而在class_ro_t中却可以找到类的相关信息。那是因为在这之前class_data_bits_t *data 指向的是一个 class_ro_t * 指针。


但是我们前面也分析了class_rw_t结构,是可以拿到类的相关信息的,这是因为执行了methodizeClass方法。methodizeClass方法就是向class_rw_t中添加类的方法列表、协议列表、属性列表,包括category的方法。

static void methodizeClass(Class cls)
{
    ...
    // Install methods and properties that the class implements itself.
    // 将class_ro_t中的methodList添加到class_rw_t结构体中的methodList
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }

    // 将class_ro_t中的propertyList添加到class_rw_t结构体中的propertyList
    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }

    // 将class_ro_t中的protocolList添加到class_rw_t结构体中的protocolList
    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rw->protocols.attachLists(&protolist, 1);
    }

    // Root classes get bonus method implementations if they don't have 
    // them already. These apply before category replacements.
    if (cls->isRootMetaclass()) {
        // root metaclass
        addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
    }

    // Attach categories.
    // 添加category方法
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);
  
    ...
}

在methodizeClass这个方法里面,将ro中的类的属性、对象方法,遵守的协议,category方法都添加到了class_rw_t中。这样就如我们前面所分析的那样,在class_rw_t结构中可以拿到类的相关信息了。由此就形成以这样的一个结构。


三、总结

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

推荐阅读更多精彩内容