iOS 底层系列 - Runtime

898 阅读7分钟

一、问题

1. 对象如何找到对应的方法去调用

根据对象的isa去对应的类查找方法,isa:判断去哪个类查找对应的方法 指向方法调用的类 。 根据传入的方法编号SEL,里面有个哈希列表,在列表中找到对应方法Method(方法名) 。 根据方法名(函数入口)找到函数实现,函数实现在方法区。

2. SEL、isa、super、cmd

  • SEL:

    • 一种类型,表示方法名称,类似字符串(可互转)
  • IMP:

    • 定义为 id (*IMP) (id, SEL, …)。
    • 这样说来, IMP是一个指向函数的指针,这个被指向的函数包括id(“self”指针),调用的SEL(方法名),再加上一些其他参数.说白了IMP就是实现方法
  • ISA:

    • 在方法底层对应的objc_msgSend调用时,会根据isa找到对象所在的类对象,类对象中包含了调度表(dispatch table),该表将类的sel和方法的实际内存地址关联起来
  • super_class:

    • 每一个类中还包含了一个super_class指针,用来指向父类对象。
  • cmd:

    • 在Objective-C的方法中表示当前方法的selector,正如同self表示当前方法调用的对象实例

3. 能否向运行时创建的类中添加实例变量?

不能

  • 因为编译后的类已经注册在runtime中,类结构体中的objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定

  • 运行时创建的类是可以添加实例变量,调用 class_addIvar函数,但是得在调用objc_allocateClassPair之后objc_registerClassPair之前,原因同上。

  • 源码如下:

 Class newClass = objc_allocateClassPair( [NSError class], "RuntimeErrorSubclass", 0 );

 class_addMethod(newClass, @selector(report), (IMP) ReportFunction, "v@:");

 objc_registerClassPair(newClass);

4. class方法和objc_getClass方法有什么区别?

  1. 当参数obj为Object实例对象 object_getClass(obj)与[obj class]输出结果一直,均获得isa指针,即指向类对象的指针。

  2. 当参数obj为Class类对象 object_getClass(obj)返回类对象中的isa指针,即指向元类对象的指针;[obj class]返回的则是其本身。

  3. 当参数obj为Metaclass类对象 object_getClass(obj)返回元类对象中的isa指针,因为元类对象的isa指针指向根类,所有返回的是根类对象的地址指针;[obj class]返回的则是其本身。

  4. obj为Rootclass类对象 object_getClass(obj)返回根类对象中的isa指针,因为跟类对象的isa指针指向Rootclass‘s metaclass(根元类),即返回的是根元类的地址指针;[obj class]返回的则是其本身。

总结:

  • 经上面初步的探索得知,object_getClass(obj)返回的是obj中的isa指针;而[obj class]则分两种情况:一是当obj为实例对象时,[obj class]中class是实例方法:- (Class)class,返回的obj对象中的isa指针;二是当obj为类对象(包括元类和根类以及根元类)时,调用的是类方法:+ (Class)class,返回的结果为其本身。

5. runtime 中,SEL和IMP的区别?

每个类对象都有一个方法列表,方法列表存储方法名、方法实现、参数类型,SEL是方法名(编号),IMP指向方法实现的首地址

6. iOS中isKindOfClass和isMemberOfClass 底层

7. 为什么要设计metaclass

  • 元类的存在巧妙的简化了实例方法和类方法的调用流程,大大提升了消息发送的效率。

  • 通过元类就可以,让各类各司其职,实例对象就干存储属性值的事,类对象存储实例方法列表,元类对象存储类方法列表。

8. _objc_msgForward 函数是做什么的,直接调用它将会发生什么?

_objc_msgForward 是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward 会尝试做消息转发

详解:_objc_msgForward 在进行消息转发的过程中会涉及以下这几个方法:

  • resolveInstanceMethod:方法 (或resolveClassMethod:)。
  • forwardingTargetForSelector:方法
  • methodSignatureForSelector:方法
  • forwardInvocation:方法

doesNotRecognizeSelector: 方法

