iOS学习——RUNLOOPNSTimer

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS学习——RUNLOOPNSTimer相关的知识,希望对你有一定的参考价值。

  每一个app的启动,开启主线程的同时,也开启了一个Runloop死循环,runloop会不断询问是否有新的任务给线程执行。runloop最常用的三块,就是网络事件,事件响应与NSTimer。网络事件现在基本上都用已经封装好的框架,但是最初用NSURLConnection进行网络请求的时候,会出现异步回调永远没法回来的情况,原因就是子线程运行完了,不会再次执行回调,对于这种情况就是让子线程上的runloop跑起来。

  

- (void)demoTimer {
    [NSThread detachNewThreadWithBlock:^{
       [NSTimer scheduledTimerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
          NSLog(@"timer执行中%s  %@",__func__,[NSThread currentThread]);

      }];
        NSLog(@"线程走了%s  %@",__func__,[NSThread currentThread]);

    }];
    
    
}

  现在做了一个类似网络请求的步骤,在子线程创建开启一个nstimer,并执行回调block,然后运行的结果是这样的

2017-11-12 10:12:21.938 hhh[30280:461580] 线程走了__27-[ViewController demoTimer]_block_invoke  <NSThread: 0x608000265380>{number = 3, name = (null)}

  执行了NSTimer,然后线程就运行完任务直接消失,并没有执行回调方法,因此这时候就需要用到NSRunloop开启循环。

- (void)demoTimer {
    [NSThread detachNewThreadWithBlock:^{
        [NSTimer scheduledTimerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
            NSLog(@"timer执行中%s  %@",__func__,[NSThread currentThread]);
            
        }];
                [[NSRunLoop currentRunLoop] run];

        NSLog(@"线程走了%s  %@",__func__,[NSThread currentThread]);
        
    }];

}
2017-11-12 11:05:49.779 hhh[31453:486745] timer执行中__27-[ViewController demoTimer]_block_invoke_2  <NSThread: 0x608000076d80>{number = 3, name = (null)}
2017-11-12 11:05:49.779 hhh[31453:486745] 线程走了__27-[ViewController demoTimer]_block_invoke  <NSThread: 0x608000076d80>{number = 3, name = (null)}

  不过这样同样会有一个问题,就是thread循环没有关闭,让线程一直没有被kill,因此,我们可以用

            [NSThread exit];

  退出当前线程,也可以才有有限制条件循环开启来保证循环不被一直开启

  

- (void)demoTimer {
    
    __block BOOL isFinshed = NO;
    [NSThread detachNewThreadWithBlock:^{
        [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"timer执行中%s  %@",__func__,[NSThread currentThread]);
            isFinshed = YES;
        }];
        while (!isFinshed) {
            [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1f]];

        }

        NSLog(@"线程走了%s  %@",__func__,[NSThread currentThread]);
        
    }];

}

 

  

  现在项目中能够主动使用得上runloop的,基本上就是开启NSTimer的时候。

- (void)demoTimer1 {
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%s  %@",__func__,[NSThread currentThread]);
    }];
    [[NSRunLoop currentRunloop] addTimer:timer forMode:NSDefaultRunLoopMode];
}

  当创建一个timer对象的时候,需要将其加入runloop中才能运行起来,而runloop则按优先级可以分为两种模式,一种NSDefaultRunLoopMode,默认模式;一种NSRunLoopCommonModes,包含程序UI处理的模式,是UI模式和默认模式的一种混合模式。当使用默认模式执行timer时,触发界面上的UI事件,就会发现timer停止计时,因为这时候当前runloop会切换到具有更高优先级的UI模式。因此,如果要想UI响应与timer不冲突,那么就不得不使用NSRunLoopCommonModes

    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

  不过有一点是需要注意的,就是timer内尽量执行轻量级代码,不要执行耗时耗性能操作,不然当结合scrollView等滚动界面使用的时候,仍然会出现卡顿的情况。

  而事件响应就不用过多赘述了,每次点击事件等事件发生,都是由当前线程上runloop接受并推动线程去执行。

  其实对于runloop还有分段执行这一操作,类似与多线程,但是工作原理大不一样,只不过由于现在多线程、并发等技术的实现,而很少需要用到。当一次runloop循环需要执行太多操作的时候,界面就会出现卡顿的情况,与nstimer执行耗时操作加入runloop中,滚动界面的效果类似。例如:当使用tableView加载图片,如果使用本地高清大图,一次加载过多的时候,滑动起来就会有强烈卡顿感,就是因为一次runloop需要执行大量图片渲染所造成的(类似的情景还有使用瀑布流加载本地大图,特别是苹果图片一般都比较大);这种情况下,可以是用多次runloop分段渲染,每次渲染一张图片(UI的改变只能在主线程,无法开启多线程渲染)。

