自助建站系统免授权版网络规划与优化技术
2025/12/30 7:48:30 网站建设 项目流程
自助建站系统免授权版,网络规划与优化技术,敬请期待英文,支付网站怎么做1. 撕开 RunLoop 的伪装#xff1a;它不仅仅是一个死循环很多兄弟在面试时把 RunLoop 背得滚瓜烂熟#xff1a;“它是管理事件循环的对象#xff0c;让线程有事做事#xff0c;没事休眠...” 听起来没毛病#xff0c;但你在写代码时真的看见过它吗#xff1f;在 main.m 那…1. 撕开 RunLoop 的伪装它不仅仅是一个死循环很多兄弟在面试时把 RunLoop 背得滚瓜烂熟“它是管理事件循环的对象让线程有事做事没事休眠...” 听起来没毛病但你在写代码时真的看见过它吗在main.m那个不起眼的入口文件里UIApplicationMain函数就像一个黑洞一旦进去主线程这辈子就别想出来了。int main(int argc, char * argv[]) { autoreleasepool { // 这一行下去你的 App 才算真正活了 return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }这背后所谓的“死循环”并不是写个while(1)那么简单。如果你真在代码里写个while(1)你的 CPU 占用率瞬间飙升到 100%手机发烫得能煎鸡蛋过不了多久 iOS 的看门狗Watchdog就会因为主线程卡死把你杀掉。RunLoop 的高明之处在于用户态和内核态的切换。关键在于mach_msg()。当 RunLoop 发现没任务Source0/Source1/Timer/Observer 都处理完了它不是在空转而是调用了内核函数告诉系统“哥们我累了有消息再叫醒我”。这时候线程进入Trap 状态CPU 资源被完全释放。这才是“没事休眠”的真相——它不是在循环里发呆而是直接挂起了。你在 Crash 堆栈里经常看到的__CFRunLoopServiceMachPort就是在等这个内核消息。线程保活的误区早些年 AFNetworking 2.x 为了在后台接收 Delegate 回调强行搞了个“常驻线程”。现在的代码里如果我还看到有人这么写通常会直接在 Code Review 里打回 (void)networkThreadEntryPoint:(id)__unused object { autoreleasepool { [[NSThread currentThread] setName:AFNetworking]; NSRunLoop *runLoop [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; // 这里的操作很骚 [runLoop run]; } }注意那个addPort。如果你不加这个 PortRunLoop 启动后发现“卧槽没 Source 也没 Timer玩个蛋”然后直接退出线程销毁。添加一个空的 MachPort 就像是给驴挂了个胡萝卜虽然它永远吃不到因为没有真正的消息发给这个 port但它会为了这个目标一直跑下去。但在今天这种做法是过时的。现在的 GCD 和NSURLSession已经管理得足够好除非你在做极度复杂的 Socket 长连接或者需要精细控制生命周期的后台任务否则尽量别自己手动去 Run 一个 RunLoop维护成本极高容易搞出僵尸线程。2. 模式Mode的博弈为什么你的 Timer 总是“装死”这是个老生常谈的问题但 90% 的人只知道解法不知道因果。当你滑动UITableView或者UIScrollView时原本定好的NSTimer突然就不走了。面试官问你为啥你答“因为 Mode 切换了”面试官点点头。但如果在生产环境这还不够。RunLoop 同一时间只能运行在一个 Mode 下。kCFRunLoopDefaultMode: App 平时溜达的状态。UITrackingRunLoopMode: 手指头按在屏幕上搓动时的状态。当你滑动列表主线程切换到TrackingMode。你的 Timer 默认是加在DefaultMode里的。RunLoop 就像个势利的管家它说“我现在只服务 Tracking 模式下的贵宾Default 模式下的穷亲戚Timer先在门口等着。”于是界面停了Timer 里的倒计时才通过“跳秒”的方式补回来或者干脆就丢了。常见的错误解法与代价很多人知道要用NSRunLoopCommonModes。// 大家都这么写觉得自己很机智 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];这行代码其实是个标记位的魔法。CommonModes不是一个真正的 Mode它是一个集合Set。默认情况下它包含了 Default 和 Tracking。把 Timer 加到 CommonModes等于告诉 RunLoop“不管你在哪个山头唱戏这个 Timer 你都得给我带着。”但是这里有个巨大的坑尤其是在涉及繁重计算的时候。假设你的 Timer 每 0.1秒触发一次做一些动画计算或者图片处理。如果你把它加到了CommonModes意味着在用户疯狂滑动列表TrackingMode的时候这个 Timer 依然在主线程抢占 CPU 资源。结果就是列表滑动变卡了。因为人眼对 60FPS 的滑动流畅度非常敏感任何在 TrackingMode 下抢夺主线程资源的行为都是犯罪。所以在做性能优化时有时候我们故意不把 Timer 加到 CommonModes而是让它在滑动时暂停等停下来再刷新 UI。这取决于你的业务是“数据实时性优先”还是“交互流畅度优先”。干货建议如果你的 Timer 只是为了更新一个不会影响核心体验的小倒计时比如 cell 上的秒杀倒计时用CommonModes没问题。但如果是为了计算复杂的粒子效果请考虑用 CADisplayLink 并扔到子线程或者直接在滑动时暂停动画。3. 刨根问底Source0 和 Source1 到底在吵什么打开一段卡顿日志的堆栈你总能看到CFRunLoopDoSources0或CFRunLoopDoSources1。这俩货是 RunLoop 处理事件的核心搞不清它们你就看不懂卡顿监控的日志。Source0由于你自己作死产生的事件它只包含应用层面的回调。也就是非基于 Port 的。 举个最直白的例子[performSelector:onThread:...]。 当你在主线程或者子线程调用这个方法时你其实是像 RunLoop 里的 Source0 集合扔了一个任务并标记为signaled待处理。关键点来了Source0 是被动的。它需要被标记并且 RunLoop 醒着的时候才会去处理。如果 RunLoop 正在休眠你仅仅扔个 Source0 它是不会立马醒的通常需要配合 wakeup。触摸事件Touch Begin/Move/End在这一层其实是个特例。虽然硬件中断是 Source1 也就是 mach port 传过来的但系统内部会把它包装处理最终经常表现为 Source0 的回调分发到UIApplication。所以在卡顿堆栈里点击按钮的响应往往在DoSources0下面。Source1系统大佬的直通车它是基于 Port 的这玩意儿直接和内核打交道。 物理按键、传感器数据、进程间通信IPC这些都是通过 Mach Port 也就是 Source1 进来的。Source1 有个特权它可以主动唤醒休眠的 RunLoop。生产环境的“灵异现象”有一次排查线上 Bug发现主线程莫名其妙卡顿堆栈停留在CFRunLoopDoSources0。查了半天代码发现是有个二货开发写了个巨大的数组遍历逻辑放在了performSelector里执行。因为它是 Source0RunLoop 在一次循环Loop中处理完所有的 Source0 才会进入休眠或处理 Source1。如果你的 Source0 任务太重就会直接导致这一帧的时间被撑爆掉帧就产生了。优化策略如果你有大量的任务需要分包处理不要试图在一个 Source0 回调里干完。可以将大任务拆分成多个小的 Source0或者利用CFRunLoopObserver在 RunLoop 的BeforeWaiting准备休眠时机去执行低优先级的任务比如预排版、图片预解码。这也就是 AsyncDisplayKit (Texture) 的核心原理之一。4. 自动释放池AutoreleasePool的幽灵它什么时候干活你可能背过“AutoreleasePool 在 RunLoop 开始时创建在休眠前销毁。”这话对也不对。我们在主线程里产生的autorelease对象如果不手动加池子确实是依赖 RunLoop 来清理的。系统在 RunLoop 中注册了两个高优先级的 Observer观察者第一个 Observer: 监听Entry进入 Loop。它会调用_objc_autoreleasePoolPush()。这就像是进门前拿了个垃圾袋。第二个 Observer: 监听BeforeWaiting准备休眠和Exit退出。在BeforeWaiting时它会先_objc_autoreleasePoolPop()把垃圾袋扔了清理内存然后紧接着_objc_autoreleasePoolPush()再拿个新袋子为下一次醒来做准备。在Exit时直接 Pop打扫战场走人。为什么知道这个很重要内存峰值High Water Mark优化。在处理大图加载或者复杂的 JSON 转 Model 列表时如果你的逻辑全在一次 RunLoop 循环里跑完中间产生了成千上万个临时的NSString、NSDictionary它们都得等到 RunLoop 准备休眠或者跑完这一圈时才释放。在这个时间点之前你的 App 内存会像过山车一样飙升。如果这时候内存报警你的 App 就 Crash 了。实战操作在for循环里或者处理密集型任务时显式地套一个autoreleasepool {}。// 错误示范坐等 RunLoop 给你擦屁股 for (int i 0; i 10000; i) { NSString *log [NSString stringWithFormat:Log info: %d, i]; // ... 产生大量临时对象 } // 正确示范自己拉屎自己冲 for (int i 0; i 10000; i) { autoreleasepool { NSString *log [NSString stringWithFormat:Log info: %d, i]; // ... 出了这个花括号内存立马释放 } }这看起来是内存管理的基础但本质上是你对 RunLoop 释放时机的不信任票。在高性能场景下不要指望 RunLoop 的那个默认 Observer它的粒度太粗了。5. 深入心脏RunLoop 内部逻辑的伪代码重构为了彻底理解我们别看那些晦涩的 C 源码了我用大白话伪代码把 RunLoop 的核心逻辑写一遍。看完这个你就知道卡顿监控的代码该插在哪了。/// 这是一个极其简化的 RunLoop 逻辑模型 void CFRunLoopRun() { // 1. 告诉 Observer这班车我要发车了 (kCFRunLoopEntry) __CFRunLoopDoObservers(kCFRunLoopEntry); do { // 2. 告诉 Observer我要处理 Timer 了 (kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(kCFRunLoopBeforeTimers); // 3. 告诉 Observer我要处理 Source0 了 (kCFRunLoopBeforeSources) __CFRunLoopDoObservers(kCFRunLoopBeforeSources); // 4. 处理 Source0 (这一步可能会消耗大量 CPU如果是卡顿大概率卡在这) __CFRunLoopDoSources0(); // 5. 关键点如果有 Source1 (基于端口的消息) 已经到了那就别睡了直接跳到第9步去处理 if (CheckIfExistMessagesInMainDispatchQueue() || CheckIfSource1Fired()) { goto handle_msg; } // 6. 告诉 Observer没啥事我要睡了 (kCFRunLoopBeforeWaiting) // **注意卡顿监控通常在这里记录时间戳因为下面就进入系统内核态了** __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting); // 7. 调用 mach_msg 等待唤醒。线程在此挂起不占用 CPU。 // 等待被 Source1、Timer、或者 GCD 主线程任务唤醒 mach_msg_trap(); // 8. 告诉 Observer我醒了 (kCFRunLoopAfterWaiting) // **注意卡顿监控在这里再次记录时间戳。如果 8 - 6 的时间极短说明只是睡了一觉 // 如果 8 - 2 的时间差减去 睡眠时间 很大说明 Loop 执行超时了** __CFRunLoopDoObservers(kCFRunLoopAfterWaiting); handle_msg: // 9. 被什么唤醒的处理对应的东西 if (TimerFired) { __CFRunLoopDoTimers(); } else if (GCDMainQueue) { // 处理 dispatch_async(dispatch_get_main_queue(), block) __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(); } else { // 处理 Source1 __CFRunLoopDoSource1(); } // 10. 处理完一波看看要不要退出循环 } while (!stop); // 11. 告诉 Observer我要下班了 (kCFRunLoopExit) __CFRunLoopDoObservers(kCFRunLoopExit); }看懂了这个流程下一章讲卡顿监控时你就明白为什么我们要创建一个子线程专门盯着主线程的 RunLoop 状态变迁了。我们实际上是在监控kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting这两个状态之间的停留时长。6. 滑动优化的降维打击利用 Mode 玩“时停”你在写UITableView的cellForRow时是不是习惯把图片设置、头像圆角裁剪、文字计算全堆在一起// 菜鸟写法 cell.imageView.image [UIImage imageNamed:heavy_image]; cell.avatar.layer.cornerRadius 25; // 离屏渲染警告 cell.avatar.clipsToBounds YES;当用户手指在屏幕上狂搓TrackingMode时主线程既要处理触摸事件又要计算布局还要去解码那张该死的大图。CPU 就像个被两个老板同时催工的社畜结果就是——掉帧。怎么破利用 RunLoop 的 Mode 切换机制我们可以玩一招“时间停止”。核心逻辑当用户在滑动时TrackingMode我们什么重活都不干只显示占位图或文字。一旦用户手指离开屏幕滑动停止回到 DefaultMode我们再把高清图和复杂的圆角切好放上去。这代码写起来比你想的要简单得多核心就是performSelector的modes参数// 在 cellForRowAtIndexPath 中 // 1. 先设置默认图保证界面不白板 [cell.avatarImageView setImage:[UIImage imageNamed:placeholder]]; // 2. 将耗时的设置任务推迟到 DefaultMode 下执行 [self performSelector:selector(setHeavyImageForCell:) withObject:cell afterDelay:0 inModes:[NSDefaultRunLoopMode]];看懂了吗afterDelay:0并不是立即执行而是“尽快执行”。但inModes:[NSDefaultRunLoopMode]这句才是灵魂。它告诉 RunLoop“只要还在滑动TrackingMode这行代码就别跑等停下来DefaultMode立马给我执行。”生产环境的坑必看这招有个致命的问题——Cell 重用。 如果用户滑得飞快Cell A 刚准备加载图片结果还没停下来就被回收给 Cell B 用了。等停下来时那个延迟的任务执行了把 Cell A 的图片贴到了 Cell B 脸上。这就是经典的“图片错乱”。修正方案你必须在prepareForReuse里或者在设置图片前取消掉之前挂在这个 Cell 上的延迟任务。 (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(id)anArgument;虽然现在 SDWebImage 等库已经处理得很好了但在处理复杂的富文本排版计算时这个技巧依然是低成本换取高 FPS的杀手锏。7. 大任务拆解RunLoop 分发中心 (Work Distribution)假设你要在主线程加载一张 4000x4000 的大图并渲染到视图上或者你要一次性从数据库读 50 条数据转成 Model。不管你多快只要这一个任务耗时超过16.7ms1/60秒这一帧就丢了。传统的并发编程会告诉你扔到后台线程去 但 UI 必须在主线程更新你算出数据总得回来吧回来的那一瞬间 commit 也是耗时的。这里介绍一个非常高级的技巧曾被用于 AsyncDisplayKit (Texture) 的早期版本RunLoop 任务分片。原理既然一个大任务会卡死那我就把它切成 100 个小任务。每次 RunLoop 循环唤醒时我只做一个小任务做完立马把控制权交还给系统去渲染 UI。这样虽然总耗时没变但每一帧都有空闲去响应触摸和绘制用户感觉不到卡。如何实现我们需要一个单例用来管理这些“碎片任务”。定义任务队列用一个NSMutableArray存 Block。监听 RunLoop注册一个CFRunLoopObserver监听kCFRunLoopBeforeWaiting准备休眠前或者kCFRunLoopAfterWaiting刚醒来。通常选BeforeWaiting比较安全因为那时该处理的都处理了偷点时间干活。消费任务回调触发时从数组里pop一个任务执行。关键代码逻辑伪代码// 定义回调函数 static void RunLoopWorkDistributionCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { // 检查是否有任务 if (tasks.count 0) return; // 取出一个任务 RunLoopTask task tasks.firstObject; [tasks removeObjectAtIndex:0]; // 执行它 task(); // 这一步非常重要 // 执行完一个任务后如果还有任务必须显式唤醒 RunLoop。 // 否则 RunLoop 处理完这个 Callback 可能会直接进入休眠导致剩下的任务要等下一次触摸才能触发。 if (tasks.count 0) { CFRunLoopWakeUp(CFRunLoopGetCurrent()); } } // 注册监听 - (void)registerObserver { CFRunLoopObserverContext context {0, (__bridge void *)self, NULL, NULL, NULL}; CFRunLoopObserverRef observer CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, // 也就是干完所有杂活准备睡觉前 YES, // 重复监听 0, // 优先级 RunLoopWorkDistributionCallback, context); CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes); }实战场景你在cellForRow里要加载 3 张高清头像。如果你直接[imgView setImage:...]三次肯定卡。 用这个分发器[[RunLoopWorkDistribution shared] addTask:^{ cell.img1.image ...; }]; [[RunLoopWorkDistribution shared] addTask:^{ cell.img2.image ...; }]; // ...此时这三个头像会在接下来的 3 次 RunLoop 循环也就是接下来的 3 帧里分别显示。视觉上几乎是同时的但主线程的压力被摊平了。8. 猎犬计划基于 RunLoop 的卡顿监控系统现在市面上最准的卡顿监控比如微信的 Matrix阿里的 BlockCanary都不是靠算 FPS 的。FPS 只是表象。FPS 低可能是因为 GPU 渲染压力大也可能是 CPU 满载。而作为 iOS 开发我们主要解决的是主线程 CPU 卡死的问题。RunLoop 才是卡顿的根源。如果主线程在执行某个方法时卡住了一定意味着 RunLoop 停在了__CFRunLoopDoSources0或者__CFRunLoopDoSource1之后迟迟没有进入BeforeWaiting状态。我们可以搞一个子线程监控者Watchdog像一条猎犬一样死死盯着主线程的 RunLoop 状态。监控架构设计创建一个子线程并在子线程里开启一个while(true)循环。主线程埋点给主线程 RunLoop 添加 Observer记录状态变更的时间戳。信号量机制这是核心。代码逻辑推演这是价值百万的监控核心// 主线程 Observer 回调 static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { MyLagMonitor *monitor (__bridge MyLagMonitor*)info; monitor-activity activity; // 记录当前状态 // 发送信号告诉子线程的看门狗“我主线程还活着状态变了” dispatch_semaphore_signal(monitor-semaphore); } // 子线程监控逻辑 - (void)startMonitoring { dispatch_async(monitoringQueue, ^{ while (isMonitoring) { // 等待信号。设置超时时间为阈值比如 250ms (0.25秒) long waitResult dispatch_semaphore_wait(self-semaphore, dispatch_time(DISPATCH_TIME_NOW, 250 * NSEC_PER_MSEC)); if (waitResult ! 0) { // 超时了 // 说明主线程在 250ms 内没有更新状态没有发信号过来。 if (!self-observer) { self-activity 0; return; } // 必须过滤掉 休眠 状态。 // 如果主线程是在 BeforeWaiting (准备睡觉) 之后超时的那是正常休眠不是卡顿。 if (self-activity kCFRunLoopBeforeSources || self-activity kCFRunLoopAfterWaiting) { // 抓到了主线程正在忙着处理 Source0 或者刚醒来处理 Timer/GCD // 结果忙了超过 250ms 还没干完。 // TODO: 此时立马抓取主线程的堆栈 (PLCrashReporter / BSBacktraceLogger) [self logStacktrace]; } } } }); }为什么是这两个状态kCFRunLoopAfterWaiting: 说明 RunLoop 刚被叫醒比如定时器响了或者主队列有 Block正在处理这些唤醒它的任务。如果这就卡了说明你的timer回调或者dispatch_async里的代码太烂。kCFRunLoopBeforeSources: 说明 RunLoop 正在处理 Source0主要是你的 UI 点击事件、方法调用。绝大多数 UI 卡顿都发生在这里。误报剔除技巧如果 App 处于后台RunLoop 可能会休眠很久这时候不要报警。 如果 CPU 负载整体很高全系统卡你的子线程可能也抢不到时间片导致误差这时候要结合 CPU 使用率来判断。9. 濒死体验利用 RunLoop 在 Crash 后“续命”这可能是 RunLoop 最“黑魔法”的应用场景了。当 App 崩溃时比如数组越界 Uncaught Exception系统会杀死进程。但在用户看来就是“闪退”。这对体验是毁灭性的尤其是用户正在编辑长文还没保存就崩了。我们能不能让 App 在崩溃后不要立马死而是弹个窗提示用户“程序出错了”然后让他有机会点个“保存”再死答案是可以接管 RunLoop。在UncaughtExceptionHandler里我们可以强行重启一个 RunLoop。void UncaughtExceptionHandler(NSException *exception) { // 1. 获取当前 RunLoop CFRunLoopRef runLoop CFRunLoopGetCurrent(); NSArray *allModes CFRunLoopCopyAllModes(runLoop); // 2. 弹窗提示 (必须在主线程) // 这里其实很危险因为 UI 系统可能已经乱了但在 desperate times值得一试 UIAlertController *alert ...; [rootVC presentViewController:alert animated:YES completion:nil]; // 3. 强行续命循环 while (!isUserDismissedAlert) { // 让 RunLoop 继续跑每次跑 0.001 秒就停类似空转 // 这样可以维持 UI 的响应处理点击事件 for (NSString *mode in allModes) { CFRunLoopRunInMode((CFStringRef)mode, 0.001, false); } } // 4. 用户点完保存后手动杀掉进程 NSSetUncaughtExceptionHandler(NULL); abort(); }这招就像是给心脏骤停的人打一针肾上腺素。警告此时 App 的内存状态可能已经脏了Corrupted继续运行可能会导致逻辑错误。所以这个“续命”模式只能用来做紧急数据保存千万别让用户继续正常用否则会产生更严重的数据污染。这也是为什么有些 App 崩了之后界面卡了一下然后弹出了一个“抱歉程序异常”的弹窗还能让你点确定的原因。他们就是在 Crash Handler 里强行 Run 了一个 Loop。10. 世纪之问GCD 和 RunLoop 到底是什么关系这是一个在面试中能杀掉 90% 候选人的问题“你在子线程dispatch_async到主线程更新 UI主线程的 RunLoop 到底知不知道”很多人的理解是模糊的觉得它俩是两套并行的系统。错。在主线程上它们是穿一条裤子的。当你写下这行代码时dispatch_async(dispatch_get_main_queue(), ^{ self.label.text Hello; });发生了什么GCD 会把这个 Block 扔进 Main Queue 的结构体里。然后GCD 发现这是主队列它需要“叫醒”主线程。它会向主线程 RunLoop 注册的那个特殊的Port还记得 Source1 吗发送一个信号。RunLoop 正在休眠Trap 状态被内核唤醒。RunLoop 醒来后检测到是被 GCD 唤醒的它甚至不会走标准的 Source0/Source1 处理流程而是直接执行一个特定的函数__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__。这个函数的名字长得像个玩笑但它真实存在。你如果不信下次在dispatch_async的 Block 里打个断点看左边的调用堆栈这行大字赫然在目。结论很惊悚RunLoop 是主线程的心脏而 GCD 的主队列任务只是寄生在 RunLoop 心跳间隙中的寄生虫。 这意味着如果你在dispatch_async到主线程的任务里写了死循环不仅 GCD 瘫痪整个 RunLoop 也会直接暴毙App 彻底失去响应。生产环境启示在做性能分析时如果看到CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE占用时间过长说明有人把太多的业务逻辑塞进了dispatch_async导致 RunLoop 这一圈跑得太累没时间去响应触摸事件Source0。这时候的优化策略是拆。把大 Block 拆成小的分多次 dispatch或者把非 UI 逻辑扔回后台队列。11. 线程保活的“现代”启示录我们在第一章提到了 AFNetworking 早期的线程保活。现在我们深入聊聊为什么你要或者不要这么做。如果你在开发一个IM即时通讯 SDK或者监控 SDK你需要一个线程它必须永远不死随时准备接收服务器推过来的消息。随叫随到有消息立马处理。没事别占 CPU没消息时必须挂起不能空转耗电。这时候普通的NSThread跑完任务就销毁了不符合要求。GCD 的并发队列又不受你精确控制。你必须手动搭建一个带有 RunLoop 的线程。标准模版可以直接抄进你的 SDKinterface MyWorkerThread : NSThread end implementation MyWorkerThread - (void)main { autoreleasepool { // 1. 获取当前 RunLoop NSRunLoop *runLoop [NSRunLoop currentRunLoop]; // 2. 添加一个 Port。 // 这里的 [NSMachPort port] 是关键。 // 我们并不真的通过这个 Port 发消息它只是为了告诉 RunLoop // 我有 input source别退出给我等着 [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; // 3. 启动 Loop。 // 注意不要用 [runLoop run]。 // 因为 run 方法是无法停止的它会一直跑在 DefaultMode。 // 我们通常自己写个 while 循环这样可以在外部通过标志位控制线程停止。 while (!self.isCancelled) { [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } } }为什么不要用[runLoop run]文档里写得很隐晦run方法本质上是一个无限循环调用runMode:beforeDate:的封装。一旦开启你很难在线程内部优雅地停止它。而上面的while写法允许你在外部调用[thread cancel]后下一次 Loop 醒来时检测到isCancelled从而优雅退出释放内存。警惕僵尸线程很多初级架构师写完这个就以为万事大吉了。结果 App 运行久了内存里堆积了十几个这样的线程因为他们忘记了退出条件。 如果你的 SDK 只是在某个模块使用比如进入直播间一定要在退出模块时显式地给这个线程发一个 performSelector让它执行退出逻辑否则这个线程就是内存泄漏。12. 那些你没注意到的“RunLoop 细节”在多年的踩坑经验中还有几个极其隐蔽的细节值得你记在小本本上。A. 定时器的误差会累积吗NSTimer是不准的大家都知道。如果 RunLoop 忙它会推迟执行。 关键是推迟后下一次触发时间是按原计划还是按推迟后的时间顺延答案是RunLoop 只有在“错过”太久的情况下才会重置时间线。如果你的 Timer 设定是 00:00, 00:01, 00:02。 结果 00:01 的时候 RunLoop 卡住了直到 00:01:50 才醒过来处理。 那么 RunLoop 会立即执行 00:01 的回调。 然后等到 00:02:00它会继续执行下一次。它会试图追赶时间线但不会把中间错过的 00:01:10, 00:01:20... 全部补执行一遍会发生合并。所以不要用NSTimer做这种需要精确计数的秒表用CADisplayLink或者 GCD Timer。B. 界面更新的“集结号”为什么你改了label.text A, 下一行label.text B, 屏幕不会闪一下 A因为 UI 渲染也是 RunLoop 的一个 Observer。 RunLoop 在BeforeWaiting准备休眠或者Exit时会执行一个系统注册的回调。这个回调会遍历所有被标记为setNeedsDisplay或setNeedsLayout的 View然后一把梭提交给 GPU 去渲染。这就是为什么你在代码里疯狂改 Frame 没关系只有最后一次修改才会被提交。这也解释了为什么在循环里改 UI 不会立即生效除非你强行调用[CATransaction flush]警告别乱用这个会破坏系统的渲染节奏。C. 触摸事件的传递黑箱当你的手指点击屏幕系统进程SpringBoard接收到硬件信号通过 Mach Port 发送给你的 App 进程。 App 的主线程 RunLoop 被 Source1 唤醒。 然后 Source1 回调会触发__IOHIDEventSystemClientQueueCallback。 这个函数会把事件包装一下分发给 Source0。 最后 Source0 调用UIApplication的sendEvent:。实战价值如果你想做无侵入的埋点系统AOPHooksendEvent:是最上层的做法。但如果你想做全局的触摸防抖或者特殊手势拦截你需要理解这个 Source1 - Source0 的过程。有些极端的黑客防守技术甚至会去监控 RunLoop 的 Source1 来源防止脚本模拟点击。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询