Objective-C:RunLoop(应用)

Page content

对RunLoop的知行合一;--RunLoop

线程常驻

说明

线程的五大状态分别是:新建状态、就绪状态、运行状态、阻塞状态、死亡状态。

我们通常创建的线程即使通过全局变量持有,当任务执行完毕之后,尽管内存中还存在这个线程,但实际上这个线程已经处于死亡状态了,如果有高频操作,就需要重复频繁开启新的线程。

线程的开启和销毁是一个损耗CPU性能的操作,在特定的需求中,如音频录制,视频录制的合成,语音对讲等,频繁的开关线程对应用的性能损耗很大,这时,我们可以借用RunLoop实现一个常驻的后台服务线程,监听这些操作,有任务则唤醒执行,无任务则休眠节省资源。

AFN常驻线程的实现

AFN中AFURLConnectionOperation是基于NSURLConnection构建的,其希望能够在后台线程来接收Delegate的回调。为此AFN创建了一个线程,然后在里面开启了一个RunLoop,然后添加item:

~~~objective-c
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
    [[NSThread currentThread] setName:@"AFNetworking"];
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runLoop run];
}
}
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{ 
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}
~~~

这里的port仅仅是为了让RunLoop不退出而添加的item,如果需要使用这个port,调用者则要持有这个port,在外部线程通过port发送消息到这个Loop内部。

~~~objective-c
- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}
~~~

在需要这个线程执行任务的时候,通过[NSObject performSelector:onThread:..]将任务扔到这个线程的RunLoop中来执行。

接口

仿照AFN的服务线程,我们提供常驻线程的一个单例接口:

~~~objective-c
@interface SeanThread : NSThread
/**
 * videoRecord线程获取方法
 */
+ (instancetype)videoRecordThread;
@end
~~~

实现

在入口方法中开启RunLoop,添加Source避免RunLoop没有item退出:

~~~objective-c
@implementation SeanThread
/**
 * videoRecord线程获取方法
 */
+ (instancetype)videoRecordThread {
    static SeanThread * _instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[self alloc] initWithTarget:self selector:@selector(videoRecordThreadEntryPoint:) object:nil];
        [_instance setName:@"VideoRecordThread"];
        [_instance start];
    });
    return _instance;
}
/**
 * videoRecord线程入口方法
 */
+ (void)videoRecordThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        NSLog(@"子线程进入");
        NSLog(@"current thread : %@", [NSThread currentThread]);
        
        NSRunLoop * currentRunloop = [NSRunLoop currentRunLoop];
        [currentRunloop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];// runloop没有mode将会退出
        [currentRunloop run];
        NSLog(@"runloop run after");// runlopp开启后这里将永不执行
    }
}
- (void)dealloc {
    NSLog(@"%s", __func__);
}
@end
~~~

使用

使用这个常驻线程执行任务:

~~~objective-c
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    
    [self performSelector:@selector(doSomethingInBackgroundThread) onThread:[SeanThread videoRecordThread] withObject:nil waitUntilDone:false];
}

- (void)doSomethingInBackgroundThread {
    @autoreleasepool {
        NSLog(@"do something begin");
        NSLog(@"do something doing");
        NSLog(@"do something end");
    }
}
~~~

线程依赖

给两个线程添加依赖性有多种方式,使用RunLoop是其中的一种。

使用GCD方式

~~~objective-c
@interface ThreadViewController ()
@property (nonatomic, assign) BOOL runLoopThreadDidFinishFlag;
@property (nonatomic, strong) NSThread * myThread;
@end
  
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    /// 线程依赖
    [self runLoopAddDependance];
}
#pragma mark - 线程依赖
- (void)runLoopAddDependance {
    self.runLoopThreadDidFinishFlag = NO;
    NSLog(@"Start a New Run Loop Thread");
    NSThread *runLoopThread = [[NSThread alloc] initWithTarget:self selector:@selector(handleRunLoopThreadTask) object:nil];
    [runLoopThread start];
    
    NSLog(@"Exit handleRunLoopThreadButtonTouchUpInside");
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        if (!_runLoopThreadDidFinishFlag) {
            self.myThread = [NSThread currentThread];
            NSLog(@"%@", self.myThread);
            NSLog(@"Begin RunLoop");
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
            NSPort *myPort = [NSPort port];
            [runLoop addPort:myPort forMode:NSDefaultRunLoopMode];
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            NSLog(@"End RunLoop");
            [self.myThread cancel];
            self.myThread = nil;
        }
    });
}
- (void)handleRunLoopThreadTask {
    NSLog(@"Enter Run Loop Thread");
    for (NSInteger i = 0; i < 5; i ++) {
        NSLog(@"In Run Loop Thread, count = %ld", i);
        sleep(1);
    }
    _runLoopThreadDidFinishFlag = YES;
    NSLog(@"Exit Normal Thread");
    
    [self performSelector:@selector(tryOnMyThread) onThread:self.myThread withObject:nil waitUntilDone:NO];
    NSLog(@"Exit Run Loop Thread");
}
- (void)tryOnMyThread {
    NSLog(@"tryOnMyThread");
}
~~~

