【iOS开发进阶】-RunTime

1.基本概念

编译时与运行时

源代码转换为可执行的程序,通常需要经过三个步骤:编译、链接、运行,不同的编译语言,这三个步骤中所进行的操作又有些不同。

编译时就是正在编译的时候,即编译器将源代码翻译成机器能识别的代码的过程。编译时知识对语言进行最基本的检查报错,包括词法分析、语法分析等,编译通过并不意味着程序就可以成功运行。

运行时就是程序通过编译后,编译好的代码被装载到内存中跑起来的阶段,这个时候会具体对类型进行检查,而不仅仅是对代码简单扫描分析,此时如果出错,程序会崩溃。

静态语言与动态语言

静态语言就是在编译阶段就已经确定了所有变量的数据类型,同时也确定好了要调用的函数,以及函数的实现,如C语言。

动态语言在编译阶段并不知道变量的具体数据类型,也不知道真正调用的哪个函数。只有在运行期间才检查变量的数据类型,同时在运行时才会根据函数名查找要调用的具体函数。程序没运行的时候,并不知道调用一个方法具体会发生什么。如Objective-C语言。

 

Objective-C将一些决定性的工作从编译阶段、链接阶段推迟到运行时阶段的机制,使得Objective-C变得更加灵活。甚至可以在程序运行的时候,动态的去修改一个方法的实现,为“热更新”机制提供可能性。

实现Objective-C语言运行时机制的一切基础就是Runtime,Runtime实际上就是一个库,这个库可以在程序运行时动态的创建对象、检查对象、修改类和对象的方法。

 

2.方法调用机制

Objective-C作为扩展于C语言的一种面向对象的编程语言,然而其方法的调用方式又和大多数面向对象语言大有不同,其采用消息传递、转发的方式进行方法的调用。这种机制使得Objective-C中对象的真正行为往往在运行时确定而非在编译时确定,所以被称为运行时动态语言。

在Objective-C语言中采用中括号包裹的方式,例如[obj function]进行方法的调用。实际上,Objective-C中的每一句方法调用最后都会被转换成一条消息进行发送。一条消息包含三部分内容:方法选择器、接收消息的对象以及参数。obj_msgSend函数就用来发送这种消息

MyObject.m

#import "MyObject.h"

@implementation MyObject
- (void)showSelf:(NSString *)name age:(int)age{
    NSLog(@"MyObject:%@,%d",name,age);
}
@end

main.m

#import 
#import "AppDelegate.h"
#import "MyObject.h"
#import "objc/message.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        MyObject *obj = [[MyObject alloc] init];
        [obj class];
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wundeclared-selector"
//        ((void(*)(id,SEL))objc_msgSend)(obj,@selector(showSelf));
        ((void(*)(id,SEL,NSString*,int))objc_msgSend)(obj,@selector(showSelf:age:),@"abc",22);
#pragma clang diagnostic pop
        
    }
    return 0;
}

如上可知,通过@selector(方法名)可以获取到一个SEL类型的对象,SEL实际上是object_selector结构体指针,也可以将其理解为函数签名,在程序的编译阶段,定义类中所有方法会生成一个方法签名列表,这个列表是类直接关联的,在运行时通过方法签名表来找到具体要执行的函数。

再看上例使用到的objc_msgSend()函数,其第一个参数为接收消息的对象,第二个参数为方法签名,之后为传递的参数,那么Objective-C运行时是如何根据一个对象实例来找到方法签名表再找到要执行的方法呢?此处则需要理解相关的内部构造

3.Runtime概念解析

1.Class(类)

在Runtime中,Class被定义为指向objc_class结构体的指针,objc_class结构体的数据结构如下:

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

struct objc_class {
    Class _Nonnull isa;                                          // objc_class 结构体的实例指针

#if !__OBJC2__
    Class _Nullable super_class;                                 // 指向父类的指针
    const char * _Nonnull name;                                  // 类的名字
    long version;                                                // 类的版本信息,默认为 0
    long info;                                                   // 类的信息,供运行期使用的一些位标识
    long instance_size;                                          // 该类的实例变量大小;
    struct objc_ivar_list * _Nullable ivars;                     // 该类的实例变量列表
    struct objc_method_list * _Nullable * _Nullable methodLists; // 方法定义的列表
    struct objc_cache * _Nonnull cache;                          // 方法缓存
    struct objc_protocol_list * _Nullable protocols;             // 遵守的协议列表
#endif

};

