美文网首页
iOS Runtime全面解析

iOS Runtime全面解析

作者: 一生随愿为剑客 | 来源:发表于2021-04-07 17:34 被阅读0次

Object-C采用"消息结构"而非”函数调用“。对于函数调用的语言,是由编译器决定的。而消息结构的语言,其运行所执行的代码由运行环境来决定。 而这个运行环境,就是Runtime

一. 消息机制

1.1. 消息传递

消息机制是Runtime的核心,方法调用的过程可以看做是消息传递的过程。

先来熟悉下类的基本结构,在iOS中,基本上所有类都直接或者间接继承于NSObject(也有NSProxy这种例外),那么来看下NSObject:

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

NSObject中持有一个Class类型的isa指针,那么这个Class是什么呢?来看一下:

typedef struct objc_class *Class;

struct objc_class {
  Class _Nonnull isa  OBJC_ISA_AVAILABILITY; // 指向metaclass
  Class _Nullable super_class   OBJC2_UNAVAILABLE; // 指向其父类
  const char * _Nonnull name    OBJC2_UNAVAILABLE; // 类名
  long version    OBJC2_UNAVAILABLE; // 类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改、读取
  long info   OBJC2_UNAVAILABLE; // 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
  long instance_size   OBJC2_UNAVAILABLE; // 该类的实例变量大小(包括从父类继承下来的实例变量);
  struct objc_ivar_list * _Nullable ivars  OBJC2_UNAVAILABLE; // 用于存储每个成员变量的地址
  struct objc_method_list * _Nullable * _Nullable methodLists  OBJC2_UNAVAILABLE; 
  // 与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法;
  struct objc_cache * _Nonnull cache  OBJC2_UNAVAILABLE; //指向最近使用的方法的指针,用于提升效率;
  struct objc_protocol_list * _Nullable protocols   OBJC2_UNAVAILABLE; // 存储该类遵守的协议
}

可以看到,objc_class中也有多个元素,除了类型父类等能够理解顾名思义的元素,特别需要注意的是isacache两个元素。cache是将用过的方法存储到其内,优先查找,是典型的时空装换。而对于类的isa, 它是指向元类的,也就是说:

mateClass(元类)生成Class(类/类对象), Class(类)生成obj(对象)。用一张经典图来说明:

有了以上的基础,那么消息传递就会容易理解很多。例如我们调用一个实例方法:

 [obj  test];

转化为汇编代码:

objc_msgSend(obj,sel_registerName("test"));

接下来会调用_class_lookupMethodAndLoadCache3方法,看下其具体实现:

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{        
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
 IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;
    runtimeLock.assertUnlocked();
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }
    runtimeLock.lock();
    checkIsKnownClass(cls);
    if (!cls->isRealized()) {
        realizeClass(cls);
    }
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
    }
 retry:    
    runtimeLock.assertLocked();
    imp = cache_getImp(cls, sel);
    if (imp) goto done;
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    break;
                }
            }
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        triedResolver = YES;
        goto retry;
    }
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);
 done:
    runtimeLock.unlock();
    return imp;
}

即开始先从cache查找:

if (cache) {
    imp = cache_getImp(cls, sel);
    if (imp) return imp;
}

如果缓存命中,直接返回imp。如果没有命中,继续往下走,先判断类有没有加载到内存,如果没有,先加载类:

  checkIsKnownClass(cls);
  if (!cls->isRealized()) {
     realizeClass(cls);
  }

判断是否实现了initialize,如果有实现,先调用initialize

if (initialize  &&  !cls->isInitialized()) {
    runtimeLock.unlock();
    _class_initialize (_class_getNonMetaClass(cls, inst));
    runtimeLock.lock();
}

在类对象的方法列表查找imp:

 Method meth = getMethodNoSuper_nolock(cls, sel);
      if (meth) {
         log_and_fill_cache(cls, meth->imp, sel, inst, cls);
         imp = meth->imp;
         goto done;
      }
 }

如果没有找到,继续在父类的缓存的方法列表中查找imp

   unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    break;
                }
            }
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }

imp还没有找到,则尝试做一次动态方法解析:

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);//这里做一次动态方法解析。
        runtimeLock.lock();
        triedResolver = YES;
        goto retry;
    }

最终没有找到imp,并且方法解析也没有处理,那么则进入消息转发流程:

  imp = (IMP)_objc_msgForward_impcache;

1.2. 消息转发

