iOS Runtime运行时

  关于Runtime的用法和详解,一直想研究了解一下,但是一直没有时间进行彻底的学习(说白了就是懒,也就是现在人们普遍存在的拖延症),最近几天公司项目不是很紧,于是抽出几天的时间来学习了一下Runtime。

废话不多说,先上源码和官方API:

在介绍Runtime之前,首先简单的列一下简单的目录,以方便了解本文的主要内容。

  • Runtime的介绍
  • Runtime中的数据结构
  • Runtime中的消息机制
  • 与Runtime的交互
  • Runtime的应用

Runtime介绍

对于Runtime,Apple官方给出的解释是:

The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps.

  我们都知道 Objective-C是一门动态语言,这意味着它不仅需要编译器,它还需要运行时系统来进行代码编译。而Runtime就执行了这一运行时系统代码编译工作。
  Runtime直译就是运行时,是Apple用C和汇编语言编写的一套C语言的API,它正是 Objective-C这门动态语言的核心。我们知道计算机唯一能识别的语言是机器语言,高级编程语言不能被直接识别,需要先编译为汇编语言,再由汇编语言编译为机器语言才能被计算机识别。而 Objective-C语言不能被直接编译为汇编语言,它必须先编译为C语言,然后再编译为汇编语言。而Runtime正是编译器将我们写的 Objective-C代码编译为C语言时用到的核心库。
  Apple和GNU各自维护一个开源的runtime版本,你可以到这里下载Apple维护的开源代码,而且这两个版本之间都在努力保持一致。

Runtime中的数据结构

研究Runtime的数据结构我们就要查看它的源码,从Runtime的源码中我们可以清晰地看到它的所有方法的实现和所有的结构体定义。

1.id的定义

在 objc.h 中我们可以找到id的定义,代码如下:

/// 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; 

从上面的代码中我们可以看到,id是一个指向结构体objc_object的指针,而结构体objc_object中只声明了一个Class类型的指针,正因为这样的定义,我们的id可以表示为任意类型。

2.Class的定义

在 objc.h 中我们也可以找到Class的定义,代码如下:

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

他是一个指向结构体objc_class的指针。而结构体objc_class的定义我们需要到 runtime.h 中去找,代码如下:

struct objc_class {
   Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
   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;
/* Use `Class` instead of `struct objc_class *` */

下面我们解释一下里面的参数:

isa指针:

在Runtime中Objc类本身也是一个对象,Runtime把类对象所属类型叫做元类(Meta Class)元类(Meta Class)用于描述类对象本身所具有的特征,最常见的类方法就被定义与此。所以objc_class中的isa指针指向的是元类。而元类的isa指针则直接指向的是根元类,并不是父类的元类。每个类仅有一个对象,而每个类对象仅有一个与之相关的元类。在objc_object结构体中也同样有一个isa指针,它指向的是类对象(即objc_class)。

super_class指针:

super_class指针指向的是object_class类所继承的父类对象,如果当前类是最顶层的类(如:NSObject),则super_class指针为nil。

name:

name指得就是当前类的类名。

version

version指的就是当前类的版本信息,默认为0

info

info:是类信息,提供运行期是用的一些标识

instance_size

instance_size指的是类的实例变量的大小

ivars

ivars用于存放所有的成员变量和属性信息

methodLists

methodLists用于存放对象的所有成员方法

cache

cache方法的缓存,为了优化性能和缩短方法的查找时间,objc_class的cache结构体中存储的是每一次类或者实例对象调用的方法名。这样当类或者实例对象调用方法时,会优先在cache中寻找相应的方法,如果找不到再去methodLists中去遍历查。

protocols

protocols用于存放对象的所有协议
上面说到指针的指向,下面我们通过一张图来解释一下:


isa和super_class指针的图解.png

通过上图我们可以看出,子类实例对象的isa指针指向的是子类对象,子类对象的isa指针指向的是子类元类,子类对象的super_class指针指向的是父类对象,父类对象的isa指针指向的是根类对象,根类对象的isa指针指向nil。子类元类的isa指针则直接指向的根元类,根元类的isa指针则指向自己。

3.Method

runtime.h中我们找到了Method的定义

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

objc_classmethodLists里面的元素就是Method。
method_name:方法名
method_types: 方法类型
method_imp:方法的实现
在这个结构体中听我们可以看到SEL(method_name)与IMP(method_name)形成了一个映射,通过SEL,我们可以很方便的找到方法的实现IMP。

4.SEL

objc.h中我们找到了的SEL的定义

///A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.
@property SEL selector;
/// An opaque type that represents a method selector.表示方法选择器的不透明类型。
typedef struct objc_selector *SEL;

SEL是一个指向objc_selector结构体的指针。它是selector在 Objective-C中标识类型(Swift中是Selector类)。SEL是方法选择器,用于表示运行时方法的名字。
selector是SEL的一个实例,其实它就是个映射到方法的C字符串,我们可以用 Objective-C的编译器命令@selector()或者Runtime系统的sel_registerName函数来获取一个SEL类型的方法选择器。
我们在Runtime中并没有找到objc_selector的定义,但是selector既然是一个字符串,我觉得应该是类似className+methodName的组合,所以其命名规则大概是:

