阅读 552

iOS开发小记-RunLoop篇

趁热打个铁,迫不及待想记录新东西了

什么是RunLoop


一般来讲,一个线程只能执行一次任务,执行完线程就会退出。如果我们需要这样一个机制,让线程能随时处理事件而不退出,通常的逻辑代码如下:

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}
复制代码

这种模型通常叫做Event Loop。这个模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免占用资源,在有消息到来时立即被唤醒。 所以,RunLoop实际是一个对象,该对象管理了其需要处理的事件和消息,并提供了入口函数来处理上面的Event Loop的逻辑。线程执行这个函数后,就会一直处在函数内部“接收消息->等待->处理”的循环中,直到接收到退出消息(如quit),函数结束。

OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。 CFRunLoopRef 是在 Core Foundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。它是基于pthread的。 NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

  • RunLoop与线程的关系

苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
 
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接从 Dictionary 里获取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到时,创建一个
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}
复制代码

从上述代码可以看出,线程和RunLoop是一一对应的,其关系保存在一个全局的Dictionary里。线程创建时是没有RunLoop的,只有第一次主动获取的时候才会创建,否则会一直没有,直到线程结束时销毁。你只能在一个线程内部获取其RunLoop对象(主线程除外)。

  • 主线程的RunLoop

在创建一个iOS程序后,会自动生成main.m文件,如下

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
复制代码

其中UIApplicationMain函数内部为主线程开启了RunLoop,逻辑代码如Event Loop模型所示。 下图为苹果官方给出的RunLoop模型图。

从上图中可以看出,RunLoop就是线程中的一个循环,RunLoop在循环中会不断检测,通过Input sources(输入源)和Timer sources(定时源)两种来源等待接受事件;然后对接受到的事件通知线程进行处理,并在没有事件的时候进行休息。

RunLoop相关类


Core Foundation框架下有关于RunLoop的5个类,如下:

  1. CFRunLoopRef:代表RunLoop的对象。
  2. CFRunLoopModeRef:RunLoop的运行模式。
  3. CFRunLoopSourceRef:就是RunLoop模型图中提到的输入源/事件源。
  4. CFRunLoopTimerRef:就是RunLoop模型图中提到的定时源。
  5. CFRunLoopObserverRef:观察者,能够监听RunLoop的状态改变。

他们的关系如下图:

一个RunLoop对象(CFRunLoopRef)包含若干个运行模式(CFRunLoopModeRef)。每个运行模式下又包含若干个输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef),他们有如下特点: 1.每次RunLoop启动时,只能选择一个运行模式启动,这个运行模式被称为CurrentMode。 2.如果要切换运行模式,只能退出RunLoop,并重新指定一个运行模式启动。 3.这样做是为了使不同组的输入源、定时源、观察者互不影响。

  • CFRunLoopRef

我们可以通过如下API来获取Core Fundation中的CFRunLoopRef。

    //获取主线程的RunLoop
    CFRunLoopRef mainRunLoop = CFRunLoopGetMain();
    //获取当前线程的RunLoop
    CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
复制代码
  • CFRunLoopModeRef

系统定义了多种运行模式:

  1. kCFRunLoopDefaultMode:App的默认运行模式,通常主线程是在这个运行模式下运行。
  2. UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)。
  3. UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用。
  4. GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到。
  5. kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式(后边会用到)。 其中kCFRunLoopDefaultMode、UITrackingRunLoopMode、kCFRunLoopCommonModes是我们开发中需要用到的模式。
  • CFRunLoopTimerRef
- (void)viewDidLoad {
    [super viewDidLoad];
    
    //将定时器加入到默认运行模式中(一旦用户交互就不会响应)
    NSTimer *timer1 = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runInDefaultMode) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer1 forMode:NSDefaultRunLoopMode];
    
    //将定时器加入到交互运行模式中(一旦停止交互就不会响应)
    NSTimer *timer2 = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runInTrackingMode) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer2 forMode:UITrackingRunLoopMode];
    
    //将定时器加入到伪模式中(无论是否交互都可以响应)
    NSTimer *timer3 = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runInCommonMode) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer3 forMode:NSRunLoopCommonModes];
}

- (void)runInDefaultMode {
    NSLog(@"我只有在默认模式下运行!");
}

- (void)runInTrackingMode {
    NSLog(@"我只有在交互模式下运行!");
}

- (void)runInCommonMode {
    NSLog(@"我在默认模式和交互模式下都能运行!");
}
复制代码

我们观察到,没有做操作时timer1能正常运行,而timer2无响应,用户操作后timer1停止运行,而timer2正常运行,与此同时timer3始终都能运行,这是为什么呢?原因如下:

  1. 我们不做任何操作时,RunLoop处于NSDefaultRunLoopMode模式下,所以timer1此时能稳定2秒运行。
  2. 一旦我们进行了操作产生了交互,RunLoop立即结束NSDefaultRunLoopMode模式,并切换到UITrackingRunLoopMode工作,所以timer1不能继续工作,转而该模式下的timer2开始工作。
  3. 由于NSRunLoopCommonModes不是一个真正的模式,并非需要中止其他模式再切换,只是使得可以在标记了Common Modes的模式下运行,也就是NSDefaultRunLoopModeNSRunLoopCommonModes,所以timer3能一直工作。

