iOS runtime

文章目录

  • OC中类和对象的本质
  • 实例对象,类,元类的关系
  • 类的属性
  • 类的方法
  • 消息发送机制
  • Runtime api的使用
  • Runtime 的应用

Runtime是什么?

Runtime是一个运行时库,它提供对Objective-C语言的动态属性的支持。Runtime是一种程序在运行时候的机制,其中最主要的是消息机制。在objective-c中,消息是在程序运行的时候才绑定到方法实现的。

OC中类和对象的本质

在程序编译运行的时候,我们用OC编译的代码其实会经历以下流程
OC代码 -> C++,C语言代码 -> 汇编代码->二进制代码

OC的实现其实是通过C++和C语言去实现的(苹果官方的源码https://opensource.apple.com/tarballs/objc4/,一般包体积最大的都是最新的)

在iOS中,几乎所有的类都继承于NSObject类,所以我们先探究一下这个NSObject这个类,NSObject 这个类里面只有Class isa这个变量

@interface NSObject  {
    Class isa  OBJC_ISA_AVAILABILITY;
}

那Class isa是什么呢?先来看看Class的定义

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

struct objc_class : objc_object {
   // Class ISA;
   Class superclass;//父类
   cache_t cache;//缓存列表
   uintptr_t data_NEVER_USE;  // class_rw_t * plus custom rr/alloc flags
   //
   class_rw_t *data() { 
       return (class_rw_t *)(data_NEVER_USE & ~CLASS_FAST_FLAG_MASK); 
   }
 
 方法列表...
};

Class其实就是一个继承于objc_object结构体objc_class。objc_object是一个包含了指向objc_class 指针isa指针的结构体。所以说,其实我们在OC中写的类,其实就是一个结构体。我们创建对象的时候,其实就是在创建结构体实例

对于objc_object包含一个objc_class类型的isa指针,而objc_class又是继承于
objc_object这个结构体,这是不是有点绕?
其实这个很好理解,就是类其实也是一个对象,所以objc_class会继承objc_object这个结构体。


实例对象,类,元类的关系

看一张经典的图


图3-1 实例对象,类和元类.png

这里有三个角色,实例对象,类,还有元类。对于这三个角色,有以下关系

  • 实例对象是类的实例,类是元类的实例

  • 类是描述实例对象的,包括对象的方法(类声明中的 - 方法),属性,变量等信息。元类是描述类对象的,包括类对象的方法(类声明中的 - 方法)等信息。所以类中的声明的方法,无论是 - 还是 + 方法,都是对象方法,区别在于是说这个对象是实例对象还是类对象。

  • 元类其实也是类,与类的实现都是objc_class结构体

  • 实例对象中的isa指针指向于对应的类,而类对象中的isa指针指向元类

  • objc_class中变量superclass是一个指针,指向父类的objc_class,也就是说指向父类的结构体。类的superclass指向父类的objc_class,根类(NSObject)的superclass是空。

  • 元类的superclass指向父类的元类的。而元类的根类的superclass指向的是类的根类。为什么元类的根类的superclass指向的是类的根类呢?因为元类本来就是类,所以元类的根类是继承于根类NSObject。

  • 关于isa指针
    上面提及到,isa是一个指针指向于类或元类的指针。在arm64架构之前,isa指针是直接指向类或是元类的地址,但是在arm64架构后,对isa指针做了优化,isa指针采用了union的存储方式,用来存储类或元类地址,nonpointer,ha s_assoc,has_cxx_dtor,shiftcls,magic,weakly_referenced,deallocating,extra_rc,has_sidetable_rc等信息(isa指针占8个字节,通过将上述字段的信息存通过位运算存放到isa指针所占的8个字节不同的位中)。


类的结构

上面已经给出了objc_class的结构体实现,其中有一个class_rw_t 类型的data,实现如下

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;//实例对象占据的内存大小
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    const method_list_t * baseMethods;//类原有的方法
    const protocol_list_t * baseProtocols;//类原有的协议
    const ivar_list_t * ivars;//类的变量

    const uint8_t * weakIvarLayout;
    const property_list_t *baseProperties;//类原有的属性
};

struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;//类原有的信息,即未融合category的方法和属性的类

    union {
//采用共用体的方式去存储方法列表
        method_list_t **method_lists;  // RW_METHOD_ARRAY == 1 //包含该类所有category的方法列表
        method_list_t *method_list;    // RW_METHOD_ARRAY == 0 //类原本的方法列表
    };
    struct chained_property_list *properties;
    const protocol_list_t ** protocols;//属性列表

    Class firstSubclass;
    Class nextSiblingClass;
};

类中的变量,属性,方法布局如下


类的结构

类的结构体中有包含了父类的指针,方法的缓存列表, class_rw_t结构的类信息。

类的信息中有一个方法的缓存列表,这个缓存列表是一个hash表。在我们调用过的方法中,会把方法的地址缓存到缓存列表中。调用方法的时候,我们会先查看方法方法列表中是否有对应的方法,没有的情况下再去类信息中查找。散列表是一种较为高效的查找方法,比遍历方法列表高效不小,所以整体上加入方法缓存可以提高调用方法的效率。

类中还包含了一个class_rw_t 类型的data,data中包含了一个class_ro_t 类型只读变量ro,这个ro包含了类的变量,属性,协议,方法等的信息。class_ro_t 既然已经包含了类的这些信息,为什么类的结构体中还要多家一层class_rw_t 类型的da ta层呢?
原因很简单,因为ro只包含了类在编译后的信息,但是我们在开发过程中会在category给类添加属性和方法等,所以需要给类添加额外的信息,而class_rw_t这一层就是承担着这种功能。

了解了类的结构后,我们可以通过runtime中的API去获取到类的变量,属性,方法列表等信息,甚至可以通过runtime去动态创建类。关于runtime的使用可以在文章底部看到

类的方法

  • 方法解析
  • load和initialize
  • class方法
  • super关键词

方法解析

在我们编写OC代码的时候,对象和类方法的调用本质是消息发送,调用objc_msgSend方法,向对象发送一个调用方法的消息。

#if !OBJC_OLD_DISPATCH_PROTOTYPES
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincompatible-library-redeclaration"
OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
#pragma clang diagnostic pop
#else

从上面的源码可知objc_msgSend存在隐藏的默认两个参数。第一个是self,也就是方法的调用者,第二个是sel,也就是需要调用的方法的名称。省略号...这是我们写的OC方法中传入的参数。OC代码中方法调用在运行过程中最终会转换成objc_msgSend的形式。
虽然绝大部分OC代码都是通过消息发送机制去完成调用的,但不是所有的OC代码都是通过objc_msgSend去调用,如load方法,是在类加载被系统直接调用的。

load和initialize的区别

在日常开发中,会经常在类做一些初始化工作,这时候需要用到类的load和initialize方法。load方法是在把类加载到内存中时候会被调用,而initialize是类第一次接收到消息的时候被调用。

加载类的时候是按build phase(如下图所示)中的从上往下的顺序去加载。
对于load方法,当加载类到内存的时候,如果类存在父类的情况下,不管父类的编译顺序先后,会先加载父类的load方法,类中的load这个方法只会由系统调用一次。如果category存在load方法,在调用完类的load方法后,就会按编译顺序调用分类中的load方法。
对于工程中所有类而言,会按照编译顺序去调用load方法。
但如果类存在父类,或是category实现了load方法的时候,调用顺序如下
父类的load(如果存在父类) -> 类的load方法 -> 类的load方法

而且因为在加载类的时候调用load方法,是通过直接调用类或是category的lo ad方法,所以不存在覆盖问题。

对于initialize方法,在第一次给类发送消息的时候(如 [类名 allock] init]),会被调用。initialize和load方法一样,如果存在父类的会优先调用父类的initialize方法,如果子类没有实现initialize方法,则会调用父类的initialize方法。所以如果一个类有多个子类,但是子类里面没有实现initialize方法,父类的initialize方法可能会调用多次。

initialize方法是通过消息发送机制去调用的,如果category实现了initialize方法会存在方法覆盖的问题。为什么覆盖,这里留在另外一篇文章去讨论。