  • 同一个类,selector不能重复
  • 不同的类,selector可以重复
    这也就可以解释为什么在 Objective-C中没有函数重载,因为它只记录了方法名,并没有记录参数,所以没法区分不同参数类型的方法。

5.IMP

objc.h中我们找到了的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

由于我们现在用的都是 Objective-C2.0版本,所以else我们可以自动忽略。
IMP函数指针指向了方法实现的首地址,当 Objective-C发起消息时,最终执行的方法有IMP决定。

Runtime中的消息机制

  在上面一章中我们介绍了Runtime的数据结构,看完以后我们可以看一下消息机制是怎样来调用的。下面我们来说一下Runtime中的消息机制的工作原理。
消息机制包括消息的发送和消息的转发,这两个我们下面分开讲。

消息的发送

当一个对象调用一个方法时[obj mothod],其实Runtime在底层就执行了一次消息发送,它的执行流程是这样的:
1.通过对象objisa指针找到对象objClass类;
2.在Classobjc_cache中找method方法,找到后就去执行method的IMP实现;找不到就执行下面第3步
3.在Class中的method_list中找method方法,找到后就执行method的实现,并且把methodmethod_name作为key,method的IMP作为value保存到objc_cache中,这样当再次执行method方法是直接去objc_cache寻找就行,节省了查找的时间(上一章介绍cache的时候说过);如果method_list中也找不到该方法,那么就去objsuperClass中的method_list中去找,以此类推,直到找到为止。
4.如果最终没有找到,Runtime提供了三种方法来处理:动态方法解析、消息接受者重定向、消息重定向,这三种方法后面讲。
  以上就是Runtime的消息发送,由于Runtime是在编译时才调用方法的实现,所以如果我们只在.h声明了方法而没有在.m中实现,在编译时期是不会报错的,只有在运行时调用方法的实现时才会报错。

消息的转发

