RunLoop

对于一个iOS开发者,如果你的水平只是停留在会用API的级别,那说明你与大神还是慢慢长路,本文章大家一块学习一些深层次的东西,RunLoop和Runtime。Runloop官方文档

RunLoop是ios和macOS开发中的一种机制,它在程序运行过程中起着非常重要的作用。
你肯定写过一个按钮点击事件,点击界面上的一个按钮,这个时候就会有对应的按钮响应事件发生。给我们的感觉就像应用一直处于随时待命的状态,在没人操作的时候它一直在休息,在让它干活的时候,它就能立刻响应。其实,这就是run loop的功劳,使用RunLoop的作用就是让你的线程有工作的时候忙于工作,没工作的时候休眠。
1.定义:
RunLoop是一个事件处理循环,用于在程序运行期间不断接受和处理各种输入源事件、如触摸事件、定时器事件、网络事件,并在没有事件时让线程进入休眠状态,以节省系统资源。
2.运行机制

  1. 循环结构:RunLoop的核心是一个循环,在这个循环中,它会不断地检查是否有事件需要处理。如果有事件,就会执行相应的处理代码;如果没有事件,就会让线程进入休眠状态,等待新的事件到来。
  2. 事件源:主要包括输入源和定时源。输入源如用户的触摸操作、键盘输入等,定时源则是通过定时器设置的定时事件
  3. 运行模式:RunLoop有多种运行模式,常见的有默认模式、UITrackingRunLoopMode等。不同的运行模式用于处理不同类型的事件,例如在UITrackingRunLoopMode模式下,主要用于处理滚动视图的跟踪事件。

3.与线程的关系

  • 线程与RunLoop的关联:每个线程都可以有一个与之关联的RunLoop,主线程默认会创建并启动一个RunLoop,而子线程的RunLoop则需要手动创建和启动。我们并不能自己创建Runloop对象,但是可以获取到系统提供的RunLoop对象。 RunLoop在第一次获取时由系统自动创建,在线程结束时销毁。
  • 线程的休眠与唤醒:当线程的RunLoop中没有事件需要处理时,线程会进入休眠状态,此时线程会释放CPU资源,等待新的事件到来。当有事件发生时,RunLoop会唤醒线程,让线程执行相应的处理代码。

4.Runloo的作用

  • 保持程序的持续运行,保持线程的持续运行,并接受用户输入。
  • 处理app中的各种事件(AutoreleasePool、事件响应、手势识别、界面更新、定时器、PerformSelecter、关于GCD、关于网络请求)
  • 调用解耦(Message Queue)
  • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息

RunLoop的源
1.配置定时源
2.配置基于端口的输入源
3.自定义输入源

Runloop 监听的事件主要分为两种:输入源(Input Source)和定时源(Timer Source)。输入源包括触摸事件、键盘事件、鼠标事件等,定时源则是基于时间的触发器,如 NSTimer、CADisplayLink 等。

RunLoop通过mach_msg()函数接收和发送消息来进行事件管理‌

Runloop运行模式

  • 一个Runloop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer
  • 每次RunLoop启动时,只能制定其中一个Mode,这个Mode被称作CurrentMode
  • 如果需要切换Mode,只能退出Loop,再从新指定一个Mode进入系统默认模式

系统默认注册了5个mode:

  1. NSDefaultRunLoopMode:App的默认Mode,通常主线程是在这个Mode下运行
  2. UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  3. UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用
  4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
  5. NSRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode。可以看成模式组,默认情况下包括了NSDefaultRunLoopMode,UITrackingRunLoopMode)两种模式.

RunLoop与Core Animation渲染的关系
iOS 图形服务接收到 VSync 信号后,会通过 IPC 通知到 App 内。App 的 Runloop 在启动后会注册对应的 CFRunLoopSource 通过 mach_port 接收传过来的时钟信号通知,随后 Source 的回调会驱动整个 App 的动画与显示。Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件。这个 Observer 的优先级是 2000000,低于常见的其他 Observer。

