【底层探索】- runtime面试题集


〇、引言

前面一步步学习了对象、类、方法、类的加载等,这些其实都是Runtime 的基础,而Runtime 又是iOS开发语言 Objective C 的精髓,因此关于Runtime 的面试题举不胜举。

下面就简单的介绍几个,并提供解题思路,希望可以帮读者更清晰的理解Runtime。

一、什么是Runtime

这个问题,问的是对Runtime 的基础理解。

Runtime是由C和C++汇编实现的一套API,为 OC 语言加入了面向对象,运行时的功能。

运行时(Runtime) 是指将数据类型确定推迟到了运行时。

举个例子:

关于类——类的扩展,在编译时作为类的一部分(ro)就已经确定好了,所以可以添加分类;而分类由于是运行时加载,只能添加属性以及对应的setter 和getter 方法,来达到模拟属性的目的。

二、方法的本质是什么?

方法的本质,就是发送消息/消息传递

让一个类(对象/类)执行一个方法的过程,就是向它发送消息,它便开始消息查找的过程。主要包含以下几个过程:

  1. 快速查找objc_msgSend),主要向类 cache_t 查找缓存的过的方法。
  2. 慢速查找:执行 lookUpImpOrForward 方法,递归自己,以及自己的父类,属性 rw 里的methodlist 中查找方法。
  3. 动态方法加解析(还是查找不到)resolveInstanceMethod 方法,看是否自定义实现过
  4. 消息转发阶段:
    1. 快速转发—— forwardingTargetForSelector 寻找特定对象来执行方法
    2. 慢速转发—— methodSignatureForSelector(获取方法的签名)以及生成相应的invocation,由 forwardInvocation 方法进行消息分发,让有能力执行的类来执行该方法。

三、简述SEL 和IMP和之间关系

定义:SEL是内存中方法编号,IMP 是方法的具体实现

两者个点关系就好比一本书:SEL 是目录页里的 章节标题,而IMP 则是文章的页码,通过页码,可以看到具体内容。

SEL 是由read_images 就已经加载注册到内存表里。

四、能否往已注册的类添加成员变量?

###

Q:已经注册好的类,能否再动态添加成员变量?为什么?

答案是不可以。

我们通过Runtime还原一下场景。

注册好的类,实现的方法是,注册到内存里:

objc_registerClassPair(LGPerson);

而实现objc_registerClassPair 这个方法,又实现了下面的内容:

    // Clear "under construction" bit, set "done constructing" bit
cls->ISA()->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
cls->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);

即对类更改了状态,更改了什么状态?RW_CONSTRUCTED这个状态,即让类处于内存开辟&注册到内存中——

// class allocated and registered
#define RW_CONSTRUCTED        (1<<25)

接下来,根据创建成员变量的函数为addIvar,创建业务代码如下

class_addIvar(LGPerson, "lgName", sizeof(NSString *), log2(sizeof(NSString *)), "@");

在源码中找到相对应的函数:

001

走到这一步,就戛然而止了……添加ivars 被拒绝——因为内存已固定,无法再添加新属性了。

原因:因为注册好的类,内存容量已经固定,无法动态添加了。

五、isKindOFClass 和 isMemberOfClass 的区别

5.1 题目

关于这两个函数,我们知道他们各自概念是:

isKindOfClass——某个对象是否是类的成员,或者继承自该类的成员(即父子关系)

isMemberOfClass——某个对象是否当前类的成员,并不考虑回溯的父子类关系。

这里有一道面试题,题目如下,要求回答各打印结果:

BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];//1
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];// 0
BOOL re3 = [(id)[Person class] isKindOfClass:[Person class]];//0
BOOL re4 = [(id)[Person class] isMemberOfClass:[Person class]];// 0
NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);

BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];//1
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];// 1
BOOL re7 = [(id)[Person alloc] isKindOfClass:[Person class]];//1
BOOL re8 = [(id)[Person alloc] isMemberOfClass:[Person class]];// 1
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);

答案是什么呢?先别忙,先冷静分析一下

5.2 概念分析

5.2.1 类方法的区别分析

