浅析RunLoop原理及其应用

引言:一个APP的启动与结束都是伴随着RunLoop循环往复的,不断的循环、不断的往复。当线程被杀掉、APP退出后被系统以占用内存为由杀掉,RunLoop就消失了。但平时开发中很少见到RunLoop,为何它如此神秘?本文跟大家分享一下RunLoop的相关知识。  

转载本文需注明出处:微信公众号EAWorld,违者必究。

目录:

1、RunLoop的概念  

2、RunLoop与线程的关系  

3、RunLoop的常用模式

4、RunLoop的应用  

1.RunLoop的概念  

将英文拆解不难理解其实RunLoop表示一直在运行着的循环或者从上面的定义源码中可以看出就是一个do..while..循环。当启动一个iOS APP时主线程启动与其对应的RunLoop也已经开启。如果不杀掉APP则APP一直运行,就是因为RunLoop循环一直为开启状态保证主线程不会被摧毁。这也是RunLoop的作用之一保证线程不退出。RunLoop在循环过程中监听事件,当前线程有任务时,唤醒当当线程去执行任务,任务执行完成以后,使当前线程进入休眠状态。当然这里的休眠不同于我们自己写的死循环(while(1);),它在休眠时几乎不会占用系统资源,当然这是由操作系统内核去负责实现的。

UIApplicationMain()函数方法会默认为主线程设置一个NSRunLoop对象,这个循环会随时监听屏幕上由用户触摸所带来的底层消息并将其传递给主线程去处理,当点击一个button事件的传递从图上的调用栈可以看出。(监听的范围还包含时钟/网络)RunLoop循环与While循环的区别在于,RunLoop会在没有事件发生时进入休眠状态从而不占用CPU消耗,有事件发生才会去找对应的 Handler 处理事件,而While则会一直占用。在 Cocoa 程序的线程中都可以通过代码NSRunLoop *runloop = [NSRunLoop currentRunLoop];来获取到当前线程的Runloop对象。

RunLoop共有两套API接口 :1. Foundation框架NSRunLoop 2. Core Foundation框架CFRunLoopRef。NSRunLoop和CFRunLoopRef都代表着RunLoop对象,它们是等价的,可以互相转换。

NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)。

2.RunLoop与线程之间的关系

RunLoop和线程是相辅相成的,一个Runloop对应着一条唯一的线程,可以这样说RunLoop是为了线程而生,没有线程,它也没有存在的必要。RunLoop是线程的基础架构部分, Cocoa 和 CoreFundation 都提供了RunLoop对象方便配置和管理线程的 RunLoop。每个线程,包括程序的主线程( main thread )都有与之相对应的 RunLoop对象。上图从 input source 和 timer source 接受事件,然后在线程中处理事件都是由RunLoop推动完成。

注意:开一个子线程创建runloop,不是通过alloc init方法创建,而是直接通过调用currentRunLoop方法来创建,它本身是一个懒加载的。在子线程中,如果不主动获取Runloop的话,那么子线程内部是不会创建Runloop的。

3.RunLoop的常用模式

RunLoop 的模式有五种。图上列出了其中两种分别是 NSDefaultRunLoopMode(默认模式) 和 UITRackingRunLoopMode(UI模式) 、NSRunLoopCommonModes(占位模式)。其实占位模式不是一个真正的模式,它相当于上面两种模式之和。苹果公开提供的 Mode 有两个NSDefaultRunLoopMode(kCFRunLoopDefaultMode) NSRunLoopCommonModes(kCFRunLoopCommonModes)。

4.RunLoop的应用

例如创建一个比较常见的注册页面,里面用NSTimer来自处理常见的验证码倒计时,每秒处理一下,如果NSTimer添加到的是默认模式的RunLoop这时候注册页面有一个展示注册协议的UITextView当用户滑动UITextView时验证码的倒计时是停止的,这是因为主线程的RunLoop模式是UI模式这个时候RunLoop循环是优先处理UI模式的任务而忽略了默认模式的计时器。此时解决上面的问题就需要用到NSRunLoopCommonModes(占位模式),这个模式相当于把NSTimer在两种模式下都添加了,这就不难理解为什么NSRunLoopCommonModes是一个复数形式了。这个模式下滑动UITextView或停止的时候RunLoop是在UITRacking和default模式下切换的(从打印日志中可以看出)。如果觉得NSTimer设置RunLoop模式很复杂可以尝试用GCD的Timer用法很简便。

