iOS开发经验(14)-runtime

目录

  1. Objective-C Runtime到底是什么
  2. Objective-C的元素认知
  3. Runtime详解
  4. 应用场景
  5. Runtime缺点及Runtime常用函数

引用:
Objective-C Runtime 1小时入门教程

一、Objective-C Runtime到底是什么

我们将C++和Objective进行对比,虽然C++和Objective-C都是在C的基础上加入面向对象的特性扩充而成的程序设计语言,但二者实现的机制差异很大。C++是基于静态类型,而Objective-C是基于动态运行时类型。也就是说用C++编写的程序通过编译器直接把函数地址硬编码进入可执行文件;而Objective-C无法通过编译器直接把函数地址硬编码进入可执行文件,而是在程序运行的时候,利用Runtime根据条件判断作出决定。函数标识与函数过程的真正内容之间的关联可以动态修改。Runtime是Objective不可缺少的重要一部分。

程序执行过程:预处理->编译->链接->运行。

  • Objective-C 是面相运行时的语言,就是说它会尽可能的把编译和链接时要执行的逻辑延迟到运行时。
  • OC之所以从C变成了面向对象的C,拥有动态特性,都是由于运行时系统的存在。
  • Objective-C动态运行库会自动注册我们代码中定义的所有的类。我们也可以在运行时创建类定义并使用objc_addClass函数来注册它们。

二、Objective-C的元素认知

2.1 对象(id)

typedef struct objc_class *Class;

struct objc_object {
    Class isa;
}
typedef struct objc_object *id;

Objective-C 中的对象的定义 struct objc_object,Objective-C 中的对象本质上是结构体,它是struct objc_object 类型的指针,objc_object被源码typedef成了id类型,这也是为什么 id 类型可以指向任意对象的原因,其中 isa 是它唯一的私有成员变量。这个对象的 isa指针指向它所属的类。

2.2 类(Class)

Objective-C 中的类的定义 struct objc_class 。同样的,Objective-C 中类也是一个结构体。所以,Objective-C 中的类本质上也是对象,我们称之为类对象。
objc_class源码如下:

typedef struct objc_class *Class;
struct objc_class { 
 Class isa                                 OBJC_ISA_AVAILABILITY; // metaclass
#if !__OBJC2__
 Class super_class                         OBJC2_UNAVAILABLE; // 父类
 const char *name                          OBJC2_UNAVAILABLE; // 类名
 long version                              OBJC2_UNAVAILABLE; // 类的版本信息,默认为0,可以通过runtime函数class_setVersion或者class_getVersion进行修改、读取
 long info                                 OBJC2_UNAVAILABLE; // 类信息,供运行时期使用的一些位标识,如CLS_CLASS (0x1L) 表示该类为普通 class,其中包含实例方法和变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
 long instance_size                        OBJC2_UNAVAILABLE; // 该类的实例变量大小(包括从父类继承下来的实例变量)
 struct objc_ivar_list *ivars              OBJC2_UNAVAILABLE; // 该类的成员变量地址列表
 struct objc_method_list **methodLists     OBJC2_UNAVAILABLE; // 方法地址列表,与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储实例方法,如CLS_META (0x2L),则存储类方法;
 struct objc_cache *cache                  OBJC2_UNAVAILABLE; // 缓存最近使用的方法地址,用于提升效率;
 struct objc_protocol_list *protocols      OBJC2_UNAVAILABLE; // 存储该类声明遵守的协议的列表
#endif
}
/* Use `Class` instead of `struct objc_class *` */

由以上代码可见:

Class是一个指向objc_class结构体的指针,而id是一个指向objc_object结构体的指针,其中的isa是一个指向objc_class结构体的指针。其中的id就是我们所说的对象,Class就是我们所说的类。

类与对象的区别就是类比对象多了很多特征成员,类也可以当做一个objc_object来对待,也就是说类和对象都是对象,分别称作类对象(class object)和实例对象(instance object),这样我们就可以区别对象和类了。

isa:

在OC中,除了NSProxy类以外,所有的类都是NSObject的子类。在Foundation框架下,NSObject和NSProxy是两个基类。id是一个指向 objc_object 结构体的指针,该结构体只有一个成员isa,所以任何继承自 NSObject 的类对象都可以用 id 来指代。

从上述两个代码块内,object和class里面分别都包含一个isa:

  1. objc_object(实例对象)中isa指针指向的类结构称为class(也就是该对象所属的类),其中存放着普通成员变量与动态方法(“-”开头的方法)。对象的实例方法调用时,通过对象的 isa 在类中获取方法的实现;
  2. objc_class中isa指针指向的类结构称为metaclass,其中存放着static类型的成员变量与static类型的方法(“+”开头的方法)。类对象的类方法调用时,通过类的 isa 在元类中获取方法的实现;

super_class:

指向该类的父类的指针,如果该类是根类(如NSObject或NSProxy),那么super_class就为nil。

元类(metaClass):

所有的metaclass中isa指针都是指向根metaclass,而根metaclass则指向自身。根metaclass是通过继承根类产生的,与根class结构体成员一致,不同的是根metaclass的isa指针指向自身。

  • 类也是对象,也称类对象,类是元类的实例,因此,我们也可以通过调用类方法,比如 [NSObject new],给类对象发送消息。同样的,类对象能否响应这个消息也要通过 isa 找到类对象所属的类(元类)才能知道。也就是说,实例方法是保存在类中的,而类方法是保存在元类中的。
  • 元类也是对象(元类对象),元类也是某个类的实例,这个类我们称之为根元类(root metaclass)。元类的isa指向NSObject(NSObject:根元类,其isa指向自己,其super_class也指向自己);
  • 存放的是静态成员变量和类方法;没有实例方法。

2.3 方法(Method)

2.3.1 SEL

SEL是selector在Objective-C中的表示类型, 表示方法的名字/签名,selector可以理解为区别方法的ID。

typedef struct objc_selector *SEL;

objc_selector的定义如下:

struct objc_selector {
    char *name;                       OBJC2_UNAVAILABLE;// 名称
    char *types;                      OBJC2_UNAVAILABLE;// 存储着方法的参数类型和返回值类型。
};

name和types都是char类型。

typedef struct objc_selector *SEL; 

2.3.2 IMP

终于到IMP了,它在objc.h中得定义如下:

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

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

