Runtime

一、runtime简介

  • RunTime简称运行时。OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制。
  • 对于C语言,函数的调用在编译的时候会决定调用哪个函数。
  • 对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
  • 事实证明:
    • 在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。
    • 在编译阶段,C语言调用未实现的函数就会报错。

二、Runtime数据结构

  • runtime的数据结构:
    • objc_object,
    • isa,
    • objc_class,
      • superClass
      • cache_t,
      • class_data_bits_t,
2.1.objc_object

我们平时使用的所有对象,都是id类型的,id类型对象对应到runtime当中,代表的就是objc_object结构体

objc_object主要包含以下几个成员部分:
1.isa_t:公用体
2.isa操作相关:比如通过objc_object结构体来获取它的isa所指向的类对象,包括类对象的isa获取它的元类对象,以及便利方法
3.弱引用相关:
4.关联对象相关:
5.内存管理相关:

2.2、objc_class

objc_class (class) 继承自objc_object,主要包含以下几个成员部分
1.Class superClass:指针,指向objc_object
2.cache_t cache: 方法缓存的一个结构,我们在进行消息传递的过程当中会使用到方法缓存的这个结构
3.class_data_bits_t bits:变量、属性、方法

2.2.1、Class superClass:

objc_class拥有一个superClass指针,所指向的类型也是Class,例如,如果一个类对象的话,superClass所指向的就是它的父类对象,也就是我们平时说的类与父类的关系就是通过objc_class中Class superClass这个成员变量来定义的

2.2.2、cache_t

用于快速查找方法执行函数
是可增量打扩展的哈希表结构
是局部性原理的最佳应用

cache_t的数据结构说明


是一张由bucket_t组成的Hash表
cache_t的成员变量:key,IMP
key对应OC语言中的selecter
IMP可以理解为无类型的函数指针

struct cache_t {
    struct bucket_t *_buckets; // 一个散列表,用来方法缓存,bucket_t类型,包含key以及方法实现IMP
           mask_t _mask;       // 分配用来缓存bucket的总数
           mask_t _occupied;   // 表明目前实际占用的缓存bucket的个数
}

struct bucket_t {
    private:
        cache_key_t _key;
        IMP _imp;
}
2.2.3、class_data_bits_t

class_data_bits_t:主要是对class_rw_t的封装
class_rw_t:代表了类相关的读写信息、对class_ro_t的封装
class_ro_t:代表了类相关的只读信息

class_rw_t


class_rw_t

class_ro_t


class_ro_t

method_t


method_t
2.3、isa指针

共用体isa_t

共用体isa_t

isa指向
对象,其指向类对象
类对象,其指向元类对象


  • isa指针是什么含义?
    isa指针有指针型的isa和非指针型的isa,指针型isa的值代表class的地址,非指针型isa的值的部分代表class的地址。
    isa指向
    关于对象,其指向类对象
    调用实例方法的时候,实际是通过对象的isa指针到对象的类对象中进行方法查找
    关于类对象,其指向元类对象
    调用类方法的时候,实际是通过类的isa指针到它的元类对象中进行方法查找

  • class这么一个类,是否是一个对象?
    class也是一个对象,被称为类对象,因为class是继承自objc_object的。
    主要包含以下几个成员部分:
    1.class superclass:指向父类
    2.cache_t:表达方法缓存的数据结构,在消息传递的时候会用到
    3.class_data_bits_t:变量、属性、方法

三、类对象与元类对象

  • 对象、类对象、元类对象
    类对象存储实例方法列表等信息。
    元类对象存储类方法列表等信息。
  • 如果我们调用的一个类方法没有对应的实现,但是有同名的实例方法的实现,这个时候会不会发生崩溃?会不会产生实际的调用?
    由于根元类的对象superclass指针指向根类对象,当我们在元类对象中去找类方法列表没有查找到的时候,根元类就会顺着isa指针去实例方法列表中查找,那么如果有同名方法,就会执行同名的实例方法。

  • 实例方法的消息传递过程:
    如果调用了实例对象方法,那么系统首先会根据当前实例的isa指针找到它的类对象,然后在它的类对象当中遍历方法列表,去查找同名的方法实现,如果没有查找到,就会顺着superclass指针的指向查找父类的类对象的方法列表,然后如果没有,再顺着superclass指针向根类对象查找方法列表,还没有查找到,就会走到消息的转发流程。

  • 类方法的消息传递过程:
    如果调用类方法,就会通过类对象的isa指针,找到它的元类对象遍历方法列表,去查找同名的方法实现,如果没有查找到,就会顺着superclass指针的指向查找父元类的元类对象的方法列表,然后如果没有,再顺着superclass指针向根元类对象查找方法列表,还没有查找到,就会顺着superclass指针的指向找到根类(Root class)方法列表,还是找不到,就会走到消息的转发流程。