RunLoop在TableView中的应用(解决滑动卡顿问题)。

如图代码展示,当加载高清大图渲染屏幕,而此时不得不在主线程操作,会引起滑动的卡顿。

tableview 在加载 cell 时如果遇到多个耗时操作会有点卡顿。将耗时操作放到 DefaultMode 里只能解决滑动时流畅,但是停止时需要加载耗时,仍然会有卡顿的感觉。正确方法是采用 RunLoop 监听,将多个耗时操作分开执行,在每次 RunLoop 唤醒时去做一个耗时任务。

阻塞原因:kCFRunLoopDefaultMode时候 多张图片(特别是高清大图)一起加载(耗时)loop不结束无法BeforeWaiting(即将进入休眠) 切换至UITrackingRunLoopMode来处理等候的UI刷新事件造成阻塞。

解决办法:每次RunLoop循环只加载一张图片 这样loop就会很快进入到BeforeWaiting处理后面的UI刷新(UITrackingRunLoopMode 优先处理)或者没有UI刷新事件继续处理下一张图片。

RunLoop 监听添加Observer (监听RunLoop的beforeWaiting)当处理完一张图片即将进入到beforeWaiting时处理数组里的tasks,这些任务就在callback里面做处理。

callBack拿到task处理了一部分就进入到了休眠 比如拿到18个任务只处理了7个就不处理了。

此处添加Timer是让RunLoop一直处于活跃状态 保证即使处理完所有task还是一直活跃状态。

注意:当CFRunLoopAddObserver(runloop, observer , kCFRunLoopDefaultMode); 添加到观察者时模式为kCFRunLoopDefaultMode 这样的的话只能监听到一般模式的BeforeWaiting,即不滑动的时候。所以图上的加载只在拖动结束时,而拖动UI时无任何加载。如下图:

所以这里可以再次优化,将模式改为kCFRunLoopCommonModes,这样的话滑动或者不滑动都可以加载图片渲染屏幕,而且是在不影响屏幕流畅性的基础上。如以下GIF:

源码:

#import "ViewController.h"
 
@interface ViewController ()<UITableViewDelegate, UITableViewDataSource>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
 
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) NSMutableArray *tasks;
@property (nonatomic, assign) NSInteger maxTaskNumber;
 
@end
 
void callBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    //C语言与OC的交换用到桥接 __bridge
    //处理控制器加载图片的事情
    ViewController *VC = (__bridge ViewController *)(info);
    if (VC.tasks.count == 0) {
        return;
    }
    void(^task)() = [VC.tasks firstObject];
    task();
    [VC.tasks removeObject:task];
    NSLog(@"COUNT:%ld",VC.tasks.count);
     
}
 
@implementation ViewController
 
- (void)viewDidLoad {
    [super viewDidLoad];
     
     
    [self addRunloopOvserver];
     
    self.maxTaskNumber = 18;
    self.tasks = [NSMutableArray array];
     
     
    [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
     
}
-(void)timerMethod{
     
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    ViewController2 *vc2 = [ViewController2 new];
    [self presentViewController:vc2 animated:YES completion:^{
         
    }];
}
 
- (void)addRunloopOvserver{
    //获取当前的RunLoop
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    //上下文 (此处为C语言 对OC的操作需要上下文)将(__bridge void *)self 传入到Callback
    CFRunLoopObserverContext context = {0, (__bridge void *)self, &CFRetain, &CFRelease};
    //创建观察者 监听BeforeWaiting 监听到就调用回调callBack
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &callBack, &context);
    //添加观察者到当前runloop kCFRunLoopDefaultMode可以改为kCFRunLoopCommonModes
    CFRunLoopAddObserver(runloop, observer , kCFRunLoopCommonModes);
    //C语言中 有create就需要release
    CFRelease(observer);
}
 
 
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return 30000;
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"identity" forIndexPath:indexPath];
    NSLog(@"---run---%@",[NSRunLoop currentRunLoop].currentMode);
    //以下两个循环的UI操作在必须放在主线程,但是弊端就是太多图片的处理会阻塞tableview的滑动流畅性
    for (int i = 1; i < 4; i++) {
        UIImageView *imageView = [cell.contentView viewWithTag:i];
        [imageView removeFromSuperview];
    }
    for (int i = 1; i < 4; i++) {
    /*
     阻塞模式
    */
    //        CGFloat leading = 10, space = 20, width = 103, height = 87, top = 15;
    //        UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake((i - 1) * (width + space) + leading, top, width, height)];
    //        [cell.contentView addSubview:imageView];
    //        imageView.tag = i;
    //        imageView.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"image" ofType:@"png"]];
 
         
     
    //阻塞原因:kCFRunLoopDefaultMode时候 多张图片一起加载(耗时)loop不结束无法BeforeWaiting(即将进入休眠) 切换至UITrackingRunLoopMode来处理等候的UI刷新事件造成阻塞
    //解决办法:每次RunLoop循环只加载一张图片 这样loop就会很快进入到BeforeWaiting处理后面的UI刷新(UITrackingRunLoopMode 优先处理)或者没有UI刷新事件继续处理下一张图片
     
        /*
         流畅模式
        */
        //下面只是把任务放到数组 不消耗性能
        void(^task)() = ^{
            CGFloat leading = 10, space = 20, width = 103, height = 87, top = 15;
            UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake((i - 1) * (width + space) + leading, top, width, height)];
            [cell.contentView addSubview:imageView];
            imageView.tag = i;
            imageView.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"image" ofType:@"png"]];
        };
        [self.tasks addObject:task];
        //保证只拿最新的18个任务处理
        if (self.tasks.count > self.maxTaskNumber) {
            [self.tasks removeObjectAtIndex:0];
        }
    }
    return cell;
}
 
 
 
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