2.3.3 Method

方法链表里面存储的是Method 类型,Method代表类中的某个方法的类型:Method = IMP + SEL + types,Method也是一个结构体对象。
Method 在头文件 objc_class.h中定义如下:

typedef struct objc_method *Method;

objc_method的定义如下:

struct objc_method {
    SEL method_name                   OBJC2_UNAVAILABLE; // 方法名(SEL、_cmd)
    char *method_types                OBJC2_UNAVAILABLE; // 方法返回值和参数的类型
    IMP method_imp                    OBJC2_UNAVAILABLE; // 方法实现,指向该方法的具体实现的函数指针
}

Method由三个部分组成:

  1. 方法名method_name类型为SEL,Selector相当于一个方法的id;
  2. 方法类型method_types是一个char指针,存储着方法的参数类型和返回值类型;
  3. 方法实现method_imp的类型为IMP,IMP是方法的实现;

这样分开的一个便利之处是selector和IMP之间的对应关系可以被改变。比如一个 IMP 可以有多个 selectors 指向它。

注:

  1. 实例方法在对象的class中找,而类方法在对象所属的类的的metaClass中找。
  2. OC中的方法实质上是一个有id self和 SEL _cmd两个参数的C方法。

2.4 Ivar

Ivar代表类中实例变量的类型

typedef struct objc_ivar *Ivar;

objc_ivar的定义如下:

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

2.5 objc_property_t

objc_property_t是属性,它的定义如下:

typedef struct objc_property *objc_property_t;

objc_property是内置的类型,与之关联的还有一个objc_property_attribute_t,它是属性的attribute,也就是其实是对属性的详细描述,包括属性名称、属性编码类型、原子类型/非原子类型等。它的定义如下:

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

2.6 Cache

Catch的定义如下:

typedef struct objc_cache *Cache

objc_cache的定义如下:

struct objc_cache {
    unsigned int mask                   OBJC2_UNAVAILABLE;
    unsigned int occupied               OBJC2_UNAVAILABLE;
    Method buckets[1]                   OBJC2_UNAVAILABLE;
};

mask: 指定分配cache buckets的总数。在方法查找中,Runtime使用这个字段确定数组的索引位置。
occupied: 实际占用cache buckets的总数。
buckets: 指定Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。
objc_msgSend(下文讲解)每调用一次方法后,就会把该方法缓存到cache列表中,下次的时候,就直接优先从cache列表中寻找,如果cache没有,才从methodLists中查找方法。

2.7 Catagory

这个就是我们平时所说的类别了,很熟悉吧。它可以动态的为已存在的类添加新的方法。
它的定义如下:

typedef struct objc_category *Category;
objc_category的定义如下:

struct objc_category {
    char *category_name                           OBJC2_UNAVAILABLE; // 类别名称
    char *class_name                              OBJC2_UNAVAILABLE; // 类名
    struct objc_method_list *instance_methods     OBJC2_UNAVAILABLE; // 实例方法列表
    struct objc_method_list *class_methods        OBJC2_UNAVAILABLE; // 类方法列表
    struct objc_protocol_list *protocols          OBJC2_UNAVAILABLE; // 协议列表
}
2. OC的动态特性**

Objective-C具有相当多的动态特性,这个动态体现在三个方面:动态类型(Dynamic typing),动态绑定(Dynamic binding)和动态加载(Dynamic loading)。

1. 动态类型
即运行时再决定对象的类型。这类动态特性在日常应用中非常常见,简单说就是id类型。id类型即通用的对象类,任何对象都可以被id指针所指。其类型需要等到运行时才能决定,在编译时id就是一个通用类型。

2. 动态绑定

动态绑定概念:基于动态类型,在某个实例对象被确定后,其类型便被确定了。该对象对应的属性和响应的消息也被完全确定,这就是动态绑定。在继续之前,需要明确Objective-C中消息的概念。由于OC的动态特性,在OC中其实很少提及“函数”的概念,传统的函数一般在编译时就已经把参数信息和函数实现打包到编译后的源码中了,****而在OC中最常使用的是消息机制。调用一个实例的方法,所做的是向该实例的指针发送消息,实例在收到消息后,从自身的实现中寻找响应这条消息的方法。

动态绑定作用:即是在实例所属类确定后,将某些属性和相应的方法绑定到实例上。这里所指的属性和方法当然包括了原来没有在类中实现的,而是在运行时才需要的新加入的实现。
在Cocoa层,我们一般向一个NSObject对象发送-respondsToSelector:或者-instancesRespondToSelector:等来确定对象是否可以对某个SEL做出响应,而在OC消息转发机制被触发之前,对应的类的+resolveClassMethod:+resolveInstanceMethod:将会被调用,在此时有机会动态地向类或者实例添加新的方法,也即类的实现是可以动态绑定的。

//该方法在OC消息转发生效前被调用
+ (BOOL) resolveInstanceMethod:(SEL)aSEL
{ 
    if (aSEL == @selector(resolveThisMethodDynamically)) {
        //向[self class]中新加入返回为void的实现,SEL名字为aSEL,实现的具体内容为dynamicMethodIMP class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, “v@:”);
        return YES;
    }
    return [super resolveInstanceMethod:aSel];
}  

3. 动态加载
根据需求加载所需要的资源,这点很容易理解,对于iOS开发来说,基本就是根据不同的机型做适配。最经典的例子就是在Retina设备上加载@2x的图片,而在老一些的普通屏设备上加载原图。随着Retina iPad的推出,和之后可能的Retina Mac的出现,这个特性相信会被越来越多地使用。

基本的动态特性在常规的Cocoa开发中非常常用,特别是动态类型和动态绑定。以下主要结合Runtime原理深入运行时特性。

3. RunTime基本概念

RunTime简称运行时,是一套底层的 C 语言 API,OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制,主要特征就是动态绑定,消息转发。

C与OC区别:

  • 在编译阶段,C语言调用未实现的函数就会报错,函数的调用在编译的时候会决定调用哪个函数;
  • 在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。

开发者在编码过程中,可以给任意一个对象发送消息,在编译阶段只是确定了要向接收者发送这条消息,而接受者将要如何响应和处理这条消息,那就要看运行时来决定了。你向一个对象发送消息并不意味着它会执行它。Object(对象)会检查消息的发送者,基于这点再决定是执行一个不同的方法还是转发消息到另一个目标对象上。

