iOS黑魔法 - Method Swizzling/KVC

454 阅读7分钟

Method Swizzling的本质

Method Swizzling本质上就是对IMPSEL进行交换。也就是我们常说的“黑魔法

Method Swizzling原理

Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。

而且Method Swizzling也是__iOS__中AOP(面相切面编程)的一种实现方式,我们可以利用苹果这一特性来实现AOP编程。

可以通过两张图来理解

上面图一中selector2原本对应着IMP2,但是为了更方便的实现特定业务需求,我们在图二中添加了selector3IMP3,并且让selector2指向了IMP3,而selector3则指向了IMP2,这样就实现了“方法互换”。

OC语言的runtime特性中,调用一个对象的方法就是给这个对象发送消息。是通过查找接收消息对象的方法列表,从方法列表中查找对应的SEL,这个SEL对应着一个IMP(一个IMP可以对应多个SEL),通过这个IMP找到对应的方法调用。

在每个类中都有一个Dispatch Table,这个Dispatch Table本质是将类中的SELIMP(可以理解为函数指针)进行对应。而我们的Method Swizzling就是对这个table进行了操作,让SEL对应另一个IMP

使用method-swizzling存在的问题

method-swizzling使用过程中的一次性问题

所谓的一次性就是:mehod-swizzling写在load方法中,而load方法会主动调用多次,这样会导致方法的重复交换,使方法sel的指向又恢复成原来的imp的问题

如图所示:

针对这种情况,应采用单利的方式进行处理

子类没有实现,父类实现了

LGPerson中实现了personInstanceMethod,而LGStudent继承自LGPerson,没有实现personInstanceMethod,运行下面这段代码会出现什么问题?

通过LGStudent的分类LG实现方法交换

调用类

运行结果:

  • 原因分析:

崩溃点在于[p personInstanceMethod];原因在于LGStudent分类中进行了方法交换,将person中的imp交换成了LGStudent中的lg_studentIndtanceMethod,然后需要去LGPerson中找lg_studentInstanceMethod,但是LGPerson没有此方法,找不到imp,就引起崩溃。

  • 优化
如果`添加成功`,即类中没有这个方法,则通过`class_replaceMethod`进行`替换`,其内部会调用`class_addMethod`进行添加,添加不成功,即类中有这个方法,则通过`method_exchangeImplementations`进行`交换`

子类没有实现,父类也没有实现

这是因为在方法交换的时候,因为`LGPerson`中没有实现`personInstanceMetheod`,所以获取到的 `oriMethod`就是`nil`,这时候去交换方法,结果就是`lg_manInstanceMethod`的`imp`并没有交换给`personInstanceMetheod`,所以在`lg_manInstanceMethod`中再调用`lg_manInstanceMethod`,实际就是递归了,所以就发生了`死循环`。
  • 优化

Method Swizzling类簇

在我们项目开发过程中,经常因为NSArray数组越界或者NSDictionarykey或者value值为nil等问题导致的崩溃,对于这些问题苹果并不会报一个警告,而是直接崩溃,感觉苹果这样确实有点“太狠了”。

由此,我们可以根据上面所学,对NSArrayNSMutableArrayNSDictionaryNSMutableDictionary等类进行Method Swizzling,实现方式还是按照上面的例子来做。但是....你发现Method Swizzling根本就不起作用,代码也没写错啊,到底是什么鬼?

这是因为Method SwizzlingNSArray这些的类簇是不起作用的。因为这些类簇类,其实是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行使用。例如我们调用NSArrayobjectAtIndex:方法,这个类会在方法内部判断,内部创建不同抽象类进行操作。

所以也就是我们对NSArray类进行操作其实只是对父类进行了操作,在NSArray内部会创建其他子类来执行操作,真正执行操作的并不是NSArray自身,所以我们应该对其“真身”进行操作。

代码示例

发现了吗,__NSArrayI才是NSArray真正的类,而NSMutableArray又不一样😂。我们可以通过runtime函数获取真正的类:objc_getClass("__NSArrayI");

下面我们列举一些常用的类簇的“真身”:

“真身”
NSArray__NSArrayI
NSMutableArray__NSArrayM
NSDictionary__NSDictionaryI
NSMutableDictionary__NSDictionaryM

