Runloop(三)、Runloop应用

267 阅读9分钟

应用

1.控制线程生命周期(线程保活)

2.解决NSTimer在滑动时停止工作的问题

3.监控应用卡顿

4.性能优化

解决NSTimer在滑动时停止工作的问题

创建一个iOS工程,在界面上放一个TextView.创建一个定时器

    static int count = 0;
    [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%d", ++count);
    }];

会发现,当滚动TextView时,定时器停止滚动。

scheduled安排。scheduledTimer开头的,这种定时器安排好了,会将NSTimer对象添加到默认模式下工作。滚动的时候切换到其他模式了,退出默认模式了,所以会停止滚动。

解决:

    static int count = 0;
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%d", ++count);
    }];
//    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
//     NSDefaultRunLoopMode、UITrackingRunLoopMode才是真正存在的模式
//     NSRunLoopCommonModes并不是一个真的模式,它只是一个标记
//     timer能在_commonModes数组中存放的模式下工作
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

  • _modes是所有模式集合
  • _commonModes可以认为标记为common的模式
  • NSDefaultRunLoopMode、UITrackingRunLoopMode才是真正存在的模式,NSRunLoopCommonModes并不是一个真的模式,它只是一个标记
  • 如果传的参数是NSRunLoopCommonModes,意味着NSTimer能在标记为common的模式下工作,也就是说能在_commonModes数组中存放的模式下工作。
  • _commonModes存放NSDefaultRunLoopMode,UITrackingRunLoopMode
  • 如果对象timer或者source0,source1等,传入的参数是_commonModes,那么就会把这个对象放在_commonModelItems里。
  • 定时器是不准时的,即使没有滑动干扰也是不准时的,因为定时器是Runloop处理的,Runloop有可能会处理其他事务。有准时的定时器,后面再说。
注意:scheduledTimerWithTimeInterval并没有开辟新的线程,而是在主线程中执行。所以还是在主线程所在的runloop,即:`[NSRunLoop mainRunLoop]`
    NSLog(@"%p %p", [NSRunLoop currentRunLoop], [NSRunLoop mainRunLoop]);
    
    [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%d", ++count);
        NSLog(@">>>>>%p %p", [NSRunLoop currentRunLoop], [NSRunLoop mainRunLoop]);
    }];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"=====%p %p", [NSRunLoop currentRunLoop], [NSRunLoop mainRunLoop]);
    });

线程保活

1.线程保活

#import "ViewController.h"
#import "MJThread.h"

