iOS 面试题分析(1)
1. load
类方法与initialize
类方法调用流程分析
1.1 load
类方法调用流程分析
load类方法是在应用程序main函数运行之前执行调用的,也就是在应用程序的加载启动阶段,dyld加载应用程序时调用objc中load_images函数对所有非懒加载类以及主类的load类方法进行了调用执行,所以首先来看一下load_images
函数的代码,如下图所示:
如果当前镜像文件中存在非懒加载的主类或分类就会调用prepare_load_methods
函数(获取所有的非懒加载主类以及非懒加载分类),其代码如下图所示:
这个函数中的代码分为两部分,第一个部分,也就是红框1
中的代码,首先调用_getObjc2NonlazyClassList
函数获取镜像文件中所有非懒加载主类表,然后调用schedule_class_load
函数将此主类以及其对应load
类方法加载到表中,schedule_class_load
函数代码如下所示:
在这个函数中,首先判断cls
是否为空,为空直接返回,然后判断是否调用了load
类方法,如果调用过了,也会返回,接着将其父类作为参数递归调用schedule_class_load
函数,然后调动add_class_to_loadable_list
函数将这个类及其对应load
类方法都添加到loadable_classes
表中,也就是说父类中的load方法会先加入loadable_classes
中,其代码如下图所示:
loadable_classes
实际上是一个loadable_class
结构体类型数组指针,代码如下所示:
add_class_to_loadable_list
函数中的逻辑是这样的,首先调用getLoadMethod
遍历cls
的元类ro
中baseMethods
中所有的Methods
,如果找到某个Method
的Sel
的编号为load
,就将返回这个Method
的IMP
(也就是load
类方法的实现),否则返回nil
,代码如下图所示:
如果获取到了主类中load
类方法的Imp
,会判断loadable_classes
中的元素数量loadable_classes_used
是否等于其总容量loadable_classes_allocated
,如果等于就会调用realloc
函数对loadable_classes
进行(2 * loadable_classes_allocated + 16
)大小的扩容,然后将cls
以及对应load
类方法method
添加到loadable_classes
对应位置中。
然后就是prepare_load_methods
函数中红框2中的代码,首先获取所有非懒加载分类列表categorylist
,然后遍历其中所有的分类,先调用remapClass
函数判断这些分类中的所属类是否为空,为空跳过此次循环,如果不为空,就调用realizeClassWithoutSwift
函数以非懒加载的形式强制加载将这个分类所属主类的数据,然后调用add_category_to_loadable_list
函数将这个分类及其对应load
类方法添加到loadable_categories
表中,loadable_categories
数据结构与类型如下图所示:
add_category_to_loadable_list
代码如下所示:
可以看到这与前面添加主类及其load
类方法到loadable_classes
表中的逻辑如出一辙,只不过在分类这里是调用_category_getLoadMethod函数获取分类中classMethods
表中load
类方法的imp
,其代码如下图所示:
获取完所有非懒加载主类以及分类及其对应类方法后,在load_images
函数中就会调用call_load_methods
函数,调用这些类方法,其代码如下图所示:
可以看到,在这个函数中还是做了很多限制的,定义了一个静态局部变量loading
,确保这些主类以及分类的load
类方法之后执行一次,并且是在autoreleasePool
中进行调用的,在这个自动释放池中会判断loadable_classes_used
是否大于0
,如果不大于0
,就会一直调用call_class_loads
函数,其代码如下图所示:
可以看到在这个函数中,首先获取到loadable_classes
表的地址classes
,将静态全局变量loadable_classes
设置为nil
,然后将loadable_classes_allocated
、loadable_classes_used
都赋值为0,然后遍历之前classes
表中的元素,拿到其中每个分类及其load
类方法的实现,然后直接调用load
类方法的实现,传入cls
以及load
类方法的sel
作为参数,遍历完之后再对classes
进行销毁,这就是主类load类方法的调用流程。
而分类中类方法的调用流程与主类是类似的,也就是调用了call_category_loads
函数,其部分代码如下所示:
以上就是对load_images
函数的详细探究,但是你只需要知道大致流程就可以了,能简单口述即可,大致如下:
- 如果
ObjC
已经初始化完毕,dyld
调用过registerObjCNotifiers
函数并且所有分类数据还未加载,就调用loadAllCategories
函数加载所有分类数据。 - 然后存在非懒加载类或者非懒加载分类,首先调用
prepare_load_methods
函数分别将所有非懒加载主类及每个主类的父类中实现load
类方法的类及其load
类方法的sel
添加到loadable_classes
表中,然后将所有非懒加载分类及其load
类方法的实现添加到loadable_categories
表中。 - 调用
call_load_methods
函数,先遍历loadable_classes
表中每个loadable_class
结构体变量,调用执行每个类的load
类方法,然后销毁释放这个表,然后遍历loadable_categories
表中的每个loadable_category
结构体变量,调用执行每个分类的load
类方法,最后销毁释放这个表。
1.2 initialize类方法调用流程分析
initialize
类方法是在类第一次消息发送时才会调用执行的,主要是在方法慢速查找函数lookupImpOrForward
中进行的调用,其部分代码如下所示:
由上图代码可知,是在realizeAndInitializeIfNeeded_locked
函数中进行initialize
类方法的调用,其代码如下图所示:
其中:
又调用了initializeAndMaybeRelock
函数,其代码如下所示:
这个函数中的代码逻辑是这样的,首先调用这个类中的isInitialized
函数来判断这个类是否已初始化,如果调用过了,程序就不会继续向下执行了,也就是保证了类只有初次发送消息的时候才会调用其实现的或者是父类中实现的initialize
类方法,之后再发送消息时就不会再次调用initialize
类方法了,其代码如下所示:
接着会调用getMaybeUnrealizedNonMetaClass
函数获取一个可能没有实现的非元类,这行是为了保证当前传入的类不是一个元类,如果是一个元类的话,就获取它的非元类(必须要获取非元类的原因在于,initialize
类方法是存在元类的方法列表中的,你不能给你一个元类发送initialize
消息,这样会报错),但是获取到的这个非元类可能还没有实现,因此接着调用cls
的isRealized
做了一个判断,如果未实现,就调用realizeClassMaybeSwiftAndUnlock
函数,然后紧接着调用initializeNonMetaClass
函数对这个非元类的的initialize
类方法进行调用,其代码如下所示:
首先在这个函数中,会获取到当前类的父类,如果父类存在并且未调用过父类中的initialize
类方法,那么就会递归调用initializeNonMetaClass
函数,这样做的目的是为了保证,如果其父类中如果实现了initialize
类方法,那么优先调用父类中的initialize
类方法,紧接着先将本类的初始化状态改为已初始化,之后就会调用callInitalize
函数,而这个函数的代码如下所示:
这个函数中就调用了objc_msgSend
发送消息,消息接收者就是当前类,而方法编号是initialize
,就是调用执行类的元类方法列表中的initialize
方法。
那么你可能就会有一个疑惑,那就是如果本类及其父类中都没有实现initialize
类方法,那么在本类第一次发送消息时初始化的流程中是一定会调用objc_msgSend
发送initialize
消息的,那么在本类的元类以及本类的父类的方法列表中都找不到这个方法,那么不会报unrecognized selector
错误然后崩溃吗?是不会的,原因就在于如果你不在类中实现initialize
类方法,但是默认其根类NSObject
中是实现了initialize
类方法的,也就是说initialize
方法存在在根元类的方法列表中,无论NSObject
的子类实现不实现这个类方法,都不会报错。
2. 方法的调用顺序(load
类方法,initialize
类方法、主程序中c++
构造方法、主程序main函数)
而实际上类initialize
类方法的调用分为如下三种情况
- 父类以及子类都实现了
intialize
类方法。
父类中intialize
类方法先调用,子类中initialize
类方法后调用。
- 父类中实现了
intialize
类方法,子类中没有实现。
父类中intialize
类方法调用两次。
- 父类、子类、父类分类以及子类分类都实现了
intialize
类方法。
3. Runtime是什么?
Runtime
是有C
、C++
以及汇编实现的一套API
,为OC
语言实现面向对象以及运行时的功能,运行时是指将数据类型的确定由编译时推迟到了运行时(举例子:extention
与category
的区别),平时编写的OC
代码,在程序运行过程中,最终会转换成Runtime
的C
语言代码,Runtime
是Objective-C
的幕后工作者。
4. 方法调用的本质是什么?
方法的本质是发送消息,其流程如下:
-
方法快速查找,调用
objc_sendMsg
(底层是汇编),查找cache_t
中是否有对应sel
的imp
,找到就调用执行这个imp
,否则; -
方法慢速查找,调用
lookupImpOrForward
函数,递归使用二分查找算法查找自己以及父类中的methodsList
中是否存在sel
对应的imp
,找到添加到cache_t
缓存中,调用执行imp
,否则; -
方法动态决议,实例方法就向此类发送
resolveInstanceMethod
消息,如果类中有重写NSObject
的resolveInstanceMethod
类方法并做了相关逻辑处理,就执行调用,类方法就向此类发送resolveClassMethod
消息,如果类中有重写NSObject
的resolveClassMethod
类方法并做了相关逻辑处理,就执行调用,否则; -
方法快速转发,如果类中实现了
forwardingTargetForSelector
实例方法,并返回了一个能处理此消息的对象,就调用执行,否则; -
消息快速转发,如果类中实现了
methodSignatureForSelector
以及forwardInvocation
方法,并作了相关处理,就会先调用methodSignatureForSelector
方法,然后调用forwardInvocation
方法,否则; 6 再走一遍方法动态决议,如果还是未找到方法实现,就会抛出异常并崩溃。
5. SEL
是什么?IMP
是什么?两者是什么关系?
SEL
是方法编号,在read_images
期间就编译进了内存。
IMP
就是函数实现指针,找IMP
就是找函数的过程。
SEL
相当于一本书中目录的条目的标题,IMP
相当于目录条目中的页码,查找具体函数就相当于你想看这本书某个条目的内容,就根据条目中的标题找页码,找到页码之后就翻到具体页码阅读内容。
6. 能否向编译后的得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?
不能向编译后的得到的类中增加实例变量,只要类还没有注册到内存还是可以添加的,是因为编译好的实例变量存储的位置在ro
,一旦编译完成,内存结构就完全确定,就无法修改了。可以添加属性以及方法。
7. [self class]
和 [super class]
的区别以及原理分析
[self class]
就是调用objc_msgSend
函数发送消息,消息接收者是self
,方法编号是class
,self
是隐藏参数。
[super class]
本质就是调用objc_msgSendSuper2
发送消息,消息的接收者还是self
,方法编号是class
,super
是关键字。
编写如下代码:
@interface Person : NSObject
@end
@implementation Person
@end
@interface Student : Person
@end
@implementation Student
- (instancetype)init
{
self = [super init];
if (self) {
NSLog(@"self_class = %@", [self class]);
NSLog(@"super_class = %@", [super class]);
}
return self;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *s = [[Student alloc] init];
}
return 0;
}
然后将这个将这个main.m
文件编译为cpp
文件,Student
中init
函数编译源码如下所示:
可以看到的是super
关键字调用的方法实际上都是objc_msgSendSuper
函数的调用,但是查看运行时objc_msgSendSuper
函数调用的汇编代码时,会发现这个时候实际上是objc_msgSendSuper2
函数的调用,首先在Student中打上断点,连接真机,然后编译运行程序,等程序卡在断点,打印self、class以及superclass如下图所示:
然后添加一个objc_sendMsgSuper2
的符号断点,过掉init中的断点,来到objc_sendMsgSuper2
的汇编代码,打印寄存器x0
(也就是传入的参数一objc_super *
指针的值)中的值,然后打印这个内存地址中的值(也就是objc_super
中成员变量receiver
以及super_class
的值),如下图所示:
可以发现的是,其中super_class
按照编译时的情况来说应该是Person
类,但实际上却是Student
类,所以有时候运行时的代码情况与编译时是不一样的,接着我们再来看看objc_msgSendSuper2
源码,如下图所示:
我们可以看到函数中的注释,意思就是objc_msgSendSuper2
函数会获取当前搜索类,而不是它的父类。而objc_msgSendSuper
函数代码注释如下所示:
也就是说objc_msgSendSuper
函数会获取当前搜索类的超类,objc_msgSendSuper
底层其实也是使用汇编来实现的,arm64
汇编代码如下图所示:
这段汇编代码的意思就是将x0
寄存器中receiver
的值赋值给p0
,将x0
中super_class
(也就是receiver
的超类)的值赋值给p16
,然后跳转执行L_objc_msgSendSuper2_body
处的汇编代码,然后再来看看objc_msgSendSuper2
的汇编代码,如下所示:
这段汇编代码的意思就是将x0
中class
(也就是receiver
的类)的值赋值给x17
寄存器,将x0
寄存器中receiver
的值赋值给x0
寄存器,然后将x17
寄存器的值加上#SUPERCLASS
(值为0x8
)也就是其超类的地址然后赋值给x17
寄存器,然后取出x17
寄存器地址中的值也就是其superclass
的地址赋值给x16
寄存器,然后调用AuthISASuper
这个宏执行了一些操作,然后就会来到L_objc_msgSendSuper2_body
处的汇编代码,到这里,实际上与objc_msgSendSuper
调用逻辑是一致的,然后在来看看CacheLookup
处的汇编代码执行方法快速查找流程,如果找不到方法,就会调用,如下所示:
根据真机(iOS
是小端)的架构,将会执行上图红框中所示的汇编代码:
- 红框1:将
x16
寄存器中超类(也就是Person
)的地址赋值给x15
寄存器。 - 红框2:将
x16
寄存器的值加上16
字节(得到Person
(objc_class
类型)的cache
的地址),然后将cache
中的值(也就是cache_t
结构体中第一个成员变量的值_bucketsAndMaybeMask
)赋值给p11
寄存器。 - 红框3:比较
p11
的第0
位,如果不为0
,就执行LLookupPreopt
处的汇编代码,否则,取出p11
的前48
位的值(也就是buckets
的地址)赋值给p10
。 - 红框4:
eor
汇编指令就是将(_cmd) ^ (p1 >> 7)
之后的值赋值给p12
,and
汇编指令是将p12 & (p11 >> 48)
的值赋值给p12
(也就是计算_cmd
在buckets
中的索引),这个与cache
中用来计算sel
在buckets
中索引的哈希函数算法一致,如下图所示:
上图红框中的代码:
- 红框1:
p12
是_cmd
通过哈希函数映射在buckets
中的索引值,由于buckets
是指向存放结构体bucket_t
(成员变量为SEL
以及IMP
,共16
字节)变量的数组首地址,因此索引值右移16
位加上buckets
首地址就能获取到索引位置所对应bucket
。 - 红框2:取出索引所对应
bucket
中的SEL
、IMP
到p17
以及p9
寄存器中,之后x13
寄存器的值减去16
字节大小,向前获取上一个bucket
的地址,然后比较p9
与_cmd
的值 - 红框3:如果
p9
与_cmd
相等,缓存命中,执行这个imp
。 - 红框4:如果
p9
为空,则执行__objc_msgSend_uncached
中的汇编代码。 - 红框5:如果
buckets
从计算的索引位置到buckets
首地址都没有找到匹配的sel
,就跳出循环。
遍历完左部分之后,还要对其右边剩余部分的buckets
进行遍历,逻辑与上图是一致的,这里就不过多阐述了,代码如下所示:
当方法快速查找流程未找到时,就会__objc_msgSend_uncached
中的汇编代码,如下图示所示:
然后执行了MethodTableLookup
中的汇编代码如下图所示:
- 红框1:将
x16
寄存器的值移动到x2
寄存器中,注意此时x16
寄存器存储的是超类Person
的地址。 - 红框2:跳转执行·lookupImpOrForward·函数中的代码,执行方法慢速查找流程。
因此当调用lookupImpOrForward
函数时,其参数三cls
就是Person
类,因此会从Person
类的方法列表中查找实例方法init
以及class
,但是x0
的值就是消息接收者Student
类的实例对象,也就是其参数一inst
的值。
- 问题1:既然
objc_msgSendSuper2
的参数一(objc_super
类型)中成员变量super_class
是Student,为何Student
类的init
方法中self = [super init];
这行代码不会产生循环调用?
因为在objc_msgSendSuper2
底层汇编中,是不会从其参数一中成员变量super_class
(Student
)查找方法的,而是获取super_class
的超类(Person
),从这个超类(Person
)中查找方法,快速查找如果找不到,就会调用lookupImpOrForward
这个函数,参数一inst
的值是Student
类实例对象,参数三cls
就是Person
超类,因此在Student
的init
方法中self = [super init];
这行代码并不会产生循环调用,写成self = [self init];
才会产生循环调用,因为[self init]
将会调用objc_msgSend
函数发送消息,在objc_msgSend
的汇编代码中会从Student
类的方法列表中查找init
方法。
- 问题2:为什么在
Student
的构造方法中打印[self class]与[super class]的值相等呢?都是Student
。
因为[self class]是使用objc_msgSend
发送消息,消息接收者是Student
实例对象self
,SEL
的编号是class
,因此查找方法时是从Student
类的方法列表中查找到class
方法的,当在底层调用class
这个方法的API
时,实际上在运行时是调用的是objc_opt_class
这个函数,其代码如下所示:
当运行到这个函数时,obj
实际上就是Student
实例对象,获取Student
实例对象的Class
得到的就是Student
类。
而[super class]是调用objc_msgSendSuper2
发送消息,消息接收者仍然是Student
实例对象self
,SEL
的编号是class
,使用objc_msgSendSuper2
查找class
方法时,查找的是Student
的超类Person
的方法列表中的class
方法,如果Person
类的方法列表中不存在class
方法,就会查找Person
类的超类也就是NSObject
方法列表中的class
方法,当调用NSObject
方法列表中的class
方法时,实际上执行的是runtime
底层API
中object_getClass
函数的代码,如下图所示:
其中self
就是Student
类的实例对象,而调用object_getClass
函数时,obj
就是Student
类的实例对象,因此获取Student
类的实例对象的isa
得到的肯定就是Student
类了。
8. 分析以下代码,说说NSLog函数打印结果是什么,为什么?
//Person.h文件中代码
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickname;
@property (nonatomic, copy) NSString *hobby;
- (void)sayHi;
@end
//Person.m文件中代码
#import "Person.h"
@implementation Person
- (void)sayHi {
NSLog(@"%s --- %@", __func__, self.hobby);
}
@end
//ViewController.m文件中代码
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
p.hobby = @"play game";
p.name = @"xxx";
p.nickname = @"szx";
[p sayHi];
Class pCls = [Person class];
void *ptr = (void *)&pCls;
[(__bridge id)ptr sayHi];
}
@end
打印结果如下图所示:
首先,根据运行过程中查看堆栈各个变量的内存地址以及存储值,画出了如下的内存结构图。
结构图中绿色区域就是ViewDidLoad
方法函数调用栈内存数据示意图,栈顶指针是在高位,栈底指针是在低位的,在ViewDidLoad
方法的调用执行的过程中,先将ViewDidLoad
方法中两个隐藏参数self
以及_cmd
(大小均为8字节)从高地址向低地址入栈,然后中间创建了一个objc_super
结构体类型的变量,之所以产生这个变量是因为[super ViewDidLoad]
这行代码的调用,上面我们已经探讨过了super
这个关键字的作用,就是会调用objc_sendMsgSuper2
这个函数发送消息,这个函数需要两个参数,一个就是指向objc_super
结构体的指针,因此需要创建一个objc_super
结构体类型的局部变量(recevier
存储self
的值,super_class
存储ViewController
类的值,也就是self
指针指向地址中isa
的值),但是你可能会很疑惑,因为其第二个SEL
类型的参数难道就不需要创建一个局部变量来存储吗?其实第二个SEL
参数是通过调用sel_registerName(const char * _Nonnull str)
(这个函数的参数是一个字符串常量,存储在常量区)函数来创建的,这个函数的返回值并没有使用一个局部变量来接收,而是直接将sel_registerName
函数的调用作为参数二传入了objc_sendMsgSuper2
函数中,而在一个函数的调用过程中只会将其参数、局部变量压入栈中,因此下面的几个局部变量p
、pCls
以及ptr
也会依次由高地址到低地址压入栈中。
Person类的实例对象p
在经过底层代码编译后得到的是一个(struct Person_IMPL
类型)结构体指针,在p
这个结构体指针指向的堆内存空间中会(地址从低到高)依次存储成员变量isa
、name
、nickname
以及hobby
的值,如上图中淡兰色存储结构图所示,因此当p
对象调用其sayHi
实例方法访问其成员变量hobby
的值时,其实是调用了objc_sendMsg
函数发送消息,消息接收者是p
对象,而方法编号就是hobby
,从p
对象中获取其类Person
(也就是isa
)的地址,然后会进行方法查找,查找Person
类的方法列表中的hobby
方法,然后进行调用,而经过底层编译之后,hobby
方法实际上是通过内存平移的方式访问到p
对象中成员变量hobby
的值的,由p
指针的值加上24
字节得到,因此当通过ptr
这个指针调用sayHi
方法时,会将ptr这个指针当做一个实例对象,同样的调用了objc_sendMsg
函数来发送消息,此时消息接收者就是ptr
指针,方法编号就是sayHi
,然后在objc_sendMsg
函数中(实际上是由汇编代码写成),会将ptr
当做实例对象,也就是将ptr
当做一个指向类对象的指针,而恰好此时ptr
指针指向的是局部变量pCls
的地址,而pCls这个局部变量存储的值正好是Person
类对象,因此会从Person
类的方法列表查找sayHi
方法,查找之后就会进行调用,然后再调用objc_sendMsg
函数发送消息,消息接收者依旧是ptr
,方法编号是hobby
,因此在调用hobby
方法的IMP
函数实现时,依旧会对ptr
的值进行内存平移试图得到其成员变量hobby
的值,最后得到的却是(0x7ffeeed750e0 + 0x18
)0x7ffeeed750f8
这个地址,也就是super_class
(ViewController
类),当打印这个地址中的值时,就会输出ViewController
这个字符串。
9. methodSwizzling坑,查看如下iOS工程中的部分代码,编译运行程序,会出现怎样的情况,该如何解决?
//Person.h文件代码
@interface Person : NSObject
- (void)personInstanceMethod;
@end
//Person.m文件代码
#import "Person.h"
@implementation Person
- (void)personInstanceMethod {
NSLog(@"Person实例方法: %s", __func__);
}
@end
//Student.h文件代码
#import "Person.h"
@interface Student : Person
@end
//Student.m文件代码
#import "Student.h"
@implementation Student
@end
//Student+CA.h文件代码
#import "Student.h"
@interface Student (CA)
- (void)categorySwizzlingMethod;
@end
//Student+CA.m文件代码
#import "Student+CA.h"
#import "RuntimeTool.h"
@implementation Student (CA)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[RuntimeTool methodSwizzlingWithClass:[self class] oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(categorySwizzlingMethod)];
});
}
- (void)categorySwizzlingMethod {
[self categorySwizzlingMethod];
NSLog(@"Student分类实例方法: %s", __func__);
}
@end
//RuntimeTool.h文件代码
#import <Foundation/Foundation.h>
@interface RuntimeTool : NSObject
/// 交换方法
/// @param cls 交换对象
/// @param oriSEL 原始方法编号
/// @param swizzledSEL 交换的方法编号
+ (void)methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL;
@end
//RuntimeTool.m文件代码
#import "RuntimeTool.h"
#import <objc/message.h>
@implementation RuntimeTool
+ (void)methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {
if (!cls) {
NSLog(@"传入的交换类不能为空!");
return;
}
Method m1 = class_getInstanceMethod(cls, oriSEL);
Method m2 = class_getInstanceMethod(cls, swizzledSEL);
method_exchangeImplementations(m1, m2);
}
@end
//ViewController.m文件代码
#import "ViewController.h"
#import "Person.h"
#import "Student.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Student *s = [[Student alloc] init];
[s personInstanceMethod];
Person *p = [[Person alloc] init];
[p personInstanceMethod];
}
@end
首先,Student
对象s
调用personInstanceMethod
方法打印输出信息是正常的,但是Person
对象p
调用PersonInstanMethod
方法会报错,如下图所示:
原因就在于在Student
类的load
类方法中调用RuntimeTool
这个工具类methodSwizzlingWithClass
方法所传递的oriSEL
通过runtime
的API
class_getInstanceMethod
获取到的m1
实际上是父类Person
中方法列表中的方法,因为Student
类中并没有重写父类的personInstanceMethod
方法,而class_getInstanceMethod
大致逻辑就是先去Student
类中查找personInstanceMethod
方法,如果找不到就递归查找父类的方法列表中是否存在personInstanceMethod
,如果找到就直接返回了,因此在调用runtime
API
method_exchangeImplementations
交换方法实现时,实际上交换的是父类中personInstanceMethod
的实现与Student
分类中methodSwizzlingWithClass
方法的实现,因此当p
对象调用自身的personInstanceMethod
方法时就会调用执行Student
分类中methodSwizzlingWithClass
方法的实现,但是在调用Student
分类中methodSwizzlingWithClass
方法的实现中又调用了[self categorySwizzlingMethod];
这行代码,此时self
实际上是Person
对象p
,因此调用objc_msgSend
函数发送消息,消息接收者是p
,方法编号是categorySwizzlingMethod
,因此会去Person
类中查找categorySwizzlingMethod
方法,但实际上categorySwizzlingMethod
是Person
子类Student
分类中实现的方法,因此在Person
类的方法列表以及其父类的方法列表中都无法找到categorySwizzlingMethod
方法,因此会出现上述报错。
那么该如何解决这个问题呢?从什么样的出发点来解决呢?首先我们应该考虑修改哪里的代码,以上的问题的根源就在于本来想修改Student
类,但是却修改了其父类Person
的方法personInstanceMethod
的IMP
指向,这样的后果就是,本来正常使用的Person
类的方法personInstanceMethod
现在使用会报错,并且Person
类的其他子类在调用personInstanceMethod
方法时,也会出现同样的报错,并且开发人员在遇到这些错误的时候可能会一头雾水,并不知道错误产生的原因,排查就会很困难,因此我们应该保证Student
这个分类中在交换方法的时候,交换IMP
实现的两个方法都在Student
的方法列表中,因此在获取到m1
、m2
后,再尝试调用runtimeAPI
class_addMethod
在Student
类中添加方法编号为oriSEL
,实现为m2
方法实现的方法,如果能添加成功,则说明Student
类中原来没有m1
方法,m1
方法可能是其父类中的方法,那么就直接替换swizzledSEL
方法编号的实现为m1
方法的实现,如果添加成功,则说明m1
方法在Student
类的方法列表中,就直接交换方法实现,修改后methodSwizzlingWithClass
方法代码如下所示:
+ (void)methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {
if (!cls) {
NSLog(@"传入的交换类不能为空!");
return;
}
Method m1 = class_getInstanceMethod(cls, oriSEL);
Method m2 = class_getInstanceMethod(cls, swizzledSEL);
bool isAddMethodSuccess = class_addMethod(cls, oriSEL, method_getImplementation(m2), method_getTypeEncoding(m2));
if (isAddMethodSuccess) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(m1), method_getTypeEncoding(m1));
} else {
method_exchangeImplementations(m1, m2);
}
}
但是这样修改之后,还会出现一个问题,如果获取到的m1
方法为空,那么swizlledSEL
的IMP
也会被替换成空,调用也会出现错误,例如,我们将Person
类中的personInstanceMethod
方法声明以及实现都注释掉,在ViewController
的ViewDidLoad
编写如下代码:
- (void)viewDidLoad {
[super viewDidLoad];
Student *s = [[Student alloc] init];
[s categorySwizzlingMethod];
}
编译运行程序,会出现如下图所示的报错:
也就是产生了categorySwizzlingMethod
方法的递归调用,产生这个错误的原因在于Runtime API
class_replaceMethod
这个函数,这个函数会根据传入的SEL
在类的方法列表中查找对应的Method
,如果没有查找到对应的Method
,则会判断传入的IMP
是否为空,为空就直接返回,如果传入的IMP
不为空就会将Method
中的IMP
替换为传入的IMP
,如果查找不到对应的Method
,就会创建一个method_list_t
的方法列表newlist
,newlist
的长度为1
,将传入的参数SEL
、IMP
(不管这个IMP
是否为空)以及types
的值设置到newlist
列表中首个Method
中,然后对newlist
这个列表进行排序,最后附着插入到类rew
的methods
中第一个位置。而此时,在methodSwizzlingWithClass
这个交换方法中获取到的m1
的IMP
为空,因此在调用class_replaceMethod
时,虽然Student
分类中存在categorySwizzlingMethod
方法,但是传入的IMP
为空,因此并没有改变categorySwizzlingMethod
方法IMP
的指向,因此产生了死递归。
因此跟完善的做法是,在methodSwizzlingWithClass
函数中,还应该对m1
进行一次判断,如果m1
为空,则在此类中添加一个方法编号为oriSEL
、IMP
为m2
的IMP
的Method
,然后为m1
添加一个默认的IMP
,这样处理的好处是不仅可以处理掉上述的错误,而且开发者在别处调用orlSEL
时不会出现找不到方法的报错,完善代码如下所示:
+ (void)methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {
if (!cls) {
NSLog(@"传入的交换类不能为空!");
return;
}
Method m1 = class_getInstanceMethod(cls, oriSEL);
Method m2 = class_getInstanceMethod(cls, swizzledSEL);
if(!m1) {
//m1为空
//为cls添加一个oriSEL编号的方法,IMP为m2的IMP,外界就可以调用oriSEL编号的方法,并且执行m2的IMP。
class_addMethod(cls, oriSEL, method_getImplementation(m2), method_getTypeEncoding(m2));
//为m1添加一个默认的IMP,以便能够与m2成功交换IMP,防止m2的IMP实现中调用自身而出现死递归
method_setImplementation(m1, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"交换的原方法为空!");
}));
}
//上面的代码虽然保证m1不为空,但是不能保证m1一定存在在cls中方法列表中,因此还需要添加一下编号为oriSEL的方法,添加成功,则说明m1可能是cls父类中的方法,添加失败,则说明m1一定存在于cls的方法列表中.
bool isAddMethodSuccess = class_addMethod(cls, oriSEL, method_getImplementation(m2), method_getTypeEncoding(m2));
if (isAddMethodSuccess) {
//如果m1不在cls的方法列表中,就替换调用方法编号为swizzledSEL的方法的实现为m1方法的实现。
class_replaceMethod(cls, swizzledSEL, method_getImplementation(m1), method_getTypeEncoding(m1));
} else {
//如果m1在cls的方法列表中,则直接交换m1于m2的imp
method_exchangeImplementations(m1, m2);
}
}
转载自:https://juejin.cn/post/6992870278988595208