在遍历类方法和实例方法当中的区别在于,类对象方法在查找到根元类对象方法列表的时候,最终会找到根类方法列表。

  • 类对象和元类对象分别是什么?类对象和元类对象之间有什么区别?
    实例对象、类对象、元类、根元类、NSObject之间有这样一种关系:
    实例对象是由类对象初始化来的
    类对象由元类初始化而来
    元类是一种虚拟的类,由系统帮我创建,不用手动创建
    元类由根元类初始化而来
    根元类由NSObject初始化而来

四、消息传递

4.1、消息传递的第一个函数
void objc_msgSend(void /* id self, SEL ор, ...* )
[self class] <--> objc_msgSend(self, @selector(class))

objc_msgSend这个函数接收两个参数,第一个参数是一个id类型的self对象,第二个参数是SEL类型的方法选择器名称,后面才是消息传递的真正的方法参数,对于任何一个消息传递的[self class]通过编译器,都会转换成关于objc_msgSend(self, @selector(class))这样的函数调用,第一个参数是消息传递的接收者self,第二个参数就是传递的消息名称或者说选择器,从中可以看出,对于消息传递,实际上是转化成函数调用,这一步骤是发生在编译器层面的。

4.2、消息传递的第二个函数
void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */)

其中两个固定的参数,第一个参数是objc_super结构体类型的super指针,第二个参数SEL类型的方法选择器,后面才是消息传递的真正的方法参数。

struct objc_super {
    // Specifies an instanceọf a class.
    __unsafe_unretained id receiver;
};

objc_super结构体当中包含一个叫receiver的成员变量,这个接收者实际上就是当前对象,因为super这个关键字实际上是一个编译器关键字,经过编译器编译之后,它实际上会给我们解析成objc_super结构体类型的指针super。这个结构体中的成员变量receiver就是当前对象。

[super class] <--> objc_msgSendSuper(super, @sletor(class))

[super class]经过编译之后转变成了objc_msgSendSuper(super, @sletor(class)),第一个参数是super,第二个参数是方法选择器,这个super里面实际上包含了receiver,就是当前对象。

所以,不论是调用[super class]还是[self class],实际上,这条消息的接收者都是当前对象。