从上可知,objc_class结构体定义了很多变量,如自身的所有实例变量(ivars)、所有方法定义(methodLists)、遵守的协议列表(protocols)等。objc_class结构体存放的数据称为元数据(metadata)。

2.Object(对象)

Object(对象)被定义为objc_object结构体,数据结构如下:

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa;       // objc_object 结构体的实例指针
};

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

此处的id被定义为一个指向objc_object的结构体指针,而objc_object结构体只包含一个Class类型的isa指针。

也就是说,Object(对象)唯一保存的就是它所属Class(类)的地址。当调用一个对象的方法时,比如[receiver selector];它就会通过objc_object结构体的isa指针去找对应的object_class结构体,然后在object_class结构体的methodLists(方法列表)中找到相应的方法,然后执行。

3.Meta Class(元类)

object_class结构体的isa指针指向的是类对象自身的Meta Class(元类)。

而元类就是一个类对象所属的类。一个对象所属的类叫作类对象,而一个类对象所属的类叫作元类

Runtime中将类对象所属类型叫作Meta Class(元类),用于描述类对象本身所具有的特征,而元类的methodLists中,保存了类的方法链表,即“类方法”。并且类对象中的isa指针指向的就是元类,每个类对象有且仅有一个与之相关的元类。

因此,方法调用的基本机制为:

对象方法调用过程

1.通过对象isa指针找到对应的Class(类)

2.在Class(类)的methodList(方法列表)中寻找对应的selector

3.执行对应的selector

例:

[@"a" stringByAppendingString:@"b"];

类方法调用过程

1.通过类对象isa指针找到所属的Meta Class(元类)

2.在Meta Class(元类)的methodList(方法列表)中寻找对应的selector

3.执行对应的selector

例:

[NSString stringWithFormat:@"%@",@"hello"];

因此,实例对象、类、元类之间的关系为:

【iOS开发进阶】-RunTime_第1张图片

4.Method(方法)

objc_class结构体的methodLists(方法列表)中存放的元素就是Method(方法)。

表示Method的objc_method结构体的数据结构为

/// An opaque type that represents a method in a class definition.
/// 代表类定义中一个方法的不透明类型
typedef struct objc_method *Method;

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

由上可知,objc_method结构体中包含了method_name、method_types和method_imp三个变量

1.SEL method_name

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

SEL是一个指向objc_selector的指针。

2.IMP method_imp

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

IMP实质上是一个函数指针,所指向的就是方法的实现。IMP用来找到函数地址,然后执行函数。

3.char *method_types

方法类型method_types是个字符串,用来存储方法的参数类型和返回值类型。

 

因此,Method将SEL(方法名)和IMP(函数指针)关联起来,当一个对象发送消息时,通过给出的SEL(方法名)去找到IMP(函数指针),然后执行。

4.消息转发机制

如果接收对象无法处理,其父类、父类的父类都无法处理,那么应该怎么办?在这种情况下,Objective-C为了增强语言的动态性,程序并不会马上Crash,在Crash前,有三次机会可以挽救本条消息的命运。

1.动态添加(resolveInstanceMethod)

如果对象的整个继承链都无法处理当前消息,那么首先会调用接收对象所属类的resolveInstanceMethod方法(如果是类方法,则会调用resolveClassMethod方法),在这个方法中,开发者有机会为类动态添加方法,如果动态添加了方法,则可以在这个方法中返回YES,那么这条消息依然会被成功处理。

例:

main.m

#import 
#import "AppDelegate.h"
#import "MyObject.h"
#import "objc/message.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        MyObject *obj = [[MyObject alloc] init];
        [obj class];
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wundeclared-selector"
        ((void(*)(id,SEL))objc_msgSend)(obj,@selector(showSelf));
#pragma clang diagnostic pop
        
    }
    return 0;
}

MyObject.m

#import "MyObject.h"
#import "objc/message.h"

@implementation MyObject
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"resolveInstanceMethod");
    if ([NSStringFromSelector(sel) isEqualToString:@"showSelf"]) {
        class_addMethod(self, sel, (IMP)newFunc, "v@");
    }
    return [super resolveInstanceMethod:sel];
}

