文章目录
- 前言
- 一、编译源码
- 二、探索对象本质
- [三、objc_setProperty 源码探索](#三、objc_setProperty 源码探索)
- [四、类 & 类结构分析](#四、类 & 类结构分析)
- [五、著名的isa走位 & 继承关系图](#五、著名的isa走位 & 继承关系图)
- [六、objc_class & objc_object](#六、objc_class & objc_object)
- 总结
前言
这篇文章主要探索OC对象的本质
首先我们需要明白我们平时编写的OC代码,底层实现都是C\C++代码
一、编译源码
首先通过终端利用clang
将main.m
编译为main.cpp
bash
//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp
//2、将 ViewController.m 编译成 ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m
//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
二、探索对象本质
我们打开编译好的源文件后找到LGPerson
,发现其在底层被编译为struct
结构体
bash
//NSObject的定义
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
//NSObject 的底层编译
struct NSObject_IMPL {
Class isa;
};
//LGPerson的底层编译
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 等效于 Class isa;
NSString *_name;
};
LGPerson_IMPL
实现结构体中的第一个属性是isa
,是继承自NSObject
,是伪继承。意味着LGPerson
拥有者NSObject
中所有成员变量
LGPerson
中的第一个属性 NSObject_IVARS
等效于 NSObject
中的 isa
这里也许我们会产生一个疑问就是为什么isa的类型是class
根本原因是由于isa 对外反馈的是类信息
总结
因此我们可以得出:
OC对象本质就是结构体
LGPerson
中的isa
就是继承自NSObject
中的isa
三、objc_setProperty 源码探索
除了LGPerson的底层定义,我们发现了属性name还有set与get方法,其中set方法依赖于runtime
中的objc_setProperty
我们通过源码来查看一下objc_setProperty
的底层实现
bash
// 定义静态内联函数,用于设置对象的属性值。
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
// 如果偏移量为0,直接将newValue设置为对象的类(可能用于特殊的目的,如改变对象的动态类型)
if (offset == 0) {
object_setClass(self, newValue);
return;
}
// 定义用来保存旧值的变量
id oldValue;
// 计算属性值在内存中的实际位置
id *slot = (id*) ((char*)self + offset);
// 如果指定了copy标志,则对newValue执行不可变拷贝
if (copy) {
newValue = [newValue copyWithZone:nil];
}
// 如果指定了mutableCopy标志,则对newValue执行可变拷贝
else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
}
// 如果没有指定拷贝,检查newValue是否已经是当前值,如果是,则无需操作;否则,增加newValue的引用计数
else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
// 如果不是原子操作,直接更新内存位置的值
if (!atomic) {
oldValue = *slot;
*slot = newValue;
}
// 如果是原子操作,使用锁来保证线程安全
else {
spinlock_t& slotlock = PropertyLocks[slot]; // 获取与属性位置相关联的锁
slotlock.lock(); // 锁定
oldValue = *slot; // 取出旧值
*slot = newValue; // 设置新值
slotlock.unlock(); // 解锁
}
// 释放旧值的引用,以防内存泄漏
objc_release(oldValue);
}
其方法原理就是retain新值->设置新值->释放旧值
四、类 & 类结构分析
本篇章的主要目的是分析 类 & 类的结构,整篇都是围绕一个类展开的一些探索
isa指针是什么
OC是一门面向对象编程的语言,每个对象都是类的实例,同时也被称为实例对象,同时每个对象都有一个isa
指针,指向对象所属的类
另外我们打开NSObject源码
bash
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
由此可知类也是一个对象,简称为类对象
类的分析
首先我们定义两个类
继承自NSObject
的类CJLPerson
bash
@interface CJLPerson : NSObject
{
NSString *hobby;
}
@property (nonatomic, copy) NSString *cjl_name;
- (void)sayHello;
+ (void)sayBye;
@end
@implementation CJLPerson
- (void)sayHello
{}
+ (void)sayBye
{}
@end
继承自CJLPerson
的类CJLTeacher
bash
@interface CJLTeacher : CJLPerson
@end
@implementation CJLTeacher
@end
在main中分别用两个定义两个对象:person & teacher
bash
int main(int argc, const char * argv[]) {
@autoreleasepool {
//ISA_MASK 0x00007ffffffffff8ULL
CJLPerson *person = [CJLPerson alloc];
CJLTeacher *teacher = [CJLTeacher alloc];
NSLog(@"Hello, World! %@ - %@",person,teacher);
}
return 0;
}
元类
首先我们通过一张图片引入元类
根据调试过程,我们产生了一个疑问:为什么图中的p/x 0x001d8001000022dd & 0x00007ffffffffff8ULL
与 p/x 0x00000001000022b0 & 0x00007ffffffffff8ULL
中的类信息打印出来都是CJLPerson
?
0x001d8001000022dd
是person
对象的isa
指针地址,其&后得到的结果是 创建person
的类CJLPerson
0x00000001000022b0
是isa
中获取的类信息所指的类的isa的指针地址,即CJLPerson
类的类 的isa
指针地址,在Apple中,我们简称CJLPerson类的类为 元类
所以,两个打印都是CJLPerson
的根本原因就是因为元类导致的
元类的说明
下面我们来解释一下什么是元类
首先我们知道对象的isa指向类,同时我们前文也说了类也是一个对象,那么类也有isa指针,类的isa
指针指向的就是元类
元类是系统给的,当我们创建类时会自动创建元类,类的归属来自于元类
首先我们之前的博客有分析过元类【iOS】isKindOfClass & isMemberOfClass比较
我们这里引出一个问题,NSObject到底有几个?
从这张图中我们可以看出根元类NSObject
只有一个,这个与我们日常开发的NSObject
是同一个吗
我们通过代码来验证一下
可以看出打印出的地址为同一个,所以NSObject
只有一个,即类对象与元类都只有一个,而实例对象可以有很多个
bash
NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];
// 打印两个实例的内存地址
NSLog(@"object1: %p", object1);
NSLog(@"object2: %p", object2);
// 获取并打印NSObject的类对象
Class objectClass1 = [object1 class];
Class objectClass2 = [object2 class];
NSLog(@"NSObject class: %p. %p", objectClass1, objectClass2);
// 获取并打印NSObject的元类对象
Class metaClass1 = object_getClass(objectClass1);
Class metaClass2 = object_getClass(objectClass2);
NSLog(@"NSObject meta-class: %p, %p", metaClass1, metaClass2);
[面试题]:类存在几份?
由于类的信息在内存中永远只存在一份,所以 类对象只有一份
五、著名的isa走位 & 继承关系图
这里的知识之前已经说过了,这里不再赘述
六、objc_class & objc_object
isa的走位我们清楚了,现在我们来讨论一个新问题,为什么对象和类会有isa属性,这里就引出了objc_class
& objc_object
首先我们通过之前的源码知道:
NSObject
的底层编译是NSObject_IMPL
结构体
首先我们知道Class
是isa
指针的类型,是由objc_class
定义的类型
objc_class实际上就是Class
在iOS中所有的Class
都是以objc_class
为模版创建的
我们查看一下objc_class的源码
我们可以看到objc_class
结构体是继承自objc_object
再搜寻objc_object
的定义
【问题】objc_class 与 objc_object 有什么关系?
通过查看源码我们可以得出几点说明:
objc_class
继承自objc_object
类型,其中objc_object
也是一个结构体,且有一个isa
类型- 在cpp底层编译中,isa的类型是Class,class的底层编码来自objc_class,因此NSObject也有了isa属性
- objc_object(结构体)是当前的根对象,所有的对象都有一个特性
objc_object
,即拥有isa属性
objc_object 与 对象的关系
-
是一个继承关系,所有对象都是由
objc_object
继承过来的 -
所有的对象都是来自
NSObject
,但最后到底层都是一个objc_object(C/C++)
结构体类型
总结:
所有对象+类+元类 都有isa
属性
所有对象都是由objc_object
继承来的
objc_class结构
首先我们在源码中找到其结构
bash
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
:主要指向类对象或是元类对象
superclass
:指向当前类的父类
cache
:方法缓存,提高调用方法的性能
bits
:封装了类的其他信息,例如成员变量,方法列表,协议,属性
元类对象结构也是这样,只不过元类对象里面存放的是类方法
superClass
这里需要注意superclass是objc_class特有的,实例对象是没有的
bits
类结构中还有一个bits
我们通过源码查看一下其数据结构class_data_bits_t
bash
struct class_data_bits_t {
friend objc_class;
// Values are the FAST_ flags above.
uintptr_t bits;
public:
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
// Get the class's ro data, even in the presence of concurrent realization.
// fixme this isn't really safe without a compiler barrier at least
// and probably a memory barrier when realizeClass changes the data field
const class_ro_t *safe_ro() const {
class_rw_t *maybe_rw = data();
if (maybe_rw->flags & RW_REALIZED) {
// maybe_rw is rw
return maybe_rw->ro();
} else {
// maybe_rw is actually ro
return (class_ro_t *)maybe_rw;
}
}
}
其中最重要的两个方法就是data
和 safe_ro
,两个方法分别返回class_rw_t
和class_ro_t
class_rw_t
首先可以看到三个方法,可以分别获取到类的方法,属性,协议
class_ro_t
可以看到有方法、属性、协议和成员变量。但方法、属性、协议的命名都是base
开通的
ro与rw的区别
ro
是编译阶段生成,rw
是运行阶段生成,从存储的角度来说,ro
中有方法,属性,协议与成员变量,而rw
中没有成员变量,rw
中的取值方法也是通过取ro
或是rwe
的值来获得的
class_rw_ext_t
在2020WWDC中有个视频对ro与rw进行了解释,由于rw是一块脏内存,但是rw总有许多用不到的数据,我们将rw中那些一般用不到的数据分离出来变为干净的内存,也就是rw_ext_t
,这样就减轻了rw对内存的占用
rw_ext_t生成条件:
-
使用分类的类
-
使用Runtime API动态修改类的结构的时候
这两种情况时,由于类的结构发生改变,但是ro是只读的,因此需要重新生成可读可写的内存结构rw_ext(Dirty Memory, 比较贵),来存放新的类结构。
由此再次读取方法,属性,列表时如果有rw_ext
,就会先从rw_ext
中读取,如果没有再去读取ro
参考博客:
iOS八股文(四)类对象的结构(下)
思考:为什么类方法存储在元类中,而不把类方法存储在类对象中?或者说设置元类的目的是什么?
- 单一职责设计原理 :一个对象或是一个类只应该有一个职责,类对象负责实例对象的行为,例如实例方法,协议,成员变量,属性等,元类对象负责类对象的行为,负责存放类方法,各司其职互不影响
- 符合消息转发机制
- 继承模型的一致性 :在Objective-C中,类方法的继承与实例方法的继承遵循相同的模式。如果没有元类,类方法的继承将需要另外一套机制来处理。
cache_t结构
cache
的作用是在objc_msgSend
过程中会先在cache
中根据方法名来hash
查找方法实现,如果能查找到就直接掉用。如果查找不到然后再去rw_t
中查找。然后再在cache
中缓存。
总结
通过这篇文章我们得知
- 所有对象都有isa属性
- 所有对象都是由objc_object继承而来的
- objc_class中存放着对象的各种信息,实例对象则存放成员变量,类对象则存放实例方法与属性等,元类对象则存放类方法,符合单一职责原则
- isa 指针指向对象所属的类
- 可以从bits中的rw中查找方法属性,但不能查找到成员变量,成员变量存储在ro中,这也是为什么分类不能添加成员变量的原因