Runloop与线程

RunLoop笔记

runloop是用来管理 事件/消息,在线程没有处理消息时,休眠避免资源占用,有消息到来时立刻被唤醒

runloop实际上就是一个对象,这个对象管理了其要处理的事件和消息。并提供了一个入口函数来

  • 主线程的RunLoop在应用启动的时候就会自动创建
  • 其他线程则需要在该线程下自己启动
  • 不能自己创建RunLoop
  • RunLoop并不是线程安全的,所以需要避免在其他线程上调用当前线程的RunLoop
  • RunLoop负责管理autorelease pools 负责处理消息事件,即输入源事件和计时器事件

子线程的runloop默认是不开启循环,比如子线程的的NSTimer会失效(可以手动开启RunLoop循环)

可以将timer加在子线程中并让runloop循环(但runloop一直在循环),可以自己写个循环来控制runloop的停止,主线程被杀死后,子线程还可以继续运行

runloop用于渲染UI,循环非常非常快

  • kCFRunLoopEntry -- 进入runloop循环
  • kCFRunLoopBeforeTimers -- 处理定时调用前回调
  • kCFRunLoopBeforeSources -- 处理input sources的事件
  • kCFRunLoopBeforeWaiting -- runloop睡眠前调用
  • kCFRunLoopAfterWaiting -- runloop唤醒后调用
  • kCFRunLoopExit -- 退出runloop

程序启动过程

从程序启动开始到view显示: start -> (加载framework,动态静态链接库,启动图片,Info.plist等) -> main函数 -> UIApplicationMain函数:

  • 初始化UIApplication单例对象
  • 初始化AppDelegate对象,并设为UIApplication对象的代理
  • 检查Info.plist设置的xib文件是否有效,如果有则解冻Nib文件并设置outlets,创建显示key window、rootViewController、与rootViewController关联的根view(没有关联则看rootViewController同名的xib),否则launch之后由程序员手动加载。
  • 建立一个主事件循环,其中包含UIApplication的Runloop来开始处理事件。

UIApplication:

  1. 通过window管理视图;
  2. 发送Runloop封装好的control消息给target;
  3. 处理URL,应用图标警告,联网状态,状态栏,远程事件等。

AppDelegate: 管理UIApplication生命周期和应用的五种状态(notRunning/inactive/active/background/suspend)。

Key Window:

  1. 显示view;
  2. 管理rootViewcontroller生命周期;
  3. 发送UIApplication传来的事件消息给view。

rootViewController:

  1. 管理view(view生命周期;view的数据源/代理;view与superView之间事件响应nextResponder的“备胎”);
  2. 界面跳转与传值;
  3. 状态栏,屏幕旋转。

view:

  1. 通过作为CALayer的代理,管理layer的渲染(顺序大概是先更新约束,再layout再display)和动画(默认layer的属性可动画,view默认禁止,在UIView的block分类方法里才打开动画)。layer是RGBA纹理,通过和mask位图(含alpha属性)关联将合成后的layer纹理填充在像素点内,GPU每1/60秒将计算出的纹理display在像素点中。
  2. 布局子控件(屏幕旋转或者子视图布局变动时,view会重新布局)。
  3. 事件响应:event和guesture。控制器生命周期

如何将线程置于休眠状态,以避免空闲时浪费 CPU 资源

以避免浪费 CPU 资源,通常需要使用运行循环(RunLoop)或 GCD(Grand Central Dispatch)等机制。这样可以让线程在没有任务时进入休眠状态,当有任务需要处理时再唤醒线程。以下是一些常见的方法:

  1. 使用 RunLoop:

使用运行循环是在 iOS/macOS 开发中控制线程休眠和唤醒的一种常见方法。RunLoop允许线程在没有任务时进入休眠状态,当有任务需要处理时自动唤醒。

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  1. 使用 GCD 队列:

GCD 提供了异步任务的调度机制,你可以创建一个串行队列,将任务提交到队列中,然后使用 dispatch_async 或 dispatch_sync 函数来执行任务。当队列为空时,线程会休眠,直到有任务到达队列。

dispatch_queue_t myQueue = dispatch_queue_create("com.example.myqueue", NULL);
dispatch_async(myQueue, ^{
    // 执行任务
});

runloop退出条件

  • app退出;线程关闭;设置最大时间到期;modeItem为空;
  • 同一时间一个runloop只能在一个mode,切换mode只能退出runloop,再重进指定mode(隔离modeItems使之互不干扰);
  • 一个item可以加到不同mode;一个mode被标记到commonModes里(这样runloop不用切换mode)

runloop如何优化