在调用对象拿到对应的selector之后,如果自己无法执行这个方法,那么该条消息要被转发。或者临时动态的添加方法实现。如果转发到最后依旧没法处理,程序就会崩溃。

如以下例子:

新建一个Person类继承于NSObject,并声明一个msgTest方法(不实现);

@interface Person : NSObject

- (void)msgTest;

@end

调用该方法:

- (void)viewDidLoad {
    [super viewDidLoad];
    Person * p = [Person new];
    [p msgTest];
}

此时我们将项目跑起来就会发现,项目是能通过编译的,但是会崩溃掉:

-[Person msgTest]: unrecognized selector sent to instance 0x6000020543e0

在方法在调用时,系统会查看这个对象能否接收这个消息(没有实现这个方法),如果不能接收,就会调用下面这几个方法,会采用拯救模式,给你“补救”的机会。

第一次补救: 动态方法解析
/*
cls:要添加方法的类
name:选择器
imp:方法实现,IMP在objc.h中的定义是:typedef id (*IMP)(id, SEL, ...);该方法至少有两个参数,self(id)和_cmd(SEL)
types:方法,参数和返回值的描述,"v@:"表示返回值为void,没有参数
*/
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(msgTest)){
        return   class_addMethod([self class],sel, (IMP)reTest, "v@:");
  }
    return [super resolveInstanceMethod:sel];
}

void reTest(id self, SEL _cmd) {
    NSLog(@"test");
}

可看到打印数据:

learn[47237:860941] test

注: resolveInstanceMethod处理对象方法,resolveClassMethod处理类方法。

第二次补救: 消息重定向

我们继续以实例方法举例:

创建一个新的类RePerson,该类包含有msgTest的实现方法。

#import "RePerson.h"

@implementation RePerson

- (void)msgTest{
    NSLog(@"rePerson");
}

@end

Person类中进行下两步操作:

  1. resolveInstanceMethod返回值设为NO。
  2. forwardingTargetForSelector返回值为RePerson对象。
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(msgTest)){
        return   NO;
    }
    return [super resolveInstanceMethod:sel];
}

- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(msgTest)){
        return  [RePerson new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

这样就可以得到结果:

 learn[47519:906599] rePerson
第三次补救: 消息转发

关于消息转发,希望您对Type Encodings 、NSMethodSignature 、NSInvocation已经有基本的认知,可查看本人呢另一篇文章Type Encodings 、NSMethodSignature 、NSInvocation三部曲

也是改变调用对象,使该消息在新对象上调用;不同是forwardInvocation方法带有一个NSInvocation对象,这个对象保存了这个方法调用的所有信息,包括SEL,参数和返回值描述等。

同样的,我们利用上文中描述的RePerson类,实现以下方法:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    if (anInvocation.selector == @selector(msgTest)){
        [anInvocation invokeWithTarget:[RePerson new]];
        return;
    }
    [super forwardInvocation:anInvocation];
}

同样的,我们也可以拿到如下答案:

learn[47574:911006] rePerson

经典图:


消息转发也是我们处理unrecognized selector crash 的主要方案,减少对应的崩溃。

1.3. 关于NSProxy

说到消息转发这一问题,NSProxy才是消息转发、消息分发的终极答案。

对比上面的一套消息查找过程,NSProxy就简单多了,接收到 unkonwn selector后,直接调用- (NSMethodSignature *)methodSignatureForSelector:- (void)forwardInvocation:进行消息转发。看下YYWeakProxy的源码:

@implementation YYWeakProxy

- (instancetype)initWithTarget:(id)target {
   _target = target;
   return self;
}

+ (instancetype)proxyWithTarget:(id)target {
   return [[YYWeakProxy alloc] initWithTarget:target];
}

- (id)forwardingTargetForSelector:(SEL)selector {
   return _target;
}

- (void)forwardInvocation:(NSInvocation *)invocation {
   void *null = NULL;
   [invocation setReturnValue:&null];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
   return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}

- (BOOL)respondsToSelector:(SEL)aSelector {
   return [_target respondsToSelector:aSelector];
}

- (BOOL)isEqual:(id)object {
   return [_target isEqual:object];
}

- (NSUInteger)hash {
   return [_target hash];
}

- (Class)superclass {
   return [_target superclass];
}

- (Class)class {
   return [_target class];
}

- (BOOL)isKindOfClass:(Class)aClass {
   return [_target isKindOfClass:aClass];
}

- (BOOL)isMemberOfClass:(Class)aClass {
   return [_target isMemberOfClass:aClass];
}

- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
   return [_target conformsToProtocol:aProtocol];
}