很明显,上半部4个判断,是判断类与类的归属,执行的是类方法判断,先看一下涉及到的两个方法的源码:

类方法的区别

+(void)isKindOfClass** 的实现

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

+(BOOL)isMemberOfClass 的实现

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}

从上面源码分析,可以看出,isKindOfClass 多了一步 tcls = tcls->superclass 的循环,即如果当前类不等于目标类,向上查找父类,看看父类与目标是否相等。

5.2.2 实例方法的区别

  • -(void)isKindOfClass 的实现

    - (BOOL)isKindOfClass:(Class)cls {
        for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
            if (tcls == cls) return YES;
        }
        return NO;
    }

    步骤为:先判断对象的类,看是否与目标类相同;否则通过tcls = tcls->superclass,递归寻找父类及其父类,是否与本类相等

  • -(void)isMemberOfClass 的实现

    - (BOOL)isMemberOfClass:(Class)cls {
        return [self class] == cls;
    }

    步骤为:判断对象指向的类,是否与目标类相同

5.2.3 答题

好的,根据这个,一个一个来进行解答。

  1. 返回1:因为根元类的父类=根元类

    左边的NSObject class 为 NSObject 的元类,判断是否与本类NSobject 相等?

    答案是不一致。

    但是此时会进入tcls = tcls->superclass 这个循环,查找本类的父类,而我们知道 NSObject 的元类的父类就是NSObject ,见下图。所以绕了一圈回来,NSObject = NSObject

  2. 返回0:因为元类与本类不相等

    和上一个问题一样,但是isMemberOfClass 在第一步就停下了,object_getClass((id)self) == cls 这里问到元类与本类是否相等,当然是否。

  3. 返回0:因为普通类的元类的父类与本类是不相等的

    我们看右边是 Person Class

    左边的 Person Class

    判断条件是 isKindOfClass

    所以,Person 开始找它的元类,看是否等于本类。我们看isa 的指向图可以得出,它的元类递归查找父类,一直都与本类不相等。所以返回为0

  4. 返回为0。因为 元类不等于本类

    这个和第2条是一样的

  1. *返回为1: * 因为本类等于本类

    给出的判断代码为:

    [(id)[NSObject alloc] isKindOfClass:[NSObject class]]

    其中左边的[NSObject alloc] ,创建了一个 NSObject 的对象,而对他指向 isKindOfClass 即执行以下方法:

    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
            if (tcls == cls) return YES;
        }

    创建的 Class tcls = [self class] ,其中tcls 即为 其父类 NSObject,取其与 NSObject 相比,相等,所以返回为1。

  2. 返回为1: 因为本类等于本类,同第5 题

  3. 返回为1:同第5题

  4. 返回为1: 同第5题

五、[self class] 与[super class] 区别

5.1 提问:以下打印什么?

#import "Student.h"
#import <objc/message.h>
@implementation Student

- (instancetype)init{
    self = [super init];
    if (self) {
        NSLog(@"%@",NSStringFromClass([self class]));
        NSLog(@"%@",NSStringFromClass([super class]));
        }
}

5.2 源码分析

因为问题都涉及到了 class 这个方法,在NSObject.mm 这个类里找一下,方法实现如下:

- (Class)class {
    return object_getClass(self);
}

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

可以见到,在object_getClass 方法里,如果给的对象self ——obj 存在,返回的是它的元类,指针指向的是它的类,所以打印应该是 Student

而第一行中,向super 发送消息了。

  • [self class] 方法,是向 对象(self)发送消息(class),走的流程是 objc_msgSend
  • [super class],是向 对象(self)发送消息(class),走的流程是 objc_msgSendSuper

那继续探寻objc_msgSendSuper 这个方法,可以看到他的结构如下:

objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

可见它执行的是 struct objc_super 类型的super,

原因:在OC中,super只是个符号标识符,objc_msgSendSuper最终接受对象还是 self

5.3 解答

打印都是NSObject

六、weak 是什么及其实现原理

6.1 分析

在OC语言到开发中,我们经常对对象前面添加 weak 来实现弱引用,从而达到避免循环引用造成的内存泄漏。那么这个weak 究竟实现了什么,作为高级开发者,不得不仔细探究一下。