void newFunc(id obj, SEL _cmd) {
    NSLog(@"newFunc");
}
@end

2.消息转发(forwardingTargetForSelector)

当通过运行时添加方法被否定后,系统会接着调用forwardingTargetForSelector方法,这个方法用来对消息进行转发。forwardingTargetForSelector方法需要返回一个id类型的对象,系统会将当前对象服务处理的消息转发给这个方法返回的对象,如果这个返回的对象可以处理,那么程序依然可以执行下去。如果返回nil,则表示不进行消息转发。

例:

SubObject.m

#import "SubObject.h"

@implementation SubObject
- (void)showSelf {
    NSLog(@"subObject");
}
@end

MyObject.m

#import "MyObject.h"
#import "SubObject.h"
#import "objc/message.h"

@implementation MyObject
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"forwardingTargetForSelector");
    if ([NSStringFromSelector(aSelector) isEqualToString:@"showSelf"]) {
        return [SubObject new];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

3.方法签名(methodSignatureForSelector)

如果消息转发策略也被否定,系统会调用methodSignatureForSelector方法,这个方法的主要用途是询问这个选择器是否是有效的,需要返回一个NSMthoedSignature对象,这个对象就是函数签名的抽象。

如果返回了有效的函数签名,那么接着系统会调用forwardInvocation方法,这个函数会直接将消息包装成NSInvocation对象传入,再直接将其发送给可以处理此消息的对象即可。

#import "MyObject.h"
#import "SubObject.h"
#import "objc/message.h"

@implementation MyObject
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"methodSignatureForSelector");
    if ([NSStringFromSelector(aSelector) isEqualToString:@"showSelf"]) {
        return [[SubObject new] methodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"forwardInvocation");
    if ([NSStringFromSelector(anInvocation.selector) isEqualToString:@"showSelf"]) {
        [anInvocation invokeWithTarget:[SubObject new]];
    } else {
        [super forwardInvocation:anInvocation];
    }
}
@end

因此,完整的流程如下:

【iOS开发进阶】-RunTime_第2张图片

 

消息发送与转发机制总结

调用[receiver selector]之后,流程如下:

编译时阶段:[receiver selector]方法被编译器转换为:

  • objc_msgSend(receiver,selector) 不带参数

  • objc_msgSend(receiver,selector,org1,org2,....) 带参数

运行时阶段:消息接受者receiver寻找对应的selector

  1. 通过receiver的isa指针找到receiver的class(类);

  2. 在class的cache(方法缓存)的散列表中寻找对应的IMP(方法实现);

  3. 如果在cache中没有找到对应的IMP的话,继续在Class的methodList(方法列表)中找对应的selector,如果找到,填充到cache中,并返回selector;

  4. 如果在Class中没有找到这个selector,就继续在它的superClass(父类)中寻找;

  5. 一旦找到对应的selector,直接执行receiver对应selector方法实现的IMP;

  6. 如果找不到对应的selector,Runtime系统进入消息转发机制

运行时消息转发阶段

  1. 动态解析:通过重写resolveInstanceMethod或者resolveClassMethod方法,利用class_addMethod方法添加其他函数实现。

  2. 消息转发:在当前对象中利用forwardingTargetForSelector方法将消息的接受者转发给其他对象

  3. 消息重定向:如果上一步返回值为nil,则利用methodSignatureForSelector方法获取函数的参数和返回值类型:

    1. 如果methodSignatureForSelector返回了一个NSMethodSignature对象,Runtime系统就会创建一个NSInvocation对象,并通过forwardInvocation消息通知当前对象,给予此次消息发送最后一次寻找IMP的机会;

    2. 如果methodSignatureForSelector返回nil,则Runtime系统会发出doesNotRecognizeSelector消息,程序Crash。

5.具体应用

动态创建一个类

main.m

#import 
#import "AppDelegate.h"
#import "MyObject.h"
#import "objc/message.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        MyObject *obj = [[MyObject alloc] init];
        [obj class];
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wundeclared-selector"
        ((void(*)(id,SEL))objc_msgSend)(obj,@selector(showSelf));
#pragma clang diagnostic pop
        
    }
    return 0;
}

MyObject.m

