java runloop_RunLoop常见的应用场景

以下关于RunLoop的资料都比较好:

RunLoop使用场景

一、保证线程长时间存活

问题描述:不希望一些花费时间较长的操作阻塞主线程而导致界面卡顿,就需要创建一个子线程,然后把该操作放在子线程中来处理。可是当子线程中的任务执行完毕后,子线程就会被销毁掉。

@interface YTThread : NSThread

@end

@implementation YTThread

- (void)dealloc {

NSLog(@"%s",__func__);

}

@end

- (void)viewDidLoad {

[super viewDidLoad];

self.title = @"RunLoop";

[self threadTest];

}

- (void)threadTest {

YTThread *thread = [[YTThread alloc] initWithTarget:self selector:@selector(subThreadOpetion) object:nil];

[thread start];

}

- (void)subThreadOpetion {

@autoreleasepool {

NSLog(@"%@----子线程任务开始",[NSThread currentThread]);

[NSThread sleepForTimeInterval:3.0];

NSLog(@"%@----子线程任务结束",[NSThread currentThread]);

}

}

2017-05-27 11:05:05.444 MXBarManagerDemo[23405:2814835] {number = 3, name = (null)}----子线程任务开始

2017-05-27 11:05:08.450 MXBarManagerDemo[23405:2814835] {number = 3, name = (null)}----子线程任务结束

2017-05-27 11:05:08.450 MXBarManagerDemo[23405:2814835] -[YTThread dealloc]

当子线程中任务执行完后线程被立刻销毁。如果程序中需要经常在子线程中执行任务,频繁的创建和销毁线程会造成资源的浪费。这时可以使用RunLoop来让该线程长时间存活而不被销毁。如下所示:

@interface TestRunLoopViewController ()

@property (nonatomic, strong) NSThread *subThread;

@end

@implementation TestRunLoopViewController

- (void)viewDidLoad {

[super viewDidLoad];

self.title = @"RunLoop";

[self threadTest];

}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

[self performSelector:@selector(subThreadOpetion) onThread:self.subThread withObject:nil waitUntilDone:NO];

}

- (void)threadTest {

YTThread *thread = [[YTThread alloc] initWithTarget:self selector:@selector(subThreadEntryPoint) object:nil];

[thread setName:@"YTThread"];

[thread start];

self.subThread = thread;

}

- (void)subThreadEntryPoint {

@autoreleasepool {

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

// NSLog(@"runLoop--%@", runLoop);

NSLog(@"启动RunLoop前--%@",runLoop.currentMode);

[runLoop run];

}

}

- (void)subThreadOpetion {

@autoreleasepool {

NSLog(@"%@----子线程任务开始",[NSThread currentThread]);

[NSThread sleepForTimeInterval:3.0];

NSLog(@"%@----子线程任务结束",[NSThread currentThread]);

}

}

@end

2017-05-27 11:17:26.064 MXBarManagerDemo[23458:2865125] 启动RunLoop前--(null)

2017-05-27 11:17:30.627 MXBarManagerDemo[23458:2865125] {number = 3, name = YTThread}----子线程任务开始

2017-05-27 11:17:33.632 MXBarManagerDemo[23458:2865125] {number = 3, name = YTThread}----子线程任务结束

2017-05-27 11:17:36.319 MXBarManagerDemo[23458:2865125] {number = 3, name = YTThread}----子线程任务开始

2017-05-27 11:17:39.325 MXBarManagerDemo[23458:2865125] {number = 3, name = YTThread}----子线程任务结束

2017-05-27 11:17:56.479 MXBarManagerDemo[23458:2865125] {number = 3, name = YTThread}----子线程任务开始

2017-05-27 11:17:59.482 MXBarManagerDemo[23458:2865125] {number = 3, name = YTThread}----子线程任务结束

注意几点:

1、获取RunLoop只能使用 [NSRunLoop currentRunLoop] 或 [NSRunLoop mainRunLoop]。