6.1.1 代码创建

为了搞清楚weak 创建对象时内部的实现,创建一个weak 对象,打印试试,这时要把汇编断点打开

NSArray *arr = @[@"jack", @"tiga", @"jade", @"obu"];
id __weak abc = arr;

可以看到运行后,执行了这行代码:

可见创建后执行了关键代码 objc_initWeak,贴到源码里查看,实现如下:

id
objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

返回到时storeWeak(id *location, objc_object *newObj)这个函数,依稀可以看出来,是一个将newObj 储存到location 的动作。继续往下分析如下:

6.1.2 源码解读 storeWeak

  1. 如果有新对象,创建新的散列表

    if (haveNew) {
       newTable = &SideTables()[newObj];

    散列表结构如下:

    struct SideTable {
        spinlock_t slock;
        RefcountMap refcnts;
        weak_table_t weak_table;
          /*精简以后的内容*/
    };

    可见散列表中有重要的3个属性

    • slock —— 自旋锁

    • refcnts——引用计数表,这里是App 维护的一张全局表,类型是

    • weak_table —— 全局的弱引用表,存储所有的弱引用对象,把对象当作key 来保持,打开看看,它的结构体实现是这样的:

      /**
       * The global weak references table. Stores object ids as keys,
       * and weak_entry_t structs as their values.
       */
      struct weak_table_t {
          weak_entry_t *weak_entries;
          size_t    num_entries;
          uintptr_t mask;
          uintptr_t max_hash_displacement;
      };

      共包含了整个应用里,弱引用实体和数量。

  2. 如果当前是创建的新弱引用haveNew,添加弱引用。

    newObj = (objc_object *)
                weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                      crashIfDeallocating);
  3. 记录弱引用实体保存的内存地址:

    weak_entry_t *entry;
        if ((entry = weak_entry_for_referent(weak_table, referent))) {
            append_referrer(entry, referrer);
        } 
  4. 插入对象到弱引用过程如下:

    1. 创建数组,插入到weak 表里

      weak_entry_t new_entry(referent, referrer);
              weak_grow_maybe(weak_table);
              weak_entry_insert(weak_table, &new_entry);
    2. 循环弱引用表,将引用实体插入,弱引用的计数增加

      static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
      {
          weak_entry_t *weak_entries = weak_table->weak_entries;
          assert(weak_entries != nil);
      
          size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);
          size_t index = begin;
          size_t hash_displacement = 0;
          while (weak_entries[index].referent != nil) {
              index = (index+1) & weak_table->mask;
              if (index == begin) bad_weak_table(weak_entries);
              hash_displacement++;
          }
      
          weak_entries[index] = *new_entry;
          weak_table->num_entries++;
      
          if (hash_displacement > weak_table->max_hash_displacement) {
              weak_table->max_hash_displacement = hash_displacement;
          }
      }
  1. 将弱引用的位值,存放在引用计数表里

    if (newObj  &&  !newObj->isTaggedPointer()) {
                newObj->setWeaklyReferenced_nolock();
            }

    并对isa 的weak引用属性设置为TRUE

    newisa.weakly_referenced = true;

    添加到计数表的方法如下:

    void 
    objc_object::sidetable_setWeaklyReferenced_nolock()
    {
    #if SUPPORT_NONPOINTER_ISA
        assert(!isa.nonpointer);
    #endif
        SideTable& table = SideTables()[this];
        table.refcnts[this] |= SIDE_TABLE_WEAKLY_REFERENCED;
    }
  2. 最终返回存储的内存地址的指针,指向这个新的对象

    *location = (id)newObj;

6.3 weak 的释放

6.1 查看 dealloc

我们知道,对象的销毁,一般是在类的dealloc 后进行,所以目光放在 dealloc 的方法是先上

查看NSObject.mm 中,得知dealloc 如下

- (void)dealloc {
    _objc_rootDealloc(self);
}

继续探寻

void
_objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}

继续继续,得到一个内联函数如下:

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

目标放在object_dispose((id)this); 这一行,函数的名字——销毁对象,即当前对象的isa还有些弱引用(isa.weakly_referenced)或者关联对象(isa.has_assoc)未处理的业务,会比较复杂,需要特别处理。继续探寻如下:

id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

继续查看解构过程objc_destructInstance(obj) 这个函数:

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}
  • if (cxx) object_cxxDestruct(obj); —— 处理析构C++ 的对象
  • _object_remove_assocations(obj); ——移除类的关联对象

那么要找寻如何移除弱引用,把目光放在obj->clearDeallocating(); 这行代码——
又是一个内联函数,解释了如何dealloc 对象

inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

很明显 clearDeallocating_slow() 才是需要找的,因为注释已经解释的很清楚:

Slow path for non-pointer isa with weak refs and/or side table data.

带弱引用或散列表数据的非指针的isa 慢速路径

NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}

分析关键代码:

  1. 如果当前isa 有弱引用,在弱引用表中,弱引用表清除当前的弱引用

    if (isa.weakly_referenced) {
            weak_clear_no_lock(&table.weak_table, (id)this);
    }

    weak_clear_no_lock 这个核心函数,具体做了什么工作,继续探究一下

    void 
    weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
    {
        objc_object *referent = (objc_object *)referent_id;
    
        weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    
        // zero out references
        weak_referrer_t *referrers;
        size_t count;
    
        if (entry->out_of_line()) {
            referrers = entry->referrers;
            count = TABLE_SIZE(entry);
        } 
        else {
            referrers = entry->inline_referrers;
            count = WEAK_INLINE_COUNT;
        }
    
        for (size_t i = 0; i < count; ++i) {
            objc_object **referrer = referrers[i];
            if (referrer) {
                if (*referrer == referent) {
                    *referrer = nil;
                }
             //
            }
        }
    
        weak_entry_remove(weak_table, entry);
    }

    这里看上去业务挺多,其实核心也就两个:

    • 移除指针

      if (*referrer == referent) {
                      *referrer = nil;
      }
    • 移除实体:

      weak_entry_remove(weak_table, entry);
      /**
       * Remove entry from the zone's table of weak references.
       */
      static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
      {
          // remove entry
          if (entry->out_of_line()) free(entry->referrers);
          bzero(entry, sizeof(*entry));
      
          weak_table->num_entries--;
      
          weak_compact_maybe(weak_table);
      }
  2. 如果当前对象有引用计数存表不为0,引用计数表清楚本对象

    if (isa.has_sidetable_rc) {
            table.refcnts.erase(this);
    }

6.3 回答

weak 的创建,在内存的散列表中的弱引用表里,存储关于该对象的弱引用,按照key-value 存入。

weak 的创建,正好相反,中dealloc 里,去弱引用表格里,找到引用进行弱引用实体销毁,以及弱引用的引用计数减少。

七、黑魔法·方法交换(Method Swizzling)坑点

7.1 一般使用

如下所示,对数组越界做保护:

#import "NSArray+Empty.h"

#import <objc/runtime.h>
@implementation NSArray (Empty)
+ (void)load{
    Method oriMethod = class_getInstanceMethod(NSClassFromString(@"__NSArrayI"), @selector(objectAtIndex:));
    Method swiMethod = class_getInstanceMethod([self class], @selector(lj_objectAtIndex:));
    method_exchangeImplementations(oriMethod, swiMethod);
}

- (instancetype)lj_objectAtIndex: (NSUInteger)index{
    if (index > self.count -1 ) {
        NSLog(@"朋友,数组越界了");
        return nil;
    }
    return [self lj_objectAtIndex:index];
}

试验一下结果如何:

    NSArray *arr = @[@"jack", @"tiga", @"jade", @"obu"];
    NSString *name =  [arr objectAtIndex:4];

打印结果如下:

2020-05-03 23:52:50.759731+0800 SWZ[7438:461811] 朋友,数组越界了

可以看到,经过方法交换,本来使用 objectAtIndex 的方法,它的实现被交换了,被交换到 lj_objectAtIndex 的实现里,执行了越界保护,保证了代码的鲁棒性。

7.2 坑点1 - 重复使用

以上是方法交换的一个简单的例子,对NSArray 添加方法实现方法交换。

7.2.1 症状