@interface ViewController ()
@property (strong, nonatomic) MJThread *thread;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.thread = [[MJThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
	// waitUntilDone传yes,会等test执行完之后,再执行NSLog(@"123");
    // 传NO就是执行test的同时,执行NSLog(@"123");
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
    // NSLog(@"123");
}

// 子线程需要执行的任务
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

// 这个方法的目的:线程保活
- (void)run {
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    
    // 往RunLoop里面添加Source\Timer\Observer
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    
    NSLog(@"%s ----end----", __func__);
}

@end
#import "MJThread.h"

@implementation MJThread

- (void)dealloc
{
    NSLog(@"%s", __func__);
}

@end

  • run方法的目的,是让线程保活。
  • 因为Runloop的mode里没有任何的,source0,source1,timer,observer,会立马退出。所以为了不让Runloop退出,这里添加一个source1.[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
  • [[NSRunLoop currentRunLoop] run];,让Runloop一直在循环。达到保活的目的。单独写这行代码,不写上面那一行代码是不行的,依然会退出。必须在循环中保证有事件。
  • [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO]; waitUntilDone传yes,会等test执行完之后,再执行NSLog(@"123");

2. 线程释放

上面的线程保活方法,会导致控制器和线程都无法释放,控制器无法释放是因为循环引用。 增加一个控制器为根控制器,控制器里有个按钮,点击跳转ViewController.发现在ViewController里点击导航返回按钮,返回根控制器后,控制器和线程并没有释放。

self.thread = [[MJThread alloc] initWithTarget:self selector:@selector(run) object:nil];

MJThread会对控制器强引用。改成:

 self.thread = [[MJThread alloc] initWithBlock:^{
        // 往RunLoop里面添加Source\Timer\Observer
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
        
        NSLog(@"%@ ----end----", [NSThread currentThread]);
        
    }];
    [self.thread start];

但是,控制器释放后,线程还是没释放。此时这个线程为全局线程。 即使该线程置为nil,该线程也不会释放

- (void)dealloc
{
    NSLog(@"%s", __func__);
    
	self.thread = nil; //线程依然不会被释放
}

ViewController里增加stop按钮。

#import "ViewController.h"
#import "MJThread.h"

@interface ViewController ()
@property (strong, nonatomic) MJThread *thread;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.thread = [[MJThread alloc] initWithBlock:^{
        // 往RunLoop里面添加Source\Timer\Observer
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
        
        NSLog(@"%@ ----end----", [NSThread currentThread]);
        
    }];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (IBAction)stop:(id)sender {
    
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];//YES时马上执行,为NO时先加入线程的runloop事件循环里面,等待runloop来调度执行。
    
}

// 子线程需要执行的任务
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

-(void)stopThread{
    
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    CFRunLoopStop(CFRunLoopGetCurrent());
    
}


// 这个方法的目的:线程保活
//- (void)run {
//    NSLog(@"%s %@", __func__, [NSThread currentThread]);
//
//    // 往RunLoop里面添加Source\Timer\Observer
//    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
//    [[NSRunLoop currentRunLoop] run];
//
//    NSLog(@"%s ----end----", __func__);
//}

-(void)dealloc{
    
    NSLog(@"%s",__func__);
    
//此方法不行,线程还是没有释放。是不是因为写在这里,来不及释放?增加个stop按钮试试呢?
// [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];//YES时马上执行,为NO时先加入线程的runloop事件循环里面,等待runloop来调度执行。
    
//    self.thread = nil;
    
}


@end

以上,发现线程仍然不会被释放。问题出在[[NSRunLoop currentRunLoop] run]; 查看官方文档:

 it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:. In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.

底层调用了runMode:beforeDate:,换句话说,相当于开启了一个无限的loop NSRunLoop的run方法是无法停止的,它专门用于开启一个永不销毁的线程(NSRunLoop)

解决方案:

#import "ViewController.h"
#import "MJThread.h"

@interface ViewController ()
@property (strong, nonatomic) MJThread *thread;
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    
    self.stopped = NO;
    self.thread = [[MJThread alloc] initWithBlock:^{
        NSLog(@"%@----begin----", [NSThread currentThread]);
        
        // 往RunLoop里面添加Source\Timer\Observer
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        while (!weakSelf.isStoped) {
            // beforeDate:超时时间,线程退出时间
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        
//        [[NSRunLoop currentRunLoop] run];
        
        NSLog(@"%@----end----", [NSThread currentThread]);
        
        // NSRunLoop的run方法是无法停止的,它专门用于开启一个永不销毁的线程(NSRunLoop)
        //        [[NSRunLoop currentRunLoop] run];
        /*
         it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:.
         In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers
         */
        
    }];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

// 子线程需要执行的任务
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

- (IBAction)stop {
    // 在子线程调用stop
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}

// 用于停止子线程的RunLoop
- (void)stopThread
{
    // 设置标记为YES
    self.stopped = YES;
    
    // 停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    
    [self stop]; //当不点击stop按钮,控制器释放的时候,让线程也释放
}

@end
  • 以上方案也是有问题的,
- (void)dealloc
{
    NSLog(@"%s", __func__);
    
    [self stop]; //当不点击stop按钮,控制器释放的时候,让线程也释放
}

会崩溃。崩在[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];,坏内存访问。

崩溃原因:

- (IBAction)stop {
    if (!self.thread) return;
    
    // 在子线程调用stop(waitUntilDone设置为YES,代表子线程的代码执行完毕后,这个方法才会往下走)
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}

waitUntilDone传入NO,不等stopThread执行完,stop就执行完了,从而dealloc执行完,从而self就不存在了,这时候self.thread的runloop通过self执行stopThread时,会出现坏内存访问,坏内存是当前控制器,也就是self.所以这里改为YES.

但这样,控制器点击stop,返回根控制器后,发现线程并没有释放。
这是因为,点击stop后,停止循环,然后控制器释放,这时再判断!weakSelf.isStoped这个条件的时候,发现weakSelf已被释放,导致条件判断有误,大括号内的代码一直会被执行。所以线程释放不了。

但是!!!以上修改后,点击stop按钮后,再点返回,发现又崩了。 修改:stopThread方法最后清空线程,再加上一些判断。
最后完美的方案:

#import "ViewController.h"
#import "MJThread.h"

@interface ViewController ()
@property (strong, nonatomic) MJThread *thread;
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    
    self.stopped = NO;
    self.thread = [[MJThread alloc] initWithBlock:^{
        NSLog(@"%@----begin----", [NSThread currentThread]);
        
        // 往RunLoop里面添加Source\Timer\Observer
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    
        while (weakSelf && !weakSelf.isStoped) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        
        NSLog(@"%@----end----", [NSThread currentThread]);
    }];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    if (!self.thread) return;
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

// 子线程需要执行的任务
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

- (IBAction)stop {
    if (!self.thread) return;
    
    // 在子线程调用stop(waitUntilDone设置为YES,代表子线程的代码执行完毕后,这个方法才会往下走)
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}

// 用于停止子线程的RunLoop
- (void)stopThread
{
    // 设置标记为YES
    self.stopped = YES;
    
    // 停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    
    // 清空线程
    self.thread = nil;
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    
    [self stop];
}

@end

3.线程保活的封装

#import <Foundation/Foundation.h>

typedef void (^MJPermenantThreadTask)(void);

@interface MJPermenantThread : NSObject

/**
 开启线程
 */
//- (void)run;

/**
 在当前子线程执行一个任务
 */
- (void)executeTask:(MJPermenantThreadTask)task;

/**
 结束线程
 */
- (void)stop;

@end


#import "MJPermenantThread.h"

/** MJPermenantThread **/
@interface MJPermenantThread()
@property (strong, nonatomic) NSThread *innerThread;
@property (assign, nonatomic, getter=isStopped) BOOL stopped;
@end

@implementation MJPermenantThread
#pragma mark - public methods
- (instancetype)init
{
    if (self = [super init]) {
        self.stopped = NO;
        
        __weak typeof(self) weakSelf = self;
        
        self.innerThread = [[MJThread alloc] initWithBlock:^{
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
            
            while (weakSelf && !weakSelf.isStopped) {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            }
        }];
        
        [self.innerThread start];
    }
    return self;
}

//- (void)run
//{
//    if (!self.innerThread) return;
//
//    [self.innerThread start];
//}

- (void)executeTask:(MJPermenantThreadTask)task
{
    if (!self.innerThread || !task) return;
    
    [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}

- (void)stop
{
    if (!self.innerThread) return;
    
    [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    
    [self stop];
}

#pragma mark - private methods
- (void)__stop
{
    self.stopped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.innerThread = nil;
}

- (void)__executeTask:(MJPermenantThreadTask)task
{
    task();
}

@end

调用:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.thread = [[MJPermenantThread alloc] init];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self.thread executeTask:^{
        NSLog(@"执行任务 - %@", [NSThread currentThread]);
    }];
}

- (IBAction)stop {
    [self.thread stop];
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
}

@end

4.使用Runloop相关C语言API进行封装:

#import <Foundation/Foundation.h>

typedef void (^MJPermenantThreadTask)(void);

@interface MJPermenantThread : NSObject

/**
 开启线程
 */
//- (void)run;

/**
 在当前子线程执行一个任务
 */
- (void)executeTask:(MJPermenantThreadTask)task;

/**
 结束线程
 */
- (void)stop;

@end

/** MJPermenantThread **/
@interface MJPermenantThread()
@property (strong, nonatomic) NSThread *innerThread;
@end

@implementation MJPermenantThread
#pragma mark - public methods
- (instancetype)init
{
    if (self = [super init]) {
        self.innerThread = [[MJThread alloc] initWithBlock:^{
            NSLog(@"begin----");
            
            // 创建上下文(要初始化一下结构体)
            CFRunLoopSourceContext context = {0};
            
            // 创建source
            CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
            
            // 往Runloop中添加source
            CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
            
            // 销毁source
            CFRelease(source);
            
            // 启动
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
            
//            while (weakSelf && !weakSelf.isStopped) {
//                // 第3个参数:returnAfterSourceHandled,设置为true,代表执行完source后就会退出当前loop
//                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
//            }
            
            NSLog(@"end----");
        }];
        
        [self.innerThread start];
    }
    return self;
}

//- (void)run
//{
//    if (!self.innerThread) return;
//
//    [self.innerThread start];
//}

- (void)executeTask:(MJPermenantThreadTask)task
{
    if (!self.innerThread || !task) return;
    
    [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}

- (void)stop
{
    if (!self.innerThread) return;
    
    [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    
    [self stop];
}

#pragma mark - private methods
- (void)__stop
{
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.innerThread = nil;
}

- (void)__executeTask:(MJPermenantThreadTask)task
{
    task();
}

@end

  • CFRunLoopSourceContext context = {0}; 没有初始化的局部变量,他的初始值是不确定的,有可能会很大,所以这里建议把结构体置为{0}.

  • CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true); 第3个参数:returnAfterSourceHandled,设置为true,代表执行完source后就会退出当前loop。所以改为false后,就可省去一些判断和标记。