应用程序并不需要自己创建RunLoop,而是要在合适的时间启动runloop。 CF框架源码中有CFRunLoopGetCurrent(void) 和 CFRunLoopGetMain(void),查看源码可知,这两个API中,都是先从全局字典中取。如果没有与该线程对应的RunLoop,那么就会帮我们创建一个RunLoop(创建RunLoop的过程在函数_CFRunLoopGet0(pthread_t t)中)。

2、即使RunLoop开始运行,如果RunLoop 中的 modes 为空,或者要执行的mode里没有item,那么RunLoop会直接在当前loop中返回,并进入睡眠状态。

如注释掉[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];(查看注释前后打印出的runLoop),点击视图,控制台不会有任何输出,因为mode 中并没有item任务。经过NSRunLoop封装后,只可以往mode中添加两类item任务:NSPort(对应的是source)、NSTimer。如果使用CFRunLoopRef,则可以使用C语言API,往mode中添加source、timer、observer。

3、自己创建的Thread中的任务是在kCFRunLoopDefaultMode这个mode中执行的。

查看modes

2017-05-27 14:13:53.475 MXBarManagerDemo[29040:3134640] runLoop--{wakeup port = 0x731b, stopped = false, ignoreWakeUps = true,

current mode = (none),

common modes = {type = mutable set, count = 1,

entries =>

2 : {contents = "kCFRunLoopDefaultMode"}

}

,

common mode items = (null),

modes = {type = mutable set, count = 1,

entries =>

2 : {name = kCFRunLoopDefaultMode, port set = 0x560b, queue = 0x6100001750c0, source = 0x6100001d40a0 (not fired), timer port = 0x7503,

sources0 = {type = mutable set, count = 0,

entries =>

}

,

sources1 = {type = mutable set, count = 1,

entries =>

1 : {signalled = No, valid = Yes, order = 200, context = {valid = Yes, port = 7603, source = 0x60000017b480, callout = __NSFireMachPort (0x1080a0737), context = }}

}

,

observers = (null),

timers = (null),

currently 517558433 (51534038374775) / soft deadline in: 1.84466925e+10 sec (@ -1) / hard deadline in: 1.84466925e+10 sec (@ -1)

},

}

}

2017-05-27 14:13:53.476 MXBarManagerDemo[29040:3134640] 启动RunLoop前--(null)

2017-05-27 14:16:58.114 MXBarManagerDemo[29040:3134640] {number = 3, name = YTThread}----子线程任务开始

2017-05-27 14:17:01.115 MXBarManagerDemo[29040:3134640] {number = 3, name = YTThread}----子线程任务结束

4、在子线程创建好后,最好所有的任务都放在AutoreleasePool中。

举例

YYKit中使用YYWebImageOperation对网络图片进行下载请求,使用[self performSelector:@selector(_startRequest:) onThread:[self.class _networkThread] withObject:nil waitUntilDone:NO];将任务丢到后台线程的 RunLoop 中。

// runs on network thread

- (void)_startOperation {

if ([self isCancelled]) return;

@autoreleasepool {

// get image from cache

if (_cache &&

!(_options & YYWebImageOptionUseNSURLCache) &&

!(_options & YYWebImageOptionRefreshImageCache)) {

UIImage *image = [_cache getImageForKey:_cacheKey withType:YYImageCacheTypeMemory];

if (image) {

[_lock lock];

if (![self isCancelled]) {

if (_completion) _completion(image, _request.URL, YYWebImageFromMemoryCache, YYWebImageStageFinished, nil);

}

[self _finish];

[_lock unlock];

return;

}

if (!(_options & YYWebImageOptionIgnoreDiskCache)) {

__weak typeof(self) _self = self;

dispatch_async([self.class _imageQueue], ^{

__strong typeof(_self) self = _self;

if (!self || [self isCancelled]) return;

UIImage *image = [self.cache getImageForKey:self.cacheKey withType:YYImageCacheTypeDisk];

if (image) {

[self.cache setImage:image imageData:nil forKey:self.cacheKey withType:YYImageCacheTypeMemory];

[self performSelector:@selector(_didReceiveImageFromDiskCache:) onThread:[self.class _networkThread] withObject:image waitUntilDone:NO];

} else {

[self performSelector:@selector(_startRequest:) onThread:[self.class _networkThread] withObject:nil waitUntilDone:NO];

}

});

return;

}

}

}

[self performSelector:@selector(_startRequest:) onThread:[self.class _networkThread] withObject:nil waitUntilDone:NO];

}