关于作者:热河,普元移动端开发工程师,互联网技术爱好者,专注于iOS开发。目前参与Mobile 8.0项目的开发,主要接触RN技术的应用,黏合前端代码与iOS底层之间的交互。

关于EAWorld:微服务,DevOps,数据治理,移动架构原创技术分享。

Image placeholder
17727569647
未设置
  36人点赞

没有讨论,发表一下自己的看法吧

推荐文章
AI 数据中台 Mega 及其应用

随着AI在各行业落地的进一步深化和应用数据量的飞速增长,越来越多的AI科学家痛苦地发现数据ETL、数据仓库和海量特征向量检索等数据处理流程花费了他们大量宝贵的时间和精力。AI数据中台Mega打破了人工

Spring-SpringAOP原理,手写Spring事务框架

一、Spring核心知识Spring是一个开源框架,Spring是于2003年兴起的一个轻量级的Java开发框架,由RodJohnson在其著作ExpertOne-On-OneJ2EEDevelopm

Docker容器实现原理及容器隔离性踩坑介绍

本文讲述了 关于容器隔离性的一个“坑”正如Docker官方的口号:“Buildonce,Runanywhere,Configureonce,Runanything”,Docker被贴上了如下标签:轻巧

JVM CPU Profiler技术原理及源码深度解析

本文介绍了JVM平台上CPUProfiler的实现原理,希望能帮助读者在使用类似工具的同时也能清楚其内部的技术实现。引言研发人员在遇到线上报警或需要优化系统性能时,常常需要分析程序运行行为和性能瓶颈。

聊一聊 MySQL 中的事务及其实现原理

说到数据库,那就一定会聊到事务,事务也是面试中常问的问题,我们先来一个面试场景:面试官:"事务的四大特性是什么?" 我:"ACID,即原子性(Atomicity)、隔离性(Isolation)、持久性

Webpack 原理浅析

课程推荐:前端开发工程师--学习猿地精品课程 背景Webpack迭代到4.x版本后,其源码已经十分庞大,对各种开发场景进行了高度抽象,阅读成本也愈发昂贵。但是为了了解其内部的工作原理,让我们尝试从一个

OpenStack容器服务Zun初探与原理分析

01Zun服务简介Zun是OpenStack的容器服务(ContainersasService),类似于AWS的ECS服务,但实现原理不太一样,ECS是把容器启动在EC2虚拟机实例上,而Zun会把容器

MySQL 百万级数据量分页查询方法及其优化

作者|大神养成记原文|  http://t.cn/RnvCJnm方法1:直接使用数据库提供的SQL语句语句样式: MySQL中,可用如下方法:SELECT*FROM表名称LIMITM,N适应场景: 适

什么是边缘计算及其重要性?

边缘计算正在改变世界上数百万台设备处理,处理和传递数据的方式。联网设备(IoT)的爆炸性增长,以及需要实时计算能力的新应用,继续推动着边缘计算系统的发展。诸如5G无线之类的更快的联网技术,使边缘计算系

HTML5新增了哪些input类型及其属性?

