iOS學習——RUNLOOP、NSTimer
每一個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學習——RUNLOOP、NSTimer