一般情况开发者只需要编写 OC 代码即可,Runtime 系统自动在幕后把我们写的源代码在编译阶段转换成运行时代码,在运行时确定对应的数据结构和调用具体哪个方法。消息直到运行时才绑定到方法实现上。

Runtime中方法的动态绑定让我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。

1. objc_msgSend函数简介
当一个对象能接收一个消息时,就会走正常的方法调用流程。但如果一个对象无法接收指定消息时,又会发生什么事呢?默认情况下,如果是以 [object message]的方式调用方法,如果object无法响应message消息时,编译器会报错。但如果是以perform…的形式来调用,则需要等到运 行时才能确定object是否能接收message消息。如果不能,则程序崩溃。

通常,当我们不能确定一个对象是否能接收某个消息时,会先调用respondsToSelector:来判断一下。不过,我们这边想讨论下不使用respondsToSelector:判断的情况。

当一个对象无法接收某一消息时,就会启动所谓”消息转发(message forwarding)“机制,通过这一机制,我们可以告诉对象如何处理未知的消息。默认情况下,对象接收到未知的消息,会导致程序崩溃
最初接触到OC Runtime,一定是从[receiver message]这里开始的。[receiver message]会被编译器转化为:

id objc_msgSend ( id self, SEL op, ... );

这是一个可变参数函数。第二个参数类型是SEL。SEL在OC中是selector方法选择器。

typedef struct objc_selector *SEL;

objc_selector是一个映射到方法的C字符串。需要注意的是@selector()选择只与函数名有关。

在receiver拿到对应的selector之后,如果自己无法执行这个方法,那么该条消息要被转发。或者临时动态的添加方法实现。如果转发到最后依旧没法处理,程序就会崩溃。

所以编译期仅仅是确定了要发送消息,而消息如何处理是要运行期需要解决的事情。

objc_msgSend函数究竟会干什么事情呢?
2. 消息发送Messaging阶段
当我们创建一个新对象时,先为其分配内存,并初始化其成员变量。其中isa指针也会被初始化,让对象可以访问类及类的继承体系。
总结一下objc_msgSend会做一下几件事情:

  1. 检查target是不是为nil。
  • 如果这里有相应的nil的处理函数,就跳转到相应的函数中。
  • 如果没有处理nil的函数,就自动清理现场并返回。这一点就是为何在OC中给nil发送消息不会崩溃的原因。
  1. 检测这个 selector是不是要忽略的。
  2. 确定不是给nil发消息之后,objc_msgSend通过对象的isa指针获取到类的结构体,在该class的缓存中查找方法对应的IMP实现。如果找到,就跳转进去执行。如果没有找到,就在方法分发表里面继续查找。如果以上尝试都失败了,接下来就会循环尝试父类的缓存和方法列表,一直找到NSObject为止(因为NSObject的superclass为nil(还是它自己?),才跳出循环)。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现;如果最后没有定位到selector,则会走消息转发流程

注:为了加速消息的处理,运行时系统缓存使用过的selector及对应的方法的地址。

至此,发送消息Messaging阶段完成。这一阶段主要完成的是通过select()快速查找IMP的过程。

默认情况下,如果是以 [object message]的方式调用方法,如果object无法响应message消息时,编译器会报错。但如果是以perform…的形式来调用,则需要等到运 行时才能确定object是否能接收message消息。如果不能,则程序崩溃。

通常,当我们不能确定一个对象是否能接收某个消息时,会先调用respondsToSelector:来判断一下。如下代码所示:

if ([self respondsToSelector:@selector(method)]) {
    [self performSelector:@selector(method)];
}

不过,我们这边想讨论下不使用respondsToSelector:判断的情况。

上面提到的,抛出判断和一般发送消息的方法:当消息传递过程中找不到对应的方法时,会抛出unrecognzed selector send to instace ...的错误,即找不到指定的方法,在此之前可以在三个方法中实现补救。

3. 动态绑定阶段
动态绑定,从名称来看就大致懂了。如果调用一个类的方法,而这个类及其父类均没有实现这个方法。那么我们就在运行时绑定此方法到该类。注意我们可以在这里动态增加方法实现,不过这种方案更多的是为了实现@dynamic属性。

resolveInstanceMethod动态方法解析:

  • 将未能识别的消息动态添加到接收者的类中,resolveInstanceMethod方法返回的是一个BOOL类型的值,用于判断是否接收这消息;
  • 这个函数首先判断是否是meta-class类,如果不是元类,就执行_class_resolveInstanceMethod,如果是元类,执行_class_resolveClassMethod

对象在接收到未知的消息时,首先会调用所属类的类方法-resolveInstanceMethod:(实例方法)或 者+resolveClassMethod:(类方法)。
在这个方法中,我们有机会为该未知消息新增一个”处理方法”“。不过使用该方法的前提是我们已经 实现了该”处理方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。如下代码所示:

#import <Foundation/Foundation.h>
@interface Father : NSObject
@end

#import "Father.h"
#import "Son.h"
@implementation Father

- (void)son {
    Son *s = [[Son alloc] init];
    // 默认Son,没有实现run方法,可以通过performSelector调用,但是会报错。
    // 动态添加方法就不会报错
    [s performSelector:@selector(run)];
}

@end

son:

#import <Foundation/Foundation.h>
@interface Son : NSObject
@end


#import "Son.h"
#import <objc/runtime.h>
@implementation Son
// void(*)()
// 默认方法都有两个隐式参数,
void testRun(id self,SEL sel) {
    [Son.new  eat];
    NSLog(@"son is runing");
}