class方法

class 方法作用是获取类的类型,默认实现是在NSObject这个类里面,所以我们即便不实现class方法,也能调用这个方法,但是既然class方法的实现在NSObject中,那怎能保证class方法能准确的返回我们的类呢?看一下下面的源码

+ (Class)class {
    return self;
}

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

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

- (Class)superclass {
    return [self class]->superclass;
}

对于类对象,返回的是self,这个self不是指NSObject这个类,指得是objc_msgSend(void /* id self, SEL op, ... */ )中传入的self,也就是调用者,所以这就能保证即便是写在NSObject中的类也能返回正确的值。对于实例对象,返回的是实例对象的isa指针。从上图的3-1可知道,isa指针会指向对应的类对象。

super方法

super 这个方法是告诉应该从父类的方法开始寻找对应的方法。在转换成最终的调用时,调用的不是objc_msgSend这个函数,而是调用objc_msgSendSuper这个函数。调用这个方法会传入两个隐藏参数,分别是objc_super 和 方法名称,objc_msgSendSuper 会根据objc_super的值去查找对应的父类。objc_super是一个结构体,内容如下

struct objc_super {
        id  receiver;
        Class   class;
  };

这里包含了两个变量,分别是调用者和调用者class类型。

由上可知,OC代码的方法调用基本都是由objc_msgSend去实现的,调用一个对象的方法就是给这个对象发送消息。


消息发送机制

上面提及到方法的调用本质上是消息发送机制,消息发送机制的执行流程如下


如果经历了上面的步骤没找到方法的话,就会转入消息动态解析流程


如果动态解析流程不处理消息的话,就会进入消息转发流程


测试消息发送流程的代码如下

#import "ViewController.h"
#import 
#import "ForWardHander.h"
@interface ViewController ()

@end

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    //测试实例方法的消息发送流程
    [self performSelector:@selector(testSendMessage)];
    //测试类方法的消息发送流程
    [ViewController performSelector:@selector(testSendMessage)];
    
}



void testResolveClassMethod(id self, SEL _cmd){
    NSLog(@"动态添加方法");
}


//实例对象的消息发送

