say something
由于近期项目比较忙, 准备的很仓促, 加之个人水平有限, 就抛砖引玉吧。有理解不对的地方, 还请大佬们指正。 🤦🤦🤦
intro
为什么程序会保持运行?
看看 APP 的入口函数:
// main.m中
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
首先看到, 程序主函数 main() 需要一个 int 类型的返回值; main() 内部调用了 UIApplicationMain()
函数, 并返回它的返回值;
UIApplicationMain()
是程序的入口函数, 其内部启动了一个 RunLoop, 这个默认启动的 RunLoop 是跟主线程相关联的, 导致了主线程常驻;
所以 UIApplicationMain()
函数一直没有返回, 从而保证了 main() 函数也没有返回, 保持了程序的持续运行。
Runloop 是什么? 先来了解下 Event Loop
Tips: swift 没有 main?
官方解释:
In Xcode, Mac templates default to including a “main.swift” file, but for iOS apps the default for new iOS project templates is to add
@UIApplicationMain
to a regular Swift file. This causes the compiler to synthesize a mainentry point for your iOS app, and eliminates the need for a “main.swift” file.
// AppDelegate.swift 中代码:
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { }
Event Loop
一般情况下, 一个线程一次只能执行一个任务, 执行完后, 线程就会退出。 如果我们想让线程不退出, 并能随时处理事件, 通常的逻辑是这样的:
func loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
这种模型通常被称作 Event Loop。 它的关键在于: 怎样让线程在没有事件处理时休眠, 以节约性能, 在有事件到来时立刻响应事件。
Event Loop 在很多系统和框架里都有实现, macOS / iOS 下, 对应实现了 Runloop。
RunLoop 简介
简介:
RunLoop 字面意思为 运行循环
, 表示在程序运行中循环着做一些事情;
其实它内部就是一个 do-while
循环, 在这个循环内部, 合理安排作息时间, 有事时做事, 没事做就休息。
基本作用:
- 保持程序的持续运行;
- 处理App中的各种事件 (比如触摸事件、定时器事件、Selector事件、GCD回主线程 等);
- 减少CPU空转, 节省CPU资源, 提高程序性能。
- 管理 autoreleasepools
注意:
- RunLoop 只能选择一个 Mode 启动, 即
CurrentMode
; - 如果需要切换 Mode, 只能退出 Loop, 再重新指定一个 Mode 进入。
- 如果
CurrentMode
中没有任何Source或者Timer, 那么就直接退出RunLoop。
应用举例:
- 子线程保活 (开启子线程的runloop, 同时保证 mode 不空)
- 解决滑动页面定时器停止问题 (将timer添加到CommonModes)
- 在特定模式下执行某些任务 (performSelector:withObject:afterDelay:inModes)
- 卡顿监测 (监听runloop, BeforeSources -> BeforeWaiting 以及 AfterWaiting -> BeforeTimers 的时间间隔)
- 解决 tableView 加载 cell 时的耗时卡顿问题 (监听RunLoop, 将多个耗时操作分开执行, 在每次 RunLoop 唤醒时去执行一个耗时任务。)
- 等等..
和 AutoreleasePool
的关系:
Runloop 管理着 AutoreleasePool 的创建和释放 第一次创建: runloop启动的时候 最后一次销毁: runloop退出的时候 其他时候的创建和销毁: 当runloop即将休眠的时候, 此时说明事件全部处理完毕, 销毁之前的释放池, 同时重新创建一个新的。
App启动后, 苹果在主线程 RunLoop 里注册了2个 Observer, 其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()
。
- 第1个 Observer 监视的事件是:
- Entry(即将进入Loop), 其回调内会调用
_objc_autoreleasePoolPush()
创建自动释放池。其 order 是-2147483647(-0x7FFFFFFF),优先级最高
, 保证创建释放池发生在其他所有回调之前。
- 第2个 Observer 监视了2个事件:
- BeforeWaiting(准备进入休眠) 时调用
_objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
释放旧池并创建新池; - Exit(即将退出Loop) 时调用
_objc_autoreleasePoolPop()
来释放自动释放池。这个 Observer 的 order 是 2147483647(0x7FFFFFFF),优先级最低
, 保证其释放池子发生在其他所有回调之后。
在主线程执行的代码, 通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着, 所以不会出现内存泄漏, 我们也不必显式创建 Pool 了。
和 线程
的关系:
- 在第一次
获取
时创建, 在线程结束时销毁; - 线程和RunLoop一一对应, RunLoop内部通过字典存储, 线程作为key, RunLoop作为value。
- 主线程的RunLoop在应用启动的时候就会自动创建, 子线程的RunLoop需要手动创建并启动。
GCD
回主线程
当调用 dispatch_async(dispatch_get_main_queue(), block)
时, libDispatch 会向主线程的 RunLoop 发送消息, RunLoop会被唤醒, 并从消息中取得这个 block, 并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
里执行这个 block。
但这个逻辑仅限于 dispatch 到主线程, dispatch 到其他线程仍然是由 libDispatch 处理的。
事件响应
苹果注册了一个 Source1 (基于 mach port) 用来接收系统事件, 其回调函数为 __IOHIDEventSystemClientQueueCallback()
。
当一个硬件事件 (触摸/锁屏/摇晃等) 发生后, 首先由 IOKit.framework
生成一个 IOHIDEvent
事件并由 SpringBoard
接收。
SpringBoard
只接收按键(锁屏/静音等)
、触摸
、加速
、接近传感器
等几种 Event, 随后用 mach port
转发给需要的App进程。
随后苹果注册的那个 Source1 就会触发回调, 并调用 _UIApplicationHandleEventQueue()
进行应用内部的分发。
_UIApplicationHandleEventQueue()
会把 IOHIDEvent
处理并包装成 UIEvent
进行处理, 分发给 UIWindow。像 UIButton 点击
、touchesBegin/Move/End/Cancel
这些事件都是在这个回调中完成的。
手势识别
如何分辨用户所做的手势是哪一种手势呢? 上面的 _UIApplicationHandleEventQueue()
识别了一个手势时, 首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting
事件, 这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver()
, 其内部会获取所有刚被标记为待处理的 GestureRecognizer, 并执行 GestureRecognizer 的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时, 这个回调都会进行相应处理。
CFRunloop 简介
Core Foundation 源码:
CFRunLoop 主要有5个类:
CFRunLoopRef // 包含ModeRef类型变量的数组成员(_modes)和当前的mode
CFRunLoopModeRef // 包含 SourceRef、TimerRef、ObserverRef 类型成员
CFRunLoopSourceRef // source0、source1
CFRunLoopTimerRef // 和 NSTimer 是 toll-free bridged, 可以混用。
CFRunLoopObserverRef
这5个类的关系:
一个RunLoop有一个 _modes
成员, 其包含若干个 Mode , 每个 Mode 又包含若干个 Source0 / Source1 / Timer / Observer
。
runloop 运行机制
先上一张从 ibireme 盗的图:
runloop 获取:
苹果不允许直接创建 RunLoop, 只提供了获取 RunLoop 的API
Foundation 中:
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
Core Foundation 中:
CFRunLoopRef CFRunLoopGetMain(void);
CFRunLoopRef CFRunLoopGetCurrent(void);
CFRunLoopRef _CFRunLoopGet0(pthread_t t); // private
CFRunLoopRef __CFRunLoopCreate(pthread_t t); // private
runloop 运行:
Foundation 中:
- (void)run; // 无超时运行在 NSDefaultRunLoopMode, 除非移除所有 timer和source, 否则无法停止
- (void)runUntilDate:(NSDate *)limitDate; // 也是运行在 NSDefaultRunLoopMode
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate; // 用指定的Mode启动, 带超时限制
Core Foundation 中:
void CFRunLoopRun(void); // kCFRunLoopDefaultMode, 1.0e10, false
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle);
SInt32 CFRunLoopRunSpecific(); // private
static int32_t __CFRunLoopRun(); // private
runloop 休眠:
__CFRunLoopServiceMachPort();
调用 mach_msg()
进行 用户态 / 内核态 的切换
用户态 -> 内核态
runloop 停止:
- 移除掉 runloop 中的所有事件源(timer 和 source)。
- 设置超时时间。
- 使用
CFRunLoopRun()
、CFRunLoopRunInMode()
方法运行起来的 runloop 可以用CFRunLoopStop()
停止。