本文开始之前,先提出两个问题,之后沿着问题的思路,逐步去剖析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),每一个类在内存中都存在元类对象和类对象,且只有一份。当我们需要创建对象的时候,可以向该类的类对象发送new
、alloc
等消息获得对象的实例。
事实上,实例对象只存放该类成员变量的值,而类的类方法则存在于该类的元类对象中,其他类信息(成员或属性信息、对象方法信息、协议信息等)则存在于类对象中。实际上大部分编程语言都这样设计,因为每一个类产生的对象不同点只在于所存储的信息不同,但是他们行为一定是相同,因此相同的部分(方法、协议等)没有必要每个对象都存储一份,只需要全局存在一份即可。这点我们在之后的分析过程中或逐步证明。
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到机器语言的一个过程大概是如下流程
目前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结构中同时含有两个指针 isa
和 superclass
,他们都是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();
}
// 省略掉一些非必要代码
}
他们的基本关系如下图
虽然我们知道了这个结构,但是在调试程序是,这些结构体对开发者是不可见的。为了能在调试时方便的去查看这些关系,我这里参照运行时定义仿写了一些结构体,这些结构体和运行时库中定义并不完全相同,省略了许多不必要的代码,方便在调试代码中使用强转方式,查看对象、类对象和元类对象之间的一些关系。
//
// 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指针的情况总结为下面一幅图
4. NSObject消息处理过程
图 3-2 很好的总结了 isa 和 superclass指针的指向情况,isa主要是关联了对象、类对象和元类对象之间的关系,spuerclass指针则主要实现了继承链。OC中的方法调用也主要是通过这两个指针的配合完成。
对象方法调用流程
实例先通过isa找到类对象,类对象中如果没有该方法,会通过superclass向父类类对象寻找,如此不断往,中间某一个如果找到方法则调用并结束,否则将一直到根类类对象,当根类类对象也没有则可能出现异常,流程图如下
类方法调用流程
类方法一般直接公共类名调用(类对象调用)。类对象先通过isa找到元类对象,元类对象中如果没有该方法,会通过superclass向父类元类对象寻找,如此不断往,中间某一个如果找到方法则调用并结束,否则将一直到根类元类对象,当根类元类对象也没有则会继续寻找根类类对象(这里有一个转弯过程),流程图如下
对于类方法的调用,这里会产生一个有意思的现象,请看代码
@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...