0%

Runtime:方法调用与对象本质

方法调用的流程:

objc_msgSend调用方法的本质是通过isa指针找到该类,然后寻找方法,找到后调用。如果没有找到则通过superClass找到父类,继续查找方法。

对象结构体中的isa指向类对象。类对象的isa指向元类。元类的isa指向NSObject的元类。

对象方法是保存在类对象的结构体中,所以调用实例方法时,要去类对象中查找。以此类推,类方法也是如此。

实例对象存放isa指针以及示例变量,通过isa指针可以找到实例对象所属的类对象。类中存放着方法列表。方法列表中SEL作为key,IMP作为value。在编译期间,根据方法名字会生成唯一的标识SEL。IMP是指向最终函数实现的函数指针。整个Runtime的核心是objc_msgSend函数,通过给类发送SEL传递消息,找到匹配的IMP再获得最终的实现,并执行方法。

消息发送阶段

  1. 判断receiver是否为空,是则直接返回,否则继续。
  2. 从receiverClass的缓存中,查找方法找到则调用方法,否则继续。
  3. 从receiverClass的class_rw_t中查找方法,如果找到了则缓存下来,调用方法,否则继续。
  4. 去父类的缓存和class_rw_t中查找,步骤同上,找到了则缓存下来,没有则继续往上找父类,都没有则消息发送阶段结束,进入第二阶段:动态方法解析。

动态方法解析阶段

调用-/+resolveClassMethod:,在方法中调用class_addMethod函数添加SEL对应的方法实现IMP。以上方法中没有处理,则进入第三阶段:消息转发。

消息转发阶段

  1. 判断-/+forwardingTargetForSelector:的返回值,非空则调用objc_msgSend(返回值, SEL),向返回值发送消息。返回空则继续。
  2. 调用-/+methodSignatureForSelector:方法,如果返回不为空,则调用-/+forwardInvocation:方法中处理。若本类无法处理则继续往父类查询。如果返回空,则继续。
  3. 调用-doesNotRecognizeSelector:方法。

注意:只能对运行时动态创建的类添加成员变量(ivars),不能向已存在的类添加成员变量。因为在编译时只读的class_ro_t结构体就被确定下来,其包含了分配对象的空间大小,在运行时不可改变。

NSProxy

NSProxy和NSObject是同一层级的,可以理解为NSProxy是一个基类,都遵循了NSObject协议。NSProxy就是专门用来解决重点对象转发的问题。

与NSObject接收消息流程不一样,NSProxy简化了其中的流程:

  1. [proxyObj message]
  2. 到proxyObj类对象寻找对应的方法,找到调用。否则继续。
  3. 尝试调用resolveClassMethod进行动态方法解析
  4. 尝试进入父类对象递归查找方法,找到调用。否则继续。
  5. 尝试调用forwardingTargetForSelector进行消息转发。返回空则继续。
  6. 尝试调用methodSignatureForSelector+forwardInvocation进行消息转发。

所以对于处理消息转发,它比NSObject更高效。也阐明了该类的使用方式就是实现消息转发的两个方法即可。

注意,无需调用init方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#import "CLProxy2.h"

@implementation CLProxy2

+(instancetype)proxyWithTarget: (id)target {
// NSProxy对象不需要调用init,因为它本来就没有init方法,直接alloc之后就可以使用
CLProxy2 *proxy = [CLProxy2 alloc];
proxy.target = target;
return proxy;
}


-(NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}

-(void)forwardInvocation:(NSInvocation *)invocation {
invocation.target = self.target;
[invocation invoke];
}

@end

isa

从arm64架构开始,isa变成了一个共用体(union)结果,使用位域来存储更多信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;
struct {
ISA_BITFIELD; // defined in isa.h
};
};

define ISA_BITFIELD \
uintptr_t nonpointer : 1; //指针是否优化过 \
uintptr_t has_assoc : 1; //是否有设置过关联对象,如果没有,释放时会更快 \
uintptr_t has_cxx_dtor : 1; //是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快 \
uintptr_t shiftcls : 33; //存储着Class、Meta-Class对象的内存地址信息 \
uintptr_t magic : 6; //用于在调试时分辨对象是否未完成初始化 \
uintptr_t weakly_referenced : 1; //是否有被弱引用指向过,如果没有,释放时会更快 \
uintptr_t deallocating : 1; //对象是否正在释放 \
uintptr_t has_sidetable_rc : 1; //引用计数器是否过大无法存储在isa中 \
uintptr_t extra_rc : 19 //里面存储的值是引用计数器减1
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
  • nonpointer:0:普通指针,存储着Class、Meta-Class对象的内存地址;1:优化过,使用位域存储更多的信息。
  • has_assoc:是否有关联过对象。否:释放更快。
  • has_cxx_dtor:是否有C++析构函数(.cxx_destruct)。否:释放更快。
  • shiftcls:存储着Class、Meta-Class对象的内存地址信息。
  • magic:调试时分辨是否完成初始化。
  • weakly_referenced:是否被弱引用指向过。否:释放更快。
  • deallocating:对象是否正在释放。
  • extra_rc:引用计数器-1。
  • has_sidetable_rc:引用计数器是否过大无法存储在isa中。1:引用计数器会存储在SideTable类的属性中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void *objc_destructInstance(id obj) 
{
if (obj) {
//是否有C++的析构函数
bool cxx = obj->hasCxxDtor();
//是否有设置过关联对象
bool assoc = obj->hasAssociatedObjects();
//有C++的析构函数,就去销毁
if (cxx) object_cxxDestruct(obj);
//有设置过关联对象,就去移除管理对象
if (assoc) _object_remove_assocations(obj);

obj->clearDeallocating();
}

return obj;
}

