在面試中經常會被問到關于Runloop的問題音五,比如:
runloop和線程有什么關系?
runloop的mode作用是什么羔沙?
猜想runloop內部是如何實現的躺涝?
等等諸如此類~~~
既然面試中問到這么多關于Runloop的問題,那Runloop在實際應用中到底有什么用呢扼雏?
先來看一個在實際中遇到的問題
TableView的每一行Cell都有三張圖片坚嗜,在剛進入到這個頁面的時候,根本滑不動诗充。因為系統(tǒng)要繪制非常多的圖片苍蔬,如果此時的圖片很大,那么就會出現動圖中的情況蝴蜓,卡頓碟绑。
出現這個問題的原因很簡單,就是同時繪制了過多的大型圖片茎匠。那么這個問題大家平時怎么解決呢格仲?這個問題也是大家平時說的 如何優(yōu)化TableView卡頓 的問題。
異步加載數據诵冒?
異步繪制凯肋?
本篇介紹的方法是使用Runloop來優(yōu)化TableView。原理非常簡單造烁,就是監(jiān)聽Runloop的空閑狀態(tài)否过,在Runloop即將休眠時(空閑時)再去繪制圖片,這樣就不會像動圖中那么卡頓了惭蟋。
初始化最簡單的TableView和Cell
首先在ViewController中構造好最簡單的TableView苗桂。TableView行高定為 70,行數隨數據源的數量而變告组。使用延遲執(zhí)行模擬網絡請求來獲取數據源煤伟。cell使用自定義的 TestTableViewCell
。
//
// ViewController.m
// RunloopOptimizeTableView
//
// Created by 崇 on 2018.
// Copyright ? 2018 崇. All rights reserved.
//
#import "ViewController.h"
#import "TestTableViewCell.h"
@interface ViewController ()<UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSMutableArray *dataArray;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self configTableView];
[self requestData];
}
- (void)requestData {
NSLog(@"請求數據中...");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
for (int i = 0; i < 100; i++) {
NSMutableArray *arrM = [NSMutableArray array];
for (int i = 0; i < 3; i++) {
NSString *imgName = [NSString stringWithFormat:@"img%d.jpg", i+3];
[arrM addObject:imgName];
}
[self.dataArray addObject:arrM];
}
[self.tableView reloadData];
});
}
- (void)configTableView {
self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
self.tableView.delegate = self;
self.tableView.dataSource = self;
[self.tableView registerClass:[TestTableViewCell class] forCellReuseIdentifier:@"TestTableViewCell"];
[self.view addSubview:self.tableView];
self.tableView.contentInset = UIEdgeInsetsMake(-20, 0, 0, 0);
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 70;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.dataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TestTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"TestTableViewCell" forIndexPath:indexPath];
[cell setData:self.dataArray[indexPath.row]];
return cell;
}
- (NSMutableArray *)dataArray {
if (_dataArray == nil) {
_dataArray = [NSMutableArray array];
}
return _dataArray;
}
@end
dataArray
的數據結構是:
[
[@"imgName1",@"imgName2",@"imgName3"],
[@"imgName1",@"imgName2",@"imgName3"],
[@"imgName1",@"imgName2",@"imgName3"]
]
接下來是cell的實現
//
// TestTableViewCell.h
// RunloopOptimizeTableView
//
// Created by 崇 on 2018.
// Copyright ? 2018 崇. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface TestTableViewCell : UITableViewCell
- (void)setData:(NSArray *)dataArray;
@end
//
// TestTableViewCell.m
// RunloopOptimizeTableView
//
// Created by 崇 on 2018.
// Copyright ? 2018 崇. All rights reserved.
//
#import "TestTableViewCell.h"
@interface TestTableViewCell()
@property (nonatomic, strong) NSArray *dataArray;
@property (nonatomic, strong) NSMutableArray *imgViewArray;
@end
@implementation TestTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.imgViewArray = [NSMutableArray array];
NSInteger count = 3;
for (int i = 0; i < count; i++) {
UIImageView *imgView = [[UIImageView alloc] init];
[self.imgViewArray addObject:imgView];
[self.contentView addSubview:imgView];
}
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat screenWidth = self.contentView.bounds.size.width;
CGFloat width = (screenWidth - (self.imgViewArray.count+1)*10.0f) / self.imgViewArray.count;
CGFloat height = self.contentView.bounds.size.height;
for (int i = 0; i < self.imgViewArray.count; i++) {
UIImageView *imgView = self.imgViewArray[i];
imgView.frame = CGRectMake( (i+1)*10 + i*width, 0, width, height);
}
}
- (void)setData:(NSArray *)dataArray {
_dataArray = dataArray;
for (int i = 0; i < 3; i++) {
UIImageView *imgView = weakSelf.imgViewArray[i];
UIImage *img = [UIImage imageNamed:dataArray[i]];
imgView.image = img;
}
}
@end
這樣實現的就是動圖中卡頓的TableView木缝。
構造Runloop的工具類
接下來介紹便锨,怎么樣構造一個基于Runloop的工具。
首先我碟,在工具類的初始化方法中開啟一個timer放案,保證Runloop一直在循環(huán)。否則監(jiān)聽到Runloop進入休眠的狀態(tài)時矫俺,我們的代碼執(zhí)行過一次后Runloop就進入休眠了吱殉。
- (instancetype)init
{
self = [super init];
if (self) {
timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(timerFiredMethod) userInfo:nil repeats:YES];
}
return self;
}
- (void)timerFiredMethod {
// 這個方法不用任何實現掸冤,只是保證Runloop一直在循環(huán)中。
}
功能核心
監(jiān)聽Runloop需要創(chuàng)建Runloop的觀察者 CFRunLoopObserverRef
友雳,這個觀察者可以根據需要監(jiān)聽Runloop的各種狀態(tài)稿湿,包括七個枚舉值:
kCFRunLoopEntry
即將進入RunLoopkCFRunLoopBeforeTimers
即將處理TimerkCFRunLoopBeforeSources
即將處理Source事件源kCFRunLoopBeforeWaiting
即將進入休眠kCFRunLoopAfterWaiting
剛從休眠中喚醒kCFRunLoopExit
即將退出RunLoopkCFRunLoopAllActivities
監(jiān)聽全部的活動類型
下面是創(chuàng)建觀察者的源碼
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
// 我們監(jiān)聽了 kCFRunLoopBeforeWaiting 即將休眠這一個狀態(tài),就是Runloop處于空閑的狀態(tài)押赊,
// 當Runloop處于kCFRunLoopBeforeWaiting狀態(tài)就會觸發(fā)這個回調
// 在這里可以做我們想做的任務了
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);
CFRunLoopObserverCreateWithHandler()
函數中的各項參數:
第一個參數
CFAllocatorRef allocator
:分配存儲空間CFAllocatorGetDefault()
默認分配第二個參數
CFOptionFlags activities
:要監(jiān)聽的狀態(tài)kCFRunLoopBeforeWaiting
監(jiān)聽即將休眠的狀態(tài)第三個參數
Boolean repeats
:YES
:持續(xù)監(jiān)聽NO
:不持續(xù)第四個參數
CFIndex order
:優(yōu)先級饺藤,一般填0即可第五個參數 :回調兩個參數
observer
:監(jiān)聽者activity
:監(jiān)聽的事件
CFRunLoopAddObserver()
函數中的參數:
第一個參數
CFRunLoopRef rl
:要監(jiān)聽哪個RunLoop,這里監(jiān)聽的是主線程的RunLoop第二個參數
CFRunLoopObserverRef observer
監(jiān)聽者第三個參數
CFStringRef mode
要監(jiān)聽RunLoop在哪種運行模式下的狀態(tài)
創(chuàng)建了監(jiān)聽者并且給當前Runloop設置后,就可以正常的監(jiān)聽Runloop的各種狀態(tài)了流礁。為了我們優(yōu)化TableView的目的涕俗,我們需要做的是在監(jiān)聽的回調中執(zhí)行最耗性能的操作,即給cell中的三個 imageView 賦值大圖崇棠。
把這個功能包裝成一個單例工具類咽袜,所有耗性能的操作保存在一個數組(taskArray
)中,注意:要把這個數組理解成 隊列 去使用枕稀。然后監(jiān)聽Runloop的空閑狀態(tài)询刹,在Runloop空閑的時候去一件一件的做這些耗性能的操作。
上源碼:
//
// GCRunloopObserver.h
// RunloopOptimizeTableView
//
// Created by 崇 on 2018.
// Copyright ? 2018 崇. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface GCRunloopObserver : NSObject
+ (instancetype)runloopObserver;
- (void)addTask:(void(^)(void))task;
@end
//
// GCRunloopObserver.m
// RunloopOptimizeTableView
//
// Created by 崇 on 2018.
// Copyright ? 2018 崇. All rights reserved.
//
#import "GCRunloopObserver.h"
@interface GCRunloopObserver(){
NSTimer *timer;
}
@property (nonatomic, strong) NSMutableArray *taskArray;
@end
@implementation GCRunloopObserver
+ (instancetype)runloopObserver {
static dispatch_once_t once;
static GCRunloopObserver *observer;
dispatch_once(&once, ^{
observer = [[GCRunloopObserver alloc] init];
});
return observer;
}
- (instancetype)init
{
self = [super init];
if (self) {
timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(timerFiredMethod) userInfo:nil repeats:YES];
[self runloopBeforeWaiting];
}
return self;
}
- (void)addTask:(void(^)(void))task {
if (task) {
[self.taskArray addObject:task];
}
}
- (void)runloopBeforeWaiting {
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
if (self.taskArray.count == 0) {
return;
}
// 取出耗性能的任務
void(^task)(void) = self.taskArray.firstObject;
// 執(zhí)行任務
task();
// 第一個任務出隊列
[self.taskArray removeObjectAtIndex:0];
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);
}
- (void)timerFiredMethod {
}
- (NSMutableArray *)taskArray {
if (_taskArray == nil) {
_taskArray = [NSMutableArray array];
}
return _taskArray;
}
@end
工具類的思路
任務數組中保存的是用戶的耗性能操作萎坷,用Block傳遞過來凹联。工具類本身是一個單例,所以任務數組是唯一的哆档,所有操作都在保存在這個像 “隊列” 一樣的數組(taskArray
)中蔽挠,按照先進先出的原則,在Runloop空閑的時候逐個完成瓜浸。這樣這些耗性能的操作不會在Runloop需要完成其它操作的時候來搶占CPU資源澳淑,卡頓的情況就會明顯得到緩解。
另外插佛,監(jiān)聽Runloop選擇的模式(RunloopMode
) 也有很大關系杠巡。比如我們的APP需求是剛進入頁面時用戶的操作就要保持流暢,不能出現無法滑動的卡頓雇寇,所以我監(jiān)聽的 RunloopMode
是 kCFRunLoopDefaultMode
氢拥,這樣在用戶滑動的時候是不加載圖片的,所以用戶的滑動操作會很流暢锨侯。如果這里選擇 kCFRunLoopCommonModes
嫩海,那么在滑動期間仍然會加載圖片,還是會有一些卡頓的情況囚痴。
使用工具類
說完道理叁怪,我們來看看怎么使用吧!創(chuàng)建完這個工具類深滚,只要一步就可以實現優(yōu)化奕谭。把cell給三個 ImageView 賦值的操作提出去耳璧,放到Runloop空閑時再做,因為卡頓就是因為它展箱,所以接下來需要對cell的 - (void)setData:(NSArray *)dataArray
進行改造。先找到耗性能的操作是哪些蹬昌。
這三行是耗性能的元兇:
UIImageView *imgView = self.imgViewArray[i];
UIImage *img = [UIImage imageNamed:dataArray[i]];
imgView.image = img;
誰耗性能混驰,就把誰放到Block中:
__weak typeof(self) weakSelf = self;
[[GCRunloopObserver runloopObserver] addTask:^{
UIImageView *imgView = weakSelf.imgViewArray[i];
UIImage *img = [UIImage imageNamed:dataArray[i]];
imgView.image = img;
}];
所以cell的 - (void)setData:(NSArray *)dataArray
方法改造完是這樣的:
- (void)setData:(NSArray *)dataArray {
_dataArray = dataArray;
for (int i = 0; i < 3; i++) {
__weak typeof(self) weakSelf = self;
[[GCRunloopObserver runloopObserver] addTask:^{
UIImageView *imgView = weakSelf.imgViewArray[i];
UIImage *img = [UIImage imageNamed:dataArray[i]];
imgView.image = img;
}];
}
}
運行情況
總結
可以看到卡頓情況得到明顯緩解,一進入頁面的時候滑動不會卡頓皂贩,圖片加載中時滑動也不會卡頓栖榨,只有圖片的加載過程是緩慢的。但是如果同時兼顧滑動和加載圖片那就一定會卡頓明刷,所以看你的需求具體是什么樣的了婴栽。
最后要說,這種方式不僅可以用在優(yōu)化TableView中辈末,還可以應用到你所有出現卡頓的情況當中去愚争。把耗性能的操作放到Runloop隊列中去,等Runloop空閑時一件一件的做挤聘,就不會造成體驗不佳的情況轰枝。