- (void)eat {
    NSLog(@"son is eating");
}
// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
// 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    //判断方法是否是run
    if ([NSStringFromSelector(sel) isEqualToString:@"run"]) {
        // 动态添加run方法

        // 第一个参数:给哪个类添加方法
        // 第二个参数:添加方法的方法编号
        // 第三个参数:添加方法的函数实现(函数地址)
        // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
        class_addMethod([self class], sel, (IMP)testRun, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end

打印输出:
son is eating
son is runing

@selector(run)被动态添加到了Son的类方法列表中。

如果也没有找到IMP的实现,resloveInstanceMethod:返回NO之后,就会进入消息转发阶段。

4. 消息转发Message Forwarding阶段
如果在上一步无法处理消息,则Runtime会继续调以下方法:
-forwardindTargetWithSelctor:方法。在这个方法中,返回的对象就是message的接收者,然后会回到resloveInstanceMethod方法,从新开始消息转发过程,如果返回nil则会进入下一个方法中(-forwardInvocation)去判断是否响应这个消息。

消息转发机制基本上分为两个步骤:
1. 备用接收者 forwardingTargetForSelector

  • 如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。
  • 可借这个对象来处理消息并返回,这样在对象外部看来,还是由该对象亲自处理了这一消息。
  • 这一步合适于我们只想将消息转发到另一个能处理该消息的对象上。但这一步无法对消息进行处理,如操作消息的参数和返回值。

当前的SEL无法找到相应的IMP的时候,开发者可以通过重写
- (id)forwardingTargetForSelector:(SEL)aSelector方法来“偷梁换柱”,把消息的接受者换成一个可以处理该消息的对象。

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(Method:)){
        return otherObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

当然也可以替换类方法,那就要重写 + (id)forwardingTargetForSelector:(SEL)aSelector方法,返回值是一个类对象。

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(xxx)) {
        return NSClassFromString(@"Class name");
    }
    return [super forwardingTargetForSelector:aSelector];
}

我们将上面的代码修改了下变成了这样:
father:

#import <Foundation/Foundation.h>
@interface Father : NSObject
@end


#import "Father.h"
#import "Son.h"
@implementation Father

- (void)son {
    Son *s = [[Son alloc] init];
    [s performSelector:@selector(run)];
}

static void fatherRun() {
    NSLog(@"father is runing");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(run)) {
        class_addMethod([self class], sel, (IMP)fatherRun, "v@:@");
        return NO;
    }
    return [super resolveInstanceMethod:sel];
}

@end

son:

#import <Foundation/Foundation.h>
@interface Son : NSObject
@end


#import "Son.h"
#import <objc/runtime.h>
@implementation Son

- (void)eat {
    NSLog(@"son is eating");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    [self eat];
    return [Father new];
}
@end

打印:
son is eating
father is runing

这就是runtime的神奇之处,消息的接收者由“本应该是”的Son转变为了Father。

这一步是替消息找备援接收者,如果这一步返回的是nil,那么补救措施就完全的失效了,Runtime系统会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。为接下来的完整的消息转发生成一个 NSMethodSignature对象。NSMethodSignature 对象会被包装成 NSInvocation 对象,forwardInvocation:方法里就可以对 NSInvocation 进行处理了。

消息转发第二步:如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。此时会进入完整转发阶段