优化运行循环(RunLoop)的主要目标是减少资源消耗、提高性能和响应性。下面是一些可以优化RunLoop的方法:

  • 避免空闲循环: 不要让RunLoop进入无限循环,尤其是在主线程上。空闲循环会浪费CPU资源。可以使用合适的运行模式,让RunLoop在没有任务时休眠。

  • 合理使用运行模式: 理解RunLoop的运行模式,根据需要选择合适的模式。使用合适的模式可以确保只在需要的时候唤醒RunLoop。

  • 限制Observer的使用: 观察者(Observer)是用于监听RunLoop事件的,但过多的观察者可能会导致性能下降。只使用必要的观察者,或者在不需要的时候暂时禁用观察者

  • 处理输入源: 如果你的应用依赖于输入源(如点击、触摸、网络数据等),及时处理这些输入源以减少事件积压。

  • 将任务移到后台线程: 长时间运行的任务应该在后台线程中执行,以免阻塞主线程的RunLoop。使用GCD或操作队列来管理后台任务。

  • 避免UI更新: 避免在常驻RunLoop的线程上执行与UI更新相关的操作,这可能导致性能下降和界面不响应。UI更新应该在主线程中执行。

  • 合理使用定时器: 使用定时器时要小心,不要创建过多的定时器。确保及时销毁不再需要的定时器。

  • 使用RunLoop源代码优化工具: Xcode提供了工具来分析和优化RunLoop的性能。你可以使用"Instruments"来检测和解决性能问题。

  • 避免长时间运行的同步任务: 避免在主线程上运行长时间运行的同步任务,这会导致RunLoop阻塞。如果需要同步任务,可以将其移到后台线程。

  • 优化事件处理: 将事件处理逻辑尽量简化,减少不必要的计算和内存分配。确保事件处理尽可能快速完成。

runloop检测卡顿

卡顿的原因

卡顿通常是由于主线程长时间被阻塞,无法响应用户输入或渲染界面而导致的现象。卡顿的原理涉及到主线程的运行机制和事件处理,主要原因包括:

  • 死锁:主线程拿到锁 A,需要获得锁 B,而同时某个子线程拿了锁 B,需要锁 A,这样相互等待就死锁了。
  • 抢锁:主线程需要访问 DB,而此时某个子线程往 DB 插入大量数据。通常抢锁的体验是偶尔卡一阵子,过会就恢复了。
  • 主线程大量 IO:主线程为了方便直接写入大量数据,会导致界面卡顿。
  • 主线程大量计算:算法不合理,导致主线程某个函数占用大量 CPU。
  • 大量的 UI 绘制:复杂的 UI、图文混排等,带来大量的 UI 绘制。

如何怎么定位问题

  • 死锁一般会伴随 crash,可以通过 crash report 来分析。
  • 抢锁不好办,将锁等待时间打出来用处不大,我们还需要知道是谁占了锁。
  • 大量 IO 可以在函数开始结束打点,将占用时间打到日志中。
  • 大量计算同理可以将耗时打到日志中。
  • 大量 UI 绘制一般是必现,还好办;如果是偶现的话,想加日志点都没地方,因为是慢在系统函数里面。

如何判定卡顿

一般来说,用户感受得到的卡顿大概有三个特征:

  1. FPS 降低
  2. CPU 占用率很高
  3. 主线程 Runloop 执行了很久

如何确定卡顿参数

FPS 不好衡量,抖动比较大。而对于抢锁或大量 IO 的情况,光有 CPU 是不行的。我们用下面两个判断卡顿了

  1. CPU 占用超过了100%
  2. 主线程 Runloop 执行了超过2秒

监测卡顿

  • 使用子线程定时向主线程中仍入空任务,超过指定时间任务未被执行则判定为卡顿。(缺点: 会不停的唤醒主线程runloop,有一定损耗)
  • 基于runloop检测,使用子线程实时检测runloop状态,执行超过指定时间则判定为卡顿。(缺点: 捕获到的卡顿堆栈,不一定是最耗时的任务。不过最耗时任务有较大的概率被捕获到)

Runloop 的起始最开始和结束最末尾位置添加 Observer,获取主线程的开始和结束状态。卡顿监控起一个子线程定时检查主线程的状态,当主线程的状态运行超过一定阈值则认为主线程卡顿,从而标记为一个卡顿

开辟一个子线程定时检查主线程的状态,并记录下主线程在各个运行状态的时间点,当主线程的运行状态超过一定的时间阈值后,则认为主线程卡顿。记录这时的堆栈信息,并进行相应的处理。

降低检测带来的性能损耗

  • 内存 dump:每1秒检查一次,如果检查到主线程卡顿,就将所有线程的函数调用堆栈 dump 到内存中。
  • 文件 dump:如果内存 dump 的堆栈跟上次捕捉到的不一样,则 dump 到文件中;否则按照斐波那契数列将检查时间递增(1,1,2,3,5,8…)直到没有遇到卡顿或卡顿堆栈不一样。这样能够避免同一个卡顿写入多个文件的情况,也能避免检测线程围着同一个卡顿空转的情况。

results matching ""

    No results matching ""

    results matching ""

      No results matching ""