1,runtime 如何动态添加方法和属性和动态属性控制
通过苹果官方文档查看,我们可以发现runtime的内部存在很多方法,
ivar表示成员变量
class_addIvar
class_addMethod
class_addProperty
class_addProtocol
class_replaceProperty
runtime通过这个几个方法就能动态的控制添加方法,添加属性,添加协议以及替换属性等,
例如 动态变量控制
在程序中,xiaoming的age是10,后来被runtime变成了20,来看看runtime是怎么做到的。
1.动态获取XiaoMing类中的所有属性[当然包括私有]
Ivar *ivar = class_copyIvarList([self.xiaoming class], &count);
2.遍历属性找到对应name字段
const char *varName = ivar_getName(var);
3.修改对应的字段值成20
object_setIvar(self.xiaoMing, var, @"20");
2,runtime 如何实现 weak 属性
weak属性的特点:
weak 表明该属性定义了一种“非拥有关系” (nonowning relationship)。
为weak属性设置新值时,设置方法既不保留新值,也不释放旧值。
同assign类似,然而在属性所指的对象释放时候,属性值也会清空(nil out)。
runtime是如何实现 weak 变量的自动置nil?
weak 对象会放入一个 hash 表中。
用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc。
假如 weak 指向的对象内存地址是addr,那么就会以addr为键, 在这个 weak 表中搜索,找到所有以addr为键的 weak 对象,从而设置为 nil。
具体实现机制:
objc_storeWeak(&weakPo, Model)函数:
objc_storeWeak函数把赋值对象(Model)的内存地址作为键值key,将weak修饰的属性变量(weakPo)的内存地址(& weakPo)作为value,注册到 weak 表中。
如果Model为0(nil),那么把变量(weakPo)的内存地址(& weakPo)从weak表中删除,
可以把objc_storeWeak(&weakPo, Model)理解为:objc_storeWeak(value, key),并且当key变nil,将value置nil。
在Model非nil时,weakPo和Model指向同一个内存地址,在Model变nil时,weakPo变nil。此时向weakPo发送消息不会崩溃:在Objective-C中向nil发送消息是安全的。
3,weak属性需要在dealloc中置nil么
在ARC环境无论是强指针还是弱指针都无需在 dealloc 设置为 nil , ARC 会自动帮我们处理
即便是编译器不帮我们做这些,weak也不需要在dealloc中置nil
在属性所指的对象遭到摧毁时,属性值也会清空
4,runtime如何通过selector 找到对应的 IMP 地址(分别考虑类方法和实例方法)
对象中有类方法和实例方法的列表,列表中记录着方法的名词、参数和实现,而selector本质就是方法名称,runtime通过这个方法名称就可以在列表中找到该方法对应的实现。
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class
const char *name
long version
long info
long instance_size
struct objc_ivar_list *ivars
struct objc_method_list **methodLists
struct objc_cache *cache
struct objc_protocol_list *protocols
#endif
} OBJC2_UNAVAILABLE;
这里声明了一个指向struct objc_method_list指针的指针,可以包含类方法列表和实例方法列表
具体实现
在寻找IMP的地址时,runtime提供了两种方法
IMP class_getMethodImplementation(Class cls, SEL name);
IMP method_getImplementation(Method m)
第一种方法
对于第一种方法而言,类方法和实例方法实际上都是通过调用class_getMethodImplementation()来寻找IMP地址的,不同之处在于传入的第一个参数不同。
类方法(假设有一个类 A)
class_getMethodImplementation(objc_getMetaClass("A"),@selector(methodName));
实例方法
class_getMethodImplementation([A class],@selector(methodName));
通过该传入的参数不同,找到不同的方法列表,方法列表中保存着下面方法的结构体,结构体中包含这方法的实现,selector本质就是方法的名称,通过该方法名称,即可在结构体中找到相应的实现。
struct objc_method {
SEL method_name
char *method_types
IMP method_imp
}
第二种方法
传入的参数只有method,区分类方法和实例方法在于封装method的函数
类方法
Method class_getClassMethod(Class cls, SEL name)
实例方法
Method class_getInstanceMethod(Class cls, SEL name)
最后调用IMP method_getImplementation(Method m) 获取IMP地址
5,使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
无论在MRC下还是ARC下均不需要
,被关联的对象在生命周期内要比对象本身释放的晚很多,它们会在被 NSObject -dealloc 调用的object_dispose()方法中释放。
NSObject 调 -dealloc
只做一件事:调用 Objective-C runtime 中的 object_dispose() 方法 调用 object_dispose()
*为 C++ 的实例变量们(iVars)调用 destructors
*为 ARC 状态下的 实例变量们(iVars) 调用 -release
- 解除所有使用 runtime Associate方法关联的对象
- 解除所有 __weak 引用
*调用 free()
1、调用 -release :引用计数变为零
对象正在被销毁,生命周期即将结束.
不能再有新的 __weak 弱引用,否则将指向 nil.
调用 [self dealloc]
2、 父类调用 -dealloc
继承关系中最直接继承的父类再调用 -dealloc
如果是 MRC 代码 则会手动释放实例变量们(iVars)
继承关系中每一层的父类 都再调用 -dealloc
>3、NSObject 调 -dealloc
只做一件事:调用 Objective-C runtime 中object_dispose() 方法
>4. 调用 object_dispose()
为 C++ 的实例变量们(iVars)调用 destructors
为 ARC 状态下的 实例变量们(iVars) 调用 -release
解除所有使用 runtime Associate方法关联的对象
解除所有 __weak 引用
调用 free()
6,_objc_msgForward函数是做什么的?直接调用它将会发生什么?
_objc_msgForward是IMP类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发,直接调用_objc_msgForward是非常危险的事,这是把双刃刀,如果用不好会直接导致程序Crash,但是如果用得好,能做很多非常酷的事。
JSPatch就是直接调用_objc_msgForward来实现其核心功能的
1.调用resolveInstanceMethod:方法,允许用户在此时为该Class动态添加实现。如果有实现了,则调用并返回。如果仍没实现,继续下面的动作。
2.调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了nil,继续下面的动作。
3.调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。
4.调用forwardInvocation:方法,将地3步获取到的方法签名包装成Invocation传入,如何处理就在这里面了。
7,objc_msgSend执行流程
oc的方法调用,其实就是转换为objc_msgSend的函数调用。简答的可以理解为发消息,如果 方法调用 之后出现了经典的错误,unrecognized selector sent to instance... 也可以从以下三个阶段进行分析。
1、消息发送;
1 接收者首先从接收者类的cache中查找方法
1.1 如果能找到方法,直接调用,结束
1.2 如果找不到方法,继续执行2
2 从接收者类的方法列表中查找
2.1 如果找到方法,调用并将方法添加到接收者类的cache中,结束
2.2 如果找不到方法,则从其superClass的cache中查找
递归2.1,直到最顶层类。
3 如果找不到方法,则判断走以下两个步骤
3.1 如果两个步骤均不涉及,则直接抛出异常 'unrecognized selector sent to instance'
详细步骤参照以下阶段
注:每个阶段结束会重新进入本阶段。
2、动态方法解析
针对未匹配的方法,我们可以通过 class_addMethod 给类添加新的方法和实现
重新进入消息发送阶段
3、消息转发或重新签名
如果在以上两个阶段均没有找到相关方法,此时就进入了消息转发阶段。消息转发主要有两个类别
直接转发
方法重签名,转发
消息直接转发
重写NSObject的 -(id)forwardingTargetForSelector:(SEL)aSelector方法
直接返回接收消息的对象实例。
方法重新签名
通过重写NSObject的方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
-
(NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector
在方法中,我们针对reSignature selector进行了重新签名
重写NSObject方法
-
(void)forwardInvocation:(NSInvocation *)anInvocation
方法中,对 reSignatureMethod selector的target进行了重新赋值
唤醒
进入方法发送阶段
8,能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
-
不能向编译后得到的类中增加实例变量;
分析:因为编译后的类已经注册在runtime中,类结构体中的objc_ivar_list 实例变量的链表和instance_size实例变量的内存大小已经确定,同时runtime 会调用class_setIvarLayout 或 class_setWeakIvarLayout来处理strong weak引用,所以不能向存在的类中添加实例变量。
-
能向运行时创建的类中添加实例变量;
运行时创建的类是可以添加实例变量,调用 class_addIvar函数,但是得在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上。
9,简述下Objective-C中调用方法的过程(runtime)
Objective-C是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector),整个过程介绍如下
- objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类
- 然后在该类中的方法列表以及其父类方法列表中寻找方法运行
- 如果,在最顶层的父类(一般也就NSObject)中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX
- 但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会,这三次拯救程序奔溃的说明
10,什么是method swizzling(俗称黑魔法)
简单说就是进行方法交换
在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。
-
每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP。
-
交换方法的几种实现方式
- 利用 method_exchangeImplementations 交换两个方法的实现
- 利用 class_replaceMethod 替换方法的实现
-
利用 method_setImplementation 来直接设置某个方法的IMP
11,对象如何找到对应的方法去调用
方法保存到什么地方?
对象方法保存到类中,类方法保存到元类(meta class),每一个类都有方法列表methodList。明确去哪个类中调用?
通过isa指针
1.根据对象的isa去对应的类查找方法,isa:判断去哪个类查找对应的方法 指向方法调用的类。
2.根据传入的方法编号SEL,里面有个哈希列表,在列表中找到对应方法Method(方法名)。
3.根据方法名(函数入口)找到函数实现,函数实现在方法区