#import "Mobile.h"
@interface Phone : Mobile
@end
@implementation Phone
- (id)init{
self = [super init];
    if (self) {
        NSLog (@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end
对于[self class],它会被转化成objc_msgSend的函数调用,objc_msgSend的第一个参数就是消息传递的接收者self,即当前对象;[super class]会转化成objc_msgSendSuper的函数调用,objc_msgSendSuper的第一个函数虽然是super,但是super这个结构体包着的一个receiver的当前对象,所以,无论转换成哪个函数,它们的接收者,都是当前对象。
所以,打印结果都是Phone。

objc_msgSend和objc_msgSendSuper有什么区别?

4.3、消息传递流程和机制:
  • 消息传递的过程
    第一步:缓存查找:
    在调用一个方法的时候,先会查找缓存,看缓存当中是否有对应选择器名称的方法实现,如果有,那么通过函数指针调用函数,完成一次消息传递;
    第二步:当前类查找
    如果缓存没有对应选择器名称的方法实现,那么会根据当前实例的isa指针去查找当前类对象的方法列表看是否有同样名称的方法,如果找到,那么通过函数指针调用函数,结束消息传递流程。当前类查找中,对于已排序好的列表,采用二分查找算法查找方法对应执行函数。对于没有排序的列表,采用一般遍历查找方法对应执行函数。
    第三步:逐级父类查找
    如果当前类方法列表当中没有对应名称的方法,那么就会逐级父类方法列表当中查找,那么这个过程,实际上就是通过当前类对象的superclass指针去查找它的父类的方法列表,如果在它的父类方法列表当中没有查到,就会根据父类的superclass指针再往上查找,直到nil为止。如果在某一个父类的方法列表当中查找到了选择器同名的方法,那么根据函数指针去调用这个函数的实现,然后结束消息传递流程。
    第四步:消息转发
    如果在父类方法列表当中,一直查到根类对象,比如NSObject,仍然没有查找到同名的方法的实现,那么就会进入转发流程,然后结束消息传递流程。

  • 方法缓存查找:哈希查找
    在缓存查找方法过程当中,实际上就是根据给定的选择器,来查找它对应的方法实现,我们给定的选择器因子,就是要到bucket_t数组当中把对应的bucket_t给找出来。
    这个过程,大概是这样的,首先,我们根据给定的方法选择器,通过一个函数来映射出对应的bucket_t在数组中的位置,这一步骤,其实就是哈希查找。哈希查找,就是通过我们给定的一个值,比如这个方法选择器,然后经过哈希函数的算法,算出这个值,实际上就是这个给定值在对应数组当中对应的索引位置,我们通过这个哈希函数在缓存查找当中,这个哈希查找表达式实际上就是根据选择器因子和一个对应的mask做位与操作来进行计算对应的bucket_t在数组当中的索引位置,这个mask实际上也是bucket_t结构体的成员变量,通过这个哈希函数算法,就可以找到给定选择器因子所对应函数的实现在数组列表中的索引位置。使用哈希查找的关键就是解决查找效率的问题。查找到选择器因子所对应bucket_t之后就可以提取它对应的IMP函数指针,然后返回给调用方就可以了。

  • 当前类查找
    对于已排序好的列表,采用二分查找算法查找方法对应执行函数。
    对于没有排序的列表,采用一般遍历查找方法对应执行函数。

  • 父类逐级查找
    逐级父类方法查找,就是根据superclass指针,一级一级往上查找,查找它的父类,遍历每个父类,对于每个父类中的方法查找也是同样两个步骤,首先查找父类的缓存,缓存中没有,再查找父类对应的方法列表,已排序好的列表,采用二分查找算法查找,没有排序的列表,采用一般遍历查找方法查找。


    父类逐级查找
  • 消息传递流程三大特点:
    1.缓存查找:哈希查找
    2.当前类查找:对于已排序好的列表,采用二分查找算法查找方法对应执行函数,对于没有排序的列表,采用一般遍历查找方法对应执行函数
    3.父类逐级查找:根据superclass指针,一级一级往上查找,查找它的父类,遍历每个父类,对于每个父类中的方法查找也是同样两个步骤,首先查找父类的缓存,缓存中没有,再查找父类对应的方法列表,已排序好的列表,采用二分查找算法查找,没有排序的列表,采用一般遍历查找方法查找。

4.4、消息转发
4.4.1、实例方法的消息转发流程
实例方法的消息转发流程
  • 实例方法的消息转发流程
    第一次机会:
    首先系统会回调一个resolveInstanceMethod方法(如果是类方法,回调的是resolveClassMethod方法),resolveInstanceMethod方法有一个参数,是方法的选择器,也就是SEL类型的参数,返回值是一个BOOL类型的,相当于告诉系统,我们要不要解决当前实例方法的实现,这个方法是一个类方法,不是实例方法,要注意,如果这一步返回的是YES,或者说我们给予的这个方法选择器所对应的方法实现了,相当于通知系统当前消息已处理,然后结束消息转发流程;
    第二次机会:
    如果resolvelnstanceMethod方法返回NO的话,系统会给我们第二次机会来处理这条消息,这个时候会回调forwardingTargetForSelector:方法,同样这个方法的参数也是SEL类型的方法选择器,返回值是一个id类型的,相当于告诉系统,这个选择器,或者说这次实例方法的调用,应该由哪个对象来处理,转发对象是谁,如果我们指定了一个转发目标的话,系统会把这条消息转发给我们返回的转发目标,同时会结束当前消息的转发流程。
    第三次机会:
    如果说在第二次机会的时候,我们仍然没有给它返回一个转发目标的情况下,系统会给我们第三次处理这条消息的机会,也是最后一次机会,首先系统会调用methodSignatureForSelector:方法,这个方法的参数仍然是是SEL类型的方法选择器,方法的返回值是一个methodSignature对象,这个对象实际上是对于这个方法选择器的返回值的的类型以及它的参数个数和参数类型的一个封装,此时,如果返回了一个方法签名的话,系统会调用forwardInvocation:,如果forwardInvocation能够处理这条消息的话,消息转发流程就结束,如果methodSignatureForSelector方法返回的是一个nil,或者forwardInvocation没有办法处理这个消息的话,就会标记为消息无法处理。
测试:
第一步:创建一个RuntimeObject类
RuntimeObject.h
#import <Foundation/Foundation.h>
@interface RuntimeObject : NSObject
- (void)test;
@end

RuntimeObject.m
#import "RuntimeObject.h"

@implementation RuntimeObject
//第一个函数
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // 如果是test方法 打印日志
    if (sel == @selector(test)) {
        NSLog(@"resolveInstanceMethod:");
        return NO;
    } else { 
        // 返回父类的默认调用
        return [super resolveInstanceMethod:sel];
    }
}

// 第二个函数
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"forwardingTargetForSelector:");
    return nil;
}