/// Network thread entry point.

+ (void)_networkThreadMain:(id)object {

@autoreleasepool {

[[NSThread currentThread] setName:@"com.ibireme.yykit.webimage.request"];

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

[runLoop run];

}

}

/// Global image request network thread, used by NSURLConnection delegate.

+ (NSThread *)_networkThread {

static NSThread *thread = nil;

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

thread = [[NSThread alloc] initWithTarget:self selector:@selector(_networkThreadMain:) object:nil];

if ([thread respondsToSelector:@selector(setQualityOfService:)]) {

thread.qualityOfService = NSQualityOfServiceBackground;

}

[thread start];

});

return thread;

}

二、RunLoop如何保证NSTimer在视图滑动时依然能正常运转

问题描述:UITableView的header 上是一个横向ScrollView,使用NSTimer每隔几秒切换一张图片,当滑动UITableView的时顶部的scollView并不会切换图片;UITableView有显示倒计时的Label,当滑动tableView时倒计时就停止了。

创建定时器的两种方法

方法1和方法2等价,区别:方法2默认也是将timer添加到NSDefaultRunLoopMode下的,并且会自动fire。

- (void)viewDidLoad {

[super viewDidLoad];

// 方法1

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

[timer fire];

// 方法2

// [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];

}

- (void)timerUpdate {

NSLog(@"当前线程:%@",[NSThread currentThread]);

NSLog(@"启动RunLoop后--%@",[NSRunLoop currentRunLoop].currentMode);

// NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);

dispatch_async(dispatch_get_main_queue(), ^{

self.count ++;

NSString *timerText = [NSString stringWithFormat:@"计时器:%ld",self.count];

self.timerLabel.text = timerText;

});

}

2017-05-27 17:21:55.418 MXBarManagerDemo[32985:3929954] 当前线程:{number = 1, name = main}

2017-05-27 17:21:55.419 MXBarManagerDemo[32985:3929954] 启动RunLoop后--kCFRunLoopDefaultMode

原因:滑动scrollView时主线程的RunLoop 会切换到UITrackingRunLoopMode这个Mode,执行的也是UITrackingRunLoopMode下的任务(Mode中的item),而timer 是添加在NSDefaultRunLoopMode下的,所以timer任务并不会执行。只有当UITrackingRunLoopMode的任务执行完毕,RunLoop切换到NSDefaultRunLoopMode后,才会继续执行timer。

解决方法:需要在添加timer 时,将mode 设置为NSRunLoopCommonModes即可,只针对方法1。方法2因为是固定添加到defaultMode中,就不要用了。

关于timer的坑

上面的示例是在主线程中使用timer。在子线程中使用timer也可解决上面的问题,但需注意的是把timer加入到当前runloop后,必须让runloop 运行起来,否则timer仅执行一次。

- (void)viewDidLoad {

[super viewDidLoad];

......

[self createThread];

}

- (void)createThread {

NSThread *subThread = [[NSThread alloc] initWithTarget:self selector:@selector(timerTest) object:nil];

[subThread start];

self.subThread = subThread;

}

- (void)timerTest {

@autoreleasepool {

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

NSLog(@"启动RunLoop前--%@",runLoop.currentMode);

NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);

// 方法1

// NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];

// [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

// [timer fire];

// 方法2

[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];

[[NSRunLoop currentRunLoop] run];

}

}

- (void)timerUpdate {

NSLog(@"当前线程:%@",[NSThread currentThread]);

NSLog(@"启动RunLoop后--%@",[NSRunLoop currentRunLoop].currentMode);

// NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);

dispatch_async(dispatch_get_main_queue(), ^{

self.count ++;

NSString *timerText = [NSString stringWithFormat:@"计时器:%ld",self.count];

self.timerLabel.text = timerText;

});

}

