Runtime机制详解和使用场景

626 阅读6分钟

详解

静态类型语言与动态类型语言

  • 静态类型语言:变量的数据类型在编译时就可以确定的语言,多数静态类型的语言要求在使用变量之前必须声明类型。函数的调用在编译的时候会决定调用哪个函数。C,C++,Java,C# 都属于静态类型语言。

  • 动态类型语言:变量的数据类型在运行时确定的语言,变量在使用之前不需要变量类型声明,通常的变量类型是被赋值的那个变量的类型。Python,ruby,OC,js这些都是动态类型的语言。

OC是一门动态语言,它不仅需要一个编译器,也需要一个运行时系统,其幕后最大的功臣就是runtime,runtime是基于C和混编来写的,其中最主要的是消息机制。在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

Runtime相关的头文件

usr/include/objc文件夹下面有这样几个文件 image.png 主要使用的函数定义在message.h和runtime.h这两个文件中,在message.h中主要包含了一些向对象发送消息的函数,这是OC对象方法调用的底层实现。runtime.h是运行时最重要的文件,其中包含了对运行时进行操作的方法。 使用时,需要导入文件,导入如:

#import <objc/runtime.h>
#import <objc/message.h>

对象(object),类(class),方法(method)这几个的结构体:

对象

它的isa指针指向类对象,

struct objc_object {
    
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

它实际上是一个指向objc_class结构体的指针,第一个成员变量也是isa指针,这就说明了Class本身其实也是一个对象,因此我们称之为类对象,类对象在编译期产生用于创建实例对象,是单例。

struct objc_class {
    // `isa`指针 类对象的`isa`指针指向元类(`metaclass`),元类中保存了创建类对象以及类方法所需的所有信息
    元类的`isa`指针又指向了自己。
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
   //父类的指针 `super_class`指针指向了父类的类对象,
     元类的`super_class`指针指向了父类的元类
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
   // 类的名字
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
   // 版本
    long version                                             OBJC2_UNAVAILABLE;

    long info                                                OBJC2_UNAVAILABLE;
   // 实例大小
    long instance_size                                       OBJC2_UNAVAILABLE;
   // 实例变量列表
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
   // 方法列表
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
   // 缓存
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
   // 遵守的协议列表
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;

#endif

} OBJC2_UNAVAILABLE;
#### 方法列表
struct objc_method_list {

    struct objc_method_list * _Nullable obsolete             OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;

#ifdef __LP64__

    int space                                                OBJC2_UNAVAILABLE;

#endif

    /* variable length structure */

    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;

}
#### 方法
struct objc_method {
// 方法名
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
// 方法类型
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
// 方法实现
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;

}

函数的定义

  • 对对象进行操作的方法一般以object_开头
  • 对类进行操作的方法一般以class_开头
  • 对类或对象的方法进行操作的方法一般以method_开头
  • 对成员变量进行操作的方法一般以ivar_开头
  • 对属性进行操作的方法一般以property_开头开头
  • 对协议进行操作的方法一般以protocol_开头

类缓存(objc_cache)

Objective-C运行时通过跟踪它的isa指针检查对象时,它可以找到一个实现许多方法的对象。然而,你可能只调用它们的一小部分,并且每次查找时,搜索所有选择器的类分派表没有意义。所以类实现一个缓存,每当你搜索一个类分派表,并找到相应的选择器,它把它放入它的缓存。所以当objc_msgSend查找一个类的选择器,它首先搜索类缓存。这是基于这样的理论:如果你在类上调用一个消息,你可能以后再次调用该消息。 为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime系统实际上非常快,接近直接执行内存地址的程序速度。

分类(objc_category)

从定义中看出,category 可以:添加实例方法(instanceMethods),类方法(classMethods),协议(protocols)和实例属性(instanceProperties)。不可以:不能够添加实例变量

虽然category可以添加实例属性@property,但是它既不会生成新的实例变量,也不会生成相应的setter、getter方法。也就是添加了其实也没啥作用。在编译的时候调用是没有问题的,编译器不会报错;但是在运行时的时候就会崩溃。因为找不到getter/setter方法。想使用就需要自己去关联对象实现,关联对象也是不会自动生成新的实例变量的,这是因为在clas里的Ivarlist、IvarLayout大小在编译器都确定了,不能修改。

Category是表示一个指向分类的结构体的指针,其定义如下:

struct category_t { 

   // 是指 class_name 而不是 category_name。
    const char *name; 
    // 要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对 应到对应的类对象。
    classref_t cls; 
    // 实例方法
    struct method_list_t *instanceMethods; 
    // 类方法
    struct method_list_t *classMethods;
    // 协议
    struct protocol_list_t *protocols;
    // 属性
    struct property_list_t *instanceProperties;
};

使用场景

runtime消息转发

当我们调用一个方法时,其实就是给对象发送一个消息,runtime会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为NSObject),如果还没有找到,runtime会给最后的三次补救机会。如果未做补救那就会报unrecognized selector

  • 动态方法解析
  • 指定对象去执行
  • 消息重定向,这个消息转发出去

image.png

Runtime应用

Runtime简直就是做大型框架的利器。它的应用场景非常多,下面就介绍一些常见的应用场景。

  • 关联对象(Objective-C Associated Objects)给分类增加属性
@property (nonatomic, strong, nullable) UIImage                   *mt_emptyDataImag;
static char const * const kEmptyDataImag             =       "mt_emptyDataImag";
- (UIImage *)mt_emptyDataImag
{
    return objc_getAssociatedObject(self, kEmptyDataImag);
}
- (void)setMt_emptyDataImag:(UIImage *)mt_emptyDataImag
{
    objc_setAssociatedObject(self, kEmptyDataImag, mt_emptyDataImag, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
  • 黑魔法(Method Swizzling)方法替换(NSArray越界处理,URL转义处理,VC全屏present处理,埋点等)
@implementation UIViewController (FullModal)
+ (void)load{
    SEL originalSel = @selector(presentViewController:animated:completion:);
    SEL overrideSel = @selector(override_presentViewController:animated:completion:);
    Method originalMet = class_getInstanceMethod(self.class, originalSel);
    Method overrideMet = class_getInstanceMethod(self.class, overrideSel);
    method_exchangeImplementations(originalMet, overrideMet);
}

#pragma mark - Swizzling
- (void)override_presentViewController:(UIViewController *)viewControllerToPresent animated: (BOOL)flag completion:(void (^ __nullable)(void))completion{
    if(@available(iOS 13.0, *)){
        if (viewControllerToPresent.modalPresentationStyle ==  UIModalPresentationPageSheet){
            viewControllerToPresent.modalPresentationStyle = UIModalPresentationFullScreen;
        }
    }
    [self override_presentViewController:viewControllerToPresent animated:flag completion:completion];

}
  • 消息转发处理crash
 // 动态解析
 + (BOOL)resolveInstanceMethod:(SEL)sel
 // 指定对象执行
 - (id)forwardingTargetForSelector:(SEL)aSelector
 //重定向转发
 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation
  • 实现NSCoding的自动归档和自动解档
- (id)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super init]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([self class], &count);
        for (int i = 0; i < count; i++) {
            Ivar ivar = ivars[i];
            const char *name = ivar_getName(ivar);
            NSString *key = [NSString stringWithUTF8String:name];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
       free(ivars);
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivars[i];
        const char *name = ivar_getName(ivar);
        NSString *key = [NSString stringWithUTF8String:name];
        id value = [self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
    free(ivars);
}
  • 实现字典和模型的自动转换(MJExtension)