1. 介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)
OC中几乎所有的类都继承自NSObject,OC的动态性也是通过NSObject实现的。 在runtime源码中的NSObject.h中,我们可以找到NSObject的定义:
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
可以看出NSObject里有一个指向Class的isa,其中对于Class的定义在objc.h:
typedef struct objc_class *Class;
*/// Represents an instance of a class.*
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
objc_class代表类对象,objc_object代表实例对象,objc_object的isa指向objc_class。这里可以得出一个结论,实例对象的isa是指向类(类对象)的。
Meta Class(元类)
这里runtime为了设计上的统一性,引入了元类(meta class)的概念。对象的实例方法调用时,通过对象的 isa 在类中获取方法的实现。类对象的类方法调用时,通过类的 isa 在元类中获取方法的实现。
objc_class
的isa指向meta class,甚至meta class也有isa指针,它指向根元类(root meta class)。
源码分析-runtime结构体
2. 为什么要设计metaclass
2.1__objc_msgSend
流程:
1、进入_objc_msgSend后首先判断消息的接受者是否为nil或者是否使用了tagPointer技术,由于本文是为了探究META-CLASS存在的意义,所以关于tagPointer的东西就直接忽略了。
2、根据消息接受者的isa指针找到metaclass(因为类方法存在元类中。如果调用的是实例方法,isa指针指向的是类对象。)
3、进入CacheLookup流程,这一步会去寻找方法缓存,如果缓存命中则直接调用方法的实现,如果缓存不存在则进入objc_msgSend_uncached流程。
2.2CacheLookup
当缓存命中时会调用TailCallCachedImp验证方法IMP的有效性并调用该方法的实现,如果缓存没有命中则进入__objc_msgSend_uncached流程。
2.3__objc_msgSend_uncached
一通操作后从后面调用到了_class_lookupMethodAndLoadCache3这个方法,该方法在objc_runtim_new.mm文件中
2.4__class_lookupMethodAndLoadCache3
该方法会去调用lookUpImpOrForward,由于lookUpImpOrForward方法篇幅有点长,这里简述一下该方法的流程。
1、首先会再一次的从类中寻找需要调用方法的缓存,如果能命中缓存直接返回该方法的实现,如果不能命中则继续往下走。
2、从类的方法列表中寻找该方法,如果能从列表中找到方法则对方法进行缓存并返回该方法的实现,如果找不到该方法则继续往下走。
3、从父类的缓存寻找该方法,如果父类缓存能命中则将方法缓存至当前调用方法的类中(注意这里不是存进父类),如果缓存未命中则遍历父类的方法列表,之后操作如同第2步,未能命中则继续走第3步直到寻找到基类。
4、如果到基类依然没有找到该方法则触发动态方法解析流程。
5、还是找不到就触发消息转发流程
走到这里一套方法发送的流程就都走完了,那这跟元类的存在有啥关系?我们都知道类方法是存储在元类中的,那么可不可以把元类干掉,在类中把实例方法和类方法存在两个不同的数组中?
答:行是肯定可行的,但是在lookUpImpOrForward执行的时候就得标注上传入的cls到底是实例对象还是类对象,这也就意味着在查找方法的缓存时同样也需要判断cls到底是个啥。
倘若该类存在同名的类方法和实例方法是该调用哪个方法呢?这也就意味着还得给传入的方法带上是类方法还是实例方法的标识,SEL并没有带上当前方法的类型(实例方法还是类方法),参数又多加一个,而我们现在的objc_msgSend()只接收了(id self, SEL _cmd, …)这三种参数,第一个self就是消息的接收者,第二个就是方法,后续的…就是各式各样的参数。
通过元类就可以巧妙的解决上述的问题,让各类各司其职,实例对象就干存储属性值的事,类对象存储实例方法列表,元类对象存储类方法列表,完美的符合6大设计原则中的单一职责,而且忽略了对对象类型的判断和方法类型的判断可以大大的提升消息发送的效率,并且在不同种类的方法走的都是同一套流程,在之后的维护上也大大节约了成本。
3. class_copyIvarList & class_copyPropertyList区别
1.class_copyIvarList:能够获取.h和.m中的所有属性以及大括号中声明的变量,获取的属性名称有下划线(大括号中的除外)。
2.class_copyPropertyList:只能获取由property声明的属性,包括.m中的,获取的属性名称不带下划线。
3.OC中没有真正的私有属性
4. class_rw_t 和 class_ro_t 的区别
class_ro_t
class_ro_t存储了当前类在编译期就已经确定的属性、方法以及遵循的协议,里面是没有分类的方法的。那些运行时添加的方法将会存储在运行时生成的class_rw_t中。
ro即表示read only,是无法进行修改的。
class_rw_t
ObjC 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t中:
分类方法加载到class_rw_t的流程
程序启动后,通过编译之后,Runtime 会进行初始化,调用 _objc_init。
然后会 map_images
再然后就是 _read_images,这个方法会读取所有的类的相关信息。
根据注释,_read_images 方法主要做了下面这些事情: _read_images 方法写了很长,其实就是做了一件事,将Mach-O文件的section依次读取,并根据内容初始化runtime的内存结构。
- 是否需要禁用isa优化。这里有三种情况:使用了swift 3.0前的swift代码。OSX版本早于10.11。在OSX系统下,Mach-O的DATA段明确指明了__objc_rawisa(不使用优化的isa).
- 判断是否禁用了tagged pointer
- 在__objc_classlist section中读取class list
- 在__objc_classrefs section中读取class 引用的信息,并调用remapClassRef方法来处理。
- 在__objc_selrefs section中读取selector的引用信息,并调用sel_registerNameNoLock方法处理。
- 在__objc_protolist section中读取cls的Protocol信息,并调用readProtocol方法来读取Protocol信息。
- 在__objc_protorefs section中读取protocol的ref信息,并调用remapProtocolRef方法来处理。
- 在__objc_nlclslist section中读取non-lazy class信息,并调用static Class realizeClass(Class cls)方法来实现这些class。realizeClass方法核心是初始化objc_class数据结构,赋予初始值。
- 在__objc_catlist section中读取category信息,并调用addUnattachedCategoryForClass方法来为类或元类添加对应的方法,属性和协议。
调用 reMethodizeClass:,这个方法是重新方法化的意思。
在 reMethodizeClass:方法内部会调用attachCategories: ,这个方法会传入 Class 和 Category,会将方法列表,协议列表等与原有的类合并。最后加入到 class_rw_t 结构体中
5. category如何被加载的,两个category的load方法的加载顺序,两个category的同名方法的加载顺序
load函数调用特点如下:
当类被引用进项目的时候就会执行load函数(在main函数开始执行之前),与这个类是否被用到无关,每个类的load函数只会自动调用一次.由于load函数是系统自动加载的,因此不需要调用父类的load函数,否则父类的load函数会多次执行。
1.当父类和子类都实现load函数时,父类的load方法执行顺序要优先于子类
2.当子类未实现load方法时,不会调用父类load方法
3.类中的load方法执行顺序要优先于类别(Category)
4.当有多个类别(Category)都实现了load方法,这几个load方法都会执行,但执行顺序不确定(其执行顺序与类别在Compile Sources中出现的顺序一致)
5.当然当有多个不同的类的时候,每个类load 执行顺序与其在Compile Sources出现的顺序一致
initialize函数调用特点如下:
initialize在类或者其子类的第一个方法被调用前调用。即使类文件被引用进项目,但是没有使用,initialize不会被调用。由于是系统自动调用,也不需要再调用 [super initialize] ,否则父类的initialize会被多次执行。假如这个类放到代码中,而这段代码并没有被执行,这个函数是不会被执行的。
1.父类的initialize方法会比子类先执行
2.当子类未实现initialize方法时,会调用父类initialize方法,子类实现initialize方法时,会覆盖父类initialize方法.
3.当有多个Category都实现了initialize方法,会覆盖类中的方法,只执行一个(会执行Compile Sources 列表中最后一个Category 的initialize方法)