#import "ViewController.h"
#import <CoreFoundation/CoreFoundation.h>

typedef void(^runloopBlock)();

#define IDENTIFIER @"ZLios"
#define MAINWIDTH [UIScreen mainScreen].bounds.size.width
#define MAXIMGCOUNT 50 //设定界面能加载的最大图片数
@interface ViewController ()<UITableViewDelegate,UITableViewDataSource>
@property (nonatomic ,strong) UITableView * tableView;
@property (nonatomic ,strong) NSMutableArray * tasks;

@end

@implementation ViewController

- (NSMutableArray *)tasks {
    if (!_tasks) {
        _tasks = [NSMutableArray array];
    }
    return _tasks;
}

#pragma mark initView
- (void)initTableView {
    
    UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleGrouped];
    tableView.delegate = self;
    tableView.dataSource = self;
    [self.view addSubview:tableView];
    self.tableView = tableView;
}

- (void)initTimerForRunloop {
    
    //每次计时都是一次runloop,设定定时器的时间,就可以设定runloop循环的速度
    [NSTimer scheduledTimerWithTimeInterval:0.01f target:self selector:@selector(runTimerMethod) userInfo:nil repeats:YES];
}
//timer执行事件
- (void)runTimerMethod {
    
}
#pragma mark VIEWLOAD
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    [self initTimerForRunloop];
    [self initTableView];
    
    [self craeteRunloopForOberServer];
}

#pragma TableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 120.f;
}

- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
    return 0.01f;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
    return 0.01f;
}
#pragma mark TableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 100;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:IDENTIFIER];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:IDENTIFIER];
    }
    
    //清理视图,释放内存,防止复用问题
    for (UIView *view in cell.contentView.subviews) {
        [view removeFromSuperview];
    }
    
    //将任务加入block中,放入任务数组,等待runloop循环执行
    __weak ViewController *vc = self;
    [self addBlockToArr:^{
        [vc cellAddImageViewOne:cell];
    }];
    [self addBlockToArr:^{
        [vc cellAddImageViewTwo:cell];
    }];
    [self addBlockToArr:^{
        [vc cellAddImageViewThree:cell];
    }];
    [self addBlockToArr:^{
        [vc cellAddImageViewFour:cell];
    }];
    return cell;
}

#pragma mark 加载图片
- (void)addBlockToArr:(runloopBlock)block {
  
 
    [self.tasks addObject:block];
    if (self.tasks.count > MAXIMGCOUNT) {
        [self.tasks removeObjectAtIndex:0];
    }
}

