前言
上篇文章分析了objc_class
里存储数据的bits
,了解到方法和属性的存储的位置class_rw_t
(以下简称rw
)。本文将继续研究rw
里包含的其他内容。
类数据的存储
书接上文,rw
结构体,找到一个class_ro_t
的结构体(以下简称ro
)。
代码验证:
// 声明
NS_ASSUME_NONNULL_BEGIN
@interface FFPhone : NSObject
@property (nonatomic, copy) NSString * name;
+ (void)phoneTest;
@end
NS_ASSUME_NONNULL_END
// 实现
@implementation FFPhone
{
NSString * _privateProperty;
}
-(instancetype)init {
if (self = [super init]) {
self.name = @"init iPhone";
}
return self;
}
+ (void)phoneTest {
NSLog(@"phoneTest");
}
@end
// 测试
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 掩码:0x00007ffffffffff8ULL
FFPhone *p = [FFPhone alloc];
NSLog(@"...");
}
return 0;
}
LLDB指令:
// (lldb) x/6gx p.class
// (lldb) p/x (class_data_bits_t *)0x100008200
// (lldb) p *$1
// (lldb) p $2.data()
// (lldb) p *$3
// (lldb) p $4.ro()
是能获取到到ro
:
既然成员变量不在rw
中,会不会在ro
里?
成员变量
在iOS中,ivar
表示成员变量,而ro
结构体里正好有个ivars
:
// (lldb) p *$5
// (lldb) p $6.ivars
如图:
得到结构体ivar_list_t
类型的指针
也是继承自模板类entsize_list_tt
struct ivar_list_t : entsize_list_tt<ivar_t, ivar_list_t, 0> {
bool containsIvar(Ivar ivar) const {
return (ivar >= (Ivar)&*begin() && ivar < (Ivar)&*end());
}
};
通过get
方法拿到第一个元素,正是私有的成员变量_privateProperty
,说明属性生成的成员变量才会放在class_rw_t
的属性列表properties()
里,而单独的成员变量只会放在ro
结构体里,因为成员变量无法从外部修改,看成是只读的。
越界也会报错:
所以属性生成的成员变量_name
也在ro
生成了一份。
offset
对应machO文件中的内存偏移量
可以确定成员对象放在类对象里,为什么它们的值放在实例对象里?
类的本质是结构体,相当于一个模板;类里有什么属性、方法等放在模板里就好了。而实例对象是根据这个模板生成的,每个对象的值可能不一样,当然只需要存放值。
rw和ro区别
ro
和 rw
有什么区别?苹果的WWDC大会有个视频Advancements in the Objective-C runtime里解释过。ro
放在纯净的内存空间(clean memory),是只读的。rw
在运行生成,存放在可以读写的内存空间中(dirty memory),一经使用,ro
就会成为rw
的一部分(通过指针引用)。
runtime
既然可以动态添加属性、协议等。而ro
又不允许修改,怎么办?
拷贝一份再进行修改!这样存在了2份ro数据,岂不是内存浪费?
苹果解决的方式也在视频提到,对于没有使用到的ro
,可以进行移除,需要时再分配。所以rw
中可以只存储一部分信息,并且rw
对于真正需要修改的内容,还会拆分出class_rw_ext_t
;以下简称rwe
。
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 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;
}
// 不包括方法
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;
}
视频里提到,在苹果的测试中,实际只有大约不到10%的类真正更改了它们。
ro、rw、rwe的关系
初始时,只有ro
。并且Swift和objc共享这一基础结构。
当需要修改类信息的时候,rw
引用ro
,并拷贝一部分信息。
将需要动态更新的部分提取出来,存⼊rwe
:
最终链接成这样:
当前demo里没有rwe
,因为没有进行动态修改(分类、runtime的api);除此以外,触发条件还包括:分类和本类都不是懒加载的类(以后单独讲分类)。
rwe
不会将成员变量剪切过去,因为无法修改。
例如获取方法列表会判断rwe
: 没有才从ro
获取
rwe
作为属性跟着类一起释放;
根据链接图可以看到,方法列表放在ro
的baseMethods
数组里,之后拷贝到了rwe
。
总结:程序加载时方法存在ro
。当类第一次使用的时候,rw
就会引用ro
;如果动态修改,就会从ro
拷贝到rwe
;修改的时候也是去操作rwe
;
元类设计的初衷
复用消息机制。类调用方法,实际上就是发送一条信息。系统通过objc_msgSend()
找到实现。
这个函数id objc_msgSend(id self, SEL op, ...)
有2个参数:消息的接收者self
,消息的方法名op
。
第一个参数消息的接收者的isa指针,找到对应的类,如果我们是通过实例对象调用方法,那么这个isa指针就会找到实例对象的类对象,如果是类对象,就会找到类对象的元类对象,然后再通过SEL
类型的方法名找到对应的imp
,就能找到方法对应的实现。
由于在oc中类方法和实例方法可以同名,通过消息接收者的isa指针来查找。类方法通过类对象的isa找到元类对象,实例方法通过实例对象的isa找到类对象。
如果没有元类的话,那这个objc_msgSend
方法还得多加俩个参数,一个参数用来判断这个方法到底是类方法还是实例方法。一个参数用来判断消息的接受者到底是类对象还是实例对象。在方法内部就会有有很多的判断,影响发送效率。消息的发送,总是越快越好。
所以还是得用isa
。根据单一职责,元类对象存储类方法,类对象存储实例方法。并且在不同种类的方法走的都是同一套流程,在之后的维护上也大大节约了成本。所以这个元类的出现,最大的好处就是能够复用消息传递这套机制。
结合之前的探究的实例方法和类方法,说明在objc底层没有区别,都是函数,通过消息机制调用。只不过存放位置的不同,类方法存储在元类对象里,实例方法存储在类对象里。
runtime的api尝试
既然说到动态修改方法,runtime提供了很多api,这里就随便试试。
成员变量列表
通过class_copyIvarList
获取成员变量列表:
// Class的成员变量
void ff_class_copyIvarList(Class ffClass) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(ffClass, &count);
for (int i = 0; i < count; i ++) {
Ivar ivar = ivars[i];
const char *name = ivar_getName(ivar);
const char *type = ivar_getTypeEncoding(ivar);
NSLog(@"ivar name = %s; ivar type = %s", name, type);
}
free(ivars);
}
这里为什么要通过free(ivars);
手动释放变量?查看方法的源码可以看到:
/***********************************************************************
* class_copyIvarList
* fixme
* Locking: read-locks runtimeLock
**********************************************************************/
Ivar *
class_copyIvarList(Class cls, unsigned int *outCount)
{
const ivar_list_t *ivars;
Ivar *result = nil;
unsigned int count = 0;
if (!cls) {
if (outCount) *outCount = 0;
return nil;
}
mutex_locker_t lock(runtimeLock);
ASSERT(cls->isRealized());
if ((ivars = cls->data()->ro()->ivars) && ivars->count) {
// 开辟了内存,需要手动释放。
result = (Ivar *)malloc((ivars->count+1) * sizeof(Ivar));
for (auto& ivar : *ivars) {
if (!ivar.offset) continue; // anonymous bitfield
result[count++] = &ivar;
}
result[count] = nil;
}
if (outCount) *outCount = count;
return result;
}
通过malloc
开辟了内存,由于ARC只会管理OC对象,所以需要手动释放。运行结果:
type
这里对应编码类型简称:i = int,c = char, d = double, s = short
;
属性列表
通过class_copyPropertyList
获取属性列表:
// Class的属性
void ff_class_copyPropertyList(Class ffClass) {
unsigned int count = 0;
objc_property_t *perperties = class_copyPropertyList(ffClass, &count);
for (int i = 0; i < count; i++) {
objc_property_t property = perperties[i];
const char *name = property_getName(property);
const char *type = property_getAttributes(property);
NSLog(@"property name = %s; property type = %s", name, type);
}
free(perperties);
}
运行结果:
这里编码怎么理解呢,以T@"NSString",C,N,V_goodsName
为例:
T
代表类型,后面接类型名称@"NSString"
;C
代表属性的Copy
关键字,是复制的;N
代表nonatomic
,该属性是原子性的;V_goodsName
代表属性生成的带下划线的成员变量_goodsName
完整编码见扩展
方法列表
通过class_copyMethodList
获取方法列表:
// Class的Method
void ff_class_copyMethodList(Class ffClass) {
unsigned int count = 0;
Method *methods = class_copyMethodList(ffClass, &count);
for (int i = 0; i < count; i++) {
Method method = methods[i];
NSString *name = NSStringFromSelector(method_getName(method));
const char *type = method_getTypeEncoding(method);
NSLog(@"method name = %@; method type = %s",name,type);
}
free(methods);
}
// 方法名称是SEL类型
SEL method_getName(Method mSigned)
{
if (!mSigned) return nil;
method_t *m = _method_auth(mSigned);
ASSERT(m->name() == sel_registerName(sel_getName(m->name())));
return m->name();
}
获取到的是Method
名称是SEL
类型,所以要打印的话还需要解析:NSStringFromSelector
;
方法编码的含义又是什么?举个栗子:v20@0:8i16
@
代表对象类型,@0
从第0个字节开始存放;:
代表方法SEL类型,:8
表示从第8个字节开始存放;i
代表int类型;i16
表示从第16字节开始存放;v
代表void
无返回值类型,v20
表示方法总共占20字节;
v20@0:8i16
正好对应这三种类型,加起来占20字节。
其他编码含义:
//编码值 含意
//c 代表char类型
//i 代表int类型
//s 代表short类型
//l 代表long类型,在64位处理器上也是按照32位处理
//q 代表long long类型
//C 代表unsigned char类型
//I 代表unsigned int类型
//S 代表unsigned short类型
//L 代表unsigned long类型
//Q 代表unsigned long long类型
//f 代表float类型
//d 代表double类型
//B 代表C++中的bool或者C99中的_Bool
//v 代表void类型
//* 代表char *类型
//@ 代表对象类型
//# 代表类对象 (Class)
//: 代表⽅法selector (SEL)
//[array type] 代表array
//{name=type…} 代表结构体
//(name=type…) 代表union
//bnum A bit field of num bits
//^type A pointer to type
//? An unknown type (among other things, this code is used for function pointers)
试试打印元类的方法,只有一个类方法,这也能证明元类只存放类方法。
实例方法
类对象和元类对象,通过class_getInstanceMethod
获取实例方法的方式,来获取一下实例方法和类方法(正常应该不能)。
// 获取类对象和元类的实例方法
void ff_class_getInstanceMethod(Class ffClass) {
const char *className = class_getName(ffClass);
Class metaClass = objc_getMetaClass(className);
Method cInstanceMethod = class_getInstanceMethod(ffClass, @selector(testInstancePrint));
Method mInstanceMethod = class_getInstanceMethod(metaClass, @selector(testInstancePrint));
Method cClassMethod = class_getInstanceMethod(ffClass, @selector(testClassPrint));
Method mClassMethod = class_getInstanceMethod(metaClass, @selector(testClassPrint));
NSLog(@"类对象的实例方法: %p", cInstanceMethod);
NSLog(@"元类对象的实例方法: %p", mInstanceMethod);
NSLog(@"类对象的类方法: %p", cClassMethod);
NSLog(@"元类对象的类方法: %p", mClassMethod);
}
运行效果:
为什么元类能通过class_getInstanceMethod
获取类方法?正常应该是class_getClassMethod
函数来获取的
查看源码可知,本质还是class_getInstanceMethod
只是参数变成了元类:cls->getMeta()
/***********************************************************************
* class_getClassMethod. Return the class method for the specified
* class and selector.
**********************************************************************/
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
return class_getInstanceMethod(cls->getMeta(), sel);
}
那么类方法其实也是实例方法?iOS底层根本没有类方法和实例方法之分。本质都是函数,而runtime
通过消息机制调用函数。
元类存放类方法,是为了复用消息机制。满足单一职责的设计原则,同时对类的结构体模板进行复用。
IMP
通过class_getMethodImplementation
获取方法的实现(IMP):
// 获取方法实现IMP
void ff_class_getMethodImpl(Class ffClass) {
const char *className = class_getName(ffClass);
Class metaClass = objc_getMetaClass(className);
IMP cInstanceMethodImpl = class_getMethodImplementation(ffClass, @selector(testInstancePrint));
IMP mInstanceMethodImpl = class_getMethodImplementation(metaClass, @selector(testInstancePrint));
IMP cClassMethodImpl = class_getMethodImplementation(ffClass, @selector(testClassPrint));
IMP mClassMethodImpl = class_getMethodImplementation(metaClass, @selector(testClassPrint));
NSLog(@"类对象的实例方法IMP: %p", cInstanceMethodImpl);
NSLog(@"元类对象的实例方法IMP: %p", mInstanceMethodImpl);
NSLog(@"类对象的类方法IMP: %p", cClassMethodImpl);
NSLog(@"元类对象的类方法IMP: %p", mClassMethodImpl);
}
运行结果:
元类里面也能找到实例方法的实现?而且mInstanceMethodImpl
和cClassMethodImpl
一样内存地址...
查找源码:
__attribute__((flatten))
IMP class_getMethodImplementation(Class cls, SEL sel)
{
IMP imp;
if (!cls || !sel) return nil;
lockdebug_assert_no_locks_locked_except({ &loadMethodLock });
imp = lookUpImpOrNilTryCache(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);
// Translate forwarding function to C-callable external version
if (!imp) {
return _objc_msgForward;
}
return imp;
}
if (!imp)
表示没找到方法时,会返回_objc_msgForward
;这个就是用来做消息转发的。
以前的文章,用到.big()
来获取方法,查看big
结构体可知,包含SEL
和IMP
。
总结
成员变量
成员变量存放在类对象的class_ro_t
结构体里,因为它们不能被外部修改。
为什么设计元类?
- 复用消息机制。
- 符合设计原则中的单一职责。
- 不同种类的方法走的都是同一套流程,易于维护。
类方法和实例方法的区别
在objc底层没有类方法和实例方法的区别,都是函数,通过消息机制调用。只不过类方法存储在元类对象里,实例方法存储在类对象里。
类信息的存储:ro、rw、rwe
class_ro_t
是在编译的时候生成的。当类在编译的时候,类的属性,实例方法,协议这些内容就存在class_ro_t这个结构体里面了,这是一块纯净的内存空间,不允许被修改。
class_rw_t
是在运行的时候生成的,类一经使用就会变成class_rw_t,它会先将class_ro_t的内容"拿"过去,然后再将当前类的分类的这些属性、方法等拷⻉到class_rw_t里面。它是可读写的。
class_rw_ext_t
可以减少内存的消耗。苹果在WWDC2020
里面说过,只有大约10%左右的类需要动态修改。所以只有10%左右的类里面需要生成class_rw_ext_t
这个结构体。这样的话,可以节约很大一部分内存。
class_rw_ext_t生成的条件:
- 用过runtime的api进行动态修改的时候。
- 有分类的时候,且分类和本类都为非懒加载类 (实现了
+ load
方法)。
扩展
类型编码
官方文档:runtime类型编码
方法的类型:
属性的类型: