1. 程式人生 > >iOS學習——RUNLOOP、NSTimer

iOS學習——RUNLOOP、NSTimer

object ces 設定 ear 並且 點擊事件 string 情況下 發現

  每一個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