2. 完整转发 forwardInvocation
我们只需要重写下面这个方法,就可以自定义我们自己的转发逻辑了。

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
         [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

补充:NSMethodSignature 和 NSInvocation
在 iOS中可以直接调用某个对象的消息方式有两种:

  • 一种是performSelector:withObject;
  • 再一种就是NSInvocation。

第一种方式比较简单,能完成简单的调用。但是对于>2个的参数或者有返回值的处理,那performSelector:withObject就显得有点有心无力了,那么在这种情况下,我们就可以使用NSInvocation来进行这些相对复杂的操作。
NSInvocation介绍:

  • NSInvocation中保存了方法所属的对象/方法名称/参数/返回值
  • NSInvocation就是将一个方法变成一个对象

NSMethodSignature:方法签名类

  • 方法签名类中保存了方法的名称/参数/返回值,协同NSInvocation来进行消息的转发
  • 方法签名类一般是用来设置参数和获取返回值的, 和方法的调用没有太大的关系

NSInvocation的基本使用
1.根据方法来初始化NSMethodSignature.方法签名类

NSMethodSignature  *signature = [ViewController instanceMethodSignatureForSelector:@selector(run:)];

2.根据方法签名类来创建NSInvocation对象

NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
//设置方法调用者
invocation.target = self;
//注意:这里的方法名一定要与方法签名类中的方法一致
invocation.selector = @selector(run:);
NSString *way = @"byCar";
//这里的Index要从2开始,以为0跟1已经被占据了,分别是self(target),selector(_cmd)
[invocation setArgument:&way atIndex:2];
//3、调用invoke方法
[invocation invoke];
//实现run:方法
- (void)run:(NSString *)method{

}

** NSMethodSignature 和 NSInvocation的基本使用补充结束!**

运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息 有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在forwardInvocation 方法中选择将消息转发给其它对象。

forwardInvocation:方法的实现有两个任务:

  • 定位可以响应封装在anInvocation中的消息的对象。这个对象不需要能处理所有未知消息。
  • 使用anInvocation作为参数,将消息发送到选中的对象。anInvocation将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。

不过,在这个方法中我们可以实现一些更复杂的功能,我们可以对消息的内容进行修改,比如追回一个参数等,然后再去触发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。

还有一个很重要的问题,我们必须重写以下方法:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象。因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。

完整的示例如下所示:
修改上面的代码为:
father:

#import <Foundation/Foundation.h>
@interface Father : NSObject
@end


#import "Father.h"
#import "Son.h"
@implementation Father

- (void)son {
    Son *s = [[Son alloc] init];
    [s performSelector:@selector(run)];
}

- (void)run {
    NSLog(@"father is running");
}

@end

son:

#import <Foundation/Foundation.h>
@interface Son : NSObject
@end


#import "Son.h"
#import <objc/runtime.h>
@implementation Son

+ (BOOL)resolveInstanceMethod:(SEL)sel {

    return NO;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {

    return nil;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {

    if ([NSStringFromSelector(aSelector) isEqualToString:@"run"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {

    Father *f = [[Father alloc] init];
    //改变selector
    [anInvocation setSelector:@selector(run)];
    //在这里指定消息接收者,如果不指定的话还是会抛出找不到方法的异常
    [anInvocation invokeWithTarget:f];
}

@end

打印:father is running

我们已经成功的将消息转发给了Father实例。以上就是消息转发的流程和具体实践。

NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。

从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象。这取决于具体的实现。

实现此方法之后,若发现某调用不应由本类处理,则会调用超类的同名方法。如此,继承体系中的每个类都有机会处理该方法调用的请求,一直到NSObject根类。如果到NSObject也不能处理该条消息,那么就是再无挽救措施了,只能抛出“does Not Recognize Selector”异常了。

消息传递流程图.jpg

至此,消息发送和转发的过程结束。

4. 应用场景

** 1.实现多继承**
** 2.Method Swizzling(方法搅拌)**
** 3.Associated Object(关联对象)**
** 4.动态的增加方法**
** 5.NSCoding的自动归档和自动解档**
** 6.字典和模型互相转换**

1. 实现多继承
根据上述任意消息转发样例,可知实现了在A中调用B中的方法b,大概可以猜到,这是不是类似继承机制?答案是肯定的,因为OC不支持多继承,此处就给了一个实现多继承的方式,因为我们可以实现任意个类消息的转发
通过forwardingTargetForSelector:方法,一个类可以做到继承多个类的效果,只需要在这一步将消息转发给正确的类对象就可以模拟多继承的效果。
在OC程序中可以借用消息转发机制来实现多继承的功能。 一个对象对一个消息做出回应,类似于另一个对象中的方法借过来或是“继承”过来一样。A实例转发了一个SEL消息到B实例中,执行B中的SEL方法,结果看起来像是A实例执行了一个和B实例一样的SEL方法,其实执行者还是B实例。

消息转发提供了许多类似于多继承的特性,但是他们之间有一个很大的不同:

  • 多继承:合并了不同的行为特征在一个单独的对象中,会得到一个重量级多层面的对象。
  • 消息转发:将各个功能分散到不同的对象中,得到的一些轻量级的对象,这些对象通过消息通过消息转发联合起来。

这里值得说明的一点是,即使我们利用转发消息来实现了“假”继承,但是NSObject类还是会将两者区分开。像respondsToSelector:和 isKindOfClass:这类方法只会考虑继承体系,不会考虑转发链。

如果非要制造假象,反应出这种“假”的继承关系,那么需要重新实现 respondsToSelector:和 isKindOfClass:来加入你的转发算法

如果一个对象转发它接受的任何远程消息,它得给出一个methodSignatureForSelector:来返回准确的方法描述,这个方法会最终响应被转发的消息。比如一个对象能给它的替代者对象转发消息,它需要像下面这样实现methodSignatureForSelector:

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
        signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}

需要引起注意的一点,实现methodSignatureForSelector方法是一种先进的技术,只适用于没有其他解决方案的情况下。它不会作为继承的替代。如果您必须使用这种技术,请确保您完全理解类做的转发和您转发的类的行为。请勿滥用!

2.Method Swizzling(方法搅拌)
它可以通过Runtime的API实现更改任意的方法,理论上可以在运行时通过类名/方法名hook到任何 OC 方法,替换任何类的实现以及新增任意类。
核心函数就是method_exchangeImplementations
Method Swizzling原理:
Method Swizzling本质上就是对IMP和SEL进行交换。Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。而且Method Swizzling也是iOS中AOP(面相切面编程)的一种实现方式,我们可以利用苹果这一特性来实现AOP编程。

优势:可以重写某个方法而不用继承,同时还可以调用原先的实现。通常的做法是在category中添加一个方法(当然也可以是一个全新的class)。

具体场景应用:替换两个方法的实现,可以做一些埋点、容错等工作;AFN也用到了,每一次resume,都会用到method swizzling。
一般我们使用都是新建一个分类,在分类中进行Method Swizzling方法的交换:
具体场景应用1:

#import <objc/runtime.h>
@implementation UIViewController (Swizzling)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}
#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}
@end

Method Swizzling可以在运行时通过修改类的方法列表中selector对应的函数或者设置交换方法实现,来动态修改方法。可以重写某个方法而不用继承,同时还可以调用原先的实现。所以通常应用于在category中添加一个方法。

补充:在我们替换的方法- (void)xxx_viewWillAppear:(BOOL)animated中,调用了[self xxx_viewWillAppear:animated];这不是死循环了么?
其实这里并不会死循环。由于我们进行了Swizzling,所以其实在原来的- (void)viewWillAppear:(BOOL)animated方法中,调用的是- (void)xxx_viewWillAppear:(BOOL)animated方法的实现。所以不会造成死循环。相反的,如果这里把[self xxx_viewWillAppear:animated];改成[self viewWillAppear:animated];就会造成死循环。因为外面调用[self viewWillAppear:animated];的时候,会交换方法走到[self xxx_viewWillAppear:animated];这个方法实现中来,然后这里又去调用[self viewWillAppear:animated],就会造成死循环了。

具体场景应用2:NSArray数组越界容错处理
常见做法是给NSArray,NSMutableArray增加分类,增加这些异常保护的方法,不过如果原有工程里面已经写了大量的AtIndex系列的方法,去替换成新的分类的方法,效率会比较低。这里可以考虑用Swizzling做。

#import "NSArray+ Swizzling.h"
#import "objc/runtime.h"
@implementation NSArray (Swizzling)
+ (void)load {
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(swizzling_objectAtIndex:));
    method_exchangeImplementations(fromMethod, toMethod);
}

- (id)swizzling_objectAtIndex:(NSUInteger)index {
    if (self.count-1 < index) {
        // 异常处理
        @try {
            return [self swizzling_objectAtIndex:index];
        }
        @catch (NSException *exception) {
            // 打印崩溃信息
            NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);
            NSLog(@"%@", [exception callStackSymbols]);
            return nil;
        }
        @finally {}
    } else {
        return [self swizzling_objectAtIndex:index];
    }
}
@end

注意:调用这个objc_getClass方法的时候,要先知道类对应的真实的类名才行,NSArray其实在Runtime中对应着__NSArrayI,NSMutableArray对应着__NSArrayM,NSDictionary对应着__NSDictionaryI,NSMutableDictionary对应着__NSDictionaryM

Method Swizzling注意点:

  • ** Swizzling应该总在+load中执行;**
  • ** Swizzling应该总是在dispatch_once中执行:**
    Swizzling会改变全局状态,所以在运行时采取一些预防措施,使用dispatch_once就能够确保代码不管有多少线程都只被执行一次。
    这里有一个很容易犯的错误,那就是继承中用了Swizzling。如果不写dispatch_once就会导致Swizzling失效!
    举个例子,比如同时对NSArray和NSMutableArray中的objectAtIndex:方法都进行了Swizzling,这样可能会导致NSArray中的Swizzling失效的。
    原因是:我们没有用dispatch_once控制Swizzling只执行一次。如果这段Swizzling被执行多次,经过多次的交换IMP和SEL之后,结果可能就是未交换之前的状态。
    3. Swizzling在+load中执行时,不要调用[super load]
    原因同注意点二,如果是多继承,并且对同一个方法都进行了Swizzling,那么调用[super load]以后,父类的Swizzling就失效了。

两个类方法区别
我们知道了 Objective-C 中绝大部分的类都继承自 NSObject 类。而在 NSObject 类中有两个非常特殊的类方法 +load 和 +initialize ,用于类的初始化。
+load

  • +load 方法是当类或分类被添加到 Objective-C runtime 时被调用的,+load会在类初始加载时调用。
  • 子类的 +load 方法会在它的所有父类的 +load 方法之后执行,而分类的 +load 方法会在它的主类的 +load 方法之后执行。但是不同的类之间的 +load 方法的调用顺序是不确定的。
  • 只调用一次
  • 不能使用Super且不沿用父类实现

+initialize

  • +initialize 方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说 +initialize 方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的 +initialize 方法是永远不会被调用的。
  • 调用次数多次

总结
+load 和 +initialize 调用机制和各自的特点

| | +load | +initialize
|-----------------
| 调用时机 | 被添加到 runtime 时 | 收到第一条消息前,可能永远不调用
| 调用顺序 | 父类->子类->分类 | 父类->子类
| 调用次数 | 1次 | 多次
| 是否需要显式调用父类实现 | 否 | 否
| 是否沿用父类的实现 | 否 | 是
| 分类中的实现 | 类和分类都执行 | 覆盖类中的方法,只执行分类的实现

3. Associated Object(关联对象)
Category
Category是表示一个指向分类的结构体的指针.
需要注意的有两点:

  • category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA
  • category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休,殊不知后面可能还有一样名字的方法。

在 Category 中,我们无法添加@property,因为添加了@property之后并不会自动帮我们生成实例变量以及存取方法。

关联对象API在runtime里,所有的关联对象都由AssociationsManager管理
使用objc_setAssociate()能够将一个变量通过指定的key值讲实例与实例变量绑定在一起,在读取的时候值调用objc_getAssociate(),在指定的实例中通过key将变量取出,可以简单理解成字典一样存取
这里涉及到了3个函数:

//setter,就像字典中的 setValue:ForKey:
void objc_setAssociatedOject(id object, void *key, id value, objc_AssociationPolicy policy)

//getter,就像字典中的 objectForKey
id objc_getAssociatedObject(id object, void *key)

//remove,就像字典中的 removeAllObject
void objc_removeAssocaitedObjected(id object)

那么,我们现在就可以通过关联对象来实现在 Category 中添加属性的功能了。

// NSObject+AssociatedObject.h
@interface NSObject (AssociatedObject)
@property (nonatomic, strong) id associatedObject;
@end
// NSObject+AssociatedObject.m
@implementation NSObject (AssociatedObject)
@dynamic associatedObject;
- (void)setAssociatedObject:(id)object {
    objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)associatedObject {
    return objc_getAssociatedObject(self, @selector(associatedObject));
}

原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 给系统NSObject类动态添加属性name

    NSObject *objc = [[NSObject alloc] init];
    objc.name = @"aaaaa";
    NSLog(@"%@",objc.name);

}


@end


// 定义关联的key
static const char *key = "name";

@implementation NSObject (Property)

- (NSString *)name
{
    // 根据关联的key,获取关联的值。
    return objc_getAssociatedObject(self, key);
}

- (void)setName:(NSString *)name
{
    // 第一个参数:给哪个对象添加关联
    // 第二个参数:关联的key,通过这个key获取
    // 第三个参数:关联的value
    // 第四个参数:关联的策略
    objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

关联对象3种使用场景

  1. 为现有的类添加私有变量
  2. 为现有的类添加公有属性
  3. 为KVO创建一个关联的观察者。

4.动态的增加方法
在消息发送阶段,如果在父类中也没有找到相应的IMP,就会执行resolveInstanceMethod方法。在这个方法里面,我们可以动态的给类对象或者实例对象动态的增加方法。

当你发送了一个object无法处理的消息时会发生什么呢?很明显,"it breaks"。大多数情况下确实如此,但Cocoa和runtime也提供了一些应对方法。
首先是动态方法处理。通常来说,处理一个方法,运行时寻找匹配的selector然后执行之。有时,你只想在运行时才创建某个方法,比如有些信息只有在运行时才能得到。要实现这个效果,你需要重写+resolveInstanceMethod: 和/或 +resolveClassMethod:。如果确实增加了一个方法,记得返回YES。

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selectorString = NSStringFromSelector(sel);
    if ([selectorString isEqualToString:@"method1"]) {
        class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:");
    }
    return [super resolveInstanceMethod:sel];
}

5.NSCoding的自动归档和自动解档
现在虽然手写归档和解档的时候不多了,但是自动操作还是用Runtime来实现的。直接取出全部的实例变量列表+for循环。

手动实现:手动的有一个缺陷,如果属性多起来,要写好多行相似的代码,虽然功能是可以完美实现,但是看上去不是很优雅。

- (void)encodeWithCoder:(NSCoder *)aCoder{
    [aCoder encodeObject:self.name forKey:@"name"];
}

- (id)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super init]) {
        self.name = [aDecoder decodeObjectForKey:@"name"];
    }
    return self;
}