// 第三个函数
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        NSLog(@"methodSignatureForSelector:");
        // v 代表返回值是void类型的  @代表第一个参数类型时id,即self
        // : 代表第二个参数是SEL类型的  即@selector(test)
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    } else {
        return [super methodSignatureForSelector:aSelector];
    }
}

// 第四个函数
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"forwardInvocation:");
}
@end

第一步:编写测试代码
#import "ViewController.h"
#import "RuntimeObject.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    RuntimeObject *obj = [[RuntimeObject alloc] init];
    // 调用test方法,只声明,没有实现
    [obj test];
}
@end

打印结果
2020-07-06 01:06:33.735960+0800 RuntimeTest[46347:3419335] resolveInstanceMethod:
2020-07-06 01:06:33.736042+0800 RuntimeTest[46347:3419335] forwardingTargetForSelector:
2020-07-06 01:06:33.736108+0800 RuntimeTest[46347:3419335] methodSignatureForSelector:
2020-07-06 01:06:33.736206+0800 RuntimeTest[46347:3419335] forwardInvocation:

五、runtime作用

1.发送消息
2.交换方法
3.动态添加方法
4.动态方法解析
5.给分类添加属性
6.字典转模型

1.发送消息
  • 方法调用的本质,就是让对象发送消息。
  • objc_msgSend,只有对象才能发送消息,因此以objc开头.
  • 使用消息机制前提,必须导入#import <objc/message.h>
  • 消息机制简单使用
//  Person.h
//  Runtime(消息机制)
#import <Foundation/Foundation.h>

@interface Person : NSObject
+ (void)eat;
- (void)run:(int)age;
- (void)eat;
@end
//  Person.m
//  Runtime(消息机制)

#import "Person.h"

@implementation Person

- (void)eat {
    NSLog(@"对象方法-吃东西");
}

+ (void)eat {
    NSLog(@"类方法-吃东西");
}

- (void)run:(int)age {
    NSLog(@"%d",age);
}
@end
//  ViewController.m
//  Runtime(消息机制)

#import "ViewController.h"
#import "Person.h"

// 运行时使用运行时的步骤
// 第一步:导入<objc/message.h>
// 第二步:Build Setting -> 搜索msg -> 设置属性为No
#import <objc/message.h>

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建person对象
    Person *p = [[Person alloc] init];
    
    // 调用对象方法
    [p eat];
    
    // OC:运行时机制,消息机制是运行时机制最重要的机制
    // 消息机制:任何方法调用,本质都是发送消息
    
    // SEL:方法编号,根据方法编号就可以找到对应方法实现
    // performSelector:动态添加方法
    [p performSelector:@selector(eat)];
    
    // 运行时,发送消息,谁做事情就那谁
    // xcode5之后,苹果不建议使用底层方法
    // xcode5之后,使用运行时.
    
    // 让p发送消息
    // 不带参数
    objc_msgSend(p, @selector(eat));
    // 带参数
    objc_msgSend(p, @selector(run:),10);
    
    // 调用类方法的方式:两种
    // 第一种通过类名调用,类名调用类方法,本质类名转换成类对象
    [Person eat];
    // 第二种通过类对象调用
    [[Person class] eat];
    
    // 获取类对象
    Class personClass = [Person class];
    
    [personClass performSelector:@selector(eat)];
    
    // 运行时
    // 用类名调用类方法,底层会自动把类名转换成类对象调用
    // 本质:让类对象发送消息
    objc_msgSend(personClass, @selector(eat));
}

@end
  • 消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现


2.交换方法
  • 开发使用场景:系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有的功能。
  • 方式一:继承系统的类,重写方法.
  • 方式二:使用runtime,交换方法.