当UI改变( Frame变化、 UIView/CALayer 的层级结构变化等)时,或手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay 方法后,这些操作最终都会被 CALayer 捕获,并通过 CATransaction 提交到一个中间状态去。

当RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。这时 CA 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,CA 会通过 DisplayLink 等机制多次触发相关流程。

RunLoop与AutoReleasePool的关系
在iOS应用启动后会注册两个Observer管理和维护AutoreleasePool。在应用程序刚刚启动时打印currentRunLoop可以看到系统默认注册了很多个Observer,其中有两个Observer的callout都是_ wrapRunLoopWithAutoreleasePoolHandler,这两个是和自动释放池相关的两个监听。

第一个Observer会监听RunLoop的进入,它会回调objc_autoreleasePoolPush()向当前的AutoreleasePoolPage增加一个哨兵对象标志创建自动释放池。这个Observer的order是-2147483647优先级最高,确保发生在所有回调操作之前。

第二个Observer会监听RunLoop的进入休眠和即将退出RunLoop两种状态,在即将进入休眠时会调用objc_autoreleasePoolPop()和 objc_autoreleasePoolPush()根据情况从最新加入的对象一直往前清理直到遇到哨兵对象。而在即将退出RunLoop时会调用objc_autoreleasePoolPop() 释放自动自动释放池内对象。这个Observer的order是2147483647,优先级最低,确保发生在所有回调操作之后。

主线程的其他操作通常均在这个AutoreleasePool之内(main函数中),以尽可能减少内存维护操作。当然你如果需要显式释放(例如循环)时可以自己创建AutoreleasePool,否则一般不需要自己创建。

RunLoop对象
iOS中有2套API来访问和使用RunLoop,
1. Foundation

获取当前线程的RunLoop对象
[NSRunLoop currentRunLoop]
获取主线程的RunLoop对象
[NSRunLoop mainRunLoop]

2.Core Foundation

获取当前线程的RunLoop对象
CFRunLoopGetCurrent()
获取主线程的RunLoop对象
CFRunLoopGetMain()
/**
 * 
 1:Runloop和线程的关系:
        1:一一对应,主线程的runloop已经默认创建,但是子线程的需要手动创建:创建子线程的runloop:
        NSRunLoop *run = [NSRunLoop currentRunLoop];
        currentRunLoop懒加载的,在同一个子线程中创建多个runloop,则返回的都是同一个对象,因为其是懒加载模式的 2:在runloop中有多个运行模式,但是runloop只能选择一种模式运行,mode里面至少要有一个timer或者是source
 2:
        1.获得主线程对应的runloop:
        NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];
        2:获得当前线程对应的runLoop:
        NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
 3:CFRunLoop:
        1:获得主线程对应的runloop:
        CFRunLoopGetMain() 
        2:获得当前线程对应的runLoop:
        CFRunLoopGetCurrent()
 *
 */

- (void)viewDidLoad {
    [super viewDidLoad];
    
}


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //1.获得主线程对应的runloop
    NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];
    
    //2.获得当前线程对应的runLoop
    NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
    
    NSLog(@"%p---%p",mainRunLoop,currentRunLoop);
    //    NSLog(@"%@",mainRunLoop);
    
    //Core
    NSLog(@"CFRunLoopGetMain()=%p",CFRunLoopGetMain());
    NSLog(@"CFRunLoopGetCurrent()=%p",CFRunLoopGetCurrent());
    
    NSLog(@"mainRunLoop.getCFRunLoop=%p",mainRunLoop.getCFRunLoop);
    
    //Runloop和线程的关系
    //一一对应,主线程的runloop已经创建,但是子线程的需要手动创建
    NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(run) object:nil];
    //开启线程
    [thread start];
    
}