#import "MyObject.h"
#import "SubObject.h"
#import "objc/message.h"

@implementation MyObject
- (void)showSelf {
   NSString *className = @"NSStringSubClass";
    //定义类名
    Class newClass = objc_allocateClassPair(NSString.class, className.UTF8String, 0);
    //动态创建类,对类进行内存分配,三个参数:父类、类名称、额外字节
    class_addMethod(newClass, @selector(eat), (IMP)EatFunction, "v@:");
    //动态为类添加方法,四个参数:为哪个类添加参数、方法名、方法实现、返回值与方法参数
    objc_registerClassPair(newClass);
    //注册该类
    
    id instanceOfClass = [[newClass alloc] init];
    //创建该类
//    [instanceOfClass eat];  编译通不过
    [instanceOfClass performSelector:@selector(eat)];
    //调用该类方法
}

void EatFunction(id self, SEL _cmd) {
    //方法实现
    NSLog(@"EatFunction....");
}
@end

动态替换一个类

#import 
#import "AppDelegate.h"
#import "MyObject.h"
#import "SubObject.h"
#import "objc/message.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        MyObject *obj = [[MyObject alloc] init];
        [obj class];
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wundeclared-selector"
        ((void(*)(id,SEL))objc_msgSend)(obj,@selector(showSelf));
        
        object_setClass(obj, [SubObject class]);
        //将实例替换成另外一个类
        ((void(*)(id,SEL))objc_msgSend)(obj,@selector(showSelf));
#pragma clang diagnostic pop
        
    }
    return 0;
}

2020-09-24 12:01:22.941989+0800 Test[32351:1216193] MyObject showSelf....

2020-09-24 12:01:22.942219+0800 Test[32351:1216193] SubObject showSelf....

动态获取类信息

NSLog(@"%s",class_getName([obj class]));
//获取类名
NSLog(@"%d",class_getVersion([obj class]));
//获取类的版本
NSLog(@"%@",class_getSuperclass([obj class]));
//获取父类
NSLog(@"%zu",class_getInstanceSize([obj class]));

动态获取所有属性(非私有与私有皆可)

unsigned int *count = malloc(sizeof(unsigned int));
Ivar *mem = class_copyIvarList([MyObject class], count);
for (int i = 0; i < *count; i++) {
    Ivar var = *(mem + i);
    const char *name = ivar_getName(var);
    const char *type = ivar_getTypeEncoding(var);
    NSLog(@"%s:%s\n",name,type);
}
free(count);
count = nil;

动态设置相关属性

unsigned int *count = malloc(sizeof(unsigned int));
Ivar *mem = class_copyIvarList([MyObject class], count);
MyObject *obj = [[MyObject alloc] init];
NSLog(@"%@",obj);
object_setIvar(obj, mem[0], (__bridge id)(void *)10);
object_setIvar(obj, mem[1], @"isTwo");
object_setIvar(obj, mem[2], @"isThree");
NSLog(@"%@",obj);

动态获取所有方法

unsigned int count = 0;
Method *mem = class_copyMethodList([MyObject class], &count);
for (int i = 0; i < count; i++) {
    SEL name = method_getName(mem[i]);
    NSString *method = [NSString stringWithCString:sel_getName(name) encoding:NSUTF8StringEncoding];
    NSLog(@"\n%@",method);
}

动态替换方法

//创建对象
MyObject *obj = [[MyObject alloc] init];
NSLog(@"%@",[obj method1]);
//替换方法
class_replaceMethod([MyObject class], @selector(method1), (IMP)logHHH, "v");
[obj method1];

动态添加方法

unsigned int count = 0;
class_addMethod([MyObject class], @selector(method3), (IMP)logHAHA, "v");
Method *mem = class_copyMethodList([MyObject class], &count);
for (int i = 0; i < count; i++) {
    SEL name = method_getName(mem[i]);
    NSString *method = [NSString stringWithCString:sel_getName(name) encoding:NSUTF8StringEncoding];
    NSLog(@"\n%@",method);
}

参考:

OC语言的动态特性

iOS 开发:『Runtime』详解

iOS Runtime 教程

Runtime-iOS运行时应用篇

iOS - OC 使用运行时来获取并修改类

runtime底层实现原理

你可能感兴趣的:(iOS开发进阶,ios)