//  UIImage+Image.h
//  Runtime(交换方法)

#import <UIKit/UIKit.h>

@interface UIImage (Image)
+ (__kindof UIImage *)ge_imageNamed:(NSString *)imageName;
@end
//  UIImage+Image.m
//  Runtime(交换方法)

#import "UIImage+Image.h"

#import <objc/message.h>

@implementation UIImage (Image)

// 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super.
// 在分类里面不能调用super,分类木有父类
//+ (UIImage *)imageNamed:(NSString *)name
//{
//    [super im]
//}

// 利用运行时
// 先写一个其他方法,实现这个功能
// 既能加载图片又能打印
+ (UIImage *)ge_imageNamed:(NSString *)imageName
{
    // 1.加载图片
    UIImage *image = [UIImage ge_imageNamed:imageName];
    
    // 2.判断功能
    if (image == nil) {
        NSLog(@"加载image为空");
    }

    return image;
}

// 加载这个分类的时候调用
+ (void)load
{
    // 交换方法实现,方法都是定义在类里面
    // class_getMethodImplementation:获取方法实现
    // class_getInstanceMethod:获取对象
    // class_getClassMethod:获取类方法
    // IMP:方法实现
    
    // imageNamed
    // Class:获取哪个类方法
    // SEL:获取方法编号,根据SEL就能去对应的类找方法
    Method imageNameMethod = class_getClassMethod([UIImage class], @selector(imageNamed:));
    
    // ge_imageNamed
    Method ge_imageNamedMethod = class_getClassMethod([UIImage class], @selector(ge_imageNamed:));
    
    // 交换方法实现
    method_exchangeImplementations(imageNameMethod, ge_imageNamedMethod);
}
@end
//  ViewController.m
//  Runtime(交换方法)

#import "ViewController.h"
//#import "UIImage+Image.h"

@interface ViewController ()

@end


@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 需求:给imageNamed方法提供功能,每次加载图片就判断下图片是否加载成功。
    // 步骤一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
    // 步骤二:交换imageNamed和imageWithName的实现,就能调用imageWithName,间接调用imageWithName的实现。
    
//    UIImage *image = [UIImage imageNamed:@"123"];
    // 不好的地方
    // 1.每次使用,都需要导入头文件
    // 2.当一个项目开发太久,使用这个方式不靠谱
    
    // imageNamed:
    // 实现方法:底层调用ge_imageNamed
    
    // 本质:交换两个方法的实现imageNamed和ge_imageNamed方法
    // 调用imageNamed其实就是调用ge_imageNamed
    
    // 系统imageNamed加载图片,并不知道图片是否加载成功
    // 交换以后调用imageNamed的时候,就知道图片是否加载
    
    [UIImage imageNamed:@"123"];
}

@end
  • 交换原理:
    • 交换之前:
    • 交换之前:

3.动态添加方法

  • 开发使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。
  • 经典面试题:有没有使用performSelector,其实主要想问你有没有动态添加过方法。
  • 简单使用
//  Person.h
//  Runtime(动态添加方法)

#import <Foundation/Foundation.h>

@interface Person : NSObject

@end

//  Person.m
//  Runtime(动态添加方法)

#import "Person.h"

#import <objc/message.h>

@implementation Person

// 动态添加方法,首先实现这个resolveInstanceMethod
// resolveInstanceMethod调用:当调用了没有实现的方法没有实现就会调用resolveInstanceMethod
// resolveInstanceMethod作用:就知道哪些方法没有实现,从而动态添加方法
// sel:没有实现方法

// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
// 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
//    NSLog(@"%@",NSStringFromSelector(sel));
    
    // 动态添加eat方法
    if (sel == @selector(eat:)) {
        
        // 第一个参数:cls:给哪个类添加方法
        // 第二个参数:SEL:添加方法的方法编号是什么
        // 第三个参数:IMP:方法实现,函数入口,函数名
        // 第四个参数:types:方法类型
            // v 表示 void
            // @ 表示对象
            // : 表示SEL
        class_addMethod(self, sel, (IMP)aaaa, "v@:@");
        
        // 处理完
        return YES;
    }
    
    // 返回系统的方法,因为上面改了,尽量不要修改系统的方法
    return [super resolveInstanceMethod:sel];
}

// 默认一个方法都有两个参数,self,_cmd,属于隐式参数
// self:方法调用者
// _cmd:调用方法的编号

