iOS 中的 NSObject 深度解析

本文开始之前,先提出两个问题,之后沿着问题的思路,逐步去剖析NSObject的本质,一层层剥开这个OC基类的神秘外衣,最终在文末将会给出问题的答案。

Q1:OC中有哪几种对象,每种对象有什么作用?
Q2:OC中类信息如何布局,方法如何调用?

原文地址快捷方式 -->

1. OC对象的产生

OC中方法的调用被称为消息机制,一切OC方法的调用最终都会被转化为objc_msgSend(obj, @selector(messageName))形势,而向一个对象发送消息的前提是对象必须存在,如果对象为nil那该消息将会发送失败。

实例对象

我们创建一个对象的语句是[NSObject new],这句代码编译后objc_msgSend)(objc_getClass("NSObject"), sel_registerName("new"));,由此可以看出,要创建一个NSObject对象的话我们必须要向某一个对象发送一条消息,通过这个对象接受消息并处理后才能才能产生我们需要创建的NSObject对象,这里会有一个疑问,我们需要借助的这个对象又是什么对象,我们有去创建过它吗?如果要去创建它有需要借助什么对象,这是否是一个鸡生蛋蛋生鸡的问题。

类对象和元类对象

实际上在OC当中,除了我们平时创建的普通OC对象以外,还存在两类对象,一个是类对象,另一个是元类对象。这两类对象在程序启动装载类的时候系统就自动创建好了,系统会为每一个类都创建好类对象和元类对象,并且只会创建一份,他们的生命周期是整个应用程序的生命周期,即程序退出才会销毁。

程序运行时,我们可以通过runtime的函数获取这两个对象,它们早已被系统准备后,以便我们随时调用。比如我们调用类方法的时候只能通过类对象调用,即向类对象发送消息。