  上文中说到当消息发送失败后Runtime提供了三种处理方法:动态方法解析、消息接受者重定向、消息重定向,下面我们来看一下这三个方法:

动态方法解析

动态解析,当没有找到方法时,Runtime为我们提供了一次动态添加方法实现的机会,让你去提供一个函数的实现。如果你添加了方法,并返回YES,那么Runtime就会重新启动一次消息发送过程。
具体实现方法如下:

//添加类方法实现
+ (BOOL)resolveClassMethod:(SEL)sel;
//添加实例方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel;

//Runtime方法:
/**
 运行时方法:向指定类中添加特定方法实现的操作
 @param cls 被添加方法的类
 @param name selector方法名
 @param imp 指向实现方法的函数指针
 @param types imp函数实现的返回值与参数类型
 @return 添加方法是否成功
 */
BOOL class_addMethod(Class _Nullable cls,
                     SEL _Nonnull name,
                     IMP _Nonnull imp,
                     const char * _Nullable types)

下面举例来说名动态解析,看一下Runtime是如何调用的。
我们新建一个Animal的类,在它的.h文件中声明方法,但并不实现,当我们通过Animal对象调用eat方法的时候我们通过Runtime的动态方法解析为他添加一个新的方法实现,具体代码如下:

///Animal.h文件
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Animal : NSObject
- (void)eat;
@end

NS_ASSUME_NONNULL_END
///Animal.m文件
#import "Animal.h"
#import <objc/runtime.h>


@implementation Animal

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"消息转发了,执行了resolveInstanceMethod方法");
    if (sel == @selector(eat)) {
        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(newEat)), "V@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

- (void)newEat {
    NSLog(@"添加了新的吃饭的方法");
}
@end

下面我们创建一个Animal的对象来调用 - (void)eat;方法

Animal *animal = [[Animal alloc] init];
[animal eat];

执行上述代码后,程序并没有Crash,我们可以看一下控制台的输出:

Runtime动态方法解析.png

通过控制台的输出我们可以看到,程序执行了+ (BOOL)resolveInstanceMethod:(SEL)sel方法,在+ (BOOL)resolveInstanceMethod:(SEL)sel方法中我们重新添加了新的实现,程序也执行了我们添加的新实现。
注:
 1.动态方法解析成功的前提是我们必须存在可以处理消息的方法:newEat,如果没有的话,动态解析依然失败。
 2.我们注意到上面class_addMethod方法的一个参数"v@:",这里的v是返回值类型为void的方法,没有参数传入。详情请转到这

消息接收者重复定向

如果在消息动态解析时没有找到你传给它的实现(或者你根本没有实现+ (BOOL)resolveInstanceMethod:(SEL)sel方法),消息发送机制会检查你是否实现了- (id)forwardingTargetForSelector:(SEL)aSelector方法,如果你实现了这个方法并返回了一个重定向对象,那么就会把消息转发给你给它的对象,继而保证程序的继续执行。
我们创建一个AnimalForwarding类,进行测试:

///AnimalForwarding.h文件
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface AnimalForwarding : NSObject

@end

NS_ASSUME_NONNULL_END
///AnimalForwarding.m文件
#import "AnimalForwarding.h"

@implementation AnimalForwarding

- (void)eat {
    NSLog(@"转发给了AnimalForwarding对象,并执行了eat方法");
}

@end

我们把Animal.m中的newEat方法注释掉,执行测试。这是程序没有Crash,而是正常执行,我们可以看一下控制台输出:

Runtime对象重定向.png

通过控制台输出我们可以看到,我们的程序执行了AnimalForwarding类中的eat方法,这就说明了我们通过- (id)forwardingTargetForSelector:(SEL)aSelector方法,将Anima的方法转发给了AnimalForwarding
在控制台输出中我们可以看到程序执行了两次+ (BOOL)resolveInstanceMethod:(SEL)sel方法,当我们使用Animal对象调用eat方法时,Runtime没有找到实现,执行了动态解析方法,这时候我们传给他一个方法名,当Runtime拿着我们给他的方法名继续执行时发现又没有这个实现,因此有调用了一次动态解析,此时我们对newEat没有做处理,所以程序直接执行了- (id)forwardingTargetForSelector:(SEL)aSelector进行消息接收者重定向,此时,我们拿到的aSelector依然是eat,而不是newEat