// 定义函数
// 没有返回值,参数(id,SEL)
// void(id,SEL)
void aaaa(id self, SEL _cmd, id param1)
{  
    NSLog(@"调用eat %@ %@ %@",self,NSStringFromSelector(_cmd),param1);
}
@end
//  ViewController.m
//  Runtime(动态添加方法)

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // performSelector:动态添加方法
    Person *p = [[Person alloc] init];
    
    // 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。
    // 通过运行时后,动态添加方法就不会报错
    
    // 动态添加方法
    // 不带参数
    [p performSelector:@selector(eat)];
    // 带参数
    [p performSelector:@selector(eat:) withObject:@111];
}
@end
4.给分类添加属性
  • 原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。
//  NSObject+Objc.h
//  Runtime(分类添加属性)

#import <Foundation/Foundation.h>

@interface NSObject (Objc)

// @property:只会生成set方法的声明,不会实现
@property (nonatomic, strong) NSString *name;

@end
//  NSObject+Objc.m
//  Runtime(分类添加属性)

#import "NSObject+Objc.h"

#import <objc/message.h>

@implementation NSObject (Objc)

// 定义关联的key
//static NSString *_name;

// set方法
- (void)setName:(NSString *)name {
    // 添加属性,跟对象
    // 给某个对象产生关联,添加属性
    // 第一个参数:object:给哪个对象添加属性
    // 第二个参数:key:属性名,根据key去获取关联的对象 ,void * == id
    // 第三个参数:value:关联的值
    // 第四个参数:policy:缓存策略
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

// get方法
- (NSString *)name {
    // 根据关联的key,获取关联的值。
    return objc_getAssociatedObject(self, @"name");
}
@end
//  ViewController.m
//  Runtime(分类添加属性)

#import "ViewController.h"
#import "NSObject+Objc.h"

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    NSObject *objc = [[NSObject alloc] init];
    objc.name = @"123";
    NSLog(@"%@",objc.name);
}
@end
5.字典转模型
  • 设计模型:字典转模型的第一步
    • 模型属性,通常需要跟字典中的key一一对应
    • 问题:一个一个的生成模型属性,很慢?
    • 需求:能不能自动根据一个字典,生成对应的属性。
    • 解决:提供一个分类,专门根据字典生成对应的属性字符串。
//  NSObject+Property.h
//  自动生成属性代码

//  通过解析字典自动生成属性代码

#import <Foundation/Foundation.h>

@interface NSObject (Property)

///  通过解析字典自动生成属性代码
+ (void)createPropertyCodeWithDict:(NSDictionary *)dict;

@end
//  NSObject+Property.m
//  自动生成属性代码

//  通过解析字典自动生成属性代码

#import "NSObject+Property.h"

@implementation NSObject (Property)

///  通过解析字典自动生成属性代码
+ (void)createPropertyCodeWithDict:(NSDictionary *)dict {
    // 拼接属性字符串代码
    NSMutableString *strM = [NSMutableString string];
    
    /*********************** 方法1 ***************************/
    // 遍历字典
    [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull propertyName, id  _Nonnull value, BOOL * _Nonnull stop) {
//        NSLog(@"%@ %@",propertyName,[value class]);
        
        // 类型经常变,抽出来
        NSString *code;
        
        if ([value isKindOfClass:NSClassFromString(@"__NSCFString")]) {
            code = [NSString stringWithFormat:@"@property (nonatomic, copy) NSString *%@;",propertyName]
            ;
        }else if ([value isKindOfClass:NSClassFromString(@"__NSCFNumber")]){
            code = [NSString stringWithFormat:@"@property (nonatomic, assign) int %@;",propertyName]
            ;
        }else if ([value isKindOfClass:NSClassFromString(@"__NSCFArray")]){
            code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSArray *%@;",propertyName]
            ;
        }else if ([value isKindOfClass:NSClassFromString(@"__NSCFDictionary")]){
            code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSDictionary *%@;",propertyName]
            ;
        }else if ([value isKindOfClass:NSClassFromString(@"__NSCFBoolean")]){
            code = [NSString stringWithFormat:@"@property (nonatomic, assign) BOOL %@;",propertyName]
            ;
        }
        // 每生成属性字符串,就自动换行。
        [strM appendFormat:@"\n%@\n",code];
    }];
    
    /*********************** 方法2 ***************************/
    // 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 = %@",strM);
}