9. 讲一下 OC 的消息机制

  • OC中的方法调用其实都是转成了objc_msgSend 函数的调用,给 receiver(方法调用者)发送了一条消息(selector方法名)

  • objc_msgSend底层有3大阶段,消息发送(当前类、父类中查找)

    • 动态方法解析(可以通过 runtime 添加方法)
    • 消息转发(指定一个对象实现)
    • 完整消息转发(可以做任何事)

10. IMP、SEL、Method的区别和使用场景

Method:是objc_method类型指针,它是一个结构体,包含 IMP,SEL。

  • IMP:是方法的实现,即:一段c函数。
  • SEL:是方法名。
struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
} 

11. class、objc_getClass、object_getclass 方法有什么区别?

+ (Class)class {
return self; 本身
}

- (Class)class {
return object_getClass(self) 类对象;
}

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa(); ISA 指向
    else return Nil;
}

二、Runtime 知识

1. 什么是 Runtime

OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行,OC的动态性就是由Runtime来支撑和实现的。 Runtime 基本是用 C/C++和 汇编写的一套 API,封装了很多动态性相关的函数平时编写的OC代码,底层都是转换成了Runtime API进行调用。 runtime 的源码是开源的,源码在:opensource.apple.com/tarballs/ob…

2. Runtime API 平时项目哪些地方会用到

  • 利用关联对象(AssociatedObject)给分类添加属性
  • 遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
  • 交换方法实现(交换系统的方法)
  • 利用消息转发机制解决方法找不到的异常问题。
  • HOOK 方法进行埋点记录等。

3. 位域

4. Class 的结构

(1)class_rw_t

class_rw_t里面包含了类的:

  • 方法列表 methods
  • 属性列表 properties
  • 协议信息 protocols
  • 只读信息 class_ro_t

methods,protocols, properties 最后会合并 class_ro_t 里面的 baseMethodList, baseProtocols, baseProperties,汇总到class_rw_t 。

(2)class_ro_t

  • class_ro_t 和 class_rw_t 的主要区别是 class_ro_t 是只读的包含:
    • 成员变量 ivars
    • 类名 name
    • 方法列表 baseMethodList
    • 属性列表 baseProperties
    • 协议信息 baseProtocols

(3)method_t

  • method_t是对方法\函数的封装,里面包含
    • 函数名 name(SEL 类型)
    • 指向函数的指针 imp
    • 函数参数返回值编码 types

1. IMP
  • IMP 是一个函数指针,这个被指向的函数包含一个接收消息的对象id(self 指针)
  • 调用方法的选标 SEL (方法名),以及不定个数的方法参数,并返回一个id。
  • 也就是说 IMP 是消息最终调用的执行代码,是方法真正的实现代码。
2. SEL

  • SEL代表 方法\函数名,一般叫做选择器,底层结构跟 char * 类似,可以说 SEL 就是方法编号。

可以通过 @selector() 和 sel_registerName() 获得。 可以通过sel_getName()和NSStringFromSelector()转成字符串。

3. types

types 包含了函数返回值、参数编码的字符串。

3.1 Type Encoding 编码表示

三、方法缓存

  • Class内部结构中有个方法缓存 (cache_t) 散列表(哈希表)来缓存曾经调用过的方法,可以 提高方法的查找速度.

  • 缓存查找

    • objc-cache.mm
    • bucket_t * cache_t::find(cache_key_t k, id receiver)

缓存查找原理

  • 通过 & Mask 找到具体的 bucket 缓存。

四、objc_msgSend 消息发送

源码跟读

消息转发

消息发送流程

  • 1,首先去该类的方法 cache 中查找,如果找到了就返回它;

  • 2,如果没有找到,就去该类的方法列表中查找。如果在该 类的方法列表中找到了,则将 IMP 返回,并将它加入cache中缓存起来。

  • 3,如果在该类的方法列表中没找到对应的 IMP,在通过该类结构中的 super_class指针 在其父类结构的方法列表中去查找,直到在某个父类的方法列表中找到对应的IMP,返回它,并加入cache中。

  • 4,如果在自身以及所有父类的方法列表中都没有找到对应的 IMP,则进入下文中要讲的 动态方法解析

(1)动态方法解析

动态方法解析 - 添加方法

(2)消息转发

消息转发 - 交给另一个对象进行处理

(3)完整消息转发

完整消息转发 - 做任何事情