- (void)cellAddImageViewOne:(UITableViewCell *)cell {
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(5, 10, (MAINWIDTH - 5 * 5) / 4, 100)];
    NSString *imgPath = [[NSBundle mainBundle] pathForResource:@"timg" ofType:@"jpeg"];
    UIImage *image = [UIImage imageWithContentsOfFile:imgPath];//图片会被多次调用,节省内存
    imageView.contentMode = UIViewContentModeScaleAspectFit;

    imageView.image = image;
    
    [cell.contentView addSubview:imageView];
}
- (void)cellAddImageViewTwo:(UITableViewCell *)cell {
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(5 + (MAINWIDTH - 5 * 4) / 4 * 1, 10, (MAINWIDTH - 5 * 5) / 4, 100)];
    NSString *imgPath = [[NSBundle mainBundle] pathForResource:@"timg" ofType:@"jpeg"];
    UIImage *image = [UIImage imageWithContentsOfFile:imgPath];//图片会被多次调用,节省内存
    imageView.contentMode = UIViewContentModeScaleAspectFit;
    imageView.image = image;
    
    [cell.contentView addSubview:imageView];
}
- (void)cellAddImageViewThree:(UITableViewCell *)cell {
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(5 + (MAINWIDTH - 5 * 4) / 4 * 2, 10, (MAINWIDTH - 5 * 5) / 4, 100)];
    NSString *imgPath = [[NSBundle mainBundle] pathForResource:@"timg" ofType:@"jpeg"];
    UIImage *image = [UIImage imageWithContentsOfFile:imgPath];//图片会被多次调用,节省内存
    imageView.contentMode = UIViewContentModeScaleAspectFit;

    imageView.image = image;
    
    [cell.contentView addSubview:imageView];
}
- (void)cellAddImageViewFour:(UITableViewCell *)cell {
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(5 + (MAINWIDTH - 5 * 4) / 4 * 3, 10, (MAINWIDTH - 5 * 5) / 4, 100)];
    NSString *imgPath = [[NSBundle mainBundle] pathForResource:@"timg" ofType:@"jpeg"];
    UIImage *image = [UIImage imageWithContentsOfFile:imgPath];//图片会被多次调用,节省内存
    imageView.contentMode = UIViewContentModeScaleAspectFit;

    imageView.image = image;
    
    [cell.contentView addSubview:imageView];
}

#pragma mark 开启多次runloop渲染图片
//回调
static void callBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    ViewController *vc = (__bridge ViewController *)info;
    if (vc.tasks.count == 0) {
        return;
    }
    runloopBlock task = vc.tasks.firstObject;
    task();
    [vc.tasks removeObjectAtIndex:0];
    
}

- (void)craeteRunloopForOberServer {
    //获取当前runloop
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
//    得到上下文
    CFRunLoopObserverContext context = {
        0,
        (__bridge void *)self,//C取用OC对象,需要桥接
        &CFRetain,
        &CFRelease,
        NULL
    };
    
    //创建观察者 kCFRunLoopBeforeWaiting 执行完任务,进入等待之前
    CFRunLoopObserverRef observerRef = CFRunLoopObserverCreate(NULL,kCFRunLoopBeforeWaiting , YES, 0, &callBack, &context);
    
    //建立监听,kCFRunLoopDefaultMode 当滑动的时候进入ui模式,runloop不会再次循环渲染
    //kCFRunLoopCommonModes让界面在滑动的同时渲染图片
    CFRunLoopAddObserver(runloop, observerRef, kCFRunLoopCommonModes);
    
    CFRelease(observerRef);
    
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

  这里,runloop的使用采用了CFRunloop这个库,用c语言实现对runloop的状态监听。这里C语言的内容其实可以总结为这几步:1.获取到当前的runloop 2.创建context,即上下文 3.创建观察者 4.加入监听 5.释放C语言对象(create后需要释放,C语言未加入ARC),这样就实现了在主线程上对大量图片渲染,并且避免卡顿情况。

 

  

 

以上是关于iOS学习——RUNLOOPNSTimer的主要内容,如果未能解决你的问题,请参考以下文章

iOS 性能优化

golang代码片段(摘抄)

iOS代码片段CodeSnippets

iOS xcode 代码片段

iOS学习之代码块(Block)

iOS常用于显示几小时前/几天前/几月前/几年前的代码片段