这里我们可以看下CFRunLoopMode 和 CFRunLoop 的大致结构:

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};
 
struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};
复制代码

一个Mode可以将自己标记为“Common”属性(将自己的ModelName添加到RunLoop中的_commonModes中)。每当RunLoop发生变化时,RunLoop会将_commonModeItems中的Source/Observer/Timer同步到所有标记了“Common”的Mode中。

另外,说到NSTimer,我们平时使用的以下方法,是自动添加到了RunLoop对象的NSDefaultRunLoopMode模式下,所以一旦交互是无法响应的。

[NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(runInDefaultMode) userInfo:nil repeats:YES];
复制代码
  • CFRunLoopSourceRef CFRunLoopSourceRef分为两种:
  1. Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  2. Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。

例如我们点击一个按钮,拦截它的响应事件的函数调用栈,可以看到

  1. 首先程序启动,调用18的main函数,main函数调用17行UIApplicationMain函数,然后一直往上调用函数,最终调用到0行的响应事件。
  2. 同时我们可以看到13行中有Sources0,也就是说我们点击事件是属于Sources0函数的,点击事件就是在Sources0中处理的。
  3. 而至于Sources1,则是用来接收、分发系统事件,然后再分发到Sources0中处理的。
  • CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,用来监听RunLoop状态的改变,可以监听的状态有以下几种:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),               // 即将进入Loop:1
    kCFRunLoopBeforeTimers = (1UL << 1),        // 即将处理Timer:2    
    kCFRunLoopBeforeSources = (1UL << 2),       // 即将处理Source:4
    kCFRunLoopBeforeWaiting = (1UL << 5),       // 即将进入休眠:32
    kCFRunLoopAfterWaiting = (1UL << 6),        // 即将从休眠中唤醒:64
    kCFRunLoopExit = (1UL << 7),                // 即将从Loop中退出:128
    kCFRunLoopAllActivities = 0x0FFFFFFFU       // 监听全部状态改变  
};
复制代码

我们可以通过如下代码来监听RunLoop状态的改变

    //创建监听者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
         NSLog(@"监听到RunLoop发生改变---%zd", activity);
    });
    //添加到当前的RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode);
    //添加完后释放
    CFRelease(observer);
复制代码

这里监听了所有的状态,打印日志可以看到RunLoop的状态不断的改变,最终会变成32,也就是马上会进入休眠状态。

注:上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会重复生效的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

RunLoop原理


根据苹果在文档里的说明,RunLoop 内部的逻辑大致如下:

在每次运行开启RunLoop时,所在线程的RunLoop会自动处理之前未处理的事件,并且通知相关观察者。

  1. 通知观察者:即将进入RunLoop。
  2. 通知观察者:即将触发Timer回调。
  3. 通知观察者:即将触发Source0回调。
  4. 触发Source0(非port)回调。
  5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转8去处理消息。
  6. 通知观察者:即将进入休眠。如果一个事件到达了基于port的源、定时器启动、自身超时或者被手动显示都会讲RunLoop从休眠中唤醒。
  7. 通知观察者:即将被唤醒。
  8. 处理事件:Timer时间到了、dispatch到主线程的block、Source1发出事件了。
  9. 通知观察者:即将结束RunLoop。

注:RunLoop 的核心就是一个 mach_msg() ,RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。

  • 函数调用栈
{
    /// 1. 通知Observers,即将进入RunLoop
    /// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {
 
        /// 2. 通知 Observers: 即将触发 Timer 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        /// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 4. 触发 Source0 (非基于port的) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 6. 通知Observers,即将进入休眠
        /// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
 
        /// 7. sleep to wait msg.
        mach_msg() -> mach_msg_trap();
        
 
        /// 8. 通知Observers,线程被唤醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
 
        /// 9. 如果是被Timer唤醒的,回调Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
 
        /// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
 
        /// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
 
 
    } while (...);
 
    /// 10. 通知Observers,即将退出RunLoop
    /// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
复制代码

系统中的应用


  • AutoreleasePool

AutoreleasePool用于在代码块结束时释放所有在代码块中创建的对象,最重要的使用场景就是临时创建了大量的对象,例如在循环中创建对象,可以在循环体内使用AutoreleasePool,及时清理内存。

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池只发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显式创建 Pool 了。

  • 事件响应

苹果注册了一个Source1(基于mach port)来接收系统事件,如果发生硬件事件(触摸/锁屏/摇晃等),Source1会触发回调__IOHIDEventSystemClientQueueCallback() ,函数内然后触发Source0,Source0再通过_UIApplicationHandleEventQueue() 分发到应用内部。 _UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。 注:网上对于点击事件是Source0还是Source1触发有争议,在 __IOHIDEventSystemClientQueueCallback 处下一个 Symbolic Breakpoint可以看到,确实是如上述逻辑。

  • 手势识别