//在runloop中有多个运行模式,但是runloop只能选择一种模式运行
//mode里面至少要有一个timer或者是source
-(void)run
{
    //如何创建子线程对应的runLoop,currentRunLoop懒加载的
    NSLog(@"[NSRunLoop currentRunLoop]=%@",[NSRunLoop currentRunLoop]);
    NSLog(@"[NSThread currentThread]---%@",[NSThread currentThread]);
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

Cocoa和Core Fundation都提供了基于端口的对象用于线程和进程间的通信。
NSMachPort对象方式
为了和NSMachPort对象建立稳定的本地连接,你需要创建端口对象并将之加入相应的线程的RunLoop中,辅助线程可以使用相同的端口对象将消息返回给原线程。辅助线程必须配置线程特定使用的端口发送消息回主线程。
当使用NSMachPort时,换句话说,一个线程创建的本地端口对象成为另一个线程的远程端口对象。
NSMessagePort对象方式
为了和NSMessagePor对象建立稳定的本地连接,你不能简单的在线程间传递端口对象,远程消息端口必须通过名字来获取。在Cocoa中这需要你给本地端口指定一个名字,并将名字传递到远程线程,以便远程线程可以获取合适的端口对象用于通信。

RunLoop应用

1.main函数中的RunLoop

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

UIApplicationMain函数内部就启用了一个RunLoop,所以,UIApplicationMain函数一直没有返回,保持了程序的持续运行。

2.RunLoop与定时器

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

/**
 * 1:NSLog(@"%@",[NSRunLoop currentRunLoop]);
 打印当前线程的RunLoop,懒加载模式,一条线程对应一个RunLoop对象,有返回,没有创建,主线程的RunLoop默认创建,子线程的RunLoop需要手动创建,[NSRunLoop currentRunLoop],同一个线程中若是创建多个RunLoop,则返回的都是同一个RunLoop对象,一个RunLoop里会有多个mode运行模式(系统提供了5个),但运行时只能指定一个RunLoop,若是切换RunLoop,则需要退出当前的RunLoop
 2:定时器NSTimer问题:1:若是创建定时器用timerWithTimeInterval,则需要手动将定时器添加到NSRunLoop中,指定的运行模式为default,但是如果有滚动事件的时候,定时器就会停止工作。
    解决办法:更改NSRunLoop的运行模式,UITrackingRunLoopMode界面追踪,此模式是当只有发生滚动事件的时候才会开启定时器。若是任何时候都会开启定时器: NSRunLoopCommonModes,
 NSRunLoopCommonModes = NSDefaultRunLoopMode + UITrackingRunLoopMode
 占用,标签,凡是添加到NSRunLoopCommonModes中的事件爱你都会被同时添加到打上commmon标签的运行模式上
 
 3:1:scheduledTimerWithTimeInterval此方法创建的定时器默认加到了NSRunLoop中,并且设置运行模式为默认。
    2:若是想在子线程开启NSRunLoop:需要手动开启:NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop];等到线程销毁的时候currentRunloop对象也随即销毁。2:在子线程的定时器,需要手动加入到runloop:不要忘记调用run方法
 
 NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop];
 
 //该方法内部自动添加到runloop中,并且设置运行模式为默认
 [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
 
 //开启runloop
 [currentRunloop run];
 
 */

- (void)viewDidLoad {
    [super viewDidLoad];
    [self timer1];
}


-(void)timer1
{
    //1.创建定时器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
//    [timer setFireDate:[NSDate distantPast]];
    
    //2.添加定时器到runLoop中,指定runloop的运行模式为NSDefaultRunLoopMode
    /*
     第一个参数:定时器
     第二个参数:runloop的运行模式
     */
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    
    //UITrackingRunLoopMode:界面追踪
//    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
    //    NSRunLoopCommonModes = NSDefaultRunLoopMode + UITrackingRunLoopMode
    //占用,标签,凡是添加到NSRunLoopCommonModes中的事件爱你都会被同时添加到打上commmon标签的运行模式上
    /*
     0 : <CFString 0x10af41270 [0x10a0457b0]>{contents = "UITrackingRunLoopMode"}
     2 : <CFString 0x10a065b60 [0x10a0457b0]>{contents = "kCFRunLoopDefaultMode"
     */
    //    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
}

-(void)timer2
{
    //该方法内部自动添加到runloop中,并且设置运行模式为默认
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
    //开启runloop
    NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop];
    [currentRunloop run];
}