用runtime实现的思路就比较简单,我们循环依次找到每个成员变量的名称,然后利用KVC读取和赋值就可以完成encodeWithCoder和initWithCoder了。

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

@implementation Student

- (void)encodeWithCoder:(NSCoder *)aCoder{
    unsigned int outCount = 0;
    Ivar *vars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i ++) {
        Ivar var = vars[i];
        const char *name = ivar_getName(var);
        NSString *key = [NSString stringWithUTF8String:name];

        id value = [self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
}

- (nullable __kindof)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super init]) {
        unsigned int outCount = 0;
        Ivar *vars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar var = vars[i];
            const char *name = ivar_getName(var);
            NSString *key = [NSString stringWithUTF8String:name];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
    }
    return self;
}
@end
  • class_copyIvarList方法用来获取当前 Model 的所有成员变量
  • ivar_getName方法用来获取每个成员变量的名称。

6.字典和模型互相转换

先来了解下KVC的底层原理:取出字典中的键值,去模型中找与之对应的属性

  1. 去模型中查找有没有setValue:,直接调用这个对象setValue:赋值
  2. 如果没有setValue:,就在模型中查找_value属性
  3. 如果没有_value属性,就查找value属性
  4. 如果还没有就报错

利用runtime转换原理:与KVC相反,先在模型中找到对应的成员变量,然后去字典中找到对应的数据进行赋值。

  1. 获取成员变量列表 :class_copyIvarList,进而获取模型中的所有实例变量
  • 将他们加入到一个数组当中,然后遍历数组,在遍历过程中获取字典中对应的value给属性对象赋值。