添加timer 前的控制台输出:

2017-05-27 22:31:41.162 MXBarManagerDemo[704:80646] 启动RunLoop前--(null)

2017-05-27 22:31:41.163 MXBarManagerDemo[704:80646] currentRunLoop:{wakeup port = 0x741b, stopped = false, ignoreWakeUps = true,

current mode = (none),

common modes = {type = mutable set, count = 1,

entries =>

2 : {contents = "kCFRunLoopDefaultMode"}

}

,

common mode items = (null),

modes = {type = mutable set, count = 1,

entries =>

2 : {name = kCFRunLoopDefaultMode, port set = 0x560b, queue = 0x600000168ac0, source = 0x6000001d8600 (not fired), timer port = 0x7603,

sources0 = (null),

sources1 = (null),

observers = (null),

timers = (null),

currently 517588301 (785273542974) / soft deadline in: 1.84467433e+10 sec (@ -1) / hard deadline in: 1.84467433e+10 sec (@ -1)

},

}

}

添加timer后的控制台输出:

2017-05-27 22:32:33.924 MXBarManagerDemo[704:80646] 当前线程:{number = 3, name = (null)}

2017-05-27 22:32:33.924 MXBarManagerDemo[704:80646] 启动RunLoop后--kCFRunLoopDefaultMode

2017-05-27 22:32:33.927 MXBarManagerDemo[704:80646] currentRunLoop:{wakeup port = 0x741b, stopped = false, ignoreWakeUps = true,

current mode = kCFRunLoopDefaultMode,

common modes = {type = mutable set, count = 1,

entries =>

2 : {contents = "kCFRunLoopDefaultMode"}

}

,

common mode items = (null),

modes = {type = mutable set, count = 1,

entries =>

2 : {name = kCFRunLoopDefaultMode, port set = 0x560b, queue = 0x600000168ac0, source = 0x6000001d8600 (not fired), timer port = 0x7603,

sources0 = (null),

sources1 = (null),

observers = (null),

timers = {type = mutable-small, count = 1, values = (

0 : {valid = Yes, firing = Yes, interval = 5, tolerance = 0, next fire date = 517588354 (-0.00657904148 @ 838031380701), callout = (NSTimer) [TestRunLoopViewController timerUpdate] (0x10cb44ec4 / 0x10ca00960) (/Users/yitudev/Library/Developer/CoreSimulator/Devices/CA10957A-B14D-4E49-80EE-E2B23C4E6183/data/Containers/Bundle/Application/8809F8A7-130A-4AC5-B9D6-798FFB53C6B1/MXBarManagerDemo.app/MXBarManagerDemo), context = }

)},

currently 517588354 (838035515992) / soft deadline in: 1.84467432e+10 sec (@ -1) / hard deadline in: 1.84467432e+10 sec (@ -1)

},

}

}

从控制台输出可以看出,timer确实被添加到NSDefaultRunLoopMode中了。可是添加到子线程中的NSDefaultRunLoopMode里,无论如何滚动,timer都能够很正常的运转。

解释:多线程与runloop的关系 —— 每一个线程都有一个与之关联的RunLoop,而每一个RunLoop可能会有多个Mode。CPU会在多个线程间切换来执行任务,呈现出多个线程同时执行的效果。执行的任务其实就是RunLoop去各个Mode里执行各个item。因为RunLoop是独立的两个,相互不会影响,所以在子线程添加timer,滑动视图时,timer能正常运行。

结论

1、如果是在主线程中运行timer,想要timer在某界面有视图滚动时依然能正常运转,那么将timer添加到RunLoop中时,就需要设置mode 为NSRunLoopCommonModes。

2、如果是在子线程中运行timer,那么将timer添加到RunLoop中后,Mode设置为NSDefaultRunLoopMode或NSRunLoopCommonModes均可,但是需要保证RunLoop在运行,且其中有任务。

三、RunLoop如何保证不影响UI卡顿

问题描述:UITableView、UICollectionView等延迟加载图片。