消息重定向

  如果上述两种方法都没有生效,那么这个对象会因为找不到相应的方法实现而无法响应消息,此时Runtime唯一能做的就是启用完整的消息转发机制。首先Runtime会发送- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector消息获得函数的参数和返回值类型,如果- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector返回为nil,Runtime则会发出doesNotRecognizeSelector消息,控制台输出unrecognized selector sent to instance程序直接Crash掉。如果返回了一个方法签名,Runtime就会创建一个 NSInvocation对象并发送给- (void)forwardInvocation:(NSInvocation *)anInvocation方法通知该对象,给予此次消息发送的最后一次寻找IMP的机会。
具体代码如下:

///Animal.m
#import "Animal.h"
#import <objc/runtime.h>
#import "ViewController.h"
#import "AnimalForwarding.h"

@implementation Animal

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"消息转发了,执行了resolveInstanceMethod方法");
    if (sel == @selector(eat)) {
        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(newEat)), "V@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"执行了forwardingTargetForSelector方法进行消息接收者重定向");
    return nil;
}


- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"执行了forwardInvocation方法,进行最后一次IMP寻找");
    SEL sel = anInvocation.selector;
    AnimalForwarding *aForwarding = [AnimalForwarding new];
    if ([aForwarding respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:aForwarding];
    } else {
        [self doesNotRecognizeSelector:sel];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"执行了methodSignatureForSelector方法,返回方法签名");
    if (aSelector == @selector(eat)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
@end

下面我们看一下控制台的输出:

消息重定向.png

通过控制台输出我们可以看出,Runtime先执行+ (BOOL)resolveInstanceMethod:(SEL)sel,然后执行- (id)forwardingTargetForSelector:(SEL)aSelector,在两个方法执行都无效后执行了- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector获取方法签名,最后执行了- (void)forwardInvocation:(NSInvocation *)anInvocation,通过- (void)forwardInvocation:(NSInvocation *)anInvocation方法找到了AnimalForwarding类中的eat方法执行了消息转发,如果AnimalForwarding类中没有eat方法的实现,那么程序就会Crash掉。
  以上就是Runtime的三次转发流程。虽然理论上我们可以重载- (void)doesNotRecognizeSelector:(SEL)aSelector方法来保证不抛出异常避免程序Crash掉,但是Apple文档着重提出“一定不能让这个函数就这么结束掉(不调用super),必须抛出异常”。

Runtime的交互

Objective-C程序有三种途径和Runtime进行交互:通过 Objective-C源码、通过Foundation框架中NSObject类的方法、直接调用运行时系统函数。

通过 Objective-C源码

  大部分情况下,运行时系统在后台自动运行,您只需编写和编译 Objective-C 源代码。
  当您编译Objective-C类和方法时,编译器为实现语言动态特性将自动创建一些数据结构和函数。这些数据 结构包含类定义和协议类定义中的信息,如在Objective-C 2.0 程序设计语言中定义类和协议类一节所讨论 的类的对象和协议类的对象,方法选标,实例变量模板,以及其它来自于源代码的信息。因为这一部分是编译器自己在后台运行的,所以这个过程对我们来说是无感的。

通过Foundation框架中NSObject类的方法

  Cocoa程序中绝大部分类都是NSObject类的子类,所以大部分都继承了NSObject类的方法,因而继承 了NSObject的行为。(NSProxy类是个例外)然而,某些情况下, NSObject类仅仅定义了完成某件事情的模板,而没有提供所有需要的代码。
  某些 NSObject 的方法只是简单地从运行时系统中获得信息,从而允许对象进行一定程度的自我检查。 例如,class 返回对象的类;isKindOfClass:和 isMemberOfClass:则检查对象是否在指定的 类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol: 检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。

直接调用运行时系统函数

  Runtime系统是一个有公开接口的动态库,由一些数据结构和函数的集合组成,这些数据结构和函数的声明 头文件在/usr/include/objc中。这些函数支持用纯C的函数来实现和Objective-C同样的功能。代码示例:

    //相当于:Animal *animal = [Animal alloc];
    Animal *animal = ((id (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Animal"), sel_registerName("alloc"));
    
    //相当于:Animal *animal = [Animal init];
    ((id (*)(id, SEL))(void *)objc_msgSend)((id)animal, sel_registerName("init"));
    [animal eat];

Runtime的应用

  • 分类增加属性
  • 获取类属性方法列表
  • 方法添加和替换
  • 修改私有属性
  • 实现NSCoding的自动归档和自动解档
  • 实现字典和模型的自动转换
    Runtime功能很强大,根据本文讲解做的Demo在这,有兴趣的可以下载看一下。

分类增加属性

我们知道分类是不能添加属性和变量的,Runtime的关联对象特性可以帮助我们在运行阶段的将任意属性和变量关联到一个对象上。下面是相关的方法:

/**
//给对象设置关联属性
@param object 需要设置关联属性的对象,即给哪个对象关联属性
@param key 关联属性对应的key,可通过key获取这个属性,
@param value 给关联属性设置的值
@param policy 关联属性的存储策略(对应Property属性中的assign,copy,retain等)
*/
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
/**
//通过key获取关联的属性
 @param object 从哪个对象中获取关联属性
 @param key 关联属性对应的key
 @return 返回关联属性的值
 */
id objc_getAssociatedObject(id object, const void *key)
/**
 //移除对象所关联的属性
 @param object 移除某个对象的所有关联属性
 */
void objc_removeAssociatedObjects(id object)

内存管理的策略

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    ///@property(assign)
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    ///@property(strong, nonatomic)
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    ///@property(copy, nonatomic)
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    ///@property(strong, atomic)
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    ///@property(copy, atomic)
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

注意:在使用上述方法时,key与关联属性一一对应,我们必须确保其全局唯一性,一般我们使用@selector(属性名)作为key。
下面我们来演示一个实例:

///Cat+CatCategory.h
#import "Cat.h"
@interface Cat (CatCategory)
@property (nonatomic, copy) NSString *color;
- (void)clearAssociateObject;
@end
///Cat+CatCategory.m
#import "Cat+CatCategory.h"
#import <objc/runtime.h>

@implementation Cat (CatCategory)
- (void)setColor:(NSString *)color {
    objc_setAssociatedObject(self, @selector(color), color, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)color {
    return objc_getAssociatedObject(self, @selector(color));
}
///清除关联属性
- (void)clearAssociateObject {
    objc_removeAssociatedObjects(self);
}
@end
//给分类添加属性
Cat *cat = [[Cat alloc] init];
cat.color = @"白色";
NSLog(@"cat的颜色是%@",cat.color);
[cat clearAssociateObject];
NSLog(@"cat的颜色是%@",cat.color);
///打印
cat的颜色是白色
cat的颜色是(null)

获取类属性方法列表

获取类的实例方法
//获取类的实例方法
unsigned int count = 0;
Method *allMothods = class_copyMethodList([Cat class], &count);
for (int i = 0; i < count; i++) {
   //Method,为runtime声明的一个宏,表示对一个方法的描述
   Method md = allMothods[i];
   //获取SEL:SEL类型,即获取方法选择器@selector()
   SEL sel = method_getName(md);
   //得到sel的方法名:以字符串格式获取sel的name,也即@selector()中的方法名称
   const char *methodName = sel_getName(sel);
   NSLog(@"(Method:%s)",methodName);
}
free(allMothods);
获取类的类方法
//获取类的类方法
Class metaClass = object_getClass([Cat class]);
unsigned int count = 0;
Method *allMothods = class_copyMethodList(metaClass, &count);
for (int i = 0; i < count; i++) {
    Method md = allMothods[i];
    SEL sel = method_getName(md);
    //得到sel的方法名:以字符串格式获取sel的name,也即@selector()中的方法名称
    const char *methodName = sel_getName(sel);
    NSLog(@"(Method:%s)",methodName);
}
free(allMothods);
获取类的属性
//获取类的属性列表
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([Cat class], &count);
for (unsigned int i = 0; i<count; i++) {
    const char *propertyName = property_getName(propertyList[i]);
    NSLog(@"PropertyName(%d): %@",i,[NSString stringWithUTF8String:propertyName]);
}
free(propertyList);
获取类的成员变量
unsigned int count = 0;
Ivar *allVariables = class_copyIvarList([Cat class], &count);
for (int i = 0; i<count; i++) {
    Ivar ivar = allVariables[i];
        const char *VariableName = ivar_getName(ivar);//获取成员变量名称
        const char *VariableType = ivar_getTypeEncoding(ivar);//获取成员变量类型
        NSLog(@"(Name:%s) ---(Type:%s)",VariableName,VariableType);
}
free(allVariables);
获取类所遵守的协议
//获取类所遵守的所有协议
unsigned int count;
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([Cat class], &count);
for (int i=0; i< count; i++) {
    Protocol *protocal = protocolList[i];
    const char *protocolName = protocol_getName(protocal);
    NSLog(@"protocol(%d): %@",i, [NSString stringWithUTF8String:protocolName]);
}
free(protocolList);

方法添加和替换

方法的添加

方法的添加我们在说消息转发的时候已经提到了:

/**
cls 被添加方法的类
name 添加的方法的名称
imp 方法的实现,提倡用class_getMethodImplementation 调用
types 上面提到了,返回值类型和参数
*/
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) ;
class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(newEat)), "V@:");

方法的替换

上面我们获取到了所有的方法,我们可以根据需要来替换掉里面的一些方法。关于方法的替换我们在这交换的是自己写的Demo的方法,并没有交换系统的方法,具体示例如下:

///Cat.h
#import "Animal.h"
@interface Cat : Animal
- (void)sleep;
@end
///Cat.m
#import "Cat.h"

@interface Cat ()

@property (nonatomic, copy) NSString *name;

@end

@implementation Cat

- (void)sleep {
    NSLog(@"cat睡觉了");
}
- (void)sleepAndDream {
    NSLog(@"cat睡觉并且做梦了");
}
@end
Cat *cat = [[Cat alloc] init];
[cat sleep];
SEL originalSleep = @selector(sleep);
SEL swizzledSleep = @selector(sleepAndDream);       
Method originalMethod = class_getInstanceMethod([cat class],originalSleep);
Method swizzledMethod = class_getInstanceMethod([cat class],swizzledSleep);     
//判断名为swizzledMethod的方法是否存在存在,返回NO表示存在,YES表示不存在。
BOOL didAddMethod = class_addMethod([cat class], originalSleep, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        
if (didAddMethod) {
    //如果swizzledSleep不存在,我们就用它替换掉我们要交换的方法
    class_replaceMethod([cat class], swizzledSleep, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
    //如果swizzledSleep存在,交换他们两个的实现
    method_exchangeImplementations(originalMethod, swizzledMethod);
}
[cat sleep];

在上面的代码中我们执行了两个方法:class_replaceMethodmethod_exchangeImplementations,我们在进行方法替换之前先判断我们替换的方法的实现在类中是否存在,如果存在的话我们执行method_exchangeImplementations交换两个方法的实现,如果不存在我们直接执行class_replaceMethod替换掉旧的方法就可以。

修改私有属性

///Cat.m
#import "Cat.h"

@interface Cat ()

@property (nonatomic, copy) NSString *name;

@end

@implementation Cat

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.name = @"小小酥";
    }
    return self;
}

+ (NSInteger)getCatAge {
    return 8;
}

- (void)sleep {
    NSLog(@"cat睡觉了");
}

- (void)sleepAndDream {
    NSLog(@"cat睡觉并且做梦了");
}

@end
Cat *cat = [[Cat alloc] init];
NSLog(@"cat的name: %@",[cat valueForKey:@"name"]); //null
//第一步:遍历对象的所有属性
unsigned int count;
Ivar *ivarList = class_copyIvarList([cat class], &count);
for (int i= 0; i<count; i++) {
    //第二步:获取每个属性名
    Ivar ivar = ivarList[i];
    const char *ivarName = ivar_getName(ivar);
    NSString *propertyName = [NSString stringWithUTF8String:ivarName];
    if ([propertyName isEqualToString:@"_name"]) {
        //第三步:匹配到对应的属性,然后修改;注意属性带有下划线
        object_setIvar(cat, ivar, @"hello小小酥");
    }
}
NSLog(@"cat的name: %@",[cat valueForKey:@"name"]);

实现NSCoding的自动归档和自动解档

归档是一种常用的轻量型文件存储方式,但是它有个弊端:在归档过程中,若一个Model有多个属性,我们不得不对每个属性进行处理,非常繁琐。
归档操作主要涉及两个方法:encodeObject 和 decodeObjectForKey,现在,我们可以利用Runtime来改进它们,其原理就是使用Runtime动态获取所有属性,关键的代码示例如下:

///解档操作
- (instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super init];
    if (self) {
        Class c = self.class;
        while (c && c != [NSObject class]) {
            unsigned int count = 0;
            Ivar *ivars = class_copyIvarList(c, &count);
            for (int i = 0; i < count; i++) {
                Ivar ivar = ivars[i];
                NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
                id value = [self valueForKey:key];
                [self setValue:value forKey:key];
            }
            c = [c superclass];
            free(ivars);
        }
    }
    return self;
}

///归档操作
- (void)encodeWithCoder:(NSCoder *)coder
{
    Class c = self.class;
    while (c && c != [NSObject class]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList(c, &count);
        for (int i = 0; i < count; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            id value = [self valueForKey:key];
            [coder encodeObject:value forKey:key];
        }
        c = [c superclass];
        free(ivars);
    }
}

实现字典和模型的自动转换

将字典转成模型,我们可以参考MJExtension,这里就不在一一赘述了,核心代码如下(将代码添加到NSObject的分类中):

- (instancetype)initWithDict:(NSDictionary *)dict {

    if (self = [self init]) {
        //(1)获取类的属性及属性对应的类型
        NSMutableArray * keys = [NSMutableArray array];
        NSMutableArray * attributes = [NSMutableArray array];
        /*
         * 例子
         * name = value3 attribute = T@"NSString",C,N,V_value3
         * name = value4 attribute = T^i,N,V_value4
         */
        unsigned int outCount;
        objc_property_t * properties = class_copyPropertyList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            objc_property_t property = properties[i];
            //通过property_getName函数获得属性的名字
            NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
            [keys addObject:propertyName];
            //通过property_getAttributes函数可以获得属性的名字和@encode编码
            NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
            [attributes addObject:propertyAttribute];
        }
        //立即释放properties指向的内存
        free(properties);

        //(2)根据类型给属性赋值
        for (NSString * key in keys) {
            if ([dict valueForKey:key] == nil) continue;
            [self setValue:[dict valueForKey:key] forKey:key];
        }
    }
    return self;

}

以上就是Runtime的用法,其实Runtime还可以根据消息转发实现热更新,但是由于Apple拒绝审核热修复的问题,所以不再此进行介绍,有兴趣的同学可以去看一下JSPatch源码

结束

  以上就是Runtime的详细介绍和在实际开发中使用,Runtime作为Objective-C的核心,学习和深入地了解它有助于我们进行更好的开发。我们在学习中也可以去尝试了解编程语言的底层实现,这样更有利于我们进行深度学习。
  文章若有不足之处还请不吝赐教,大家互相学习。如果您觉得我的文章有用,点一下喜欢就可以了哦。

参考文章:
1.Runtime-iOS运行时基础篇
2.Runtime-iOS运行时应用篇
3.iOS Runtime详解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容