iOS SDK 中的 Method Swizzling 导致的无限循环

最近在集成一个第三方 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

记住这个方法的作用,方便理解下面的例子,以下用到的完整例子在这里

  1. P1 继承于 NSObjectC1 继承于 P1P1 中有个对外暴露的方法 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
    

    下面测试 P1C1 调用 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
          */
     }
    

    根据结果,不管是父类还是子类,都完成了方法的互换,没有问题。

  2. 在例 1 的基础上,子类实现 load 方法。注意这里使用 P2C2 类,以便区分,下面都会这样子使用。这里只有 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 方法, 但是里面除了打印一句话,没有做额外操作,所以,不影响方法交换的结果。

  3. 在例 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 没有实现父类的方法, 所以两次交换的方法、方法实现都是一样的,最终经过两次交换,方法的实现等于是没有变化。

  4. 在例 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 方法的实现。

  5. 在例 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 方法的实现,所以输出了父类的结果。

  6. 这个例子有点不同,父类代码不变,子类代码如下:

     #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
          */
     }
    

    这里,由于子类重写了方法的实现,所以,方法的替换只有父类有效。

  7. 在例 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
          */
     }
    

    由于子类分别重写了两个方法,所以子类跟父类方法实现的替换互不干扰。

  8. 在例 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

  9. 在例 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 方法。

但是,实际中我们可能会用到不同的第三方,他们的代码质量不一样,所以,遇到这种问题,只能一个个排查了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值