字典转模型

  1. 调用 class_getProperty 方法获取当前 Model 的所有属性。
  2. 调用 property_copyAttributeList 获取属性列表。
  3. 根据属性名称生成 setter 方法。
  4. 使用 objc_msgSend 调用 setter 方法为 Model 的属性赋值(或者 KVC)
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    // 解析Plist文件
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil];

    NSDictionary *statusDict = [NSDictionary dictionaryWithContentsOfFile:filePath];

    // 获取字典数组
    NSArray *dictArr = statusDict[@"statuses"];

    // 自动生成模型的属性字符串
//    [NSObject resolveDict:dictArr[0][@"user"]];


    _statuses = [NSMutableArray array];

    // 遍历字典数组
    for (NSDictionary *dict in dictArr) {

        Status *status = [Status modelWithDict:dict];

        [_statuses addObject:status];

    }

    // 测试数据
    NSLog(@"%@ %@",_statuses,[_statuses[0] user]);


}

@end
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    // 思路:遍历模型中所有属性-》使用运行时

    // 0.创建对应的对象
    id objc = [[self alloc] init];

    // 1.利用runtime给对象中的成员属性赋值

    // class_copyIvarList:获取类中的所有成员属性
    // Ivar:成员属性的意思
    // 第一个参数:表示获取哪个类中的成员属性
    // 第二个参数:表示这个类有多少成员属性,传入一个Int变量地址,会自动给这个变量赋值
    // 返回值Ivar *:指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到。
    /* 类似下面这种写法

     Ivar ivar;
     Ivar ivar1;
     Ivar ivar2;
     // 定义一个ivar的数组a
     Ivar a[] = {ivar,ivar1,ivar2};

     // 用一个Ivar *指针指向数组第一个元素
     Ivar *ivarList = a;

     // 根据指针访问数组第一个元素
     ivarList[0];

     */
    unsigned int count;

    // 获取类中的所有成员属性
    Ivar *ivarList = class_copyIvarList(self, &count);

    for (int i = 0; i < count; i++) {
        // 根据角标,从数组取出对应的成员属性
        Ivar ivar = ivarList[i];

        // 获取成员属性名
        NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];

        // 处理成员属性名->字典中的key
        // 从第一个角标开始截取
        NSString *key = [name substringFromIndex:1];

        // 根据成员属性名去字典中查找对应的value
        id value = dict[key];

        // 二级转换:如果字典中还有字典,也需要把对应的字典转换成模型
        // 判断下value是否是字典
        if ([value isKindOfClass:[NSDictionary class]]) {
            // 字典转模型
            // 获取模型的类对象,调用modelWithDict
            // 模型的类名已知,就是成员属性的类型

            // 获取成员属性类型
           NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
          // 生成的是这种@"@\"User\"" 类型 -》 @"User"  在OC字符串中 \" -> ",\是转义的意思,不占用字符
            // 裁剪类型字符串
            NSRange range = [type rangeOfString:@"\""];

           type = [type substringFromIndex:range.location + range.length];

            range = [type rangeOfString:@"\""];

            // 裁剪到哪个角标,不包括当前角标
          type = [type substringToIndex:range.location];


            // 根据字符串类名生成类对象
            Class modelClass = NSClassFromString(type);


            if (modelClass) { // 有对应的模型才需要转

                // 把字典转模型
                value  =  [modelClass modelWithDict:value];
            }


        }

        // 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
        // 判断值是否是数组
        if ([value isKindOfClass:[NSArray class]]) {
            // 判断对应类有没有实现字典数组转模型数组的协议
            if ([self respondsToSelector:@selector(arrayContainModelClass)]) {

                // 转换成id类型,就能调用任何对象的方法
                id idSelf = self;

                // 获取数组中字典对应的模型
                NSString *type =  [idSelf arrayContainModelClass][key];

                // 生成模型
               Class classModel = NSClassFromString(type);
                NSMutableArray *arrM = [NSMutableArray array];
                // 遍历字典数组,生成模型数组
                for (NSDictionary *dict in value) {
                    // 字典转模型
                  id model =  [classModel modelWithDict:dict];
                    [arrM addObject:model];
                }

                // 把模型数组赋值给value
                value = arrM;

            }
        }


        if (value) { // 有值,才需要给模型的属性赋值
            // 利用KVC给模型中的属性赋值
            [objc setValue:value forKey:key];
        }

    }

    return objc;
}

@end

注意:这段代码里面有一处判断typeString的,这里判断是防止model嵌套,比如说Student里面还有一层Student,那么这里就需要再次转换一次,当然这里有几层就需要转换几次。

几个出名的开源库JSONModel、MJExtension等都是通过这种方式实现的(利用runtime的class_copyIvarList获取属性数组,遍历模型对象的所有成员属性,根据属性名找到字典中key值进行赋值,当然这种方法只能解决NSString、NSNumber等,如果含有NSArray或NSDictionary,还要进行第二步转换,如果是字典数组,需要遍历数组中的字典,利用objectWithDict方法将字典转化为模型,在将模型放到数组中,最后把这个模型数组赋值给之前的字典数组)

模型转字典
这里是上一部分字典转模型的逆步骤:

  1. 调用 class_copyPropertyList 方法获取当前 Model 的所有属性。
  2. 调用 property_getName 获取属性名称。
  3. 根据属性名称生成 getter 方法。
  4. 使用 objc_msgSend 调用 getter 方法获取属性值(或者 KVC)