以UITableView 的 cell 上显示网络图片为例,需要两步:1、下载网络图片;2、将网络图片设置到UIImageView上。为了不影响滑动第1步一般都是放在子线程处理,第2步回到主线程设置。model切换调用方法performSelector:withObject:afterDelay:inModes:,如下(方法2):

UIImage *downloadedImage = ....;

// 方法1

// self.myImageView.image = downloadedImage;

// 方法2

[self.myImageView performSelector:@selector(setImage:) withObject:downloadedImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

static NSString *identifier = @"cell";

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];

if (cell == nil) {

cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];

}

for (NSInteger i = 1; i <= 5; i++) {

[[cell.contentView viewWithTag:i] removeFromSuperview];

}

UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(5, 5, 300, 25)];

label.backgroundColor = [UIColor clearColor];

label.textColor = [UIColor redColor];

label.text = [NSString stringWithFormat:@"%zd - Drawing index is top priority", indexPath.row];

label.font = [UIFont boldSystemFontOfSize:13];

label.tag = 1;

[cell.contentView addSubview:label];

UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(105, 20, 85, 85)];

imageView.tag = 2;

NSString *path = [[NSBundle mainBundle] pathForResource:@"timg" ofType:@"jpeg"];

UIImage *image = [UIImage imageWithContentsOfFile:path];

imageView.contentMode = UIViewContentModeScaleAspectFit;

imageView.image = image; // 方法1设置图片

// [imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO modes:@[NSDefaultRunLoopMode]]; // 方法2设置图片

NSLog(@"current:%@", [NSRunLoop currentRunLoop].currentMode);

[cell.contentView addSubview:imageView];

UIImageView *imageView2 = [[UIImageView alloc] initWithFrame:CGRectMake(200, 20, 85, 85)];

imageView2.tag = 3;

UIImage *image2 = [UIImage imageWithContentsOfFile:path];

imageView2.contentMode = UIViewContentModeScaleAspectFit;

imageView2.image = image2;

// [imageView2 performSelectorOnMainThread:@selector(setImage:) withObject:image2 waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];

[cell.contentView addSubview:imageView2];

UILabel *label2 = [[UILabel alloc] initWithFrame:CGRectMake(5, 99, 300, 35)];

label2.lineBreakMode = NSLineBreakByWordWrapping;

label2.numberOfLines = 0;

label2.backgroundColor = [UIColor clearColor];

label2.textColor = [UIColor colorWithRed:0 green:100.f / 255.f blue:0 alpha:1];

label2.text = [NSString stringWithFormat:@"%zd - Drawing large image is low priority. Should be distributed into different run loop passes.", indexPath.row];

label2.font = [UIFont boldSystemFontOfSize:13];

label2.tag = 4;

UIImageView *imageView3 = [[UIImageView alloc] initWithFrame:CGRectMake(5, 20, 85, 85)];

imageView3.tag = 5;

UIImage *image3 = [UIImage imageWithContentsOfFile:path];

imageView3.contentMode = UIViewContentModeScaleAspectFit;

imageView3.image = image3;

// [imageView3 performSelectorOnMainThread:@selector(setImage:) withObject:image3 waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];

[cell.contentView addSubview:label2];

[cell.contentView addSubview:imageView3];

return cell;

}

如上所示,一个Cell里有两个Label,和三个imageView,这里的图片是非常高清的。

1、方法1:为imageView设置image,是在UITrackingRunLoopMode中进行的,如果图片很大,图片解压缩和渲染肯定会很耗时,那么卡顿就是必然的。

2、方法2: 切换到NSDefaultRunLoopMode中,一个runloop循环要解压和渲染18张大图(假如一个页面能显示6行,每行3张图),耗时肯定超过50ms(1/60s)。我们可以继续来优化,一次runloop循环,仅渲染一张大图片,分18次来渲染,这样每一次runloop耗时就比较短了,滑动起来就会非常顺畅。这也是 RunLoopWorkDistribution 中的做法,即:首先创建一个单例,单例中定义了几个数组,用来存要在runloop循环中执行的任务,然后为主线程的runloop添加一个CFRunLoopObserver,当主线程在NSDefaultRunLoopMode中执行完任务,即将睡眠前,执行一个单例中保存的一次图片渲染任务。关键代码看 RunLoopWorkDistribution 类即可。