类对象获取方式:[对象 class] 或者 [类名 class] 或者 object_getClass(对象)
元类对象获取方式:``object_getClass(类对象)判断一个对象是否为类对象:class_isMetaClass(类对象 或 元类对象)`

注意:- (Class)class 或者 + (Class)class 无论调用多少次,都只返回类对象,比如[NSObject class][[[NSObject class] class] class] 返回结果一致,对象方法class特点相同。实际上这可以从Foundation源码得到答案

// NSObject.mm 文件
+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

通过以下测试代码,可以证明元类对象和类对象在内存中都存在,且他们都仅存在一份

#import 
#import 

@interface Person : NSObject {
    int _age;
}
@end
@implementation Person
@end
  
int main(int argc, const char * argv[]) {
    
    // 创建两个Person对象
    Person *person1 = [Person new];
    Person *person2 = [Person new];

    // 判断是否为元类对象
    // 打印结果:[person1 class]:                 0 (类对象)
    //         object_getClass([person1 class]):1 (元类对象)
    NSLog(@"%d", class_isMetaClass([person1 class]));
    NSLog(@"%d", class_isMetaClass(object_getClass([person1 class])));
    
    // 对象内存地址
    // 打印结果:person1: 0x1007ab810
    //         person2: 0x1007a9d10
    NSLog(@"person1: %p", person1);
    NSLog(@"person2: %p", person2);
    
    // Person类对象地址
    // 以下打印结果均为:0x100002258
    NSLog(@"%p", [Person class]);
    NSLog(@"%p", [person1 class]);
    NSLog(@"%p", [person2 class]);
    NSLog(@"%p", object_getClass(person1));
    NSLog(@"%p", object_getClass(person2));
    
    // Person元类对象地址(传入类对象获取元类对象)
    // 以下打印结果均为:0x100002230
    NSLog(@"%p", object_getClass([person1 class]));
    NSLog(@"%p", object_getClass([person2 class]));
    NSLog(@"%p", object_getClass([Person class]));
    
    return 0;
}

person1和person2两个地址不同的对象,获取到类对象和元类对象地址相同

结论:

OC中存在3种对象:实例对象(instance)、类对象(class)和元类对象(meta-class),每一个类在内存中都存在元类对象和类对象,且只有一份。当我们需要创建对象的时候,可以向该类的类对象发送newalloc等消息获得对象的实例。

事实上,实例对象只存放该类成员变量的值,而类的类方法则存在于该类的元类对象中,其他类信息(成员或属性信息、对象方法信息、协议信息等)则存在于类对象中。实际上大部分编程语言都这样设计,因为每一个类产生的对象不同点只在于所存储的信息不同,但是他们行为一定是相同,因此相同的部分(方法、协议等)没有必要每个对象都存储一份,只需要全局存在一份即可。这点我们在之后的分析过程中或逐步证明。

2. NSObject对象的本质

NSObject底层实现

作为一个使用Objective-C的iOSer,每天我们都在接触的一个类是NSObject,这个NSObject是OC中的根类,基本我们使用的类都继承自它(NSProxy除外),进入到NSObject.h发现它的一个定义如下

@interface NSObject  {
    Class isa  OBJC_ISA_AVAILABILITY;
}

从这里分析可以看出,NSObject类拥有一个isa成员,由于看不到.m实现文件,因此无法得知NSObject是否还存在其他私有成员。为了研究一下NSObject拥有哪些成员,我们可以从侧面研究一下,由于OC底层是基于C/C++实现,OC到机器语言的一个过程大概是如下流程

图2-1 oc-to-machine-language

目前Xcode使用的是LLVM+Clang前端的架构,因此可以使用clang编译器对OC代码进行编译,编译的结果和使用Xcode编译的结果基本相同,为了获取编译后的结果,可以使用命令行进行调用编译

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件

iphoneos表示真机环境;如果需要链接其他框架,使用-framework参数。比如-framework UIKit

这里我们可以使用clang编译器指令随便将一个简单的OC文件(只有引入Foundation即可,可以不写任何代码)编译一下,会发现NSObject的C/C++结构如下

struct NSObject_IMPL {
    Class isa;
};

这个NSObject_IMPL结构体就是NSObject编译为C++的结果,可以看出,实际上OC中的类,底层实现就是C++的结构体

自定义对象底层实现

为了更进一步证明真个说法,我们写一个自定义的额类Person,定义如下

@interface Person : NSObject {
    int _age;
}
@end
@implementation Person
@end

clang编译后的结果为

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
};

可以发现,Person编译后的C++结构体为Person_IMPL,它除了含有我们自定义的成员变量_age外,还含有一个NSObject_IMPL类型的成员,从前边的分析可知,NSObject_IMPL是NSObject编译的结果。换句话说Person结构体的第一个成员就是其父类对应的结构体,从等效性上来说,我们可以将Person对象的结构体视为如下结果

struct Person_IMPL {
    Class isa; // NSObject_IMPL就一个isa成员,从逻辑上可以这样等效理解
    int _age;
};

如果我们再添加一个Person的子类Student,并给予一个height成员

@interface Student : Person {
    int _height;
}
@end
@implementation Student
@end

它的表编译结果如下

struct Student_IMPL {
    struct Person_IMPL Person_IVARS;
    int _height;
};

整理一下可得如下结果

struct Student_IMPL {
    Class isa; 
    int _age;
    int _height;
};

可以认为父类的成员会被copy到子类中,即子类结构体中,始终将父类的成员放到自己的前边位置,而自己本身的成员则会排在父类成员的后面

从上边可以看出:OC中的类,底层实现就是C++的结构体,而每一个类至少会有一个isa成员

为了考虑更通用情况,我们给Student兑现添加一个name属性

@interface Student : Person {
    int _height;
}
@property (nonatomic, copy) NSString *name;
@end
@implementation Student
@end

编译后的C++代码如下(等效处理后结果)

struct Student_IMPL {
    Class isa;
    int _age;
    int _height;
    NSString *_name;
};

属性name在编译后,编译器会自动为属性生产待下划线的成员变量_name

结论:

也就是说当一个类在编译为C++后,会将该类的所有成员(包含属性成员)都构建为C++结构体的成员,而每一个类都一定有一个isa成员,这个isa成员从父类继承而来。

3. isa 和 superclass

objc_class结构体分析

经过先前的分析,我们发现每一个类都有一个isa成员,但这个isa成员到底是什么以及有什么作用,这里我们可以通过runtime源码分析进行分析。之前C++代码中isa的类型为Class,从源码中发现Class的定义如下

typedef struct objc_class *Class

Class是一个 struct objc_class类型的指针,继续追源码发现objc_class继承自objc_object结构体,如下:

struct objc_object { 
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    // 此处省略其他代码...
}

从结构中可以看出,一个对象的isa指向了一个Class即objc_class类型的结构体,而这个结构体通过继承关系,也拥有一个isa指针,而这个isa也指向了一个和自身类型一样的结构体,至于这个isa有什么作用,我们暂不讨论

再仔细看发现objc_class结构体中还有一个Class类型的superclass,有刚刚的分析值Class是isa指针的类型,因此这里superclass也是一个和isa同样类型的指针。

由此可以看出:objc_class结构中同时含有两个指针 isasuperclass,他们都是objc_class类型的指针。

先前1中提到,OC对象分为三种,实例对象、类对象和元类对象,我们可以通过向类对象发送消息获得实例对象,那消息发送过程中,NSObject是如何处理这些消息流向的呢?

调试分析

获取一个对象的类对象或者元类对象的方式,我们可以使用方法 Class object_getClass(id obj),查看该方法的实现,发现它实际上是返回一个isa指针

Class object_getClass(id obj) {
    if (obj) return obj->getIsa();
    else return Nil;
}

对于任何一个对象,我们知道isa指针指向一个objc_class的结构体,而对象通过这个方法获取到的是类对象,因此可以得出类对象是一个objc_class结构体。类对象通过该方法获取到的是元类对象,因此元类对象也是一个objc_class的结构体,由此可得类对象和元类对象具有相同的结构,都是同一类型的结构体。

这里可以看出一个基础isa指针关系 对象 --> 类对象 --> 元类对象

查看objc_class以及其关联的一些结构体的定义,并简化处理后得到如下代码

struct class_ro_t {
    // 这个结构体的信息为制度信息(不包含运行时动态改变的)
    uint32_t instanceSize;      // 实例对象大小
    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;// 基础属性列表
    // 省略掉一些非必要代码
};

struct class_rw_t {
    const class_ro_t *ro;   // 该类的只读信息表
    method_array_t methods; // 方法列表
    property_array_t properties; // 属性列表
    protocol_array_t protocols;  // 协议列表
    // 省略掉一些非必要代码
};

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
  
    class_rw_t *data() {  // 该类的可读写信息表
        return bits.data();
    }
    // 省略掉一些非必要代码
}

他们的基本关系如下图


图3-1 objc_class的基本结构

虽然我们知道了这个结构,但是在调试程序是,这些结构体对开发者是不可见的。为了能在调试时方便的去查看这些关系,我这里参照运行时定义仿写了一些结构体,这些结构体和运行时库中定义并不完全相同,省略了许多不必要的代码,方便在调试代码中使用强转方式,查看对象、类对象和元类对象之间的一些关系。

//
//  WJClassInfo.h
//  认识NSObject
//
//  Created by nius on 2020/4/14.
//  Copyright © 2020 nius. All rights reserved.
//

#import 

#ifndef WJClassInfo_h
#define WJClassInfo_h

// isa指针并非直接指向类对象或者元类对象,需要 (isa & ISA_MASK) 才能获取类对象或者元类对象
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
# endif

#if __LP64__
typedef uint32_t mask_t;
#else
typedef uint16_t mask_t;
#endif
typedef uintptr_t cache_key_t;

// 方法地址缓存结构
struct bucket_t {
    cache_key_t _key;
    IMP _imp;
};

// 方法缓存结构体
struct cache_t {
    bucket_t *_buckets; // 缓存数组
    mask_t _mask;
    mask_t _occupied;
};

struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
};

// 方法结构体
struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};

struct method_list_t : entsize_list_tt {
    method_t first;
};

// 成员变量
struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    uint32_t alignment_raw;
    uint32_t size;
};

struct ivar_list_t : entsize_list_tt {
    ivar_t first;
};

// 属性
struct property_t {
    const char *name;
    const char *attributes;
};

struct property_list_t : entsize_list_tt {
    property_t first;
};

struct chained_property_list {
    chained_property_list *next;
    uint32_t count;
    property_t list[0];
};

typedef uintptr_t protocol_ref_t;
struct protocol_list_t {
    uintptr_t count;
    protocol_ref_t list[0];
};

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;  // instance对象占用的内存空间
#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;
};

struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    method_list_t * methods;    // 方法列表
    property_list_t *properties;    // 属性列表
    const protocol_list_t * protocols;  // 协议列表
    Class firstSubclass;
    Class nextSiblingClass;
    char *demangledName;
};

#define FAST_DATA_MASK          0x00007ffffffffff8UL
struct class_data_bits_t {
    uintptr_t bits;
public:
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
};

// OC对象
struct wj_objc_object {
    void *isa;
};

// 类对象 或 元类对象
struct wj_objc_class : wj_objc_object {
    Class superclass;      // 父类指针
    cache_t cache;         // 调用过的方法缓存结构体
    class_data_bits_t bits;
public:
    class_rw_t* data() {   // 该类的可读写信息表
        return bits.data();
    }
    
    wj_objc_class* metaClass() {
        return (wj_objc_class *)((long long)isa & ISA_MASK);
    }
};

#endif /* WJClassInfo_h */

使用如下测试代码,加断掉调试,可以得到一些结果

#import 
#import 
#import "WJClassInfo.h"

// Person
@interface Person : NSObject {
    int _age;
}
@end
@implementation Person
@end

// Student
@interface Student : Person {
    int _height;
}
@property (nonatomic, copy, readonly) NSString *name;
- (void)testObj1;
- (void)testObj2;
+ (void)testClass1;
+ (void)testClass2;
@end
@implementation Student
- (void)testObj1{}
- (void)testObj2{}
+ (void)testClass1{}
+ (void)testClass2{}
@end

int main(int argc, const char * argv[]) {
    
    Student *stu1 = [Student new];
    Class stu1Class = object_getClass(stu1);
    Class stu1MetaClass = object_getClass(stu1Class);
    
    wj_objc_class* wjstu1Class = (__bridge wj_objc_class* )stu1Class;
    class_rw_t* wjstu1Class_rw_t = wjstu1Class->data();
    
    wj_objc_class* wjstu1MetaClass = (__bridge wj_objc_class* )stu1MetaClass;
    class_rw_t* wjstu1MetaClass_rw_t = wjstu1MetaClass->data();
    
    return 0;
}

根据上边方法,逐步调试程序,可以得到下列实例对象、类对象和元类对象的组成情况如下

     实例对象     isa指向               类对象                isa指向        元类对象
---------------------------------------------------------------------------------------
 │     obj     │           │            class            │            │   meta-class  │
 │  ---------  │           │          ---------          │            │   ---------   │
 │     isa     │   --->    │            isa              │    --->    │     isa       │
 │  其他成员变量 │           │          superclass         │            │  superclass   │
 │    ...      │           │   属性、对象方法、协议、成员变量 │            │    类方法      │
 │             │           │             ...             │            │     ...       │

isa指针和superclass指针的指向情况

isa:        --->   
superclass: ===>

实例对象 ---> 类对象 ---> 元类对象 ---> 跟元类对象(根元类对象的isa指向自己) 

类对象   ===> 父类类对象  ===> 根类类对象   ===> nil
元类对象 ===> 父类元类对象 ===> 根类元类对象 ===> 根类类对象
结论:

1)实例对象仅存储类的成员变量值
2)类对象存储属性、对象方法、协议、成员变量等信息
3)元类对象存储类方法信息
4)OC继承体系实际上通过spuerclass指针实现
5)isa和spuerclass指针的情况总结为下面一幅图

图3-2 isa 和 spuerclass

4. NSObject消息处理过程

图 3-2 很好的总结了 isa 和 superclass指针的指向情况,isa主要是关联了对象、类对象和元类对象之间的关系,spuerclass指针则主要实现了继承链。OC中的方法调用也主要是通过这两个指针的配合完成。

对象方法调用流程

实例先通过isa找到类对象,类对象中如果没有该方法,会通过superclass向父类类对象寻找,如此不断往,中间某一个如果找到方法则调用并结束,否则将一直到根类类对象,当根类类对象也没有则可能出现异常,流程图如下


图4-1 对象方法调用流程
类方法调用流程

类方法一般直接公共类名调用(类对象调用)。类对象先通过isa找到元类对象,元类对象中如果没有该方法,会通过superclass向父类元类对象寻找,如此不断往,中间某一个如果找到方法则调用并结束,否则将一直到根类元类对象,当根类元类对象也没有则会继续寻找根类类对象(这里有一个转弯过程),流程图如下


图4-2 类方法调用流程

对于类方法的调用,这里会产生一个有意思的现象,请看代码

@implementation NSObject(Test)
- (void)test { // NSObjet中写了一个同名的方法,但这里是一个对象方法
    NSLog(@"NSObject的实例方法test");
}
@end

@interface Person : NSObject
+ (void)test; // Person对象定义一个类方法,但不写实现,这样Person类对象中就没有该方法
@end
@implementation Person
@end

int main(int argc, const char * argv[]) {
    [Person test];
    return 0;
}

// 结果:Person调用了NSObject的对象方法

事实上会发生上面结果的原因是由于:Person元类对象没有找到test消息,则会不断向上直到NSObject元类对象也没有test消息,但是NSObject元类对象的spuerclass指向NSObject对象,因此,该消息会继续向NSObject类对象匹配,结果在NSObject中寻找到了test消息,因此会发生Person类方法调用,NSObject对象方法响应的情况。由此可以发现一个问题就是,在OC底层实现时其实并不会区分方法是对象方法还是类方法,值不过是将对象方法存储在了类对象中,类方法存储在了元类对象中,因此在消息发送的时候,会延续spuerclass链条不断寻找,只要发现方法名相同就直接调用,没有而不区分是对象方法还是类方法。

总结:

1)类方法和对象方法底层实际上只是存储位置不同,其实没有本质区别
2)方法调用流程如下

定义:
isa:              -->               
superclass: ==>

对象方法调用:对象 --> 类对象==>父类类对象==>根类类对象==>失败
类方法调用:类对象-->元类对象==>父类元类对象==>根类元类对象==>根类类对象==>失败

5. Q&A

这里回答我们开始提到的问题

5.1 Q1:OC中有哪几种对象,每种对象有什么作用?

OC中有3种对象:
实例对象:存储成员变量的值;用户创建,通过向类对象发送alloc、new消息
类对象:存储成员、属性、对象方法、协议等信息;在内存中仅有一份,系统创建
元类对象:储存类方法;在内存中仅有一份,系统创建

5.2 Q2:OC中类信息如何布局,方法如何调用?

对象和类信息布局

     实例对象     isa指向               类对象                isa指向        元类对象
---------------------------------------------------------------------------------------
 │     obj     │           │            class            │            │   meta-class  │
 │  ---------  │           │          ---------          │            │   ---------   │
 │     isa     │   --->    │            isa              │    --->    │     isa       │
 │  其他成员变量 │           │          superclass         │            │  superclass   │
 │    ...      │           │   属性、对象方法、协议、成员变量 │            │    类方法      │
 │             │           │             ...             │            │     ...       │

方法调用

定义:
isa:        -->               
superclass: ==>

对象方法调用:对象 --> 类对象==>父类类对象==>根类类对象==>失败
类方法调用:类对象-->元类对象==>父类元类对象==>根类元类对象==>根类类对象==>失败
5. 一些网络问题面试题

一个NSObject对象占用多少内存?
NSObject对象的结构体为中仅有一个isa指针,而isa指针在64位环境下占8个字节,因此该结构体实际上要使用8个字节空间,但iOS系统在分配内存的时候,默认对象的内存空间必须为16的倍数,因此会至少给NSObject对象分配16个字节的存储空间。

通过malloc_size函数获取系统给对象分配的内存空间的大小
通过class_getInstanceSize函数获取对象实际需要的内存空间大小(这里需要考虑结构体字节对齐因素)

typedef struct objc_class *Class
struct NSObject_IMPL {
    Class isa;
};

OC中的id为何物?

// runtime源码中id定义
struct objc_object {
    Class ISA();    // 非tagged pointer对象
    Class getIsa(); // tagged pointer对象
}
typedef struct objc_object *id;

从源码中可以看出,id是一个objc_object类型的指针,该结构体可以获取isa指针,而OC中对象都包含有isa指针,因此可以使用id指向OC中任意类型的对象。OC实际上可以看成一个包含了isa成员的结构体指针

struct objc_object {
    void* isal
}

对象的isa指针指向哪里?
对象isa指向类对象,类对象的isa指向元类对象,每个元类对象的isa指向根元类对象(NSObject元类对象)

OC的类信息存放在哪里?
类的属性、成员、对象方法、协议等存放在类对象中
类方法存储放在元类对象中

The end...

你可能感兴趣的:(iOS 中的 NSObject 深度解析)