Runtime笔记

2020-01-15  本文已影响0人  MichealXXX

Runtime是iOS开发中最绕不开的一个话题,它可以说就是iOS的根本,之前关于Runtime看过很多次,过一段时间就会忘记一些细节,这次做个笔记算是加深印象。

Objective-C Runtime是一个将C语言转化为面向对象语言的扩展C++和Objective-C都是在C语言的基础上增加了面向对象的概念,但是二者实现的方式截然不同。C++是静态语言,而Objective-C则是一门动态语言。

C++编写的程序是会在编译期将函数地址加入到可执行文件中的,而Objective-C在编译的时候是不会把地址符号加入到可执行文件中去的,之前写过一篇关于符号绑定到地址上的文章,Objective-C的编译器会生成可执行文件,也就是我们的函数和变量,但是他们与地址的绑定是链接器的工作。函数标识与函数内容之间的关联可以利用Runtime动态修改。RuntimeObjective-C不可缺少的重要部分。

Objective-C Runtime实现的基础

要想彻底的了解Runtime,那么我们就必须了解iOS中的类和对象,又是老生常谈的问题,之前也有写过相关笔记《iOS中的class,SEL,IMP》,这里就再复习一遍,我们都知道在iOS中是由类生成对象,那么我们就来看看iOS中的“”与“对象”。

打开objc.h文件,我们可以看到如下代码:

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

Class是指向objc_class的指针,id是指向objc_object的指针,这就是为什么Objective-C中大部分类型都可以使用id来修饰,objc_object内有一个叫做isa的结构体指针,它则指向了我们的objc_class,可以这么理解isa=英文的is a XXX,它告诉我们这个对象属于哪个类。

我们再来看看objc_class结构体,进入runtime.h文件,objc_class的定义如下:

struct objc_class {
    //metaclass
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    //父类
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    //类名
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    //版本号
    long version                                             OBJC2_UNAVAILABLE;
    // 类信息,供运行时期使用的一些位标识,如CLS_CLASS (0x1L) 表示该类为普通 class,其中包含实例方法和变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
    long info                                                OBJC2_UNAVAILABLE;
    // 该类的实例变量大小(包括从父类继承下来的实例变量)
    long instance_size                                       OBJC2_UNAVAILABLE;
    // 该类的成员变量地址列表
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    // 方法地址列表,与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储实例方法,如CLS_META (0x2L),则存储类方法;
    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;

相比较objc_objectobjc_class中多了很多特征成员,其实类也是对象,分别称作类对象(class object)和实例对象(instance object)

objc_object中的isa指向类对象,我们的实例对象通过isa指针去访问类对象中的成员变量实例方法(“-”方法),而objc_class中的isa则指向metaclass,通过它去访问类方法(“+”方法)

isa与superclass的指向图

objc_class结构体上方还有四个重要的定义:

#if !OBJC_TYPES_DEFINED

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;

/// An opaque type that represents a category.
typedef struct objc_category *Category;

/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;

ivar

类对象中所成员变量ivar,以下是ivar的结构体:

struct objc_ivar {
    // 变量名
    char * _Nullable ivar_name                               OBJC2_UNAVAILABLE;
    // 变量类型
    char * _Nullable ivar_type                               OBJC2_UNAVAILABLE;
    // 基地址偏移字节
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    // 占用空间
    int space                                                OBJC2_UNAVAILABLE;
#endif
} 

objc_class结构体中的objc_ivar_list中就是存储我们类对象中的成员变量。

Method

objc_class这个的objc_method_list里面所包含的内容是我们类里面存在的方法,也就是method如果本类是一个普通的类对象则存储实例方法,如果是metaclass则存储类方法

那我们再来看看方法(method)的结构体定义:

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

这里就又引出两个重要的概念SELIMP

SELselectorObjective-C中的表示类型,selector可以理解为区别方法的ID。

typedef struct objc_selector *SEL;

selector的结构体定义:

struct objc_selector {
    char *name;                       OBJC2_UNAVAILABLE;// 名称
    char *types;                      OBJC2_UNAVAILABLE;// 类型
};

IMPobjc.h中的定义如下:

typedef id (*IMP)(id, SEL, ...);

它是由编译器生成的一个函数指针。当你发起一个消息后(下文介绍),这个函数指针决定了最终执行哪段代码

在了解了SEL和IMP之后,我们就对方法的调用原理有了更深层次的认识,它通过SEL找到相应的函数指针IMP,然后实现函数的实现。

objc_property_t

这个则是我们所定义的属性,与之关联的还有一个objc_property_attribute_t,它是属性的attribute,也就是其实是对属性的详细描述,包括属性名称、属性编码类型、原子类型/非原子类型等,以下是属性的定义:

typedef struct {
    const char *name; // 名称
    const char *value;  // 值(通常是空的)
} objc_property_attribute_t;

Category

这个就是我们所熟悉的类别,或者叫分类,刚刚总结过iOS中的分类

struct objc_category {
    char * _Nonnull category_name                            OBJC2_UNAVAILABLE;
    char * _Nonnull class_name                               OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable instance_methods     OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable class_methods        OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
} 

iOS中的消息传递

iOS中的方法调用我们常常会说是发送消息,也就是iOSobjc_msgSend方法,比如我们创建了一个对象实例objc,让它执行一个实例方法testMethod,转换成我们的OC代码就是下面这样的:

[objc testMethod];

Runtime会将其转成类似这样的代码:

objc_msgSend(objc,testMethod)

来看一下objc_msgSend是如何进行工作的:

第一步:检查这个selector是否要忽略的。

第二步:检查这个对象是否为nilnil对象发送任何一个消息都会被忽略掉。

第三步:
(1)调用实例方法时,它会首先在自身isa指针指向的类(class)methodLists中查找该方法,如果找不到则会通过classsuper_class指针找到父类的类对象结构体,然后从methodLists中查找该方法,如果仍然找不到,则继续通过super_class向上一级父类结构体中查找,直至根class
(2)当我们调用某个某个类方法时,它会首先通过自己的isa指针找到metaclass,并从其中methodLists中查找该类方法,如果找不到则会通过metaclasssuper_class指针找到父类的metaclass对象结构体,然后从methodLists中查找该方法,如果仍然找不到,则继续通过super_class向上一级父类结构体中查找,直至根metaclass
第四步:以上方法都没有找到,则进入消息转发阶段(后面详细说明)。

iOS是一门动态语言

一直说iOS是一门动态语言,那么它动态在哪里呢?在编译期过后我们又可以利用Runtime做些什么呢?

我在看runtime.h的源码中还发现了几个熟悉的函数方法

首先是第一个关于方法交换:

method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

之前我们说过,方法的调用是通过SEL找到对应的IMP来进行函数的实现,那么runtime可以做到在运行期将SELIMP的对应进行更改,从而进行方法的替换。

Method originalMethod = class_getInstanceMethod(aClass, originalSel);
Method swizzleMethod = class_getInstanceMethod(aClass,swizzleSel);
method_exchangeImplementations(originalMethod, swizzleMethod);

使用method_exchangeImplementations便可以将我们的SEL原本所对应的函数指针进行替换。

其次就是我们的关联对象:

/** 
 * Sets an associated value for a given object using a given key and association policy.
 * 
 * @param object The source object for the association.
 * @param key The key for the association.
 * @param value The value to associate with the key key for object. Pass nil to clear an existing association.
 * @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”
 * 
 * @see objc_setAssociatedObject
 * @see objc_removeAssociatedObjects
 */
OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

/** 
 * Returns the value associated with a given object for a given key.
 * 
 * @param object The source object for the association.
 * @param key The key for the association.
 * 
 * @return The value associated with the key \e key for \e object.
 * 
 * @see objc_setAssociatedObject
 */
OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

使用关联对象我们可以动态的为一个类添加属性,用法如下:

-(void)setName:(NSString *)name
{
    objc_setAssociatedObject(self, @"name",name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)name
{
    return objc_getAssociatedObject(self, @"name");    
}

关联对象搭配category使用就可以为一个类动态的增加属性,在SDWebImage中就多次使用关联对象为类增加一个属性,用于管理请求的URL

消息转发

消息转发流程图

当我们的对象想调用方法,始终无法找到就会进入消息转发过程,这是我们最后的补救的机会,看一下消息转发的流程:

第一步:通过resolveInstanceMethod:方法决定是否动态添加方法。如果返回Yes则通过class_addMethod动态添加方法,消息得到处理,结束;如果返回No,则进入下一步;

第二步:这步会进入forwardingTargetForSelector:方法,用于指定备选对象响应这个selector,不能指定为self。如果返回某个对象则会调用对象的方法,结束。如果返回nil,则进入第三步;

第三步:这步我们要通过methodSignatureForSelector:方法签名,如果返回nil,则消息无法处理。如果返回methodSignature,则进入下一步;

第四步:这步调用forwardInvocation:方法,我们可以通过anInvocation对象做很多处理,比如修改实现方法,修改响应对象等,如果方法调用成功,则结束。如果失败,则进入doesNotRecognizeSelector方法,若我们没有实现这个方法,那么就会crash

用源码给大家解释一下,首先我定义一个类叫做BadStudent,我让他去学习,可是他没有任何方法。

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface BadStudent : NSObject

@end

NS_ASSUME_NONNULL_END

我让他去执行学习这个动作,结果不出意外找不到方法,而且程序闪退

BadStudent *bad = [[BadStudent alloc] init];
[bad performSelector:@selector(doStudy)];

//控制台输出
-[BadStudent doStudy]: unrecognized selector sent to instance 0x6000034ec290

这时我强制让他去学习,在消息转发阶段动态为其增加一个方法,在BadStudent.m中增加如下代码:

@implementation BadStudent

+(BOOL)resolveInstanceMethod:(SEL)sel{
    
    if ([NSStringFromSelector(sel) isEqualToString:@"doStudy"]) {
        class_addMethod(self, sel, (IMP)otherStudy, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];

}

void otherStudy(id self, SEL cmd)
{
    NSLog(@"我在学习!");
}


@end

这时就可以正常执行方法了

2020-01-15 16:35:34.308702+0800 MethodForwarding[53375:8110706] 我在学习!

接下来为大家演示一下完整的消息转发流程,我不强迫他学习,但是他找到了一个好学生替他完成学习,我又创立了一个GoodStudent类,其中含有关于学习的方法:

@interface GoodStudent : NSObject

- (void)doStudy;

@end

@implementation GoodStudent

- (void)doStudy{
    NSLog(@"帮BadStudent学习");
}

@end

我们再来看BadStudent中做了什么来完成这一操作的:

@implementation BadStudent
//不添加方法
+(BOOL)resolveInstanceMethod:(SEL)sel{
    return NO;
}
//暂时没找到新的对象
- (id)forwardingTargetForSelector:(SEL)aSelector{
    return nil;
}
//方法签名找doStudy这个方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if ([NSStringFromSelector(aSelector) isEqualToString:@"doStudy"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    
    return [super methodSignatureForSelector:aSelector];
}
//找到有人能完成学习这个动作,找到那个对象让他去学习
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    SEL sel = anInvocation.selector;
    
    GoodStudent *good = [GoodStudent new];
    if ([good respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:good];
    }
    else{
        [self doesNotRecognizeSelector:sel];
    }
}

@end

//控制台输出
2020-01-15 16:40:32.662954+0800 MethodForwarding[53451:8145439] 帮BadStudent学习

这就是完整的消息转发流程,热更新控件JSPatch就是利用了消息转发来实现动态修复线上代码。

上一篇 下一篇

猜你喜欢

热点阅读