@end
//  ViewController.m
//  Runtime(自动生成属性代码)

#import "ViewController.h"

#import "NSObject+Property.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 解析Plist
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil];
    NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:filePath];
    NSArray *dictArr = dict[@"statuses"];
    
    // 设计模型属性代码,生成打印后赋值粘贴到模型即可
    [NSObject createPropertyCodeWithDict:dictArr[0]];
}
@end
  • 字典转模型的方式一:KVC
//  Status.m
//  字典转模型KVC实现

#import "Status.h"

@implementation Status

// 字典转模型 - 模型的属性名跟字典一一对应
+ (Status *)statusWithDict:(NSDictionary *)dict {
    Status *status = [[self alloc] init];
    
    // KVC
    [status setValuesForKeysWithDictionary:dict];
    
    return status;
}
@end
  • KVC字典转模型弊端:必须保证,模型中的属性和字典中的key一一对应。
    • 如果不一致,就会调用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:] 报key找不到的错。
    • 分析:模型中的属性和字典的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。
    • 解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖, 就能继续使用KVC,字典转模型了。
//  Status.m
// 转模型KVC实现

#import "Status.h"

@implementation Status

// 字典转模型 - 模型的属性名跟字典一一对应
+ (Status *)statusWithDict:(NSDictionary *)dict
{
    Status *status = [[self alloc] init];
    
    // KVC
    [status setValuesForKeysWithDictionary:dict];
    
    return status;
}

// 解决KVC报错
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
    if ([key isEqualToString:@"id"]) {
        _ID = [value integerValue];
    }
    // key:没有找到key
    // value:没有找到key对应的值
    NSLog(@"没有找到key = %@,没有找到key对应的值 = %@",key,value);
}

@end
//  ViewController.m
//  Runtime(字典转模型KVC实现)

#import "ViewController.h"
#import "NSObject+Property.h"
#import "Status.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 解析Plist
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil];
    NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:filePath];
    NSArray *dictArr = dict[@"statuses"];
    
    // 设计模型属性代码,生成打印后赋值粘贴到模型即可
//    [NSObject createPropertyCodeWithDict:dictArr[0]];
    
    NSMutableArray *statuses = [NSMutableArray array];
    
    for (NSDictionary *dict in dictArr) {
        // 字典转模型
        Status *status = [Status statusWithDict:dict];
        
        [statuses addObject:status];
    }
    
    NSLog(@"%@",statuses);
}

@end
  • 字典转模型的方式二:Runtime
    • 思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。
    • 步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类转。
//  User.h
//  Runtime(字典转模型)

#import <Foundation/Foundation.h>

@interface User : NSObject

@property (nonatomic, copy) NSString *profile_image_url;

@property (nonatomic, assign) BOOL vip;

@property (nonatomic, copy) NSString *name;

@property (nonatomic, assign) int mbrank;

@property (nonatomic, assign) int mbtype;

@end
//  User.m
//  Runtime(字典转模型)

#import "User.h"

@implementation User

@end
//  Status.h
//  Runtime(字典转模型)

#import <Foundation/Foundation.h>

@class User;

@interface Status : NSObject

// 写一段程序自动生成属性代码

@property (nonatomic, assign) NSInteger ID;
// 解析字典自动生成属性代码
@property (nonatomic, strong) NSString *source;

@property (nonatomic, assign) NSInteger reposts_count;

@property (nonatomic, strong) NSArray *pic_urls;

@property (nonatomic, strong) NSString *created_at;

@property (nonatomic, assign) int attitudes_count;

@property (nonatomic, strong) NSString *idstr;

@property (nonatomic, strong) NSString *text;

@property (nonatomic, assign) int comments_count;

@property (nonatomic, strong) User *user;

@property (nonatomic, strong) NSDictionary *retweeted_status;

@end
//  Status.m
//  Runtime(字典转模型)

#import "Status.h"

@implementation Status

@end
//  NSObject+Model.h
//  Runtime(字典转模型)

//  Runtime字典转模型分类

#import <Foundation/Foundation.h>

@interface NSObject (Model)

+ (instancetype)modelWithDict:(NSDictionary *)dict;

@end
//  NSObject+Model.m
//  Runtime(字典转模型)

//  Runtime字典转模型分类

#import "NSObject+Model.h"