//模型转字典
-(NSDictionary *)keyValuesWithObject{
    unsigned int outCount = 0;
    objc_property_t *propertyList = class_copyPropertyList([self class], &outCount);
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    for (int i = 0; i < outCount; i ++) {
        objc_property_t property = propertyList[i];

        //生成getter方法,并用objc_msgSend调用
        const char *propertyName = property_getName(property);
        SEL getter = sel_registerName(propertyName);
        if ([self respondsToSelector:getter]) {
            id value = ((id (*) (id,SEL)) objc_msgSend) (self,getter);

            /*判断当前属性是不是Model*/
            if ([value isKindOfClass:[self class]] && value) {
                value = [value keyValuesWithObject];
            }

            if (value) {
                NSString *key = [NSString stringWithUTF8String:propertyName];
                [dict setObject:value forKey:key];
            }
        }

    }
    free(propertyList);
    return dict;
}

注:中间注释那里的判断也是防止model嵌套,如果model里面还有一层model,那么model转字典的时候还需要再次转换,同样,有几层就需要转换几次。
不过上述的做法是假设字典里面不再包含二级字典,如果还包含数组,数组里面再包含字典,那还需要多级转换。

7.动态获取 class 和 slector
比较基础的一个动态特性是通过String来生成Classes和Selectors。Cocoa提供了NSClassFromString和NSSelectorFromString方法,使用起来很简单:

Class stringclass = NSClassFromString(@"NSString");
NSString *myString = [stringclass stringWithString:@"Hello World"];
NSClassFromString(@"MyClass");
NSSelectorFromString(@"showShareActionSheet");

在NSObject协议中,有以下5个方法,我们可以通过NSObject的一些方法获取运行时信息或动态执行一些消息:

// 获取对象对应的class
- (Class)class;
// 判断一个对象或者类是不是某个class或者这个class的派生类
- (BOOL)isKindOfClass:(Class)aClass;
// 判断是否是该类的实例,不包括子类或者父类;
- (BOOL)isMemberOfClass:(Class)aClass;
// 判断一个对象或者类对应的objc_class里面是否实现了某个协议
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
// 判断一个对象或者类对应的objc_class里面有没有某个方法
- (BOOL)respondsToSelector:(SEL)aSelector;

自动打印属性字符串

    @implementation NSObject (Log)


// 自动打印属性字符串
+ (void)resolveDict:(NSDictionary *)dict{

    // 拼接属性字符串代码
    NSMutableString *strM = [NSMutableString string];

    // 1.遍历字典,把字典中的所有key取出来,生成对应的属性代码
    [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {

        // 类型经常变,抽出来
         NSString *type;

        if ([obj isKindOfClass:NSClassFromString(@"__NSCFString")]) {
            type = @"NSString";
        }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFArray")]){
            type = @"NSArray";
        }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFNumber")]){
            type = @"int";
        }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFDictionary")]){
            type = @"NSDictionary";
        }


        // 属性字符串
        NSString *str;
        if ([type containsString:@"NS"]) {
            str = [NSString stringWithFormat:@"@property (nonatomic, strong) %@ *%@;",type,key];
        }else{
            str = [NSString stringWithFormat:@"@property (nonatomic, assign) %@ %@;",type,key];
        }

        // 每生成属性字符串,就自动换行。
        [strM appendFormat:@"\n%@\n",str];

    }];

    // 把拼接好的字符串打印出来,就好了。
    NSLog(@"%@",strM);
}
@end
5.Runtime缺点及Runtime常用函数**

1.危险性主要体现以下几个方面:

  • Method swizzling不是原子性操作。如果在+load方法里面写,是没有问题的,但是如果写在+initialize方法中就会出问题。

  • 调用super方法会出问题
    如果你在一个类中重写一个方法,并且不调用super方法,你可能会导致一些问题出现。在大多数情况下,super方法是期望被调用的(除非有特殊说明)。如果你使用同样的思想来进行Swizzling,可能就会引起很多问题。如果你不调用原始的方法实现,那么你Swizzling改变的太多了,而导致整个程序变得不安全。

2. 日常可能用的比较多的Runtime函数可能就是下面这些

//获取cls类对象所有成员ivar结构体
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
//获取cls类对象name对应的实例方法结构体
Method class_getInstanceMethod(Class cls, SEL name)
//获取cls类对象name对应类方法结构体
Method class_getClassMethod(Class cls, SEL name)
//获取cls类对象name对应方法imp实现
IMP class_getMethodImplementation(Class cls, SEL name)
//测试cls对应的实例是否响应sel对应的方法
BOOL class_respondsToSelector(Class cls, SEL sel)
//获取cls对应方法列表
Method *class_copyMethodList(Class cls, unsigned int *outCount)
//测试cls是否遵守protocol协议
BOOL class_conformsToProtocol(Class cls, Protocol *protocol)
//为cls类对象添加新方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
//替换cls类对象中name对应方法的实现
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
//为cls添加新成员
BOOL class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types)
//为cls添加新属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
//获取m对应的选择器
SEL method_getName(Method m)
//获取m对应的方法实现的imp指针
IMP method_getImplementation(Method m)
//获取m方法的对应编码
const char *method_getTypeEncoding(Method m)
//获取m方法参数的个数
unsigned int method_getNumberOfArguments(Method m)
//copy方法返回值类型
char *method_copyReturnType(Method m)
//获取m方法index索引参数的类型
char *method_copyArgumentType(Method m, unsigned int index)
//获取m方法返回值类型
void method_getReturnType(Method m, char *dst, size_t dst_len)
//获取方法的参数类型
void method_getArgumentType(Method m, unsigned int index, char *dst, size_t dst_len)
//设置m方法的具体实现指针
IMP method_setImplementation(Method m, IMP imp)
//交换m1,m2方法对应具体实现的函数指针
void method_exchangeImplementations(Method m1, Method m2)
//获取v的名称
const char *ivar_getName(Ivar v)
//获取v的类型编码
const char *ivar_getTypeEncoding(Ivar v)
//设置object对象关联的对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取object关联的对象
id objc_getAssociatedObject(id object, const void *key)
//移除object关联的对象
void objc_removeAssociatedObjects(id object)
这些API看上去不好记,其实使用的时候不难,关于方法操作的,一般都是method开头,关于类的,一般都是class开头的,其他的基本都是objc开头的,剩下的就看代码补全的提示,看方法名基本就能找到想要的方法了。当然很熟悉的话,可以直接打出指定方法,也不会依赖代码补全。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容