首先,本文借鉴Haley_Wong - 的文章。
每次当大家提起runloop的时候,脑海中总是浮现的是那么几个概念性的东西,所以我觉得应该学习和总结一下runloop的具体应用场景和作用。这样便于加强对runloop的理解。深入理解RunLoop | Garan no dou。
RunLoop 就是一个事件处理的循环,用来不停的调度工作以及处理输入事件。使用 RunLoop 的目的是让你的线程在有工作的时候忙于工作,而没工作的时候处于休眠状态。 runloop 的设计是为了减少 cpu 无谓的空转。
1、runloop保证线程的长久存活
多线程开发在我们的工作工程中是常常用到,一个子线程当它的任务执行完毕之后都会销毁,所以每次执行异步任务都会频繁去创建和销毁线程,这样无疑是耗费资源的。这种情况下我们可以利用runloop来保证线程在执行完任务后不背销毁而进入“休眠”状态,等待下一个任务的执行再被唤醒。
用代码验证首先创建一个类集成于NSThread --》MyThread
@implementation MyThread
-(void)dealloc{
NSLog(@"%@",NSStringFromSelector(_cmd));
}
@end
重写他的dealloc方法
在viewdidload里面写
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self threadTest];
}
-(void)threadTest{
MyThread* thread = [[MyThreadalloc]initWithBlock:^{
NSLog(@"threadTest");
}];
[threadstart];
}
控制台输出:
threadTest
dealloc
得出结论线程执行任务后会自动销毁,我们为了防止这种情况可以利用Runloop来实现。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//[self threadTest];
[self runloopThreadTest];
}
-(void)runloopThreadTest{
MyThread* thread = [[MyThreadalloc]initWithBlock:^{
NSLog(@"runloopThreadTest");
//如果注释了下面这一行,子线程中的任务并不能正常执行
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
//一定要开启
[[NSRunLoop currentRunLoop] run];
}];
[threadstart];
}
输出:
runloopThreadTest
demo地址 GitHub - SionChen/Runloop RunlooprThread
2、保证NSTimer正常运转。
一般我们创建NSTimer计时器有两种方法一种是:
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
这种是默认将timer加入到当前runloop中模式为NSDefaultRunLoopMode,而且为自动fire 。
另一种是
NSTimer*timer = [NSTimertimerWithTimeInterval:1.0target:selfselector:@selector(timerUpdate) userInfo:nilrepeats:YES];
[[NSRunLoopcurrentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[timer fire]
这样在视图滑动的时候NSEventTrackingRunLoopMode 模式下 timer是不会计时的
将第二种创建方法中的mode改为NSRunLoopCommonModes 即可在滚动的时候计时器也计时。
从RunLoop官方文档和iPhonedevwiki中的CFRunLoop可以看出,NSRunLoopCommonModes并不是一种Mode,而是一种特殊的标记,关联的有一个set ,官方文档说:For Cocoa applications, this set includes the default, modal, and event tracking modes by default.(默认包含NSDefaultRunLoopMode、NSModalPanelRunLoopMode、NSEventTrackingRunLoopMode)
还有一种情况,当开辟子线程设置定时器的时候,不用设置mode为NSRunLoopCommonModes 也能再滑动的时候正常计时。因为主线程的Runloop和子线程的Runloop是不互相影响的。mainrunloop 滑动NSEventTrackingRunLoopMode 时子线程并没有改变mode。
3、滚动视图流畅性优化
在我们的开发过程中经常遇到列表型上面有图片的,一般下载图片用异步,setimage则使用同步。为imageView设置image,是在UITrackingRunLoopMode中也可以进行的,如果图片很大,图片解压缩和渲染肯定会很耗时,那么卡顿就是必然的。我们可以再setImage的时候手动设置runloop的mode:
[imageView performSelector:@selector(setImage:) withObject:image afterDelay:0inModes:@[NSDefaultRunLoopMode]];
这样就解决了。
4、监测iOS卡顿
在写代码之前我们首先要了解几个原理,dispatch_semaphore_t这个类,dispatch_semaphore_t 是一个信号量机制,信号量到达、或者 超时会继续向下进行,否则等待,如果超时则返回的结果必定不为0,信号量到达结果为0。具体参考文章GCD信号量-dispatch_semaphore_t - 。
在就是CFRunLoopActivity的几个状态以及Runloop执行的顺序过程:
/* kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop*/
/*RunLoop 顺序
1、进入
2、通知Timer
3、通知Source
4、处理Source
5、如果有 Source1 调转到 11
6、通知 BeforWaiting
7、wait
8、通知afterWaiting
9、处理timer
10、处理 dispatch 到 main_queue 的 block
11、处理 Source1、
12、进入 2
13、退出
*/
根据以上的原理我们可以通过监听mainRunloop的状态和信号量阻塞线程的特点来检测卡顿了。RunLoop 监控卡顿为什么要用kCFRunLoopBeforeSources和kCFRun... - 。
首先创建一个用于检测的类SemaphoreDetection:
@interfaceSemaphoreDetection :NSObject
/*单例获取*/
+ (instancetype) sharedInstance;
/*开始检测*/
- (void) startDetection;
/*停止检测*/
- (void) endDetection;
@end
在startDetection 初始化相关实例:
//设置Run loop observer的运行环境
CFRunLoopObserverContextcontext = {0, (__bridgevoid*)(self),NULL,NULL,NULL};
//创建Run loop observer对象
//第一个参数用于分配observer对象的内存
//第二个参数用以设置observer所要关注的事件,详见回调函数myRunLoopObserver中注释
//第三个参数用于标识该observer是在第一次进入run loop时执行还是每次进入run loop处理时均执行
//第四个参数用于设置该observer的优先级
//第五个参数用于设置该observer的回调函数
//第六个参数用于设置该observer的运行环境
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runloopObserverAction, &context);
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
//创建初始信号量为0 的dispatch_semaphore
_semaphore = dispatch_semaphore_create(0);
//开辟线程监听延时
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//死循环监听 通过控制信号量 来实现 mainrunloop循环或者超时的时候才会执行
while(YES) {
// 累计延迟超过250ms包含--》 (设置连续5次超时50ms认为卡顿(当然也包含了单次超时250ms))
//dispatch_semaphore_t 是一个信号量机制,信号量到达、或者 超时会继续向下进行,否则等待,如果超时则返回的结果必定不为0,信号量到达结果为0。信号量为小于等于0的时候会阻塞当前线程
longsemaphoreInt =dispatch_semaphore_wait(self->_semaphore,dispatch_time(DISPATCH_TIME_NOW,50*NSEC_PER_MSEC));
if(semaphoreInt!=0) {//超时
/* kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop*/
if (self->_activity==kCFRunLoopBeforeSources || self->_activity==kCFRunLoopAfterWaiting)
{
if(++self->_countTime<5)
continue;
[selflogStack];//记录卡顿堆栈信息
NSLog(@"*************lag******************");
}
}
self->_countTime=0;
}
});
附demo地址:GitHub - SionChen/Runloop RunloopDetection工程查看代码。