-(void)run
{
    NSLog(@"run-----%@---%@",[NSThread currentThread],[NSRunLoop currentRunLoop].currentMode);
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

3.ImageView显示

在特定模式下执行某些操作,图片设置与拖拽分别在不同模式

4.常驻线程

某些操作,需要重复开辟子线程,重复开辟内存过于消耗性能,可以设定子线程常驻。

+ (void)entryPoint
{
    //设置当前线程名为MyThread
    [[NSThread currentThread] setName:@"MyThread"];
    //获取NSRunLoop对象,第一次获取不存在时系统会创建一个
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    /*
    添加一个Source1事件的监听端口
    RunLoop对象会一直监听这个端口,由于这个端口不会有任何事件到来所以不会产生影响
    监听模式是默认模式,可以修改为Common
    */
    [runloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    //启动RunLoop
    [runloop run];
}

+ (NSThread *)longTermThread
{
    //静态变量保存常驻内存的线程对象
    static NSThread *longTermThread = nil;
    //使用GCD dispatch_once 在应用生命周期只执行一次常驻线程的创建工作
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //创建一个线程对象,并执行entryPoint方法
        longTermThread = [[NSThread alloc] initWithTarget:self selector:@selector(entryPoint) object:nil];
        //启动线程,启动后就会执行entryPoint方法
        [longTermThread start];
    });
    return longTermThread;
} 

- (void)viewDidLoad
{
    //获取这个常驻内存的线程
    NSThread *thread =  [ViewController longTermThread];
    //在该线程上提交任务
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:NO];
}

5.性能优化-RunLoop卡顿监控
哪些情况会导致主线程卡顿呢?大体有如下几个方面:

1.布局计算‌:复杂的页面布局和频繁的数据更新;
2.CPU过高‌:主线程进行网络同步请求;
3.主线程阻塞‌:常见的导致主线程阻塞的原因包括复杂的界面布局和绘制操作、大量的IO操作(如网络请求和文件读写)、大量的计算密集型操作(如图像处理和数据解析)等‌12
4.图片加载。
5.死锁和主子线程抢锁。

**RunLoop 实现卡顿检测的基本思路:**通过监听 RunLoop 的状态变化来判断主线程的执行时长。
我们知道程序中的任务都是在线程中执行,而线程依赖于 RunLoop,并且RunLoop总是在相应的状态下执行任务,执行完成以后会切换到下一个状态,如果在一个状态下执行时间过长导致无法进入下一个状态就可以认为发生了卡顿,所以可以根据主线程 RunLoop 的状态变化检测任务执行时间是否太长。至于多长时间算作卡顿可以依据自己的需要来设置,一般情况下可以设置200ms作为阀值。

RunLoop的状态如下:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), // 进入Runloop
    kCFRunLoopBeforeTimers = (1UL << 1), // 处理Timer事件
    kCFRunLoopBeforeSources = (1UL << 2), // 处理Source事件
    kCFRunLoopBeforeWaiting = (1UL << 5), // 进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6), // 唤醒
    kCFRunLoopExit = (1UL << 7), // 退出Runloop
    kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有状态
};

Runtime

runtime简介
Runtime有两个版本, Legacy(早期版本Objective-C 1.0) 和Modern(现行版本Objective-C 2.0)。

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

发送消息

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

    • 方法调用的本质就是让对象发送消息
    • objc_msgSend,只用对象才能发送消息,因此以objc开头。
    • 使用消息机制前提,必须导入 #import <objc/message.h>