当上面的_UIApplicationHandleEventQueue() 接收到一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。 苹果注册了一个Observer来监听BeforeWaiting(即将进入休眠),其回调函数内部会获取所有刚才标记了未处理的手势,并触发它们的回调。 当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

  • 界面更新

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。 苹果注册了一个Observer来监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,并在回调函数里遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

  • 定时器

NSTimer实际也就相当于CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。 如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。 CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。

  • PerformSelector

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。 当调用 performSelector:onThread: 时,实际上其会创建一个Source0加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。()

  • 关于GCD

RunLoop底层会用到GCD的东西,GCD的实现也用到了RunLoop,比如dispatch_async()函数。 当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

实际应用


  • UITableView内容延迟加载

例如我们需要在cell上展示分时图,那么在滚动的时候如果有一堆的分时图需要重复的清空再计算绘制,就有可能造成卡顿。 首先cell复用分时图需要使用到两个方法clearTimeLinerefreshTimeLine,我们可以利用PerformSelector调用refreshTimeLine将其放在主线程的NSDefaultRunLoopMode下,这样避免滚动时还会触发绘图操作,减少计算和绘制,提高性能,同时也减少了内存占用。

  • 后台常驻线程

如果在实际开发中有大量的耗时操作需要在后台完成,频繁的新建子线程并不是好的方案,我们可以选择让这条线程常驻内存。

- (void)viewDidLoad {
    [super viewDidLoad];

    //强引用子线程,初始化该线程并启动
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
    
    //通过performSelector来在子线程中处理耗时操作,避免重复创建
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void)run {
    //开启当前线程的RunLoop,此处添加port是避免RunLoop退出
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
}
复制代码
  • 监测卡顿(后续详细补一篇)

由于我们绝大多数操作都是基于非port通信的,也就是source0,所以我们可以通过使用子线程来检测RunLoop中kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting两个状态之间的时间来判断这一轮操作是否卡顿,并把当前线程的堆栈信息存储到文件中,在某个合适的时机上传到服务器。

大致步骤如下:

  1. 创建一个子线程,打开其RunLoop并注册一个定时器用来监听主线程RunLoop的状态变化。
  2. 在主线程的RunLoop中注册一个观察者,然后监听kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting两个状态。
  3. 在观察者回调方法中,在kCFRunLoopBeforeSources记录更新时间,并且记录状态为NO,用于定时器区分状态;在kCFRunLoopBeforeWaiting时将状态置为YES。
  4. 在定时器的执行函数里,判断如果当前状态为NO,那么表示主线程的RunLoop还在运行,计算当前时间与记录的更新时间的差值,如果大于阈值,那么将堆栈信息保存在文件中,择时上传。

优化点

  1. 卡顿时间:按照微信团队的标准,这个卡顿时间为2秒。
  2. 函数调用堆栈:可以使用三方开源库获取。
  3. 写入策略:首先与内存中上次的卡顿函数调用堆栈作比较,如果不同,才需要写入文件。
  4. 上传策略:抽样用户上传,上传前20个堆栈文件。
  5. 本地保存策略:仅保存7天。

相关代码

#import "ViewController.h"

static CGFloat lagTimeInterval = 0.5;

@interface ViewController ()

//监听子线程
@property (nonatomic, strong) NSThread *monitorThread;
//是否进入休眠
@property (nonatomic, assign) BOOL isBeforeWaiting;
//即将处理source0的时间
@property (nonatomic, strong) NSDate *beforeSource0Time;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //创建监听子线程,打开其RunLoop
    _monitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(openRunLoop) object:nil];
    [_monitorThread start];
    //添加定时器
    [self performSelector:@selector(startMonitorTimer) onThread:_monitorThread withObject:nil waitUntilDone:NO];
    
    //主线程RunLoop添加观察者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopBeforeSources:
            {
                _beforeSource0Time = [NSDate date];
                _isBeforeWaiting = NO;
            }
                break;
            case kCFRunLoopBeforeWaiting:
            {
                _isBeforeWaiting = YES;
            }
                break;
                
            default:
                break;
        }
    });
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    CFRelease(observer);
}

//打开子线程的RunLoop对象
- (void)openRunLoop {
    @autoreleasepool {
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSPort port] forMode:NSRunLoopCommonModes];
        [runLoop run];
    }
}

//添加定时器到子线程的RunLoop中
- (void)startMonitorTimer {
    NSTimer *timer = [NSTimer timerWithTimeInterval:0.5*lagTimeInterval repeats:YES block:^(NSTimer * _Nonnull timer) {
        //如果_isBeforeWaiting状态为YES,表示主线程RunLoop即将进入休眠
        if(!_isBeforeWaiting) {
            //获取当前时间与记录时间的差值
            NSTimeInterval timeInterval = [[NSDate date] timeIntervalSinceDate:_beforeSource0Time];
            //如果大于卡顿时间,则打印出来
            if(timeInterval >= lagTimeInterval) {
                NSLog(@"##############卡了");
                [self logStack];
            } else {
                NSLog(@"##############没卡");
            }
        }
    }];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

- (void)logStack {
    NSLog(@"%@", [NSThread callStackSymbols]);
}


@end
复制代码
关注下面的标签,发现更多相似文章
评论