class

image-20211111175507815
1
2
3
4
5
6
7
8
9
10
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; //方法缓存
class_data_bits_t bits; // 用于获取具体的类的信息
}

method_array_t、property_array_t、protocol_array_t是可读写的二维数组,包含了类的初始内容、分类内容。

如:method_array_t包含多个一位数组method_list_t,method_list_t里面存放多个method_t,method_t存放在方法imp指针、名称、类型等信息。

method_t

方法、函数的封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct method_t {
SEL name; // 函数名
const char *types; // Type Encoding 编码(返回值类型,参数类型)
MethodListIMP imp; // 指向函数的指针(函数地址)

struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};

IMP:函数的具体实现。

SEL:方法、函数名,底层结构与char *类似。可以通过@selector()sel_registerName()获得。可以通过sel_getName()NSStringFromSelector()转成字符串。名字相同的方法,SEL也是相同的。

class_ro_t

描述的是类的初始内容,其中的baseMethodListbaseProtocolsivarsbaseProperties是只读的一维数组。

cache_t

方法缓存。用哈希表缓存调用过的方法,可以提高方法查找速度。

当方法缓存太多的时候,超过了表容量的3/4的时候,就要扩容为原来的2倍。

类的本质

一个NSObject的本质是包含一个isa指针的结构体:

1
2
3
struct NSObject_IMPL {
Class isa;
};

而其子类是在isa指针的基础上再加上自身的成员变量:

1
2
3
4
5
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _no;
};

所以一个子类的底层结构体是其父类结构体里的所有成员变量 + 子类自身定义的成员变量所组成的结构体。

class_getInstanceSize:获取OC类实例对象的实际大小。这个大小可以理解为该实例对象至少需要的空间大小,实际分配大小需要使用malloc_size

malloc_size:得到一个指针指向的内存空间大小,这是系统为这个对象最终分配的内存大小。

所以回到问题:一个NSObject对象占用多少内存?

  • 系统分配了16字节(通过malloc_size获取);
  • NSObject内部只使用了8个字节来存放isa指针变量。

对象的种类

OC对象主要分为3类:

  • 实例对象(instance)
  • 类对象(class)
  • 元类对象(meta-class)

实例对象通过类alloc方法创建出来。存放的信息:

  • isa指针(指向类)。
  • 成员变量。

类对象在内存中是唯一。的类对象用来描述一个实例对象,存放信息:

  • isa指针(指向元类)和superclass指针
  • 属性
  • 对象方法
  • 协议
  • 成员变量

元类对象在内存中也是唯一的。元类对象用来描述一个类对象,存放信息:

  • isa指针和superclass指针(指向该类父类的元类)
  • 类方法

类和元类都是objc_class(继承自objc_object),也有isa指针,也是对象。

元类的superclass指向基类的类对象,者决定了:

当我们调用一个类方法时,会通过类的isa指针找到元类,在元类中查找有无该类方法,如果没有则通过superclass逐级查询父元类,一直找到基类的元类,如果还没有,则去找基类中的同名的实例方法实现。

super

1
2
3
4
5
6
7
8
9
10
class Person: NSObject {}
class Student: Person {
override init() {
super.init()
print("className: \(self.className), super.className: \(super.className)")
// FoundationSwift.Student, FoundationSwift.Student
print("superclass: \(self.superclass!), super.superclass: \(super.superclass!)")
// superclass: Person, super.superclass: Person
}
}

从上面发现, super和self调用的结构都是相同的。

super调用方法实际上是调用了objc_msgSendSuper(arg, SEL)函数。重点是第一个参数,其类型是__rw_objc_super

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//♥️♥️♥️C++中间代码里的定义
struct __rw_objc_super {
struct objc_object *object;
struct objc_object *superClass;
__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
};

//⚠️⚠️⚠️objc源码中的定义
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;

/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};

objc_super结构体成员:

  • id receiver:消息接收者,实参传递的就是self,即Student对象。
  • Class super_class:父类。
1
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
  • struct objc_super *super:结构体指针,内容是{消息接收者(recv), 消息接收者的父类类对象([[recv superclass] class])}objc_msgSendSuper会将消息接收者的父类对象作为消息查找的起点。
  • SEL op:要查找的方法。

所以说,调用super与调用self的不同只是super把查找方法的起点改为从父类开始而已,所以像一些父类没有实现,而NSObject基类实现的方法,两者调用结果无异,因为最终的消息接收者还是self,即当前对象。

若要想super和self调用方法结果不一致,必须是当前类和父类都实现了相同的方法,若只有父类实现了,就都是父类的结果。这与一般方法查找父类实现的逻辑一致。

参考

欢迎关注我的其它发布渠道