+(BOOL)resolveInstanceMethod:(SEL)sel{
    //1 动态添加方法
    if (sel == @selector(testSendMessage)) {
        class_addMethod([self class], sel, (IMP)testResolveClassMethod, "v16@0:8:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

-(id)forwardingTargetForSelector:(SEL)aSelector{
    //2 消息转发-转发方法给其他对象
    if(aSelector == @selector(testSendMessage)){
        return [[ForWardHander alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}



-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    //获取方法签名
    if(aSelector == @selector(testSendMessage)){
        NSMethodSignature *signature = [NSMethodSignature methodSignatureForSelector:@selector(forwardingTargetForSelector:)];
        return  signature;
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    //3 消息转发- 转发方法,在这里可以指定方法调用者和用什么方法代替原本调用的方法。forwardingTargetForSelector只能制定代替处理的对象
    anInvocation.target = self;
    anInvocation.selector = @selector(forwariTestMethodSignature);
    [anInvocation invoke];
}

-(void)forwariTestMethodSignature{
    NSLog(@"测试实例对象的methodSignatureForSelector");
}


//类对象的消息发发送
+(BOOL)resolveClassMethod:(SEL)sel{
    return NO;
}

+(id)forwardingTargetForSelector:(SEL)aSelector{
    if(aSelector == @selector(testSendMessage)){
        return [[ForWardHander alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

+(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if(aSelector == @selector(testSendMessage)){
        NSMethodSignature *signature = [NSMethodSignature methodSignatureForSelector:@selector(forwardingTargetForSelector:)];
        return  signature;
    }
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation{
    anInvocation.target = [self class];
    anInvocation.selector = @selector(forwariTestMethodSignature);
    [anInvocation invoke];
}

+(void)forwariTestMethodSignature{
    NSLog(@"测试类的methodSignatureForSelector");
}
@end

ForWardHander.m的实现如下

#import "ForWardHander.h"

@implementation ForWardHander
-(void)testSendMessage{
    NSLog(@"测试消息转发");
}
@end

这里顺道提一下class_addMethod中最后一个参数types,types传入的是一个字符指针,里面的值构成是

‘返回类型+方法参数总共占的字节数+类型+参数1的字节数开始 +类型+ 参数2的字节数开始+ ...类型+ 参数n的字节数开始’

对于方法,最终转换成objc_msgSend,默认会传入两个参数,objc_msgSend(void /* id self, SEL op, ... */ ),一个是方法调用者,一个是方法名。

所以对于空方法,如test(),types传的值是'v16@0@8'。对于有一个参数的方法,types串的是‘v20@0@8i16’


NSProxy

OC的对象中有两个大的基类,一个是NSObect,一个是NSProxy。NSObject我们在开发中经常用到,但是NSProxy是什么呢?NSProxy是专门用来做消息转发的,相当于一个中介,把你的需求交给其他类去实现。关系如下


NSProxy实现消息转发跟上面消息发送流程最后一步大致一样,需要实现下面的两个方法

- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");

NSProxy和消息发送机制都可以用来做消息转发,但是NSProxy更高效,因为NSProxy不需要经历方法查找,动态方法解析等步骤。

Runtime Api的使用

关于类的API

//动态创建一个类(参数:父类,类名,额外的内存空间)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)
//注册一个类(要在类注册之前添加成员变量)
void objc_registerClassPair(Class cls) 
//销毁一个类
void objc_disposeClassPair(Class cls)
//获取isa指向的Class
Class object_getClass(id obj)
//设置isa指向的Class
Class object_setClass(id obj, Class cls)
//判断一个OC对象是否为Class
BOOL object_isClass(id obj)
//判断一个Class是否为元类
BOOL class_isMetaClass(Class cls)
//获取父类
Class class_getSuperclass(Class cls)

关于变量的API

//获取一个实例变量信息
Ivar class_getInstanceVariable(Class cls, const char *name)
//拷贝实例变量列表(最后需要调用free释放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
//设置成员变量的值
void object_setIvar(id obj, Ivar ivar, id value)
//获取成员变量的值
id object_getIvar(id obj, Ivar ivar)
//动态添加成员变量(已经注册的类是不能动态添加成员变量的)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)
//获取成员变量的名称
const char *ivar_getName(Ivar v)
//获取成员变量的编码类型
const char *ivar_getTypeEncoding(Ivar v)

关于属性的API

//获取一个属性
objc_property_t class_getProperty(Class cls, const char *name)
//拷贝属性列表(最后需要调用free释放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
//动态添加属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                  unsigned int attributeCount)
//动态替换属性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                      unsigned int attributeCount)
//获取属性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)

关于方法的API

//获得实例方法
Method class_getInstanceMethod(Class cls, SEL name)
//获得类方法
Method class_getClassMethod(Class cls, SEL name)
//获取方法实现
IMP class_getMethodImplementation(Class cls, SEL name) 
//设置方法实现
IMP method_setImplementation(Method m, IMP imp)
//替换方法实现
void method_exchangeImplementations(Method m1, Method m2) 
//拷贝方法列表(最后需要调用free释放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)
//动态添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
//动态替换方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
//获取方法名
SEL method_getName(Method m)
//获取方法实现
IMP method_getImplementation(Method m)
//获取方法的编码
const char *method_getTypeEncoding(Method m)
//获取参数数量
unsigned int method_getNumberOfArguments(Method m)
//获取返回类型
char *method_copyReturnType(Method m)
//拷贝
char *method_copyArgumentType(Method m, unsigned int index)
//获取选择器名称
const char *sel_getName(SEL sel)
//注册一个选择器
SEL sel_registerName(const char *str)
//设置block作为方法实现
IMP imp_implementationWithBlock(id block)
//获取block
id imp_getBlock(IMP anImp)
//移除block
BOOL imp_removeBlock(IMP anImp)


Runtime的应用

  • Method Swizzling
  • category中属性关联对象
  • 对象和模型的相互转换

你可能感兴趣的:(iOS runtime)