// 创建person对象
Person *p = [[Person alloc] init];
// 调用对象方法
[p eat];
// 本质:让对象发送消息
objc_msgSend(p, @selector(eat));
// 调用类方法的方式:两种
// 第一种通过类名调用
[Person eat];
// 第二种通过类对象调用
[[Person class] eat];
// 用类名调用类方法,底层会自动把类名转换成类对象调用
// 本质:让类对象发送消息
objc_msgSend([Person class], @selector(eat));

交换方法

开发使用场景:系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有的功能。

  • 方式一.继承系统的类,重写方法。

  • 方式二.使用runtime,交换方法。

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

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
//    UIImage *image = [UIImage imageNamed:@"123"];
    // 1.每次使用,都需要导入头文件
    // 2.当一个项目开发太久,使用这个方式不靠谱
    [UIImage imageNamed:@"123"];
    
    // imageNamed:
    // 实现方法:底层调用xz_imageNamed
    
    // 本质:交换两个方法的实现imageNamed和xz_imageNamed方法
    // 调用imageNamed其实就是调用xz_imageNamed
    
    
    // imageNamed加载图片,并不知道图片是否加载成功
    // 以后调用imageNamed的时候,就知道图片是否加载
}

#import "UIImage+Image.h"

#import <objc/message.h>

@implementation UIImage (Image)
// 加载这个分类的时候调用
+ (void)load
{

    // 交换方法实现,方法都是定义在类里面
    // class_getMethodImplementation:获取方法实现
    // class_getInstanceMethod:获取对象
    // class_getClassMethod:获取类方法
    // IMP:方法实现
    
    // imageNamed
    // Class:获取哪个类方法
    // SEL:获取方法编号,根据SEL就能去对应的类找方法
    Method imageNameMethod = class_getClassMethod([UIImage class], @selector(imageNamed:));
    
    // xmg_imageNamed
    Method xz_imageNamedMethod = class_getClassMethod([UIImage class], @selector(xz_imageNamed:));
    
    // 交换方法实现
    method_exchangeImplementations(imageNameMethod, xz_imageNamedMethod);
    
}

// 运行时

// 先写一个其他方法,实现这个功能

// 在分类里面不能调用super,分类木有父类
//+ (UIImage *)imageNamed:(NSString *)name
//{
//    [super im]
//}

+ (UIImage *)xz_imageNamed:(NSString *)imageName
{
    // 1.加载图片
    UIImage *image = [UIImage xz_imageNamed:imageName];
    
    // 2.判断功能
    if (image == nil) {
        NSLog(@"加载image为空");
    }
    
    return image;
}

动态添加方法

  1. 开发使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。
  2. 经典面试题:有没有使用performSelector,其实主要想问你有没有动态添加过方法。
  // performSelector:动态添加方法
    Person *p = [[Person alloc] init];
    // 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错
    // 动态添加方法就不会报错
//    [p performSelector:@selector(eat)];
    [p performSelector:@selector(eat:) withObject:@111];
#import "Person.h"

#import <objc/message.h>

@implementation Person

// 定义函数
// 没有返回值,参数(id,SEL)
// void(id,SEL)
void aaaa(id self, SEL _cmd, id param1)
{
    NSLog(@"调用eat %@ %@ %@",self,NSStringFromSelector(_cmd),param1);
}

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

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

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
//    NSLog(@"%@",NSStringFromSelector(sel));
    
    // 动态添加eat方法
    
    if (sel == @selector(eat:)) {
        
        /*
         cls:给哪个类添加方法
         SEL:添加方法的方法编号是什么
         IMP:方法实现,函数入口,函数名
         types:方法类型
         第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
         */
        // @:对象 :SEL
        class_addMethod(self, sel, (IMP)aaaa, "v@:@");
        // 处理完
        return YES;   
    }
    return [super resolveInstanceMethod:sel];
}
@end

给分类添加属性

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

这里写图片描述

这里写图片描述

字典转模型:Runtime

  • 思路:利用运行时,遍历模型中所有的属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。
  • 步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类转。
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // 解析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
@implementation NSObject (Model)
+ (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
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