使用NSThread方式

~~~objective-c
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    _runLoopThreadDidFinishFlag = NO;
    NSThread *runLoopThread = [[NSThread alloc] initWithTarget:self selector:@selector(handleRunLoopThreadTask) object:nil];
    [runLoopThread start];
    
    self.myThread = [[NSThread alloc] initWithTarget:self selector:@selector(myThreadEntryPoint) object:nil];
    [self.myThread start];
}
- (void)myThreadEntryPoint {
    @autoreleasepool {
        if (!_runLoopThreadDidFinishFlag) {
            NSLog(@"Begin RunLoop");
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
            NSPort *myPort = [NSPort port];
            [runLoop addPort:myPort forMode:NSDefaultRunLoopMode];
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            NSLog(@"End RunLoop");
            [self.myThread cancel];
            self.myThread = nil;
        }
    }
}
- (void)handleRunLoopThreadTask {
    NSLog(@"Enter Run Loop Thread");
    for (NSInteger i = 0; i < 5; i ++) {
        NSLog(@"In Run Loop Thread, count = %ld", i);
        sleep(1);
    }
    _runLoopThreadDidFinishFlag = YES;
    NSLog(@"Exit Normal Thread");
    [self performSelector:@selector(tryOnMyThread) onThread:self.myThread withObject:nil waitUntilDone:NO];
}
- (void)tryOnMyThread {
    NSLog(@"tryOnMyThread");
}
~~~

流畅度优化

场景:UITableView,Cell数量1000,每个Cell含有3个imageView,每个imageView均本地加载高清大图(主线程中存在不可避免的耗时操作)。

分析:卡顿原因产生的原因,在于一个Runloop周期需要绘制显示到屏幕上的所有的点(或者有变化的点),此场景下为一个周期绘制多张高清图。

优化:

  1. 为imageView设置image,是在UITrackingRunLoopMode中进行的,在滑动的Mode下更新UI操作是第一个问题点,这里可以通过更改设置image的Mode避免与手势滑动抢时间:[imageView performSelector:@selector(setImage:) withObject:image afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
  2. 将每张高清图片的绘制(一个操作单元)派发给一个Runloop周期,每个Runloop周期绘制一张高清图片(执行一个操作单元)。由于Runloop循环非常迅速,所以图片的额加载速度不受影响。

普通加载

~~~objective-c
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    TableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"TableViewCell"];
    
    NSString *path1 = [[NSBundle mainBundle] pathForResource:@"spaceship" ofType:@"png"];
    UIImage *image1 = [UIImage imageWithContentsOfFile:path1];
    cell.image1 = image1;
    cell.title1 = [@(indexPath.row).description stringByAppendingString:@"First"];
    NSString *path2 = [[NSBundle mainBundle] pathForResource:@"spaceship" ofType:@"png"];
    UIImage *image2 = [UIImage imageWithContentsOfFile:path2];
    cell.image2 = image2;
    cell.title2 = [@(indexPath.row).description stringByAppendingString:@"Second"];
    NSString *path3 = [[NSBundle mainBundle] pathForResource:@"spaceship" ofType:@"png"];
    UIImage *image3 = [UIImage imageWithContentsOfFile:path3];
    cell.image3 = image3;
    cell.title3 = [@(indexPath.row).description stringByAppendingString:@"Third"];
    
    return cell;
}
~~~

使用RunLoop拆分任务

先看匿名类别:

~~~objective-c
/// 任务的Block定义
typedef BOOL(^TaskBlock)(void);
@interface ViewController ()
<UITableViewDelegate,
UITableViewDataSource>
@property (nonatomic, strong) UITableView * tableView;
/// 用于唤醒RunLoop的timer
@property (nonatomic, strong) NSTimer * timer;
/// 任务存储
@property (nonatomic, strong) NSMutableArray * tasks;
/// 最大任务数
@property (nonatomic, assign) NSInteger maxTaskCount;
@end
~~~

属性懒加载:

~~~objective-c
- (UITableView *)tableView {
    if (!_tableView) {
        _tableView = [UITableView new];
        _tableView.backgroundColor = [UIColor whiteColor];
        _tableView.frame = (CGRect){0, 0, KSWIDTH, KSHEIGHT};
        _tableView.delegate = self;
        _tableView.dataSource = self;
        [_tableView registerClass:[TableViewCell class] forCellReuseIdentifier:@"TableViewCell"];
        _tableView.rowHeight = 100.f;
        [self.view addSubview:_tableView];
    }
    return _tableView;
}
~~~

添加RunLoop观察者:

~~~objective-c
#pragma mark - RunLoopObserver
/**
 * NSTimer保证runloop高效监听
 */
- (void)keepRunloopAwake {
    
}
/**
 * 添加RunloopObserver
 */
- (void)addRunloopObserver {
    
    /// 获取当前Runloop
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    /// 定义一个上下文
    CFRunLoopObserverContext observerContext = {
        0,
        (__bridge void *)self,
        &CFRetain,
        &CFRelease,
        NULL
    };
    /// 创建一个观察者
    static CFRunLoopObserverRef observer;
    observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, true, NSIntegerMax - 999, &RunloopObserverCallback, &observerContext);
    /// 添加观察者到当前的Runloop
    CFRunLoopAddObserver(runloop, observer, kCFRunLoopDefaultMode);
    /// Release
    CFRelease(observer);
}
/**
 * RunloopObserver监听回调
 */
static void RunloopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    ViewController * vc = (__bridge ViewController *)info;
    
    /// 回调频繁调用,添加条件判断拦截不必要的执行,以提高性能
    if (vc.tasks.count <= 0) {
        return;
    }
    BOOL isCurrentIndexPath = false;
    while (!isCurrentIndexPath && vc.tasks.count > 0) {
        TaskBlock task = vc.tasks.firstObject;
        isCurrentIndexPath = task();
        [vc.tasks removeObjectAtIndex:0];
    }
}
/**
 * 添加要分发执行的任务到数组
 */
- (void)addTask:(TaskBlock)taskBlock {
    [self.tasks addObject:taskBlock];
    /// 移除不需要执行的任务(只绘制展示到屏幕上的最新的30张图片)
    if (self.tasks.count > self.maxTaskCount) {
        [self.tasks removeObjectAtIndex:0];
    }
}
~~~

TableView代理回调:

~~~objective-c
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 
    return 1000;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"TableViewCell"];
    cell.currentIndexPath = indexPath;
    [self addTask:^BOOL{
        if (![cell.currentIndexPath isEqual:indexPath]) {
            return false;
        }
        NSString *path1 = [[NSBundle mainBundle] pathForResource:@"spaceship" ofType:@"png"];
        UIImage *image1 = [UIImage imageWithContentsOfFile:path1];
        [cell performSelector:@selector(setImage1:) withObject:image1 afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
        //cell.image1 = image1;
        cell.title1 = [@(indexPath.row).description stringByAppendingString:@"First"];
        return true;
    }];
    [self addTask:^BOOL{
        if (![cell.currentIndexPath isEqual:indexPath]) {
            return false;
        }
        NSString *path2 = [[NSBundle mainBundle] pathForResource:@"spaceship" ofType:@"png"];
        UIImage *image2 = [UIImage imageWithContentsOfFile:path2];
        [cell performSelector:@selector(setImage2:) withObject:image2 afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
        //cell.image2 = image2;
        cell.title2 = [@(indexPath.row).description stringByAppendingString:@"Second"];
        return true;
    }];
    [self addTask:^BOOL{
        if (![cell.currentIndexPath isEqual:indexPath]) {
            return false;
        }
        NSString *path3 = [[NSBundle mainBundle] pathForResource:@"spaceship" ofType:@"png"];
        UIImage *image3 = [UIImage imageWithContentsOfFile:path3];
        [cell performSelector:@selector(setImage3:) withObject:image3 afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
        //cell.image3 = image3;
        cell.title3 = [@(indexPath.row).description stringByAppendingString:@"Third"];
        return true;
    }];
    
    return cell;
}
~~~

视图加载运行:

~~~objective-c
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(keepRunloopAwake) userInfo:nil repeats:true];
    self.maxTaskCount = 30;
    self.tasks = [NSMutableArray array];
    
    [self addRunloopObserver];
    [self.tableView reloadData];
}
~~~

Cell内部内存优化

~~~objective-c
- (void)prepareForReuse {
    [super prepareForReuse];
    
    self.imageView1.image = nil;
    self.imageView2.image = nil;
    self.imageView3.image = nil;
    self.image1 = nil;
    self.image2 = nil;
    self.image3 = nil;
    
    self.titleLabel1.text = nil;
    self.titleLabel2.text = nil;
    self.titleLabel3.text = nil;
    self.title1 = nil;
    self.title2 = nil;
    self.title3 = nil;
}
~~~

主线程卡顿监测

测试只能反馈一部分问题,一款优秀的应用是经得起在真实的用户环境中的考验的,一些对用户体验要求较为严苛的应用,当投向市场,要随时获得应用的使用情况并据此进行优化。

参照微信团队的卡顿检测方案,我们实现一下线上卡顿监测并报告给服务器的功能。

贴上主线程卡顿监测的类LagMonitor的实现。

接口

~~~objective-c
#import <Foundation/Foundation.h>
@interface LagMonitor : NSObject
+ (instancetype)sharedInstance;
/**
 * Start With Standard
 */
- (void)start;
/**
 * Start With Define Available
 */
- (void)startWithTimerInterval:(NSTimeInterval)timerTimeInterval TimeoutInterval:(NSTimeInterval)timeOutInterval;
/**
 * Stop
 */
- (void)stop;
@end
~~~

类别

~~~objective-c
#import "LagMonitor.h"
@interface LagMonitor ()
{
    /// Observer in Main RunLoop
    CFRunLoopObserverRef _observer;
    /// Timer for time judge in Monitor Thread
    CFRunLoopTimerRef _timer;
    /// Interval of timer
    NSTimeInterval _timerTimeInterval;
}
/// 监控线程
@property (nonatomic, strong) NSThread * monitorThread;
/// Task is executiving in Main RunLoop or not
@property (nonatomic, assign) BOOL executiving;
/// Task start date in Main RunLoop
@property (nonatomic, strong) NSDate * taskStartDate;
/// Interval which task time out in Main RunLoop
@property (nonatomic, assign) NSTimeInterval timeOutInterval;
@end
~~~

实现

~~~objective-c
@implementation LagMonitor
+ (instancetype)sharedInstance {
    static LagMonitor * _instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[self alloc] init];
        _instance.monitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(monitorThreadEntryPoint:) object:nil];
        [_instance.monitorThread setName:@"LagMonitorThread"];
        [_instance.monitorThread start];
    });
    return _instance;
}
+ (void)monitorThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        NSRunLoop * runloop = [NSRunLoop currentRunLoop];
        [runloop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
        [runloop run];
    }
}

#pragma mark - Public Functions
/**
 * Start With Standard
 */
- (void)start {
    [self startWithTimerInterval:1 TimeoutInterval:1.5];
}
/**
 * Start With Define Available
 */
- (void)startWithTimerInterval:(NSTimeInterval)timerTimeInterval TimeoutInterval:(NSTimeInterval)timeOutInterval {
    _timerTimeInterval = timerTimeInterval;
    _timeOutInterval = timeOutInterval;
    /// 创建Observer
    [self configObserver];
    /// 创建Timer
    [self performSelector:@selector(configTimer) onThread:self.monitorThread withObject:nil waitUntilDone:false modes:@[NSRunLoopCommonModes]];
}
/**
 * Stop
 */
- (void)stop {
    /// 移除Observer
    [self removeObserver];
    
    /// 移除Timer
    [self performSelector:@selector(removeTimer) onThread:self.monitorThread withObject:nil waitUntilDone:false modes:@[NSRunLoopCommonModes]];
}

#pragma mark - Observer Callback
/**
 * 创建Observer
 */
- (void)configObserver {
    if (_observer) {
        return;
    }
    CFRunLoopObserverContext context = {
        0,
        (__bridge void *)self,
        &CFRetain,
        &CFRelease,
        NULL
    };
    _observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                        kCFRunLoopAllActivities,
                                        true,
                                        0,
                                        &RunLoopObserverCallback,
                                        &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
}
/**
 * 移除Observer
 */
- (void)removeObserver {
    if (!_observer) {
        return;
    }
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
    CFRelease(_observer);
    _observer = NULL;
}
/**
 * Observer回调
 */
static void RunLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    LagMonitor * monitor = (__bridge LagMonitor *)info;
    switch (activity) {
        case kCFRunLoopEntry:
        case kCFRunLoopBeforeTimers:
        {
            break;
        }
        case kCFRunLoopBeforeSources:
        {
            monitor.executiving = true;
            monitor.taskStartDate = [NSDate date];
            break;
        }
        case kCFRunLoopBeforeWaiting:
        {
            monitor.executiving = false;
            break;
        }
        case kCFRunLoopAfterWaiting:
        case kCFRunLoopExit:
        default:
        {
            break;
        }
    }
}
#pragma mark - Timer Callback 
/**
 * 创建Timer
 */
- (void)configTimer {
    if (_timer) {
        return;
    }
    CFRunLoopTimerContext context =  {
        0,
        (__bridge void *)self,
        &CFRetain,
        &CFRelease,
        NULL
    };
    _timer = CFRunLoopTimerCreate(kCFAllocatorDefault,
                                  0.1,
                                  _timerTimeInterval,
                                  0,
                                  0,
                                  &MonitorTimerCallback,
                                  &context);
    
    CFRunLoopAddTimer(CFRunLoopGetCurrent(), _timer, kCFRunLoopCommonModes);
}
/**
 * 移除Timer
 */
- (void)removeTimer {
    if (!_timer) {
        return;
    }
    CFRunLoopRemoveTimer(CFRunLoopGetCurrent(), _timer, kCFRunLoopCommonModes);
    CFRelease(_timer);
    _timer = NULL;
}
/**
 * Timer回调
 */
static void MonitorTimerCallback(CFRunLoopTimerRef timer, void *info) {
    LagMonitor * monitor = (__bridge LagMonitor *)info;
    if (!monitor.executiving) {
        return;
    }
    /// MainRunLoop正在执行任务,且本次loop此时尚未执行完
    NSTimeInterval executeTime = [[NSDate date] timeIntervalSinceDate:monitor.taskStartDate];
    /// 主线程一次RunLoop延迟超过阈值,获取当前调用栈,上传服务器
    if (executeTime >= monitor.timeOutInterval) {
        NSLog(@"主线程本次RunLoop卡顿时间:%.2f", executeTime);
    }
}
#pragma mark - StackInfo Handle
@end
~~~

使用

~~~objective-c
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    ViewController * rootVC = [ViewController new];
    UINavigationController * navVC = [[UINavigationController alloc] initWithRootViewController:rootVC];
    self.window.rootViewController = navVC;
    [self.window makeKeyAndVisible];
    /// LagMonitor
    [[LagMonitor sharedInstance] startWithTimerInterval:1 TimeoutInterval:1];
    /// CrashHandler
    [CrashHandler sharedInstance];
    return YES;
}
~~~

让闪退的应用起死回生并获取闪退信息

既然要处理闪退信息,那肯定需要一个全局的类,我们定为CrashHandler。

接口

~~~objective-c
#import <Foundation/Foundation.h>
@interface CrashHandler : NSObject
{
    BOOL ignore;
}
+ (instancetype)sharedInstance;
@end
~~~

类内部常变量定义

~~~objective-c
#import "CrashHandler.h"
#import <UIKit/UIKit.h>
#include <libkern/OSAtomic.h>
#include <execinfo.h>
NSString * const kSignalExceptionName = @"kSignalExceptionName";
NSString * const kSignalKey = @"kSignalKey";
NSString * const kCaughtExceptionStackInfoKey = @"kCaughtExceptionStackInfoKey";
void HandleException(NSException *exception);
void SignalHandler(int signal);
static NSString * _exceptionInfo;
static NSString * _signalInfo;
~~~

实现

~~~objective-c
@implementation CrashHandler
static CrashHandler *instance = nil;
+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[[self class] alloc] init];
    });
    return instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [super allocWithZone:zone];
    });
    return instance;
}
- (instancetype)init
{
    self = [super init];
    if (self) {
        [self setCatchExceptionHandler];
    }
    return self;
}
- (void)setCatchExceptionHandler
{
    // 1.捕获一些异常导致的崩溃
    NSSetUncaughtExceptionHandler(&HandleException);
    
    // 2.捕获非异常情况,通过signal传递出来的崩溃
    signal(SIGABRT, SignalHandler);
    signal(SIGILL, SignalHandler);
    signal(SIGSEGV, SignalHandler);
    signal(SIGFPE, SignalHandler);
    signal(SIGBUS, SignalHandler);
    signal(SIGPIPE, SignalHandler);
}
+ (NSArray *)backtrace
{
    void* callstack[128];
    int frames = backtrace(callstack, 128);
    char **strs = backtrace_symbols(callstack, frames);
    
    NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
    for (int i = 0; i < frames; i++) {
        [backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
    }
    free(strs);
    
    return backtrace;
}
- (void)handleException:(NSException *)exception
{
    /// info
    NSString *message = [NSString stringWithFormat:@"异常捕获崩溃,堆栈信息:\n%@\n%@",
                         [exception reason],
                         [[exception userInfo] objectForKey:kCaughtExceptionStackInfoKey]];
    NSLog(@"%@",message);
    _exceptionInfo = message;
    
    /// alert
    UIAlertController * alertVC = [UIAlertController alertControllerWithTitle:@"反馈提示" message:@"应用程序崩溃了,为了准确定位闪退原因,我们需要您将当前程序信息反馈给我们,是否反馈?" preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction * sureAction = [UIAlertAction actionWithTitle:@"反馈" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        NSLog(@"问题报告:\n %@\n%@", _exceptionInfo, _signalInfo);
    }];
    UIAlertAction * cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        ignore = YES;
    }];
    [alertVC addAction:sureAction];
    [alertVC addAction:cancelAction];
    [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alertVC animated:true completion:nil];
    
    /// runloop
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
    
    while (!ignore) {
        for (NSString *mode in (__bridge NSArray *)allModes) {
            CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
        }
    }
    
    CFRelease(allModes);
    
    NSSetUncaughtExceptionHandler(NULL);
    signal(SIGABRT, SIG_DFL);
    signal(SIGILL, SIG_DFL);
    signal(SIGSEGV, SIG_DFL);
    signal(SIGFPE, SIG_DFL);
    signal(SIGBUS, SIG_DFL);
    signal(SIGPIPE, SIG_DFL);
    
    if ([[exception name] isEqual:kSignalExceptionName]) {
        kill(getpid(), [[[exception userInfo] objectForKey:kSignalKey] intValue]);
    } else {
        [exception raise];
    }
}
@end
  
void HandleException(NSException *exception)
{
    // 获取异常的堆栈信息
    NSArray *callStack = [exception callStackSymbols];
    NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
    [userInfo setObject:callStack forKey:kCaughtExceptionStackInfoKey];
    
    CrashHandler *crashObject = [CrashHandler sharedInstance];
    NSException *customException = [NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:userInfo];
    [crashObject performSelectorOnMainThread:@selector(handleException:) withObject:customException waitUntilDone:YES];
}
void SignalHandler(int signal)
{
    // 这种情况的崩溃信息,就另某他法来捕获吧
    NSArray *callStack = [CrashHandler backtrace];
    NSLog(@"信号捕获崩溃,堆栈信息:%@",callStack);
    _signalInfo = [NSString stringWithFormat:@"信号捕获崩溃,堆栈信息:%@",callStack];
    
    CrashHandler *crashObject = [CrashHandler sharedInstance];
    NSException *customException = [NSException exceptionWithName:kSignalExceptionName
                                                           reason:[NSString stringWithFormat:NSLocalizedString(@"Signal %d was raised.", nil),signal]
                                                         userInfo:@{kSignalKey:[NSNumber numberWithInt:signal]}];
    
    [crashObject performSelectorOnMainThread:@selector(handleException:) withObject:customException waitUntilDone:YES];
}
~~~

使用

~~~objective-c
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    ViewController * rootVC = [ViewController new];
    UINavigationController * navVC = [[UINavigationController alloc] initWithRootViewController:rootVC];
    self.window.rootViewController = navVC;
    [self.window makeKeyAndVisible];
    /// LagMonitor
    [[LagMonitor sharedInstance] startWithTimerInterval:1 TimeoutInterval:1];
    /// CrashHandler
    [CrashHandler sharedInstance];
    return YES;
}
~~~

找个控制器测试一下:

~~~objective-c
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSArray * array = [NSArray array];
    NSLog(@"%@", array[0]);
}
~~~

可以看到预期的结果。

UITableView滑动性能影响因素

