对象的分类
Objective-C中所有对象可以分为3类:实例对象,类对象,元类对象。其中我们开发者常用的继承自NSObject
都属于实例对象,实例对象通过isa
指针指向的是类对象。类对象通过isa
指向的是元类对象。类对象和元类对象拥有同样的结构,都是来自objc_class
。关于这部分可以先学习一下我前面的博客:[OC学习笔记]浅学元类。
objc_class结构
因为所有的类都是继承于objct_class
,在源码中查找objc_class
可以找到其结构:
struct objc_class : 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
...
其中isa
指针是继承自objc_object
的。isa
在之前的内容里面已经讲过,主要是指向类对象或者元类对象的。类结构里面默认一个Class ISA
同时包含Class superclass
,cache_t cache
,class_data_bits_t bits
。superclass
是体现出继承关系的。cache
是方法缓存。bits
是类的其他信息,例如成员变量,方法列表,协议,属性。
注意:元类对象结构也是如此,但元类对象里面没有成员变量,协议,属性这些内容。
isa指向
放出这张经典图片:
对isa
指向做一个简单的总结:
实例对象的isa指针指向对应的类对象,类对象的isa指针指向对应的元类对象,元类对象的isa指向基类的元类对象(根元类)。
可以用lldb
调试来验看下。
- (void)test {
NSObject *objc = [[NSObject alloc] init];
// isa指向的就是类对象
Class objcClass1 = [NSObject class];
Class objcMetaClass = object_getClass([NSObject class]);
}
根据上一篇内容,我们先获取objc的isa
指针的值,然后用这个值与运算ISA_MASK
,得到的值,正好是类对象的地址。然后用同样的方法获取类对象的值,和ISA_MASK
与运算得到元类对象的地址。
可以继续这样再操作一轮,得到的还是元类对象的地址。这是因为该类是NSObject
,NSObject
元类对象的isa
是指向自己的。
define ISA_MASK 0x007ffffffffffff8ULL
0x00007ffffffffff8
转二进制为
0001 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000
其实就是,000…000…中间44个1…000。其他位抹零,只保留中间44位,即取到shiftcls
类信息。
可看到NSObject
的isa
走势为: NSObject →根元类 →根元类自身
。
superClass指向
再看上面那张图:superClass
的指向也在上面那个经典的图中。这里要注意superclass
是objc_class
才有的,所以实例对象是没有的。类对象的superclass
指向父类对象,元类对象的superclass
指向父类的元类对象,根元类对象的superclass
指针指向根类对象。根类对象的superclass
是nil
。
bits
类的结构中还有一个bits
,里面也存了很多信息。我们之前知道,已知首地址,可以通过平移方法,得到我们64位下结构体指针类型占8字节,即isa
占8字节,superclass
同理也是结构体指针类型占8字节,即superclass
占8字节。
接下来我们看下cache
的大小:
因为cache
为cache_t
类型, 最简单的方法lldb
命令 po
读一下cache_t
。
可以看到,大小为16字节。
这一小节,我们主要来看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.
void setAndClearBits(uintptr_t set, uintptr_t clear)
{
ASSERT((set & clear) == 0);
uintptr_t newBits, oldBits = LoadExclusive(&bits);
do {
newBits = (oldBits | set) & ~clear;
} while (slowpath(!StoreReleaseExclusive(&bits, &oldBits, newBits)));
}
void setBits(uintptr_t set) {
__c11_atomic_fetch_or((_Atomic(uintptr_t) *)&bits, set, __ATOMIC_RELAXED);
}
void clearBits(uintptr_t clear) {
__c11_atomic_fetch_and((_Atomic(uintptr_t) *)&bits, ~clear, __ATOMIC_RELAXED);
}
public:
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
void setData(class_rw_t *newData)
{
ASSERT(!data() || (newData->flags & (RW_REALIZING | RW_FUTURE)));
// Set during realization or construction only. No locking needed.
// Use a store-release fence because there may be concurrent
// readers of data and data's contents.
uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
atomic_thread_fence(memory_order_release);
bits = newBits;
}
// 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;
}
}
#if SUPPORT_INDEXED_ISA
void setClassArrayIndex(unsigned Idx) {
// 0 is unused as then we can rely on zero-initialisation from calloc.
ASSERT(Idx > 0);
data()->index = Idx;
}
#else
void setClassArrayIndex(__unused unsigned Idx) {
}
#endif
unsigned classArrayIndex() {
#if SUPPORT_INDEXED_ISA
return data()->index;
#else
return 0;
#endif
}
bool isAnySwift() {
return isSwiftStable() || isSwiftLegacy();
}
bool isSwiftStable() {
return getBit(FAST_IS_SWIFT_STABLE);
}
void setIsSwiftStable() {
setAndClearBits(FAST_IS_SWIFT_STABLE, FAST_IS_SWIFT_LEGACY);
}
bool isSwiftLegacy() {
return getBit(FAST_IS_SWIFT_LEGACY);
}
void setIsSwiftLegacy() {
setAndClearBits(FAST_IS_SWIFT_LEGACY, FAST_IS_SWIFT_STABLE);
}
// fixme remove this once the Swift runtime uses the stable bits
bool isSwiftStable_ButAllowLegacyForNow() {
return isAnySwift();
}
_objc_swiftMetadataInitializer swiftMetadataInitializer() {
// This function is called on un-realized classes without
// holding any locks.
// Beware of races with other realizers.
return safe_ro()->swiftMetadataInitializer();
}
};
其实最最重要的是其中的2个方法。data
和 safe_ro
,两个方法分别返回class_rw_t
和class_ro_t
。
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
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;
}
}
仔细观察获取class_ro_t
的方法,里面也用到了data()
方法,也可以理解为class_ro_t
也在class_rw_t
之中。
class_rw_t
继续看class_rw_t
的数据结构:
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif
explicit_atomic<uintptr_t> ro_or_rw_ext;
Class firstSubclass;
Class nextSiblingClass;
private:
using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t, class_rw_ext_t, PTRAUTH_STR("class_ro_t"), PTRAUTH_STR("class_rw_ext_t")>;
const ro_or_rw_ext_t get_ro_or_rwe() const {...}
void set_ro_or_rwe(const class_ro_t *ro) {...}
void set_ro_or_rwe(class_rw_ext_t *rwe, const class_ro_t *ro) {...}
class_rw_ext_t *extAlloc(const class_ro_t *ro, bool deep = false);
public:
void setFlags(uint32_t set)
{...}
void clearFlags(uint32_t clear)
{...}
// set and clear must not overlap
void changeFlags(uint32_t set, uint32_t clear)
{...}
class_rw_ext_t *ext() const {...}
class_rw_ext_t *extAllocIfNeeded() {...}
class_rw_ext_t *deepCopy(const class_ro_t *ro) {...}
const class_ro_t *ro() const {...}
void set_ro(const class_ro_t *ro) {...}
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
}
}
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
}
}
};
可以看到有三个主要的方法:通过这三个方法分别能获取到类的方法、属性、协议。
属性列表打印
已知首地址以及 ISA
占 8字节,superclass
占8字节,cache
占16字节,所以bits
前面总共8+8+16 = 32
字节, 可通过首地址平移32字节获取bits
信息。
@interface MyPerson : NSObject {
NSString *myHobby;
NSString *myCars;
}
@property (nonatomic, strong) NSString *myName;
@property (nonatomic, assign) int age;
- (void)sayHello;
- (void)sayBye;
+ (void)sayFuckOff;
@end
在lldb
调试区调试:
bits
数据信息在前面已经讲过了,我们从读bits
数据信息$2之后开始
p $3.properties()
获得的属性列表的list结构, 其中list中的ptr就是属性数组的参数指针地址。(p $3.properties()
命令中的properties
方法是由class_rw_t
提供的, 方法中返回的实际类型为property_array_t
)p *$4.list.ptr
读一下指针地址指向内容, 可看到获得属性list信息, count = 2, 也符合我们建的2个属性p $5.get(0)
可获取到myName
对应属性(property_t) $6 = (name = "myName", attributes = "T@\"NSString\",&,N,V_myName")
p $5.get(1)
可获取到age
属性(property_t) $7 = (name = "age", attributes = "Ti,N,V_age")
p $5.get(2)
数组越界, 因为我们只建立了2个属性
方法列表打印
先仔细看一下上面的那三个方法,可以看到一个叫class_rw_ext_t
的东西,先了解一些东西:
struct class_rw_ext_t {
DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
class_ro_t_authed_ptr<const class_ro_t> ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
char *demangledName;
uint32_t version;
};
struct property_t {
const char *name;
const char *attributes;
};
struct method_t {
static const uint32_t smallMethodListFlag = 0x80000000;
method_t(const method_t &other) = delete;
// The representation of a "big" method. This is the traditional
// representation of three pointers storing the selector, types
// and implementation.
struct big {
SEL name;
const char *types;
MethodListIMP imp;
};
private:
...
};
想必与属性列表不同,方法列表的相关内容name
、types
、imp
储存在struct big
里面(818新改动),所以获取方法列表里面的信息也要稍微变一下。看别人是这样取的:
人家使用p $5.get(0).big()
可以取到,但我就不行,反而能用small()
取到。通过观察,我只好换用getDescription()
获取:
只获取了实例方法,没法获取类方法。或者我们直接上代码获取,哎,这里真的没太理解啊。代码获取:
Class metaClass = objc_getClass("MyPerson");
unsigned int count = 0;
Method *methods = class_copyMethodList(metaClass, &count);
NSLog(@"%u", count);
for (unsigned int i = 0; i < count; i++) {
Method const method = methods[i];
//获取方法名
NSString *key = NSStringFromSelector(method_getName(method));
NSLog(@"Method, name: %@", key);
}
metaClass = objc_getMetaClass("MyPerson");
count = 0;
methods = class_copyMethodList(metaClass, &count);
NSLog(@"%u", count);
for (unsigned int i = 0; i < count; i++) {
Method const method = methods[i];
//获取方法名
NSString *key = NSStringFromSelector(method_getName(method));
NSLog(@"Method, name: %@", key);
}
free(methods);
输出:
可以看出来,本类有6个方法(实例方法),元类有一个方法(类方法)。
class_ro_t
其实,我们刚刚也可以发现,成员变量以及类方法并没有在属性列表、方法列表里面。回过头我们再看struct class_rw_t
,看到里面有这么一个方法:
const class_ro_t *ro() const {
auto v = get_ro_or_rwe();
if (slowpath(v.is<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
}
return v.get<const class_ro_t *>(&ro_or_rw_ext);
}
看下ro
底层:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};
explicit_atomic<const char *> name;
WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
// This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
_objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];
_objc_swiftMetadataInitializer swiftMetadataInitializer() const {...}
const char *getName() const {...}
class_ro_t *duplicate() const {...}
Class getNonMetaclass() const {...}
const uint8_t *getIvarLayout() const {...}
};
可以看到有方法、属性、协议和成员变量。但方法、属性、协议的命名都是base
开头的。
探索成员变量
可看到const ivar_list_t * ivars;
,有一个ivars
属性(ivars
: 实例变量),我们仿照下上面也读一下ro
。
p $3.ro()
获得的ro
的里面的信息p $5.ivars
获得的ivars_list_t
即成员变量的列表
接下来我们仿照属性列表去读取,发现实例变量储存在ivars_list_t
里面,同时也会发现还有属性的成员变量。
小结:
- 通过
XXXX {}
定义的成员变量,会存储在类的bits
属性中,通过bits --> data() -->ro() --> ivars
获取成员变量列表,除了包括成员变量,还包括属性的成员变量。 - 通过
@property
定义的属性,存储在bits
属性中,通过bits --> data() --> properties() --> list
获取属性列表,其中只包含property
属性。
探索类方法
所谓的对象/实例方法
、类方法
其实是OC上层或者说苹果官方人为加入的概念, 其底层是都是函数,不区分+
、-
。但实例方法与类方法还是有必要区分的,则苹果将实例方法存在类里面,而类方法存在元类里面。一方面避免对象存储太大会发生混乱,一方面也是为了有个调用区分。
所以说类方法要在元类中查找。
x/4gx MyPerson.class
以4片段16进账形式打印MyPerson
类的内存段,这里留意,我们要取的是元类中的类信息,所以要用类去打印。得到0x0000000100008360
即isa
- 用
0x0000000100008360
&0x007ffffffffff8ULL
即:isa
&掩码
,得到类信息0x0000000100008360
p (class_data_bits_t *)0x0000000100008380
,字节平移32位,得到bits,并转换class_data_bits_t *
,这里要留意下千万别忘平移,不然获取的是系统给定isa
类方法,几十W条。p $2->data()
读取bits
里面的data
p $4.methods()
读取方法列表p *$5.list.ptr
获取方法列表里面的信息p $6.get(0).getDescription()->name
获取方法列表里面第一条数据, 可看到有(SEL) $8 = "sayFuckOff"
综上:
- 实例方法: 存在对应类的bits中
- 类方法: 存在对应元类的bits中
ro 和 rw的区别
从生成时机的角度来说,ro
编译阶段生成,rw
运行的时候生成。从存储的内容角度来讲,ro
中有方法、属性、协议和成员变量,而rw
中并没有成员变量。rw
中的方法属性协议的取值方法中,也是通过取ro
或者rwe
中的值来获得。ro
中的方法、属性、协议都是base
,也就是只有本类中的方法属性和协议。
class_rw_ext_t
在2020的WWDC有个视频:Advancements in the Objective-C runtime,其中一部分是和这里有关的,我们看截图:
rw_ext_t
生成条件:
- 使用分类的类。
- 使用Runtime API动态修改类的结构的时候。
在遇到以上2种情况的时候,类的结构(属性、协议、方法)发生改变,原有的ro
(Claer Memory,便宜)已经不能继续记录类的属性、协议、方法信息了,于是系统重新生成可读可写的内存结构rw_ext
(Dirty Memory, 比较贵),来存放新的类结构。
然后在取方法、属性、列表的时候,在方法实现中来做区分,如果有rw_ext
的类,其列表就错那个rw_ext
中获得,如果没有,从ro
中读取。
思考
元类里面存放的内容只有类方法,那么为什么不把类方法存放在类对象中?或者说设计元类的目的是什么呢?
- 单一职责设计原理。
实例对象存储成员变量的值,类对象存放,实例方法、协议、成员变量、属性,元类对象存放类方法,各司其职,互不影响。 - 复用
msgSend
消息发送机制。
类方法、实例方法是在上层的定义,在底层并不区分类方法实例方法,但在runtime
这一层,需要承接上层类方法和实例方法,对接到底层方法调用。使用了msgSend
,如果msgSend
的时候需要再区分类对象,实例对象,会在内部增加判读逻辑,从而降低了效率,有了元类的存在,问题迎刃而解。