- (BOOL)isProxy {
   return YES;
}

- (NSString *)description {
   return [_target description];
}

- (NSString *)debugDescription {
   return [_target debugDescription];
}

@end

其实就是简单的实现这两种方法而已。

它的主要功能之一就是避免循环引用:

 @implementation MyView {
    NSTimer *_timer;
 }
 
 - (void)initTimer {
    YYWeakProxy *proxy = [YYWeakProxy proxyWithTarget:self];
    _timer = [NSTimer timerWithTimeInterval:0.1 target:proxy selector:@selector(tick:) userInfo:nil repeats:YES];
 }
 
 - (void)tick:(NSTimer *)timer {...}
 @end

如上例子, MyView持有Timer, Timer强引用Proxy, Proxy虽然能发送消息到MyView却不会形成强引用。

二. Runtime 应用

Runtime的是iOS中的高频词,具体的使用大致分为以下几个类别:

  • 关联对象(Objective-C Associated Objects)添加对象。
  • 方法交换Method Swizzling
  • 字典和模型的自动转换。

2.1. 关联对象

首先抛出一个问题:分类Category为什么不能直接添加属性。

从逻辑角度来说,Category本来就不是一个真实的类,是在Runtime期间,动态的为相关类添加方法。在编译期间连相关对象都没拿到,如何添加属性?

另一方面,从Category的结构体组成也能证明这一点:

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods; // 对象方法
    struct method_list_t *classMethods; // 类方法
    struct protocol_list_t *protocols; // 协议
    struct property_list_t *instanceProperties; // 属性
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

虽然其中包括了属性的list,但是并不包含成员变量的list, 属性是要自动合成相关的成员变量的,而其明显不具备这一特点。so,该如何做呢 ? 当然还是回到Runtime

Runtime提供了三个函数进行属性关联:

// 关联对象 setter
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
// objec: 被关联对象。key:关联key, 唯一标识。 value:关联的对象。policy: 内存管理的策略。

// 获取关联的对象 getter
id objc_getAssociatedObject(id object, const void *key); 
// 移除关联对象  delloc
void objc_removeAssociatedObjects(id object);

内存策略:

OBJC_ASSOCIATION_ASSIGN,    //等价于 @property(assign)。
OBJC_ASSOCIATION_RETAIN_NONATOMIC,  //等价于 @property(strong, nonatomic)。
OBJC_ASSOCIATION_COPY_NONATOMIC,   //等价于 @property(copy, nonatomic)。
OBJC_ASSOCIATION_RETAIN  //等价于@property(strong,atomic)。
OBJC_ASSOCIATION_COPY   //等价于@property(copy, atomic)。

如我们给一个UIViewController分类添加一个params字典用户接受传递过来的参数:

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UIViewController (Base)

@property (nonatomic, strong) NSDictionary * params;

@end

NS_ASSUME_NONNULL_END

#import "UIViewController+Base.h"
#import <objc/runtime.h>

static const void * jParamsKey = &jParamsKey;

@implementation UIViewController (Base)

- (void)setParams:(NSDictionary *)params{
    objc_setAssociatedObject(self, jParamsKey, params, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSDictionary *)params{
    return objc_getAssociatedObject(self, jParamsKey);
}

@end

2.2. 方法交换 (Method Swizzling)

Method Swizzling 被称为黑魔法, 在iOS编程具有不可动摇的核心地位,修改原有方法指向的特性使其能够十分出色完成以下任务:

  • hook系统方法,例如hook系统字体设置动态修改不同屏幕下字体大小,hook系统生命周期方法达到埋点统计的目的。
  • debug过程中hook原方法来进行bug修复。hook 例如NSArrayindexof去防崩溃。
  • 实现KVO类的观察者方案。

先看代码吧,如果我们实现UIFont的动态方案:

#import "UIFont+Adapt.h"

@implementation UIFont (Adapt)

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self exchangeMethod];
    });
}

+ (void)exchangeMethod{
    Class class = [self class];
    SEL originalSelector = @selector(systemFontOfSize:);
    SEL swizzledSelector = @selector(runTimeFitFont:);
    Method systemMethod = class_getClassMethod(class, originalSelector );
    Method swizzledMethod  = class_getClassMethod(class, swizzledSelector);
    method_exchangeImplementations(systemMethod, swizzledMethod);
}

