文章目录
前言
本篇博客主要是笔者在学习类&类的结构的底层探索和分析时所作的笔记,主要涉及实例对象的类以及类的结构。Objective-C的类结构是其动态性和面向对象特性的核心,理解类的内存布局和内部机制,对开发、调试和性能优化至关重要。
类的分析
类的本质
在 OC 中,类(Class)本身是一个对象(objc_class 结构体),在探索类的本质之前,我们先在main文件里自定义一个继承自NSobjetc的TCJPerson类:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface TCJPerson : NSObject
@end
@implementation TCJPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
TCJPerson *person = [TCJPerson alloc];
Class cls = object_getClass(person);
}
return 0;
}
然后利用clang工具将oc语言的main文件输出为cpp文件,命令行如下:
clang -rewrite-objc main.m -o main.cpp
将文件拖到我们的objc源码中打开,方便我们调试和查找,可以发现,这段代码在cpp文件文件中如下:
//类型定义部分
#ifndef _REWRITER_typedef_TCJPerson
#define _REWRITER_typedef_TCJPerson
typedef struct objc_object TCJPerson; //将 TCJPerson 定义为 objc_object 结构体
typedef struct {} _objc_exc_TCJPerson; //空结构体,用于异常处理占位(实际未使用)
#endif
//类的实现结构
struct TCJPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS; //继承自 NSObject 的实例变量
};
/* @end */
// @implementation TCJPerson
// @end
//main函数的底层转换
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
TCJPerson *person = ((TCJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("TCJPerson"), sel_registerName("alloc"));
//objc_getClass("TCJPerson"):获取 TCJPerson 的类对象(Class)
//sel_registerName("alloc"):注册方法名 alloc,返回 SEL 类型的选择子
//objc_msgSend:发送 alloc 消息给 TCJPerson 类,创建实例
//强制类型转换 (TCJPerson *(*)(id, SEL))(void *) 是为了匹配 objc_msgSend 的函数指针签名
Class cls = object_getClass(person);//获取 person 实例的类(即 TCJPerson 的类对象)
}
return 0;
}
- TCJPerson 被定义为 objc_object,说明在底层,Objective-C 类实例的本质就是 objc_object 结构体。
- 因为 TCJPerson 继承自 NSObject,所以它的底层结构会包含父类的实例变量。其中,NSObject_IVARS 就是 NSObject 的实例变量,通常就是 isa 指针(指向类的元数据)。
然后我们发现类在底层是用class接收的。
typedef struct objc_class *Class;
在左侧搜索栏查找objc_class,我们可以发现objc_class继承自objc_object。
点击进入objc_object的源码实现中:
小结
- Objective-C 对象的本质:
类实例(如 person)本质是 objc_object 结构体,包含 isa 指针(来自 NSObject_IVARS)。类(如 TCJPerson)本质是 objc_class 结构体,继承自 objc_object。所以满足万物皆对象。 - 方法调用的本质:[TCJPerson alloc] 被编译为 objc_msgSend 的调用,动态查找并执行方法。
- 内存布局:TCJPerson_IMPL 只包含 NSObject 的 isa,因为没有自定义实例变量。
objc_class 、objc_object和NSObject
objc_object:所有对象的基类型
底层源码:
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; //必须为非空的类指针(即指向 objc_class 结构体的指针)
};
这证明objc_object是所有 Objective-C 对象的最底层结构,包括实例对象、类对象、元类对象。其通过 isa 指针实现对象与类的关联(即“对象是什么类”)。
特点:
实例对象的 isa 指向它的类(如 TCJPerson 实例的 isa 指向 TCJPerson 类)。
类对象的 isa 指向元类(Meta Class)。
元类的 isa 指向根元类(Root Meta Class)。
关于类 元类 根元类
- 类(Class)
作用:定义对象的实例变量和实例方法(如 -init、-description)。
内存结构:每个类是一个 objc_class 结构体实例,包含方法列表、父类指针、缓存等。
对象关系:实例对象(如 MyObject *obj)的 isa 指针指向其类对象。 - 元类(Meta-class)
作用:定义类的类方法(如 +alloc、+new)。
元类本身也是一个类,它的实例是类对象。
内存结构:元类也是一个 objc_class 结构体,但其方法列表存储类方法。
对象关系:类对象(如 MyObject.class)的 isa 指针指向其元类。 - 根元类(Root Meta-class)
作用:所有元类的最终基类,通常是 NSObject 的元类。
根元类的类方法(如 +alloc)会被所有类的元类继承。
根元类的 isa 指针指向自身,形成闭环。
Instance (MyObject): 一个对象实例
Class (MyObject class): 定义该实例的类
Meta-class (MyObject meta): 定义该类的元类
isa 指针:形成继承链的核心,每个实例、类和元类都有一个指向其类型的指针
实例通过 isa 指向类,类通过 isa 指向元类,元类通过 isa 指向根元类
super_class 指针:形成继承体系
类通过 super_class 指向父类,元类通过 super_class 指向父元类
Root Meta-class (NSObject meta): 所有元类的根元类
根元类的 isa 指向自己(self),形成闭环
根元类的 super_class 指向 NSObject 类
运行时方法查找路径:
实例方法首先在实例所属的类中查找
如果没找到,则沿着 super_class 链向上查找
类方法则在元类及其父元类中查找
继承机制:实例继承自类,类继承自元类,元类继承自父元类,最终根元类继承自NSObject类
objc_class:类的底层结构
底层代码:
struct objc_class : objc_object { //继承自objc_object
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// 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 //类的方法、属性、协议等数据
Class getSuperclass() const {
#if __has_feature(ptrauth_calls)
# if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
if (superclass == Nil)
return Nil;
......
}
这说明objc_class是 objc_object 的子类,说明类本身也是对象(即“类对象”)。
存储类的元数据:方法列表、属性列表、协议列表、父类指针等。
因为其继承自 objc_object,所以其类对象也有 isa 指针(指向元类)。
其中,Class 是指向 objc_class 的指针(typedef struct objc_class *Class)。
NSObject:面向用户的根类
底层代码:
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY; //指向类对象的指针
#pragma clang diagnostic pop
}
NSObject 是 Objective-C 中所有类的根类(除了 NSProxy),提供面向开发者的基础方法(如 alloc、init、description)。
与 objc_object 的关系:
NSObject 的实例对象在底层就是 objc_object。
NSObject 的类对象在底层是 objc_class。
编译器会将 NSObject 的代码转换为对 objc_object 和 objc_class 的操作。
小结
通过对objc_class、objc_object和NSObject底层的大致了解,我们就可以明白三者存在以下金字塔关系:
objc_object:所有对象的终极基类(C结构体)
objc_class:继承objc_object,说明"类也是对象"
NSObject:继承链最顶端的公开类,封装了objc_class的面向对象接口
小结
所有的对象 + 类 + 元类 都有isa属性
所有的对象都是由objc_object继承来的
简单概括就是万物皆对象,万物皆来源于objc_object,有以下两点结论:
所有以 objc_object为模板创建的对象,都有isa属性
所有以 objc_class为模板创建的类,都有isa属性
在结构层面可以通俗的理解为上层OC 与 底层的对接:
下层是通过 结构体 定义的 模板,例如objc_class、objc_object
上层是通过底层的模板创建的一些类型,例如TCJPerson
objc_class、objc_object、isa、object、NSObject等的整体的关系如下图:
指针内存偏移
在分析类的结构之前,我们需要先来学习一下指针内存偏移作为前缀知识。
普通指针----值拷贝
//普通指针
//值拷贝
int a = 10;
int b = 10;
NSLog(@"&a:%d--%p", a, &a);
NSLog(@"&b:%d--%p", b, &b);
通过代码及其运行结果可以看出来,变量a和b虽然都是被赋值为10,但是变量a和b的内存地址是不一样的,我们称这种方式为值拷贝。
对象----指针拷贝或引用拷贝
//对象
NSObject *obj1 = [[NSObject alloc] init];
NSObject *obj2 = [[NSObject alloc] init];
NSLog(@"%@--%p", obj1, &obj1);
NSLog(@"%@--%p", obj2, &obj2);
通过运行结果,我们可以看到obj1和obj2对象不仅自身内存地址不一样,其指向的对象的内存地址也不一样,这被称为指针拷贝或引用拷贝。
用数组指针引出----内存偏移
//数组指针
int arr[4] = {1, 2, 3, 4};
int *c = arr;
NSLog(@"&arr:%p--%p--%p", arr, &arr[0], &arr[1]);
NSLog(@"&c:%p--%p--%p", c, c + 1, c + 2);
for (int i = 0; i < 4; i++) {
int value = *(c + i);
NSLog(@"value:%d", value);
}
通过运行结果可以看到:
- &a和&a[0]的地址是相同的——即首地址就代表数组的第一个元素的地址。
- 第一个元素地址0x16fdff2f8和第二个元素地址0x16fdff2fc相差4个字节,也就是int的所占的4字节,因为他们的数据类型相同。
- d、d+1、d+2这个地方的指针相加就是偏移地址。地址加1就是偏移,偏移一个位数所在元素的大小。
- 可以通过地址,取出对应地址的值。
小结
类的结构
从objc_class的定义可以得出,类有4个属性:isa、superclass、cache、bits。
Class ISA
不但实例对象中有isa指针,类对象中也有isa指针关联着元类。
Class本身就是一个指针,占用8字节。
Class surperclass
顾名思义就是类的父类(一般为NSObject)superclass是Class类型,所以占用8字节。
cache_t cache
进入cache_t的实现源码,我们能看到(笔者对部分进行了注释,方便理解):
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
//原子存储缓存桶指针或掩码(根据配置不同复用同一内存)
//原子性:保证并发访问时的线程安全(如 objc_msgSend 高频调用场景)
union {
// Note: _flags on ARM64 needs to line up with the unused bits of
// _originalPreoptCache because we access some flags (specifically
// FAST_CACHE_HAS_DEFAULT_CORE and FAST_CACHE_HAS_DEFAULT_AWZ) on
// unrealized classes with the assumption that they will start out
// as 0.
struct {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED && !__LP64__
// Outlined cache mask storage, 32-bit, we have mask and occupied.
explicit_atomic<mask_t> _mask; //缓存掩码
uint16_t _occupied; //已占用槽位数
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED && __LP64__
// Outlined cache mask storage, 64-bit, we have mask, occupied, flags.
explicit_atomic<mask_t> _mask;
uint16_t _occupied;
uint16_t _flags; //状态标志(FAST_CACHE_HAS_DEFAULT_CORE: 是否有默认核心实现 FAST_CACHE_HAS_CUSTOM_DEALLOC_INITIATION: 是否自定义释放逻辑)
# define CACHE_T_HAS_FLAGS 1
#elif __LP64__
// Inline cache mask storage, 64-bit, we have occupied, flags, and
// empty space to line up flags with originalPreoptCache.
//
// Note: the assembly code for objc_release_xN knows about the
// location of _flags and the
// FAST_CACHE_HAS_CUSTOM_DEALLOC_INITIATION flag within. Any changes
// must be applied there as well.
uint32_t _unused;
uint16_t _occupied;
uint16_t _flags;
# define CACHE_T_HAS_FLAGS 1
#else
// Inline cache mask storage, 32-bit, we have occupied, flags.
uint16_t _occupied;
uint16_t _flags;
# define CACHE_T_HAS_FLAGS 1
#endif
cache在英文中的意思是缓存。
cache_t是一个结构体,内存长度由所有元素决定:
_bucketsAndMaybeMask是long类型,它是一个指针,占用8字节;
mask_t是个uint32_t类型,_mask占用4字节;
_occupied和_flags都是uint16_t类型,uint16_t是 unsigned short 的别名,所以_occupied占用2字节;
_flags占用2字节;
所以,cache_t共占用16字节。
这里对cache简单了解一下,后面还会呢详细学习。
class_data_bits_t bits
class_data_bits_t实现源码如下:
struct class_data_bits_t {
friend objc_class;
// Values are the FAST_ flags above.
uintptr_t bits;
private:
bool getBit(uintptr_t bit) const
{
return bits & bit;
}
// Atomically set the bits in `set` and clear the bits in `clear`.
// set and clear must not overlap. If the existing bits field is zero,
// this function will mark it as using the RW signing scheme.
void setAndClearBits(uintptr_t set, uintptr_t clear)
{
ASSERT((set & clear) == 0);
uintptr_t newBits, oldBits = LoadExclusive(&bits);
do {
uintptr_t authBits
= (oldBits
? (uintptr_t)ptrauth_auth_data((class_rw_t *)oldBits,
CLASS_DATA_BITS_RW_SIGNING_KEY,
ptrauth_blend_discriminator(&bits,
CLASS_DATA_BITS_RW_DISCRIMINATOR))
: FAST_IS_RW_POINTER);
newBits = (authBits | set) & ~clear;
newBits = (uintptr_t)ptrauth_sign_unauthenticated((class_rw_t *)newBits,
CLASS_DATA_BITS_RW_SIGNING_KEY,
ptrauth_blend_discriminator(&bits,
CLASS_DATA_BITS_RW_DISCRIMINATOR));
} while (slowpath(!StoreReleaseExclusive(&bits, &oldBits, newBits)));
}
void setBits(uintptr_t set) {
setAndClearBits(set, 0);
}
void clearBits(uintptr_t clear) {
setAndClearBits(0, clear);
}
public:
void copyRWFrom(const class_data_bits_t &other) {
bits = (uintptr_t)ptrauth_auth_and_resign((class_rw_t *)other.bits,
CLASS_DATA_BITS_RW_SIGNING_KEY,
ptrauth_blend_discriminator(&other.bits,
CLASS_DATA_BITS_RW_DISCRIMINATOR),
CLASS_DATA_BITS_RW_SIGNING_KEY,
ptrauth_blend_discriminator(&bits,
CLASS_DATA_BITS_RW_DISCRIMINATOR));
}
void copyROFrom(const class_data_bits_t &other, bool authenticate) {
ASSERT((flags() & RO_REALIZED) == 0);
if (authenticate) {
bits = (uintptr_t)ptrauth_auth_and_resign((class_ro_t *)other.bits,
CLASS_DATA_BITS_RO_SIGNING_KEY,
ptrauth_blend_discriminator(&other.bits,
CLASS_DATA_BITS_RO_DISCRIMINATOR),
CLASS_DATA_BITS_RO_SIGNING_KEY,
ptrauth_blend_discriminator(&bits,
CLASS_DATA_BITS_RO_DISCRIMINATOR));
} else {
bits = other.bits;
}
}
虽然我们不是很能看懂,但至少能看出来这里是oc运行时用来存储数据的。
根据上面的分析,class指针各为8字节,cache_t cache为16字节,所以想要获取bits的中的内容,只需通过类的首地址平移32字节即可。
总结
在学习过程中,笔者关于LLDB调试出了比较多的问题,至今还没解决,所以这篇笔记还有待完善,等笔者解决完问题后,会再次进行编撰,LLDB在源码学习中还是很不错的工具。