那么会不会有什么漏洞呢?留意到方法实现是在 load 方法就实现,假设在实现方法之前,主动加载一次load 会怎么样?

假设是这样:

    NSArray *arr = @[@"jack", @"tiga", @"jade", @"obu"];
    [NSArray load];
    NSString *name =  [arr objectAtIndex:4];

结果,出现了越界崩溃

7.2.2 追踪分析

往核心方法load 里添加一段打印标记,看看是否与他有关:

通过追踪,得知load 方法,执行了2次。想一想,执行load 本来是为了交换方法,那执行2次,意思是将原本交换过的方法实现,又交换回去了——白干了,这也是出现越界崩溃的原因。

7.2.3 解决

解决的方法很简单,将该方法设置成单例,通过 onceToken 来保证交换过程只会走一次:

+ (void)load{
    NSLog(@"执行load!");
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
            Method oriMethod = class_getInstanceMethod(NSClassFromString(@"__NSArrayI"), @selector(objectAtIndex:));
        Method swiMethod = class_getInstanceMethod([self class], @selector(lj_objectAtIndex:));
        method_exchangeImplementations(oriMethod, swiMethod);
    });
}

这样,继续执行虽然执行2次load, 但是内部的交换实现,只会执行一次。

2020-05-04 00:13:53.061523+0800 SWZ[7778:482862] 执行load!
2020-05-04 00:13:56.011281+0800 SWZ[7778:482862] 执行load!

结果如下,虽然load 执行2次,交换方法只执行1次

7.3 坑点2 - 待交换子类未实现

7.3.1 症状

先创建一个案发现场,如下:

  • 父类某方法A并实现
  • 子类继承父类
  • 其他业务向子类交换该A方法
  • 执行父类该方法A,查看结果

如下所示:

  1. 创建父类Animal,以及子类Dog, 其中父类拥有并实现run 的类方法

    @interface Animal : NSObject
    - (void)run;
    @end
    
    @interface Dog : Animal
    @end
  2. 其他业务场景,向子类申请交换了 run 的实现,run 换成了play,在Dog.m 执行:

    + (void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Method oriMethod = class_getInstanceMethod(self, @selector(run));
            Method swiMethod = class_getInstanceMethod(self, @selector(play));
            method_exchangeImplementations(oriMethod, swiMethod);
        });
    }
    
    - (void)play{
        NSLog(@"%s", __func__);
    }
  3. 检查此时子类与父类的的run 方法

    生成两个实例,执行run 方法

       Animal *a = [[Animal alloc] init];
       [a run];
    
       Dog *d = [[Dog alloc ] init];
       [d run];

    结果如下:

    2020-05-04 00:51:33.474658+0800 SWZ[8188:509782] -[Dog(Exchange) play]
    2020-05-04 00:51:33.474795+0800 SWZ[8188:509782] -[Dog(Exchange) play]

总结:可见子类方法执行了新方法play,但是同时父类的run 方法也给换走了,执行了新的play方法

7.3.2. 追踪分析

在分析之前,先明白现在的场景:

  • 父类有该方法及其实现
  • 子类并无该方法极其实现

可见,业务场景向子类交换了该方法,子类查询方法并未找到,于是递归向父类查找,在父类的方法列表里查找成功,进而完成了交换过程。而父类下次调用该方法,结果使用了交换来的新方法。

好一个坑爹滴子类……

7.3.3 解决

面对如此坑爹的子类,解决方法只有一个,方法没有——自己实现。

当子类需要交换某方法的时候,尝试向自己添加待交换走的方法,以防自己未实现,不得不去找父类交换:

BOOL success = class_addMethod([Dog class], oriSEL, swiIMP, method_getTypeEncoding(oriMethod));

然后,如果添加成功,即自身本来并未实现,借着添加的机会实现了。

接下来做的就是,将新添加的方法与目标方法交换:

       if (success) 
            class_replaceMethod([Dog class],
                                swiSEL,
                                oriIMP,
                                method_getTypeEncoding(oriMethod)
                                );

当然, 如果添加失败,即原本就有,那么按原来的逻辑,直接交换两种方法即可

