每一个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),这样就实现了在主线程上对大量图片渲染,并且避免卡顿情况。