最近在集成一个第三方 SDK
的过程中,发现了一个知识点。
首先说下问题的现象。该 SDK
在其 demo
中运行正常,但是在我的项目中启动就崩溃,真机上运行的错误信息是 could not execute support code to read Objective-C class data in the process. at real iPhone device
。模拟器上不会崩溃,app 会卡住无响应。
根据崩溃日志,结合网上的搜索结果,可以判断出这应该是代码无限循环的问题了。由于 app
启动就挂了,所以判断是与某个第三方库中的代码冲突了,经过排查,最后发现还是两个 SDK
中关于 Method Swizzling
的使用不规范导致的。
通常情况下,一些第三方都会重写 load
方法,并使用 method_exchangeImplementations
方法来实现 hook
操作,以便做些额外处理。但是,如果子类重写 load
方法,并调用 [super load]
的时候,可能就会出现问题了。
为了说明这个问题,下面会列举出一些例子。看例子以前,首先说明下 method_exchangeImplementations
方法的作用。文档中相关的描述为:
/**
* Exchanges the implementations of two methods.
*
* @param m1 Method to exchange with second method.
* @param m2 Method to exchange with first method.
*
* @note This is an atomic version of the following:
* \code
* IMP imp1 = method_getImplementation(m1);
* IMP imp2 = method_getImplementation(m2);
* method_setImplementation(m1, imp2);
* method_setImplementation(m2, imp1);
* \endcode
*/
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
如上,其实就是交换两个方法的实现。
交换之前,方法与其实现的关系为:
graph LR
m1-->imp1
graph LR
m2-->imp2
交换之后,他们的关系为:
graph LR
m1-->imp2
graph LR
m2-->imp1
记住这个方法的作用,方便理解下面的例子,以下用到的完整例子在这里:
-
P1
继承于NSObject
,C1
继承于P1
。P1
中有个对外暴露的方法log
,并在P1
中,将log
方法的实现与cy_log
互换。代码如下:#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface P1 : NSObject - (void)log; @end NS_ASSUME_NONNULL_END #import "P1.h" #import <objc/runtime.h> @implementation P1 + (void)load{ method_exchangeImplementations(class_getInstanceMethod(self, @selector(log)), class_getInstanceMethod(self, @selector(cy_log))); } - (void)log { NSLog(@"log"); } - (void)cy_log { NSLog(@"cy_log"); [self cy_log]; } @end #import "P1.h" NS_ASSUME_NONNULL_BEGIN @interface C1 : P1 @end NS_ASSUME_NONNULL_END #import "C1.h" @implementation C1 @end
下面测试
P1
、C1
调用log
方法的结果:- (void)test1 { P1 *p1 = [[P1 alloc] init]; [p1 log]; // 打印日志 /* 2019-01-04 14:30:49.924393+0800 Test11111111111[41830:1171574] cy_log 2019-01-04 14:30:49.924545+0800 Test11111111111[41830:1171574] log */ } - (void)test11 { C1 *c1 = [[C1 alloc] init]; [c1 log]; // 打印日志 /* 2019-01-04 14:31:41.150870+0800 Test11111111111[41876:1172421] cy_log 2019-01-04 14:31:41.151062+0800 Test11111111111[41876:1172421] log */ }
根据结果,不管是父类还是子类,都完成了方法的互换,没有问题。
-
在例 1 的基础上,子类实现
load
方法。注意这里使用P2
、C2
类,以便区分,下面都会这样子使用。这里只有C2.m
中的代码有变化,最终代码如下:#import "C2.h" @implementation C2 + (void)load{ NSLog(@"C2"); } @end
测试用例如下:
- (void)test2 { P2 *p2 = [[P2 alloc] init]; [p2 log]; // 打印日志 /* 2019-01-04 14:35:45.203917+0800 Test11111111111[42124:1176051] C2 2019-01-04 14:35:46.156888+0800 Test11111111111[42124:1176051] cy_log 2019-01-04 14:35:46.157056+0800 Test11111111111[42124:1176051] log */ } - (void)test22 { C2 *c2 = [[C2 alloc] init]; [c2 log]; // 打印日志 /* 2019-01-04 14:36:17.575144+0800 Test11111111111[42151:1176660] C2 2019-01-04 14:36:18.327939+0800 Test11111111111[42151:1176660] cy_log 2019-01-04 14:36:18.328086+0800 Test11111111111[42151:1176660] log */ }
虽然子类重写了
load
方法, 但是里面除了打印一句话,没有做额外操作,所以,不影响方法交换的结果。 -
在例 2 的基础上,子类调用
[super load]
,最终代码如下:#import "P3.h" #import <objc/runtime.h> @implementation P3 + (void)load{ NSLog(@"P3"); method_exchangeImplementations(class_getInstanceMethod(self, @selector(log)), class_getInstanceMethod(self, @selector(cy_log))); } - (void)log { NSLog(@"log"); } - (void)cy_log { NSLog(@"cy_log"); [self cy_log]; } @end #import "C3.h" @implementation C3 + (void)load{ [super load]; NSLog(@"C3"); } @end
测试用例如下:
- (void)test3 { P3 *p3 = [[P3 alloc] init]; [p3 log]; // 打印日志 /* 2019-01-04 14:39:55.724185+0800 Test11111111111[42361:1180047] P3 2019-01-04 14:39:55.724871+0800 Test11111111111[42361:1180047] P3 2019-01-04 14:39:55.725081+0800 Test11111111111[42361:1180047] C3 2019-01-04 14:39:56.547522+0800 Test11111111111[42361:1180047] log */ } - (void)test33 { C3 *c3 = [[C3 alloc] init]; [c3 log]; // 打印日志 /* 2019-01-04 14:40:28.221793+0800 Test11111111111[42392:1180643] P3 2019-01-04 14:40:28.222474+0800 Test11111111111[42392:1180643] P3 2019-01-04 14:40:28.222664+0800 Test11111111111[42392:1180643] C3 2019-01-04 14:40:28.758676+0800 Test11111111111[42392:1180643] log */ }
由于子类调用了
[super load]
,所以方法交换进行了两次。虽然两次调用方法的类不同(一个是P3
,一个是C3
) ,但是C3
没有实现父类的方法, 所以两次交换的方法、方法实现都是一样的,最终经过两次交换,方法的实现等于是没有变化。 -
在例 3 的基础上,子类实现父类的
log
方法,最终代码如下:#import "C4.h" @implementation C4 + (void)load{ [super load]; NSLog(@"C4"); } - (void)log { NSLog(@"C4 -- log"); } @end
测试用例如下:
- (void)test4 { P4 *p4 = [[P4 alloc] init]; [p4 log]; // 打印日志 /* 2019-01-04 14:43:48.547728+0800 Test11111111111[42579:1183469] P4 2019-01-04 14:43:48.547941+0800 Test11111111111[42579:1183469] P4 2019-01-04 14:43:48.548092+0800 Test11111111111[42579:1183469] C4 2019-01-04 14:43:49.296572+0800 Test11111111111[42579:1183469] cy_log 2019-01-04 14:43:49.296722+0800 Test11111111111[42579:1183469] C4 -- log */ } - (void)test44 { C4 *c4 = [[C4 alloc] init]; [c4 log]; // 打印日志 /* 2019-01-04 14:45:05.075842+0800 Test11111111111[42638:1184538] P4 2019-01-04 14:45:05.076060+0800 Test11111111111[42638:1184538] P4 2019-01-04 14:45:05.076276+0800 Test11111111111[42638:1184538] C4 2019-01-04 14:45:05.630284+0800 Test11111111111[42638:1184538] log */ }
这里父类的
load
方法会先调用,进行方法实现的替换,然后子类调用load
方法,继续进行方法实现的替换。注意,子类实现了log
方法, 所以,这里的替换有区别。替换之前:
graph LR p4.log --> p4.imp.log p4.cy_log --> p4.imp.cy_log c4.log --> c4.imp.log
父类调用
load
之后,graph LR p4.log --> p4.imp.cy_log p4.cy_log --> p4.imp.log c4.log --> c4.imp.log
子类调用
load
之后,graph LR p4.log --> p4.imp.cy_log p4.cy_log --> c4.imp.log c4.log --> p4.imp.log
所以,当
p1
调用log
方法的时候,其实现就是NSLog(@"cy_log"); [self cy_log];
先输出
cy_log
,然后调用自身的cy_log
方法, 此时,父类cy_log
的实现为:NSLog(@"C4 -- log");
所以,最终输出结果为:
2019-01-04 14:43:49.296572+0800 Test11111111111[42579:1183469] cy_log 2019-01-04 14:43:49.296722+0800 Test11111111111[42579:1183469] C4 -- log
同样道理,子类调用
log
方法时,其实调用的是父类log
方法的实现。 -
在例 4 基础上,子类的
log
方法,会调用父类的实现:#import "C5.h" @implementation C5 + (void)load{ [super load]; NSLog(@"C5"); } - (void)log { [super log]; NSLog(@"C5 -- log"); } @end
测试用例:
- (void)test5 { P5 *p5 = [[P5 alloc] init]; [p5 log]; // 打印日志 /* 2019-01-04 14:50:10.030135+0800 Test11111111111[42886:1187884] P5 2019-01-04 14:50:10.030344+0800 Test11111111111[42886:1187884] P5 2019-01-04 14:50:10.030502+0800 Test11111111111[42886:1187884] C5 019-01-04 14:50:10.841267+0800 Test11111111111[42886:1187884] cy_log 2019-01-04 14:50:10.841420+0800 Test11111111111[42886:1187884] cy_log 2019-01-04 14:50:10.841508+0800 Test11111111111[42886:1187884] cy_log 2019-01-04 14:50:10.841585+0800 Test11111111111[42886:1187884] cy_log 2019-01-04 14:50:10.841678+0800 Test11111111111[42886:1187884] cy_log 2019-01-04 14:50:10.841745+0800 Test11111111111[42886:1187884] cy_log 2019-01-04 14:50:10.841813+0800 Test11111111111[42886:1187884] cy_log 2019-01-04 14:50:10.841879+0800 Test11111111111[42886:1187884] cy_log ... 无限循环 */ } - (void)test55 { C5 *c5 = [[C5 alloc] init]; [c5 log]; // 打印日志 /* 2019-01-04 14:51:02.086052+0800 Test11111111111[42923:1188558] P5 2019-01-04 14:51:02.086300+0800 Test11111111111[42923:1188558] P5 2019-01-04 14:51:02.086474+0800 Test11111111111[42923:1188558] C5 2019-01-04 14:51:02.910528+0800 Test11111111111[42923:1188558] log */ }
注意,这里已经出现了不容忽视的问题了,无限循环。其实,这里每个方法的最终实现跟上面的例 4 一样。父类调用
log
方法,走父类cy_log
方法的实现,然后会调用cy_log
方法,走子类log
方法的实现,而子类中通过[super log]
导致了无限循环。这里,子类的
log
方法的实现被替换为了父类log
方法的实现,所以输出了父类的结果。 -
这个例子有点不同,父类代码不变,子类代码如下:
#import "C6.h" @implementation C6 + (void)load{ NSLog(@"C6"); } - (void)log { NSLog(@"C6 -- log"); } - (void)cy_log { NSLog(@"C6 -- cy_log"); [self cy_log]; } @end
测试用例:
- (void)test6 { P6 *p6 = [[P6 alloc] init]; [p6 log]; // 打印日志 /* 2019-01-04 14:59:05.455644+0800 Test11111111111[43263:1193332] P6 2019-01-04 14:59:05.455851+0800 Test11111111111[43263:1193332] C6 2019-01-04 14:59:06.295442+0800 Test11111111111[43263:1193332] cy_log 2019-01-04 14:59:06.295770+0800 Test11111111111[43263:1193332] log */ } - (void)test66 { C6 *c6 = [[C6 alloc] init]; [c6 log]; // 打印日志 /* 2019-01-04 14:59:52.848316+0800 Test11111111111[43304:1194120] P6 2019-01-04 14:59:52.848575+0800 Test11111111111[43304:1194120] C6 2019-01-04 14:59:53.321274+0800 Test11111111111[43304:1194120] C6 -- log */ }
这里,由于子类重写了方法的实现,所以,方法的替换只有父类有效。
-
在例 6 的基础上,子类调用
[super load]
方法。代码如下:#import "C7.h" @implementation C7 + (void)load{ [super load]; NSLog(@"C7"); } - (void)log { NSLog(@"C7 -- log"); } - (void)cy_log { NSLog(@"C7 -- cy_log"); [self cy_log]; } @end
测试用例:
- (void)test7 { P7 *p7 = [[P7 alloc] init]; [p7 log]; // 打印日志 /* 2019-01-04 15:03:02.488361+0800 Test11111111111[43486:1196658] P7 2019-01-04 15:03:02.488547+0800 Test11111111111[43486:1196658] P7 2019-01-04 15:03:02.488732+0800 Test11111111111[43486:1196658] C7 2019-01-04 15:03:03.371679+0800 Test11111111111[43486:1196658] cy_log 2019-01-04 15:03:03.371844+0800 Test11111111111[43486:1196658] log */ } - (void)test77 { C7 *c7 = [[C7 alloc] init]; [c7 log]; // 打印日志 /* 2019-01-04 15:03:31.248458+0800 Test11111111111[43521:1197293] P7 2019-01-04 15:03:31.248605+0800 Test11111111111[43521:1197293] P7 2019-01-04 15:03:31.248763+0800 Test11111111111[43521:1197293] C7 2019-01-04 15:03:31.788730+0800 Test11111111111[43521:1197293] C7 -- cy_log 2019-01-04 15:03:31.788890+0800 Test11111111111[43521:1197293] C7 -- log */ }
由于子类分别重写了两个方法,所以子类跟父类方法实现的替换互不干扰。
-
在例 6 的基础上,子类调用
[super log]
。子类代码如下:#import "C8.h" @implementation C8 + (void)load{ NSLog(@"C8"); } - (void)log { [super log]; NSLog(@"C8 -- log"); } - (void)cy_log { NSLog(@"C8 -- cy_log"); [self cy_log]; } @end
测试用例:
- (void)test8 { P8 *p8 = [[P8 alloc] init]; [p8 log]; // 打印日志 /* 2019-01-04 16:06:46.952819+0800 Test11111111111[46258:1232560] P8 2019-01-04 16:06:46.960383+0800 Test11111111111[46258:1232560] C8 2019-01-04 16:06:47.673991+0800 Test11111111111[46258:1232560] cy_log 2019-01-04 16:06:47.674171+0800 Test11111111111[46258:1232560] log */ } - (void)test88 { C8 *c8 = [[C8 alloc] init]; [c8 log]; // 打印日志 /* 2019-01-04 16:07:33.510610+0800 Test11111111111[46290:1233272] P8 2019-01-04 16:07:33.518123+0800 Test11111111111[46290:1233272] C8 2019-01-04 16:07:34.182573+0800 Test11111111111[46290:1233272] cy_log 2019-01-04 16:07:34.182728+0800 Test11111111111[46290:1233272] C8 -- cy_log 2019-01-04 16:07:34.182821+0800 Test11111111111[46290:1233272] C8 -- cy_log 2019-01-04 16:07:34.182925+0800 Test11111111111[46290:1233272] C8 -- cy_log 2019-01-04 16:07:34.183017+0800 Test11111111111[46290:1233272] C8 -- cy_log 2019-01-04 16:07:34.183084+0800 Test11111111111[46290:1233272] C8 -- cy_log 2019-01-04 16:07:34.183158+0800 Test11111111111[46290:1233272] C8 -- cy_log 2019-01-04 16:07:34.183244+0800 Test11111111111[46290:1233272] C8 -- cy_log 2019-01-04 16:07:34.183529+0800 Test11111111111[46290:1233272] C8 -- cy_log 2019-01-04 16:07:34.183746+0800 Test11111111111[46290:1233272] C8 -- cy_log 2019-01-04 16:07:34.183947+0800 Test11111111111[46290:1233272] C8 -- cy_log 2019-01-04 16:07:34.184140+0800 Test11111111111[46290:1233272] C8 -- cy_log 2019-01-04 16:07:34.184332+0800 Test11111111111[46290:1233272] C8 -- cy_log 2019-01-04 16:07:34.186880+0800 Test11111111111[46290:1233272] C8 -- cy_log 2019-01-04 16:07:34.187209+0800 Test11111111111[46290:1233272] C8 -- cy_log 2019-01-04 16:07:34.187493+0800 Test11111111111[46290:1233272] C8 -- cy_log 2019-01-04 16:07:34.187827+0800 Test11111111111[46290:1233272] C8 -- cy_log 2019-01-04 16:07:34.188097+0800 Test11111111111[46290:1233272] C8 -- cy_log ... 无限循环 */ }
注意,这里仍然出现了无限循环。父类进行方法实现的替换没有问题,这里,子类并没有进行方法的替换。子类调用
log
方法,通过[super log]
调用父类的log
方法,而父类的log
方法的实现被替换为了cy_log
方法的实现,所以会首先输出cy_log
信息,然后调用[self cy_log]
, 注意这里的self
是子类的一个实例,而子类中的cy_log
方法由于没有进行方法实现的替换,所以会不停的调用自己,并一直输出C8 -- cy_log
。 -
在例 8 的基础上,子类调用
[super load]
方法。子类代码如下:#import "C9.h" @implementation C9 + (void)load{ [super load]; NSLog(@"C9"); } - (void)log { [super log]; NSLog(@"C9 -- log"); } - (void)cy_log { NSLog(@"C9 -- cy_log"); [self cy_log]; } @end
测试用例:
- (void)test9 { P9 *p9 = [[P9 alloc] init]; [p9 log]; // 打印日志 /* 2019-01-04 15:06:29.856458+0800 Test11111111111[43692:1199799] P9 2019-01-04 15:06:29.857411+0800 Test11111111111[43692:1199799] P9 2019-01-04 15:06:29.857576+0800 Test11111111111[43692:1199799] C9 2019-01-04 15:06:30.686701+0800 Test11111111111[43692:1199799] cy_log 2019-01-04 15:06:30.686872+0800 Test11111111111[43692:1199799] log */ } - (void)test99 { C9 *c9 = [[C9 alloc] init]; [c9 log]; // 打印日志 /* 2019-01-04 15:07:02.796892+0800 Test11111111111[43718:1200375] P9 2019-01-04 15:07:02.797921+0800 Test11111111111[43718:1200375] P9 2019-01-04 15:07:02.798087+0800 Test11111111111[43718:1200375] C9 2019-01-04 15:07:03.332852+0800 Test11111111111[43718:1200375] C9 -- cy_log 2019-01-04 15:07:03.333016+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.333204+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.333294+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.333401+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.333487+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.333559+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.333628+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.333918+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.334161+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.334385+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.334627+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.334858+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.335089+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.335300+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.335502+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.335699+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.335895+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.336088+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.357983+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.358114+0800 Test11111111111[43718:1200375] cy_log 2019-01-04 15:07:03.358274+0800 Test11111111111[43718:1200375] cy_log ... 无限循环 */ }
这里,父类方法实现进行了交换,是没有问题的。子类实现了这两个方法,并进行了方法实现的交换。所以,当子类调用
log
方法的时候,会走cy_log
的实现,打印出C9 -- cy_log
,然后调用cy_log
方法,走log
方法的实现,而子类中log
方法会调用父类的实现,这时父类的log
方法使用的是父类cy_log
方法的实现,所以,输出cy_log
,接下来继续调用[self cy_log];
,注意这里self
是子类的一个实例,继续调用子类log
方法的实现,进入无限循环中。
上面便是一些关于方法实现替换的例子,可以看到,代码不同,结果不同。并且,有些情况会造成巨大影响,导致程序崩溃。
其实只要我们在进行替换的时候,代码规范起来,就不会出现这种问题了。通常好的替换方式是使用 dispatch_once
保证替换一次,而且,子类的 load
方法中,不调用父类的 load
方法。
但是,实际中我们可能会用到不同的第三方,他们的代码质量不一样,所以,遇到这种问题,只能一个个排查了。