四、使用RunLoop 监测主线程卡顿

问题描述:用RunLoop 监测主线程的卡顿,并将卡顿时的线程堆栈信息保存下来,下次上传到服务器。

RunLoop 的内部逻辑:

9df12d66829d?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

image

伪代码如下:

{

/// 1. 通知Observers,即将进入RunLoop

/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);

do {

/// 2. 通知 Observers: 即将触发 Timer 回调。

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);

/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);

__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 4. 触发 Source0 (非基于port的) 回调。

__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);

__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 6. 通知Observers,即将进入休眠

/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);

/// 7. sleep to wait msg.

mach_msg() -> mach_msg_trap();

/// 8. 通知Observers,线程被唤醒

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);

/// 9. 如果是被Timer唤醒的,回调Timer

__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);

/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block

__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);

/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件

__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);

} while (...);

/// 10. 通知Observers,即将退出RunLoop

/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);

}

实现思路:主线程的RunLoop是在应用启动时自动开启的,也没有超时时间,所以正常情况下,主线程的RunLoop 只会在 步骤2—9 之间无限循环下去。

那么,我们只需要在主线程的RunLoop中添加一个observer,检测从 kCFRunLoopBeforeSources 到 kCFRunLoopBeforeWaiting 花费的时间是否过长。如果花费的时间大于某一个阙值,我们就认为有卡顿,并把当前的线程堆栈转储到文件中,并在以后某个合适的时间,将卡顿信息文件上传到服务器。

代码如下:

#import

@interface FluencyMonitor : NSObject

+ (instancetype)shareMonitor;

/**

开始监控

@param interval 定时器间隔时间

@param fault 卡顿的阙值

*/

- (void)startWithInterval:(NSTimeInterval)interval fault:(NSTimeInterval)fault;

/**

开始监控

*/

- (void)start;

/**

停止监控

*/

- (void)stop;

@end

#import "FluencyMonitor.h"

#import

@interface FluencyMonitor ()

@property (strong, nonatomic) NSThread *monitorThread; /**< 监控线程 */

@property (assign, nonatomic) CFRunLoopObserverRef observer; /**< 观察者 */

@property (assign, nonatomic) CFRunLoopTimerRef timer; /**< 定时器 */

@property (strong, nonatomic) NSDate *startDate; /**< 开始执行的时间 */

@property (assign, nonatomic) BOOL excuting; /**< 执行时长 */

@property (assign, nonatomic) NSTimeInterval interval; /**< 定时器间隔时间 */

@property (assign, nonatomic) NSTimeInterval fault; /**< 卡顿的阙值 */

@end

@implementation FluencyMonitor

static FluencyMonitor *instance = nil;

/**

第一步:创建一个子线程,在线程启动时,启动其RunLoop

@return

*/

+ (instancetype)shareMonitor {

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

instance = [[[self class] alloc] init];

instance.monitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(monitorThreadEntryPoint) object:nil];

[instance.monitorThread start];

});

return instance;

}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

instance = [super allocWithZone:zone];

});

return instance;

}

/**

子线程中启动RunLoop

*/

+ (void)monitorThreadEntryPoint {

@autoreleasepool {

[[NSThread currentThread] setName:@"FluencyMonitor"];

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

[runLoop run];

}

}

/**

第二步:开始监控,往主线程的RunLoop中添加一个observer,并往子线程中添加一个定时器,每0.5秒检测一次耗时的时长

*/

- (void)start {

[self startWithInterval:1.0 fault:2.0];

}

/**

开始监控

@param interval 定时器间隔时间

@param fault 卡顿的阙值:超出该阙值则被视为卡顿

*/

- (void)startWithInterval:(NSTimeInterval)interval fault:(NSTimeInterval)fault {

_interval = interval;

_fault = fault;

if (_observer) {

return;

}

// 1.创建observer

CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL, NULL};

_observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context);

// 2.将observer添加到主线程的RunLoop中

CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);