+ (UIFont *)runTimeFitFont:(CGFloat)fontSize{
    UIFont *fitFont = nil;
    //这里并不会造成循环调用,方法已经被交换
    fitFont = [UIFont runTimeFitFont:fontSize * (Main_Screen_Width / 375 )];
    return fitFont;
}

@end

这里解释下这些代码:

一般情况下,都会写一个分类来实现Method Swizzling。 一般情况下会在load方法里调用,保证在该方法调用之前,已经完成了方法交换。

load方法在不同系统下有不同表现,在iOS10或者其它情况下,会出现多次调用的情况,所以使用dispatch_once方案保证方法交换只实现一次。

hook完成后,我们调用原方法,最终就会调用到交换后的方法,而在交换方法如需调用原方法,类似上面的本来该调用systemFontOfSize:的,但是systemFontOfSize:已经被交换了,所以调用runTimeFitFont:(CGFloat)fontSize就是调用systemFontOfSize:,并不会引起循环调用。

另一个需要注意点是在hook父类的方法时候存在的问题,比如我们有一个HookViewController继承于 BaseViewController继承于UIViewController,如果我们想hook它的viewDidAppear,如果我们直接hook:

+ (void)load{
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    Class class = [self class];
    // 原方法名和替换方法名
    SEL originalSelector = @selector(viewDidAppear:);
    SEL swizzledSelector = @selector(swizzle_viewDidAppear:);
    // 原方法结构体和替换方法结构体
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    // 调用交互两个方法的实现
    method_exchangeImplementations(originalMethod, swizzledMethod);
  });
}

就会报错:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[JTabbarController swizzle_viewDidAppear:]: unrecognized selector sent to instance 0x7fb191811400'

修改成:

+ (void)load {
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
     Class class = [self class];
     // 原方法名和替换方法名
     SEL originalSelector = @selector(viewDidAppear:);
     SEL swizzledSelector = @selector(swizzle_viewDidAppear:);
     // 原方法结构体和替换方法结构体
     Method originalMethod = class_getInstanceMethod(class, originalSelector);
     Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
     // 如果当前类没有原方法的实现IMP,先调用class_addMethod来给原方法添加默认的方法实现IMP
     BOOL didAddMethod = class_addMethod(class,
                       originalSelector,
                       method_getImplementation(swizzledMethod),
                       method_getTypeEncoding(swizzledMethod));

   
     if (didAddMethod) {
     // 添加方法实现IMP成功后,修改替换方法结构体内的方法实现IMP和方法类型编码TypeEncoding
       class_replaceMethod(class,
                 swizzledSelector,
                 method_getImplementation(originalMethod),
                 method_getTypeEncoding(originalMethod));

     } else {
     // 添加失败,调用交互两个方法的实现
       method_exchangeImplementations(originalMethod, swizzledMethod);
     }
  });

}

当然如果我们重写这个方法,也是可以的。

为什么会这样呢?

根据方法的的查找路径,没有重写的话实质会去调用父类的方法,但是父类没有实现Imp,就会失败。

2.3. 字典和模型的自动转换

根据上文,我们已经明白, 类的结构体中包含了成员变量的list, 那么在这个前提下,我们就很轻松的做到字典到模型或者说json到模型的转换。

具体方案如下:

+ (instancetype)modelWithDict:(NSDictionary *)dict{
    id objc = [[self alloc] init];
    //1.获取成员变量
    unsigned int count = 0;
    //获取成员变量数组
    Ivar *ivarList = class_copyIvarList(self, &count);
    for (int i = 0; i < count; i++) {
        //获取成员变量
        Ivar ivar = ivarList[i];
        //获取成员变量名称
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        //获取成员变量类型
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
        //获取key
        NSString *key = [ivarName substringFromIndex:1];
        id value = dict[key];
        // 二级转换:判断下value是否是字典,如果是,字典转换层对应的模型
        // 并且是自定义对象才需要转换
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]){
            //获取class
            Class modelClass = NSClassFromString(ivarType);
            value = [modelClass modelWithDict:value];
        }
        if (value) {
            [objc setValue:value forKey:key];
        }
    }
    return objc;
}

搭配Type Encodings 、NSMethodSignature 、NSInvocation三部曲,相信就能轻松理解这段代码,就不多叙。

三. 总结

这篇文章算是写的比较快的,大致就是想到哪就写一写,Runtime这个话题其实也有无数人写过了,我只是想用自己的思路把这个话题顺一下,有什么问题,欢迎留言。

相关文章

网友评论

      本文标题:iOS Runtime全面解析

      本文链接:https://www.haomeiwen.com/subject/ffndrhtx.html