人眼能识别的帧率是60左右,这也是为什么电脑屏幕的最佳刷新帧率是60HZ。

屏幕1秒钟会刷新60次(重新渲染60次),每次刷新的处理时间就是1/60(秒)。

所以,所有会导致计算、渲染耗时的操作都会影响UITableVIew的流畅。

在主线程中执行耗时操作

这种情况不做赘述。

动态计算Cell高度运算量过大

在iOS7之前,每一个Cell的高度,只会计算一次,后面再次滑到这个Cell这里,都会读取缓存的高度,也即高度计算的代理方法不会再执行。

但是到了iOS8,不会再缓存Cell的高度了,也就是说每次滑到某个Cell,代理方法都会执行一次,重新计算这个Cell的高度(iOS 9以后没测试过)。

所以,如果计算Cell高度的这个过程过于复杂,或者某个计算使用的算法耗时很长,可能会导致计算时间大于1/60,那么必然导致界面的卡顿,或不流畅。

一种解决方法是:在Cell中定义一个public方法,用来计算Cell高度,然后计算完高度后,将高度存储在Cell对应的Model中(Model里定义一个属性来存高度),然后在渲染Cell时,我们依然需要动态计算各个子视图的高度。

更优的方案是:再定义一个ModelFrame对象,在子线程请求服务器接口返回后,转换为对象的同时,也把各个子视图的frame计算好,存在ModelFrame中,ModelFrame 和 Model 合并成一个Model存储到数组中。这样在为Cell各个子控件赋值时,仅仅是取值、赋值,在计算Cell高度时,也仅仅是加法运算。

界面汇总透明背景色视图过多

屏幕上显示的内容,是通过一个个像素点呈现出来的,每一个像素点都是通过三原色的组合才呈现出不同的颜色,最终才是我们看到的内容。

在 iPhone5 的液晶显示器上有1,136×640=727,040个像素,因此有2,181,120个颜色单元。在15寸视网膜屏的 MacBook Pro 上,这一数字达到15.5百万以上。所有的图形堆栈一起工作以确保每次正确的显示。当你滚动整个屏幕的时候,数以百万计的颜色单元必须以每秒60次的速度刷新,这是一个很大的工作量。

每一个像素点的颜色计算是这样的:

R = S + D (1 - Sa)

结果的颜色 是子视图这个像素点的颜色 + 父视图这个像素点的颜色 (1 - 子视图的透明度)

当然,如果有两个兄弟视图叠加,那么上面的中文解释可能并不贴切,只是为了更容易理解。

如果两个兄弟视图重合,计算的是重合区域的像素点:结果的颜色 是 上面的视图这个像素点的颜色 + 下面这个视图该像素点的颜色 * (1 - 上面视图的透明度)

只有当透明度为1时,上面的公式变为R = S,就简单的多了。否则的话,就非常复杂了。

每一个像素点是由三原色组成,例如父视图的颜色和透明度是(Pr,Pg,Pb,Pa),子视图的颜色颜色和透明度是(Sr,Sg,Sb,Sa),那么我们计算这个重合区域某像素点的颜色,需要先分别计算出红、绿、蓝。

Rr = Sr + Pr (1 - Sa),

Rg = Sg + Pg (1 - Sa),

Rb = Sb + Pb * (1 - Sa)。

如果父视图的透明度,即Pa = 1,那么这个像素的颜色就是(Rr,Rg,Rb)。

但是,如果父视图的透明Pa 不等 1,那么我们需要将这个结果颜色当做一个整体作为子视图的颜色,再去与父视图组合计算颜色,如此递推。

所以设置不透明时,可以为GPU节省大量的工作,减少大量的消耗。

主线程RunLoop切换到UITrackingRunLoopMode时,视图有过多的修改

这也就是上面介绍的RunLoop的使用,避免在主线程RunLoop切换到UITrackingRunLoopMode时,修改视图。

结语

纯理论往往是枯燥无味的,较为深入的更是晦涩难懂,最佳的学习方式就是所见即所得,理论指导实践,实践检验理论,逆向学习,逆向分析往往会更加轻松。 RunLoop是iOS/MacOS中基础的知识点,但却是整个系统中较为核心的部分,涉及到操作系统、端口、内核、进程(线程)通信,比较深入且复杂。

在实际开发中很少用到这些底层的知识点,我们只需要对RunLoop的理论进行一些掌握,然后可以应用RunLoop的知识进行分析、优化、实现,就足够了。