其他的可以去搜索下

KVC

KVC概念

Key Value Coding 也即 KVCiOS 开发中一个很重要的概念,中文翻译过来是 键值编码 ,关于这个概念的具体定义可以在 Apple官方文档处找到。

Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties.
【译】KVC 是通过 NSKeyValueCoding 这个非正式协议启用的一种机制,而遵循了这个协议的对象就提供了对其属性的间接访问。

基本用法

  • 直接对属性或者成员变量进行取值和赋值
  • 针对集合属性,可以直接通过mutableArrayValueForKey对对象的集合属性进行操作更改

  • 针对结构体,KVC也可以直接操作,但是操作时候需要将结构体转成NSValue类型

  • 假如对象的属性也是对象,那么KVC可以通过keyPath来操作对象属性的属性

  • 假如字典的key和一个对象的属性都一样,那么可以通过setValuesForKeysWithDictionary直接将字典的value赋值给对象相应的属性,同样,也可以通过dictionaryWithValuesForKeys将对象转换成字典

  • 通过api可以拿到数组元素的长度,也可以对数组元素进行操作得到新的数组

  • 聚合操作符

KVC 的底层实现

  • KVC-赋值过程

根据上面官方文档得知:

  1. 先依次查询有没有相关的方法:set< Key>:_set< Key>:setIs< Key>: 找到直接进行调用赋值。
  2. 若没有相关方法时,会查看类方法accessInstanceVariablesDirectly是否为YES时进入下一步。否则进入步骤4
  3. YES时,可以直接访问成员变量的来进行赋值,依次寻找变量 _< key >_is< Key>< key>is< Key>。找到则直接赋值,否则进入下一步。
  4. 将会调用**setValue:forUndefinedKey:**方法进行抛出异常。可以自定义的实现为未找到的场景来避免抛出异常的行为。
  • KVC-取值过程

valueForKey: 方法会在调用者传入 key 之后会在对象中按下列的步骤进行模式搜索:

  1. get<Key>, <key>, is<Key> 以及 _<key> 的顺序查找对象中是否有对应的方法。
  • 如果找到了,将方法返回值带上跳转到第 5 步
  • 如果没有找到,跳转到第 2 步
  1. 查找是否有 countOf<Key>objectIn<Key>AtIndex: 方法(对应于 NSArray 类定义的原始方法)以及 <key>AtIndexes: 方法(对应于 NSArray 方法 objectsAtIndexes:)
  • 如果找到其中的第一个(countOf<Key>),再找到其他两个中的至少一个,则创建一个响应所有 NSArray 方法的代理集合对象,并返回该对象。(翻译过来就是要么是 countOf<Key> + objectIn<Key>AtIndex:,要么是 countOf<Key> + <key>AtIndexes:,要么是 countOf<Key> + objectIn<Key>AtIndex: + <key>AtIndexes:)
  • 如果没有找到,跳转到第 3 步
  1. 查找名为 countOf<Key>enumeratorOf<Key>memberOf<Key> 这三个方法(对应于NSSet类定义的原始方法)
  • 如果找到这三个方法,则创建一个响应所有 NSSet 方法的代理集合对象,并返回该对象
  • 如果没有找到,跳转到第 4 步
  1. 判断类方法 accessInstanceVariablesDirectly 结果
  • 如果返回 YES,则以 _<key>, _is<Key>, <key>, is<Key> 的顺序查找成员变量,如果找到了,将成员变量带上跳转到第 5 步,如果没有找到则跳转到第 6 步
  • 如果返回 NO,跳转到第 6 步
  1. 判断取出的属性值
  • 如果属性值是对象,直接返回
  • 如果属性值不是对象,但是可以转化为 NSNumber 类型,则将属性值转化为 NSNumber 类型返回
  • 如果属性值不是对象,也不能转化为 NSNumber 类型,则将属性值转化为 NSValue 类型返回
  1. 调用 valueForUndefinedKey:。 默认情况下,这会引发一个异常,但是 NSObject 的子类可以提供特定于 key 的行为。

总结:

kvc的使用如下图所示(IOS13有些私有属性的异常情况