likes
comments
collection
share

iOS 面试题分析(1)

作者站长头像
站长
· 阅读数 36

1. load类方法与initialize类方法调用流程分析

1.1 load类方法调用流程分析

  load类方法是在应用程序main函数运行之前执行调用的,也就是在应用程序的加载启动阶段,dyld加载应用程序时调用objc中load_images函数对所有非懒加载类以及主类的load类方法进行了调用执行,所以首先来看一下load_images函数的代码,如下图所示:

iOS 面试题分析(1)

iOS 面试题分析(1)

  如果当前镜像文件中存在非懒加载的主类或分类就会调用prepare_load_methods函数(获取所有的非懒加载主类以及非懒加载分类),其代码如下图所示:

iOS 面试题分析(1)

  这个函数中的代码分为两部分,第一个部分,也就是红框1中的代码,首先调用_getObjc2NonlazyClassList函数获取镜像文件中所有非懒加载主类表,然后调用schedule_class_load函数将此主类以及其对应load类方法加载到表中,schedule_class_load函数代码如下所示:

iOS 面试题分析(1)

  在这个函数中,首先判断cls是否为空,为空直接返回,然后判断是否调用了load类方法,如果调用过了,也会返回,接着将其父类作为参数递归调用schedule_class_load函数,然后调动add_class_to_loadable_list函数将这个类及其对应load类方法都添加到loadable_classes表中,也就是说父类中的load方法会先加入loadable_classes中,其代码如下图所示:

iOS 面试题分析(1)

  loadable_classes实际上是一个loadable_class结构体类型数组指针,代码如下所示:

iOS 面试题分析(1)

iOS 面试题分析(1)

  add_class_to_loadable_list函数中的逻辑是这样的,首先调用getLoadMethod遍历cls的元类robaseMethods中所有的Methods,如果找到某个MethodSel的编号为load,就将返回这个MethodIMP(也就是load类方法的实现),否则返回nil,代码如下图所示:

iOS 面试题分析(1)

  如果获取到了主类中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数据结构与类型如下图所示:

iOS 面试题分析(1)

iOS 面试题分析(1)

  add_category_to_loadable_list代码如下所示:

iOS 面试题分析(1)

  可以看到这与前面添加主类及其load类方法到loadable_classes表中的逻辑如出一辙,只不过在分类这里是调用_category_getLoadMethod函数获取分类中classMethods表中load类方法的imp,其代码如下图所示:

iOS 面试题分析(1)

  获取完所有非懒加载主类以及分类及其对应类方法后,在load_images函数中就会调用call_load_methods函数,调用这些类方法,其代码如下图所示:

iOS 面试题分析(1)

  可以看到,在这个函数中还是做了很多限制的,定义了一个静态局部变量loading,确保这些主类以及分类的load类方法之后执行一次,并且是在autoreleasePool中进行调用的,在这个自动释放池中会判断loadable_classes_used是否大于0,如果不大于0,就会一直调用call_class_loads函数,其代码如下图所示:

iOS 面试题分析(1)

  可以看到在这个函数中,首先获取到loadable_classes表的地址classes,将静态全局变量loadable_classes设置为nil,然后将loadable_classes_allocated、loadable_classes_used都赋值为0,然后遍历之前classes表中的元素,拿到其中每个分类及其load类方法的实现,然后直接调用load类方法的实现,传入cls以及load类方法的sel作为参数,遍历完之后再对classes进行销毁,这就是主类load类方法的调用流程。

  而分类中类方法的调用流程与主类是类似的,也就是调用了call_category_loads函数,其部分代码如下所示:

iOS 面试题分析(1)

  以上就是对load_images函数的详细探究,但是你只需要知道大致流程就可以了,能简单口述即可,大致如下:

  1. 如果ObjC已经初始化完毕,dyld调用过registerObjCNotifiers函数并且所有分类数据还未加载,就调用loadAllCategories函数加载所有分类数据。
  2. 然后存在非懒加载类或者非懒加载分类,首先调用prepare_load_methods函数分别将所有非懒加载主类及每个主类的父类中实现load类方法的类及其load类方法的sel添加到loadable_classes表中,然后将所有非懒加载分类及其load类方法的实现添加到loadable_categories表中。
  3. 调用call_load_methods函数,先遍历loadable_classes表中每个loadable_class结构体变量,调用执行每个类的load类方法,然后销毁释放这个表,然后遍历loadable_categories表中的每个loadable_category结构体变量,调用执行每个分类的load类方法,最后销毁释放这个表。

1.2 initialize类方法调用流程分析

  initialize类方法是在类第一次消息发送时才会调用执行的,主要是在方法慢速查找函数lookupImpOrForward中进行的调用,其部分代码如下所示:

iOS 面试题分析(1)

  由上图代码可知,是在realizeAndInitializeIfNeeded_locked函数中进行initialize类方法的调用,其代码如下图所示:

iOS 面试题分析(1)

  其中:

iOS 面试题分析(1)

  又调用了initializeAndMaybeRelock函数,其代码如下所示:

iOS 面试题分析(1)

  这个函数中的代码逻辑是这样的,首先调用这个类中的isInitialized函数来判断这个类是否已初始化,如果调用过了,程序就不会继续向下执行了,也就是保证了类只有初次发送消息的时候才会调用其实现的或者是父类中实现的initialize类方法,之后再发送消息时就不会再次调用initialize类方法了,其代码如下所示:

iOS 面试题分析(1)

  接着会调用getMaybeUnrealizedNonMetaClass函数获取一个可能没有实现的非元类,这行是为了保证当前传入的类不是一个元类,如果是一个元类的话,就获取它的非元类(必须要获取非元类的原因在于,initialize类方法是存在元类的方法列表中的,你不能给你一个元类发送initialize消息,这样会报错),但是获取到的这个非元类可能还没有实现,因此接着调用clsisRealized做了一个判断,如果未实现,就调用realizeClassMaybeSwiftAndUnlock函数,然后紧接着调用initializeNonMetaClass函数对这个非元类的的initialize类方法进行调用,其代码如下所示:

iOS 面试题分析(1)

  首先在这个函数中,会获取到当前类的父类,如果父类存在并且未调用过父类中的initialize类方法,那么就会递归调用initializeNonMetaClass函数,这样做的目的是为了保证,如果其父类中如果实现了initialize类方法,那么优先调用父类中的initialize类方法,紧接着先将本类的初始化状态改为已初始化,之后就会调用callInitalize函数,而这个函数的代码如下所示:

iOS 面试题分析(1)

  这个函数中就调用了objc_msgSend发送消息,消息接收者就是当前类,而方法编号是initialize,就是调用执行类的元类方法列表中的initialize方法。

  那么你可能就会有一个疑惑,那就是如果本类及其父类中都没有实现initialize类方法,那么在本类第一次发送消息时初始化的流程中是一定会调用objc_msgSend发送initialize消息的,那么在本类的元类以及本类的父类的方法列表中都找不到这个方法,那么不会报unrecognized selector错误然后崩溃吗?是不会的,原因就在于如果你不在类中实现initialize类方法,但是默认其根类NSObject中是实现了initialize类方法的,也就是说initialize方法存在在根元类的方法列表中,无论NSObject的子类实现不实现这个类方法,都不会报错。

2. 方法的调用顺序(load类方法,initialize类方法、主程序中c++构造方法、主程序main函数)

  而实际上类initialize类方法的调用分为如下三种情况

  1. 父类以及子类都实现了intialize类方法。

  父类中intialize类方法先调用,子类中initialize类方法后调用。

  1. 父类中实现了intialize类方法,子类中没有实现。

  父类中intialize类方法调用两次。

  1. 父类、子类、父类分类以及子类分类都实现了intialize类方法。

3. Runtime是什么?

  Runtime是有CC++以及汇编实现的一套API,为OC语言实现面向对象以及运行时的功能,运行时是指将数据类型的确定由编译时推迟到了运行时(举例子:extentioncategory的区别),平时编写的OC代码,在程序运行过程中,最终会转换成RuntimeC语言代码,RuntimeObjective-C的幕后工作者。

4. 方法调用的本质是什么?

  方法的本质是发送消息,其流程如下:

  1. 方法快速查找,调用objc_sendMsg(底层是汇编),查找cache_t中是否有对应selimp,找到就调用执行这个imp,否则;

  2. 方法慢速查找,调用lookupImpOrForward函数,递归使用二分查找算法查找自己以及父类中的methodsList中是否存在sel对应的imp,找到添加到cache_t缓存中,调用执行imp,否则;

  3. 方法动态决议,实例方法就向此类发送resolveInstanceMethod消息,如果类中有重写NSObjectresolveInstanceMethod类方法并做了相关逻辑处理,就执行调用,类方法就向此类发送resolveClassMethod消息,如果类中有重写NSObjectresolveClassMethod类方法并做了相关逻辑处理,就执行调用,否则;

  4. 方法快速转发,如果类中实现了forwardingTargetForSelector实例方法,并返回了一个能处理此消息的对象,就调用执行,否则;

  5. 消息快速转发,如果类中实现了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,方法编号是classself是隐藏参数。

  [super class]本质就是调用objc_msgSendSuper2发送消息,消息的接收者还是self,方法编号是classsuper是关键字。

  编写如下代码:

@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文件,Studentinit函数编译源码如下所示:

iOS 面试题分析(1)

  可以看到的是super关键字调用的方法实际上都是objc_msgSendSuper函数的调用,但是查看运行时objc_msgSendSuper函数调用的汇编代码时,会发现这个时候实际上是objc_msgSendSuper2函数的调用,首先在Student中打上断点,连接真机,然后编译运行程序,等程序卡在断点,打印self、class以及superclass如下图所示:

iOS 面试题分析(1)

  然后添加一个objc_sendMsgSuper2的符号断点,过掉init中的断点,来到objc_sendMsgSuper2的汇编代码,打印寄存器x0(也就是传入的参数一objc_super *指针的值)中的值,然后打印这个内存地址中的值(也就是objc_super中成员变量receiver以及super_class的值),如下图所示:

iOS 面试题分析(1)

  可以发现的是,其中super_class按照编译时的情况来说应该是Person类,但实际上却是Student类,所以有时候运行时的代码情况与编译时是不一样的,接着我们再来看看objc_msgSendSuper2源码,如下图所示:

iOS 面试题分析(1)

  我们可以看到函数中的注释,意思就是objc_msgSendSuper2函数会获取当前搜索类,而不是它的父类。而objc_msgSendSuper函数代码注释如下所示:

iOS 面试题分析(1)

  也就是说objc_msgSendSuper函数会获取当前搜索类的超类,objc_msgSendSuper底层其实也是使用汇编来实现的,arm64汇编代码如下图所示:

iOS 面试题分析(1)

  这段汇编代码的意思就是将x0寄存器中receiver的值赋值给p0,将x0super_class(也就是receiver的超类)的值赋值给p16,然后跳转执行L_objc_msgSendSuper2_body处的汇编代码,然后再来看看objc_msgSendSuper2的汇编代码,如下所示:

iOS 面试题分析(1)

  这段汇编代码的意思就是将x0class(也就是receiver的类)的值赋值给x17寄存器,将x0寄存器中receiver的值赋值给x0寄存器,然后将x17寄存器的值加上#SUPERCLASS(值为0x8)也就是其超类的地址然后赋值给x17寄存器,然后取出x17寄存器地址中的值也就是其superclass的地址赋值给x16寄存器,然后调用AuthISASuper这个宏执行了一些操作,然后就会来到L_objc_msgSendSuper2_body处的汇编代码,到这里,实际上与objc_msgSendSuper调用逻辑是一致的,然后在来看看CacheLookup处的汇编代码执行方法快速查找流程,如果找不到方法,就会调用,如下所示:

iOS 面试题分析(1)

  根据真机(iOS是小端)的架构,将会执行上图红框中所示的汇编代码:

  • 红框1:将x16寄存器中超类(也就是Person)的地址赋值给x15寄存器。
  • 红框2:将x16寄存器的值加上16字节(得到Personobjc_class类型)的cache的地址),然后将cache中的值(也就是cache_t结构体中第一个成员变量的值_bucketsAndMaybeMask)赋值给p11寄存器。
  • 红框3:比较p11的第0位,如果不为0,就执行LLookupPreopt处的汇编代码,否则,取出p11的前48位的值(也就是buckets的地址)赋值给p10
  • 红框4:eor汇编指令就是将(_cmd) ^ (p1 >> 7)之后的值赋值给p12and汇编指令是将p12 & (p11 >> 48)的值赋值给p12(也就是计算_cmdbuckets中的索引),这个与cache中用来计算selbuckets中索引的哈希函数算法一致,如下图所示:

iOS 面试题分析(1)

iOS 面试题分析(1)

  上图红框中的代码:

  • 红框1:p12_cmd通过哈希函数映射在buckets中的索引值,由于buckets是指向存放结构体bucket_t(成员变量为SEL以及IMP,共16字节)变量的数组首地址,因此索引值右移16位加上buckets首地址就能获取到索引位置所对应bucket
  • 红框2:取出索引所对应bucket中的SELIMPp17以及p9寄存器中,之后x13寄存器的值减去16字节大小,向前获取上一个bucket的地址,然后比较p9_cmd的值
  • 红框3:如果p9_cmd相等,缓存命中,执行这个imp
  • 红框4:如果p9为空,则执行__objc_msgSend_uncached中的汇编代码。
  • 红框5:如果buckets从计算的索引位置到buckets首地址都没有找到匹配的sel,就跳出循环。

  遍历完左部分之后,还要对其右边剩余部分的buckets进行遍历,逻辑与上图是一致的,这里就不过多阐述了,代码如下所示:

iOS 面试题分析(1)

  当方法快速查找流程未找到时,就会__objc_msgSend_uncached中的汇编代码,如下图示所示:

iOS 面试题分析(1)

  然后执行了MethodTableLookup中的汇编代码如下图所示:

iOS 面试题分析(1)

  • 红框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_classStudent)查找方法的,而是获取super_class的超类(Person),从这个超类(Person)中查找方法,快速查找如果找不到,就会调用lookupImpOrForward这个函数,参数一inst的值是Student类实例对象,参数三cls就是Person超类,因此在Studentinit方法中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实例对象selfSEL的编号是class,因此查找方法时是从Student类的方法列表中查找到class方法的,当在底层调用class这个方法的API时,实际上在运行时是调用的是objc_opt_class这个函数,其代码如下所示:

iOS 面试题分析(1)

  当运行到这个函数时,obj实际上就是Student实例对象,获取Student实例对象的Class得到的就是Student类。

  而[super class]是调用objc_msgSendSuper2发送消息,消息接收者仍然是Student实例对象selfSEL的编号是class,使用objc_msgSendSuper2查找class方法时,查找的是Student的超类Person的方法列表中的class方法,如果Person类的方法列表中不存在class方法,就会查找Person类的超类也就是NSObject方法列表中的class方法,当调用NSObject方法列表中的class方法时,实际上执行的是runtime底层API中object_getClass函数的代码,如下图所示:

iOS 面试题分析(1)

iOS 面试题分析(1)

  其中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

  打印结果如下图所示:

iOS 面试题分析(1)

  首先,根据运行过程中查看堆栈各个变量的内存地址以及存储值,画出了如下的内存结构图。

iOS 面试题分析(1)

  结构图中绿色区域就是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函数中,而在一个函数的调用过程中只会将其参数、局部变量压入栈中,因此下面的几个局部变量ppCls以及ptr也会依次由高地址到低地址压入栈中。

  Person类的实例对象p在经过底层代码编译后得到的是一个(struct Person_IMPL类型)结构体指针,在p这个结构体指针指向的堆内存空间中会(地址从低到高)依次存储成员变量isanamenickname以及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 + 0x180x7ffeeed750f8这个地址,也就是super_classViewController类),当打印这个地址中的值时,就会输出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方法会报错,如下图所示:

iOS 面试题分析(1)

  原因就在于在Student类的load类方法中调用RuntimeTool这个工具类methodSwizzlingWithClass方法所传递的oriSEL通过runtimeAPI 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方法,但实际上categorySwizzlingMethodPerson子类Student分类中实现的方法,因此在Person类的方法列表以及其父类的方法列表中都无法找到categorySwizzlingMethod方法,因此会出现上述报错。

  那么该如何解决这个问题呢?从什么样的出发点来解决呢?首先我们应该考虑修改哪里的代码,以上的问题的根源就在于本来想修改Student类,但是却修改了其父类Person的方法personInstanceMethodIMP指向,这样的后果就是,本来正常使用的Person类的方法personInstanceMethod现在使用会报错,并且Person类的其他子类在调用personInstanceMethod方法时,也会出现同样的报错,并且开发人员在遇到这些错误的时候可能会一头雾水,并不知道错误产生的原因,排查就会很困难,因此我们应该保证Student这个分类中在交换方法的时候,交换IMP实现的两个方法都在Student的方法列表中,因此在获取到m1m2后,再尝试调用runtimeAPI class_addMethodStudent类中添加方法编号为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方法为空,那么swizlledSELIMP也会被替换成空,调用也会出现错误,例如,我们将Person类中的personInstanceMethod方法声明以及实现都注释掉,在ViewControllerViewDidLoad编写如下代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Student *s = [[Student alloc] init];

    [s categorySwizzlingMethod];
    
}

  编译运行程序,会出现如下图所示的报错:

iOS 面试题分析(1)

  也就是产生了categorySwizzlingMethod方法的递归调用,产生这个错误的原因在于Runtime API class_replaceMethod这个函数,这个函数会根据传入的SEL在类的方法列表中查找对应的Method,如果没有查找到对应的Method,则会判断传入的IMP是否为空,为空就直接返回,如果传入的IMP不为空就会将Method中的IMP替换为传入的IMP,如果查找不到对应的Method,就会创建一个method_list_t的方法列表newlistnewlist的长度为1,将传入的参数SELIMP(不管这个IMP是否为空)以及types的值设置到newlist列表中首个Method中,然后对newlist这个列表进行排序,最后附着插入到类rewmethods中第一个位置。而此时,在methodSwizzlingWithClass这个交换方法中获取到的m1IMP为空,因此在调用class_replaceMethod时,虽然Student分类中存在categorySwizzlingMethod方法,但是传入的IMP为空,因此并没有改变categorySwizzlingMethod方法IMP的指向,因此产生了死递归。

  因此跟完善的做法是,在methodSwizzlingWithClass函数中,还应该对m1进行一次判断,如果m1为空,则在此类中添加一个方法编号为oriSELIMPm2IMPMethod,然后为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
评论
请登录