// 3.创建一个timer,并添加到子线程的RunLoop中

[self performSelector:@selector(addTimerToMonitorThread) onThread:self.monitorThread withObject:nil waitUntilDone:NO modes:@[NSRunLoopCommonModes]];

}

/**

创建一个定时器timer并添加到子线程的RunLoop中

*/

- (void)addTimerToMonitorThread {

if (_timer) {

return;

}

// 1.创建一个timer

CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();

CFRunLoopTimerContext context = {0, (__bridge void *)self, NULL, NULL, NULL};

_timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, _interval, 0, 0, &runLoopTimerCallBack, &context);

// 2.添加到子线程的RunLoop中

CFRunLoopAddTimer(currentRunLoop, _timer, kCFRunLoopCommonModes);

}

/**

移除定时器

*/

- (void)removeTimer {

if (_timer) {

CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();

CFRunLoopRemoveTimer(currentRunLoop, _timer, kCFRunLoopCommonModes);

CFRelease(_timer);

_timer = NULL;

}

}

/**

从主线程中移除观察者observer

*/

- (void)stop {

if (_observer) {

CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);

CFRelease(_observer);

_observer = NULL;

}

[self performSelector:@selector(removeTimer) onThread:self.monitorThread withObject:nil waitUntilDone:NO modes:@[NSRunLoopCommonModes]];

}

/**

处理卡顿信息:如上传到服务器等

*/

- (void)handleStackInfo {

NSData *lagData = [[[PLCrashReporter alloc] initWithConfiguration:[[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll]] generateLiveReport];

PLCrashReport *lagReport = [[PLCrashReport alloc] initWithData:lagData error:NULL];

NSString *lagReportString = [PLCrashReportTextFormatter stringValueForCrashReport:lagReport withTextFormat:PLCrashReportTextFormatiOS];

//将字符串上传服务器

NSLog(@"lag happen, detail below: \n %@", lagReportString);

}

/**

观察者回调处理:主线程中的block、交互事件、以及其他任务都是在kCFRunLoopBeforeSources 到 kCFRunLoopBeforeWaiting 之前执行,所以我在即将开始执行Sources 时,记录一下时间,并把正在执行任务的标记置为YES,将要进入睡眠状态时,将正在执行任务的标记置为NO

@param observer

@param activity

@param info

*/

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {

FluencyMonitor *monitor = (__bridge FluencyMonitor *)info;

NSLog(@"MainRunLoop---%@", [NSThread currentThread]);

switch (activity) {

case kCFRunLoopEntry:

NSLog(@"kCFRunLoopEntry");

break;

case kCFRunLoopBeforeTimers:

NSLog(@"kCFRunLoopBeforeTimers");

break;

case kCFRunLoopBeforeSources:

NSLog(@"kCFRunLoopBeforeSources");

monitor.startDate = [NSDate date];

monitor.excuting = YES;

break;

case kCFRunLoopBeforeWaiting:

NSLog(@"kCFRunLoopBeforeWaiting");

monitor.excuting = NO;

break;

case kCFRunLoopAfterWaiting:

NSLog(@"kCFRunLoopAfterWaiting");

break;

case kCFRunLoopExit:

NSLog(@"kCFRunLoopExit");

break;

default:

break;

}

}

/**

定时器回调

@param timer

@param info

*/

static void runLoopTimerCallBack(CFRunLoopTimerRef timer, void *info) {

FluencyMonitor *monitor = (__bridge FluencyMonitor *)info;

if (!monitor.excuting) {

return;

}

// 如果主线程正在执行任务,并且这一次loop执行到 现在还没执行完,那就需要计算时间差

NSTimeInterval excuteTime = [[NSDate date] timeIntervalSinceDate:monitor.startDate];

NSLog(@"定时器---%@", [NSThread currentThread]);

NSLog(@"主线程执行了---%f秒", excuteTime);

if (excuteTime >= monitor.fault) {

// 执行时间大于阈值时处理卡顿信息

NSLog(@"线程卡顿了%f秒", excuteTime);

[monitor handleStackInfo];

}

}

@end

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值