HTML5新增了哪些input类型及其属性?1、url类型、email、tel类型说明:当输入非url、email的字符串时,浏览器会自动提醒。 2、number类型。说明:只能输入数字,min表

Js获取当前日期时间及其它操作

课程推荐:web全栈开发就业班--拿到offer再缴学费--融职教育 Js获取当前日期时间及其它操作 varmyDate=newDate(); myDate.getYear();//获取当前年份(2位

Laravel-Binlog 扩展(用于实时监听 MySQL 数据变更、数据同步等场景)

Laravel-Binlogv0.2.1 (该扩展当前用于我司测试环境实时同步Mysql数据变更到ElasticSearch,稳定性待测试!!哈哈哈)我司正式环境走的阿里云DTS数据订阅 基于Sw

TiDB Binlog 组件正式开源

TiDBBinlog组件用于收集TiDB的binlog,并准实时同步给下游,如:TiDB/MySQL等。该组件在功能上类似于MySQL的主从复制,会收集各个TiDB实例产生的binlog,并按事务提交

SpringBoot个人应用开发框架(SpringBoot版本2.1)+IDEA

前言: 此笔记为本人首个SpringBoot项目框架学习实践记录,期间参考了许多大神的笔记和心得。 参考文档如下: 项目git地址: 一、创建SpringBoot工程 1.1创建父POM工程结

浅析 PHP7 的垃圾回收机制

垃圾回收机制 垃圾回收机制是一种动态存储分配方案。它会自动释放程序不再需要的已分配的内存块。自动回收内存的过程叫垃圾收集。垃圾回收机制可以让程序员不必过分关心程序内存分配,从而将更多的精力投入到业务逻

浅析 PHP7 底层运行机制

PHP7代码执行过程 PHP是解释型语言,其执行过程需先编译成中间代码,再经由特定的虚拟机,翻译成特定的指令被执行。其执行过程如下: PHP代码=>Token=>抽象语法树=>Opcodes=>执行

浅析 PHP7 底层运行机制

PHP7代码执行过程 PHP是解释型语言,其执行过程需先编译成中间代码,再经由特定的虚拟机,翻译成特定的指令被执行。其执行过程如下: PHP代码=>Token=>抽象语法树=>Opcodes=>执行

shadow DOM 浅析

引言 Shadow-dom游离在DOM树之外的节点树,但是他的创建基于普通DOM元素(非document),并且创建后的Shadow-dom节点可以从界面上直观的看到。更重要的是,Shadow-do

浅析 PHP-FPM、CGI、Fast CGI 的关系

作为一个phper,一直会接触到php、nignx等的相关配置,相关的名词比如PHP-FPM、CGI、FastCGI、SAPI等等应该都或多或少接触过,但自己对于这些知识点的掌握其实相对比较分散且形成

Linux系统——架构浅析

导语:掐指一算自己从研究生开始投入到Linux的海洋也有几年的时间,即便如此依然对其各种功能模块一知半解。无数次看了Linux内核的技术文章后一头雾水,为了更系统地更有方法的学Linux,特此记录。历

css: clip浅析

前言css中裁剪和遮罩相关的属性一般来说是比较少用到的,但是最近写项目的时候遇到一个问题,要给一张图片上加个白色遮罩,产生合成效果,这就不得不用到css遮罩相关的属性,顺便把裁剪相关属性一起学习来,做

面试问烂的 Spring AOP 原理、SpringMVC 过程

  正文  SpringAOP,SpringMVC,这两个应该是国内面试必问题,网上有很多答案,其实背背就可以。但今天笔者带大家一起深入浅出源码,看看他的原理。以期让印象更加深刻,面试的时候游刃有余。

MVVM原理(Object.defineProperty和订阅者模式)

想着去了解vue的mvvm数据驱动是怎么实现的,百度中看了这篇文章,demo很好。其他文章只是讲到defineProperty的set,get。彻底理解Vue中的Watcher、Observer、De

Python可视化 | Seaborn5分钟入门(二)——barplot&countplot&pointplot

微信公众号:「Python读财」如有问题或建议,请公众号留言Seaborn是基于matplotlib的Python可视化库。它提供了一个高级界面来绘制有吸引力的统计图形。Seaborn其实是在matp

React Developer Tools是什么?

ReactDeveloperTools是一款由facebook开发的有用的Chrome浏览器扩展。通过它我们可以查看应用程序的React组件分层结构,而不是更加神秘的浏览器DOM表示。注意:该插件只对