iOS开发之Runloop和Runtime

RunLoop

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

你肯定写过一个按钮点击事件,点击界面上的一个按钮,这个时候就会有对应的按钮响应事件发生。给我们的感觉就像应用一直处于随时待命的状态,在没人操作的时候它一直在休息,在让它干活的时候,它就能立刻响应。其实,这就是run loop的功劳。

Runloo的作用

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

RunLoop与线程

  • 每个线程(包括主线程)都有一个对应的RunLoop对象
  • 我们并不能自己创建Runloop对象,但是可以获取到系统提供的RunLoop对象
  • 主线程的RunLoop默认是启动的,用于接收各种输入sources;其他线程的RunLoop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动
  • RunLoop在第一次获取时由系统自动创建,在线程结束时销毁

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对象
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 *)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];
}

RunLoop应用

1.main函数中的RunLoop

#import 
#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 : {contents = "UITrackingRunLoopMode"}
     2 : {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.常驻线程

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

Runtime

runtime简介

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

runtime作用

发送消息

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

    • 方法调用的本质就是让对象发送消息
    • objc_msgSend,只用对象才能发送消息,因此以objc开头。
    • 使用消息机制前提,必须导入 #import
// 创建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 

@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 

@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

给分类添加属性

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

    iOS开发之Runloop和Runtime_第1张图片

iOS开发之Runloop和Runtime_第2张图片

字典转模型: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

你可能感兴趣的:(技术文档)