贴一下完整的完善的逻辑如下:

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        SEL oriSEL = @selector(run);
        SEL swiSEL = @selector(play);

        Method oriMethod = class_getInstanceMethod(self, @selector(run));
        Method swiMethod = class_getInstanceMethod(self, @selector(play));

        IMP oriIMP = method_getImplementation(oriMethod);
        IMP swiIMP = method_getImplementation(swiMethod);

        BOOL success = class_addMethod([Dog class], oriSEL, swiIMP, method_getTypeEncoding(oriMethod));
        if (success) {
            class_replaceMethod([Dog class],
                                swiSEL,
                                oriIMP,
                                method_getTypeEncoding(oriMethod)
                                );
        }else{
            method_exchangeImplementations(oriMethod, swiMethod);
        }
    });
}

7.4 坑点2 - 待交换子类父类实现

7.4.1 症状

上面的代码,解决了子类未实现不得不向父类索取,并拿出来交换的弊端。

那么如果该方法,父类都没实现呢?就是将父类的 run方法实现注释,会如何,再走一遍——

不出意外,瘪犊子了。

7.4.2 追踪分析

这里很明白得出结论是,想要像某个类交换方法,结果该类以及向上的父类都没实现,毫无意外会造成崩溃。该怎么办呢?

先看一下一级一级的关系:

  • 交换方法——>找当前类要
    • 找不到当前类的方法实现——找父类要
      • 找不到父类的方法实现——崩溃

结合7.4 里的逻辑,追根溯源,是父类没有实现,那么需要做的是,先给父类添加一个空的实现,以避免崩溃。

然后在子类交换方法的过程中,子类会完善自身的添加方法实现,再去交换方法(这部分的逻辑是7.3)

7.4.3 解决

在交换方法时,先判断原方法是否存在,如果不存在,添加一个空方法实现。

添加部分如下:

        if (!oriIMP) {
            class_addMethod(self, oriSEL, swiIMP, method_getTypeEncoding(swiMethod));
            method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
            }));
        }

全文如下:

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        SEL oriSEL = @selector(run);
        SEL swiSEL = @selector(play);

        Method oriMethod = class_getInstanceMethod(self, @selector(run));
        Method swiMethod = class_getInstanceMethod(self, @selector(play));

        IMP oriIMP = method_getImplementation(oriMethod);
        IMP swiIMP = method_getImplementation(swiMethod);

        if (!oriIMP) {
            class_addMethod(self, oriSEL, swiIMP, method_getTypeEncoding(swiMethod));
            method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
            }));
        }

        BOOL success = class_addMethod(self, oriSEL, swiIMP, method_getTypeEncoding(oriMethod));
        if (success) {
            class_replaceMethod([Dog class],
                                swiSEL,
                                oriIMP,
                                method_getTypeEncoding(oriMethod)
                                );
        }else{
            method_exchangeImplementations(oriMethod, swiMethod);

        }
    });
}

Z、总结

在这篇文章,初步汇总了一下关于Runtime 几个典型的问题,比如基础的SEL、IMP 的关系,以及self 和super 的区别,以及后面深一点的weak 的底层实现,还有业务上用得最多的黑魔法——方法交换使用过程中的几点坑,如果深刻理解了其内部实现,自然能避开这些坑。

希望在日后更深刻的理会这些原理,欢迎大家有问题留言交流。


文章作者: 李佳
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 李佳 !
评论
 上一篇
【类的加载】-(2)懒加载类与分类 【类的加载】-(2)懒加载类与分类
本页所使用的objc runtime 756.2,来自 Apple 开源文档 类的加载探寻系列:1、【类的加载】-(1)类的启动 2、【类的加载】-(2)懒加载类与分类 3、【类的加载】-(3)loading_images 一、懒加
2020-03-31 李佳
下一篇 
【类的加载】-(1)类的启动 【类的加载】-(1)类的启动
本页所使用的objc runtime 756.2,来自 Apple 开源文档 类的加载探寻系列:1、【类的加载】-(1)类的启动 2、【类的加载】-(2)懒加载类与分类 3、【类的加载】-(3)loading_images 1、ob
2020-03-24 李佳
  目录