#import <objc/message.h>
/*
 Ivar ivar1;
 Ivar ivar2;
 Ivar ivar3;
 Ivar a[] = {ivar3,ivar1,ivar2};
 Ivar *ivar = &a;
 */

@implementation NSObject (Model)

+ (instancetype)modelWithDict:(NSDictionary *)dict{
    
    // 1.创建对应类的对象
    id objc = [[self alloc] init];
    
    // 2.利用runtime给对象中的成员属性赋值
    
    // runtime:遍历模型中所有成员属性,去字典中查找
    // 属性定义在哪,定义在类,类里面有个属性列表(数组)
    
    // 遍历模型所有成员属性
    // ivar:成员属性
    // class_copyIvarList:把成员属性列表复制一份给你
    // Ivar *:指向Ivar指针
    // Ivar *:指向一个成员变量数组
    // class:获取哪个类的成员属性列表
    // count:成员属性总数
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList(self, &count);
    for (int i = 0 ; i < count; i++) {
        // 获取成员属性
        Ivar ivar = ivarList[I];
        
        // 获取成员名
        NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
    
        // 获取key
        NSString *key = [propertyName substringFromIndex:1];
        
        // user value:字典
        // 获取字典的value
        id value = dict[key];
        // 给模型的属性赋值
        // value:字典的值
        // key:属性名
        
        // 获取成员属性类型
        NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        
        // user:NSDictionary
        // 二级转换
        // 值是字典,成员属性的类型不是字典,才需要转换成模型
        if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"]) {
            
            // 字典转模型
            // 获取模型的类对象,调用modelWithDict
            // 模型的类名已知,就是成员属性的类型
            
            // 需要字典转换成模型
            // 转换成哪个类型
//            NSLog(@"转换成哪个类型 = %@",propertyType);
            
            /*********** *********** 字符串截取 *********** ***********/
            // 字符串截取
            // 生成的是这种@"@\"User\"" 类型 -> @"User"  在OC字符串中 \" -> ",\是转义的意思,不占用字符
            // @"@\"User\"" User
            // \":算一个字符
            NSRange range = [propertyType rangeOfString:@"\""];
            propertyType = [propertyType substringFromIndex:range.location + range.length];
            // User\"";
            // 裁剪到哪个角标,不包括当前角标
            range = [propertyType rangeOfString:@"\""];
            propertyType = [propertyType substringToIndex:range.location];
            /*********** *********** 字符串截取 *********** ***********/
            
            // 获取需要转换类的类对象
            // 根据字符串类名生成类对象
            Class modelClass =  NSClassFromString(propertyType);
            
            // 有对应的模型才需要转
            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
//  ViewController.m
//  Runtime(字典转模型)

#import "ViewController.h"
#import "Status.h"
#import "User.h"
#import "NSObject+Model.h"

@interface ViewController ()

@end

@implementation ViewController

/*
 KVC:遍历字典中所有key,去模型中查找有没有对应的属性名,没有对应的key就会报错
 runtime:遍历模型中所有属性名,去字典中查找,这样不会像KVC那样报错
 */

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 解析Plist
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil];
    NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:filePath];
    NSArray *dictArr = dict[@"statuses"];
        
    NSMutableArray *statuses = [NSMutableArray array];
    // 遍历字典数组
    for (NSDictionary *dict in dictArr) {
        //  Runtime(字典转模型)
        Status *status = [Status modelWithDict:dict];
        [statuses addObject:status];
        User *user = status.user;
        NSString *name = user.name;
        NSLog(@"name = %@",name);
    }
    
//    NSLog(@"%@",statuses);
}
@end
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容

  • 对于从事 iOS 开发人员来说,所有的人都会答出【runtime 是运行时】什么情况下用runtime?大部分人能...
    梦夜繁星阅读 3,700评论 7 64
  • 前言: 关于Runtime的资料网上一搜很多,但总是写的只言片语,不太全面。最近花了一个星期的时间重新学习Runt...
    小霍同学阅读 950评论 0 2
  • 1. runtime的简介runtime是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语...
    凸阿滨阅读 312评论 0 0
  • 在我们的心灵深处 总是有一种力量使我们——茫然不安 让我们无法安静,这种力量叫浮躁。 ...
    找个先生阅读 179评论 0 0
  • 生命中唯一重要的事情是爱情和工作---弗洛伊德 我已退休,妻子也于三年前去世了,我非常想念她。对于退休生活,我一直...
    Honey_Jane阅读 634评论 0 0