当前位置: 首页 > news >正文

RunLoop 深度解析

RunLoop 深度解析

目录

  1. RunLoop 基础概念
  2. RunLoop 底层原理
  3. RunLoop 源码分析
  4. RunLoop 的实际应用
  5. 常见应用场景
  6. 最佳实践
  7. 实际代码示例

1. RunLoop 基础概念

1.1 什么是 RunLoop

RunLoop(运行循环)是 iOS/macOS 开发中的一个核心概念,它是一个事件处理循环,用于管理和调度线程上的任务。

核心作用:

  • 保持线程存活,避免线程执行完任务后立即退出
  • 处理各种事件(触摸事件、定时器、网络请求等)
  • 在没有事件时让线程休眠,节省 CPU 资源
  • 在有事件时唤醒线程,及时处理

1.2 RunLoop 与线程的关系

重要特性:

  • 一对一关系:每个线程都有且仅有一个 RunLoop
  • 懒加载:RunLoop 不会自动创建,需要时才会创建
  • 主线程 RunLoop:应用启动时自动创建并运行
  • 子线程 RunLoop:默认不创建,需要手动获取或创建
// 获取当前线程的 RunLoop(如果不存在会自动创建)
NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];// 获取主线程的 RunLoop
NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];

1.3 RunLoop 的基本结构

RunLoop 包含以下几个核心组件:

1.3.1 Mode(模式)

RunLoop 运行在特定的 Mode 下,不同 Mode 包含不同的 Source、Timer 和 Observer。

常见的 Mode:

  • NSDefaultRunLoopMode:默认模式,大多数操作在此模式下
  • UITrackingRunLoopMode:UI 追踪模式,ScrollView 滑动时切换到此模式
  • NSRunLoopCommonModes:占位模式,包含 Default 和 Tracking 模式

Mode 切换的意义:

  • 当 ScrollView 滑动时,RunLoop 切换到 UITrackingRunLoopMode
  • 此时 NSDefaultRunLoopMode 下的 Timer 会暂停
  • 只有添加到 NSRunLoopCommonModes 的 Timer 才会继续执行
1.3.2 Source(事件源)

Source 是 RunLoop 要处理的事件源,分为两类:

Source0:

  • 需要手动标记为待处理(signal)
  • 处理 App 内部事件,如触摸事件、performSelector 等
  • 不能主动唤醒 RunLoop

Source1:

  • 基于 mach_port,可以主动唤醒 RunLoop
  • 处理系统级事件,如端口通信、系统信号等
1.3.3 Timer(定时器)

Timer 是基于时间的触发器,必须添加到 RunLoop 才能正常工作。

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
// 添加到 RunLoop
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
1.3.4 Observer(观察者)

Observer 用于监听 RunLoop 的状态变化,可以监控以下时机:

  • RunLoop 即将进入
  • RunLoop 即将处理 Timer
  • RunLoop 即将处理 Source
  • RunLoop 即将进入休眠
  • RunLoop 被唤醒
  • RunLoop 即将退出

1.4 RunLoop 的生命周期

启动 → 进入循环 → 处理事件 → 休眠 → 唤醒 → 处理事件 → ... → 退出

关键点:

  • RunLoop 在没有事件时会进入休眠状态
  • 有事件时会自动唤醒
  • 可以通过 CFRunLoopStop() 主动停止 RunLoop

2. RunLoop 底层原理

2.1 RunLoop 的运行机制

RunLoop 的核心是一个 do-while 循环,不断检查是否有事件需要处理。

伪代码表示:

void CFRunLoopRun() {int32_t result;do {result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, seconds, returnAfterSourceHandled);} while (result != kCFRunLoopRunStopped);
}

运行流程:

  1. 通知 Observer:即将进入 RunLoop
  2. 通知 Observer:即将处理 Timer
  3. 通知 Observer:即将处理 Source0
  4. 处理 Source0
  5. 如果有 Source1 就绪,立即处理
  6. 通知 Observer:线程即将休眠
  7. 休眠,等待被唤醒
    • 端口有消息(Source1)
    • Timer 到时间
    • RunLoop 超时
    • 被手动唤醒
  8. 通知 Observer:线程被唤醒
  9. 处理唤醒时收到的事件
    • 如果是 Timer,处理 Timer 回调
    • 如果是 dispatch 到 main queue 的 block,执行 block
    • 如果是 Source1,处理 Source1
  10. 根据结果决定是否继续循环
    • 如果处理了事件,回到步骤 2
    • 如果超时,回到步骤 2
    • 如果被停止,退出循环

2.2 Mode 切换机制

RunLoop 在同一时间只能运行在一个 Mode 下,切换 Mode 需要退出当前循环,重新以新 Mode 进入。

Mode 切换的触发时机:

  • ScrollView 开始滑动:切换到 UITrackingRunLoopMode
  • ScrollView 停止滑动:切换回 NSDefaultRunLoopMode
  • 系统事件:可能切换到其他 Mode

为什么需要 Mode 切换?

  • 隔离不同场景的事件处理
  • 避免滑动时 Timer 影响滚动性能
  • 提供更精细的事件控制

2.3 Source 的分类和处理

Source0(非基于 Port)

特点:

  • 需要手动标记为待处理状态
  • 不能主动唤醒 RunLoop
  • 处理 App 内部事件

处理流程:

  1. 事件发生(如触摸事件)
  2. 系统标记 Source0 为待处理
  3. RunLoop 被其他方式唤醒(如 Source1)
  4. RunLoop 检查并处理所有待处理的 Source0
  5. 调用 Source0 的回调函数
Source1(基于 Port)

特点:

  • 基于 mach_port(进程间通信端口)
  • 可以主动唤醒 RunLoop
  • 处理系统级事件

处理流程:

  1. 系统事件发生(如端口收到消息)
  2. 内核通过 mach_port 发送消息
  3. Source1 接收到消息,唤醒 RunLoop
  4. RunLoop 处理 Source1 事件
  5. 调用 Source1 的回调函数

2.4 Timer 的实现原理

Timer 在底层是基于 mach_absolute_time() 实现的,精度很高。

Timer 的工作机制:

  1. 创建 Timer:设置时间间隔和回调
  2. 添加到 RunLoop:Timer 被添加到当前 Mode 的 Timer 列表
  3. RunLoop 检查:每次循环检查 Timer 是否到期
  4. 触发回调:如果到期,调用 Timer 的回调方法
  5. 重复执行:如果是重复 Timer,重新计算下次触发时间

Timer 的精度问题:

// Timer 不是实时的,会受到 RunLoop 的影响
// 如果 RunLoop 正在处理耗时操作,Timer 可能延迟触发
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
// 添加到 CommonModes 可以避免滑动时暂停
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

2.5 Observer 的监控机制

Observer 使用回调函数监听 RunLoop 的状态变化。

可监控的状态:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {kCFRunLoopEntry = (1UL << 0),              // 即将进入 RunLoopkCFRunLoopBeforeTimers = (1UL << 1),       // 即将处理 TimerkCFRunLoopBeforeSources = (1UL << 2),      // 即将处理 SourcekCFRunLoopBeforeWaiting = (1UL << 5),     // 即将进入休眠kCFRunLoopAfterWaiting = (1UL << 6),      // 刚从休眠中唤醒kCFRunLoopExit = (1UL << 7),               // 即将退出 RunLoopkCFRunLoopAllActivities = 0x0FFFFFFFU     // 所有状态
};

Observer 的应用:

  • 性能监控:检测主线程卡顿
  • 自动释放池:在合适的时机释放对象
  • 日志记录:跟踪 RunLoop 的运行状态

2.6 RunLoop 的休眠机制

RunLoop 的休眠不是简单的 sleep(),而是基于 mach_msg() 的休眠。

休眠原理:

  1. RunLoop 调用 mach_msg() 等待消息
  2. 如果没有消息,线程进入内核态休眠
  3. 有消息时,内核唤醒线程
  4. 线程回到用户态,继续执行

优势:

  • 真正的休眠,不占用 CPU 资源
  • 可以被精确唤醒
  • 响应速度快

3. RunLoop 源码分析

3.1 CFRunLoopRef 结构体解析

CFRunLoop 在底层是一个结构体,包含以下关键成员:

struct __CFRunLoop {CFRuntimeBase _base;pthread_mutex_t _lock;              // 锁,保证线程安全__CFPort _wakeUpPort;               // 用于唤醒 RunLoop 的端口Boolean _stopped;                   // 是否已停止Boolean _ignoreWakeUps;             // 是否忽略唤醒CFMutableSetRef _commonModes;       // 存储所有 common modesCFMutableSetRef _commonModeItems;   // 存储所有 common mode itemsCFRunLoopModeRef _currentMode;      // 当前运行的 modeCFMutableSetRef _modes;             // 存储所有 modesstruct _block_item *_blocks_head;   // 待执行的 block 链表头struct _block_item *_blocks_tail;   // 待执行的 block 链表尾CFAbsoluteTime _runTime;            // RunLoop 开始运行的时间CFAbsoluteTime _sleepTime;          // RunLoop 进入休眠的时间CFTypeRef _counterpart;             // 对应的 NSRunLoop
};

关键字段说明:

  • _currentMode:当前运行的 Mode,RunLoop 一次只能运行在一个 Mode 下
  • _modes:所有注册的 Mode 集合
  • _commonModes:所有标记为 common 的 Mode 名称集合
  • _commonModeItems:所有添加到 common modes 的 items(Source、Timer、Observer)

3.2 CFRunLoopMode 结构体解析

每个 Mode 包含该 Mode 下的所有 Source、Timer 和 Observer:

struct __CFRunLoopMode {CFRuntimeBase _base;pthread_mutex_t _lock;CFStringRef _name;                  // Mode 名称,如 "kCFRunLoopDefaultMode"Boolean _stopped;CFMutableSetRef _sources0;          // Source0 集合CFMutableSetRef _sources1;          // Source1 集合CFMutableArrayRef _observers;       // Observer 数组CFMutableArrayRef _timers;          // Timer 数组CFMutableDictionaryRef _portToV1SourceMap;  // port 到 Source1 的映射__CFPortSet _portSet;               // 所有 port 的集合CFIndex _observerMask;              // Observer 的掩码uint64_t _timerSoftDeadline;        // Timer 软截止时间uint64_t _timerHardDeadline;        // Timer 硬截止时间
};

关键字段说明:

  • _sources0_sources1:分别存储两种类型的 Source
  • _timers:存储该 Mode 下的所有 Timer
  • _observers:存储该 Mode 下的所有 Observer
  • _portToV1SourceMap:用于快速查找 port 对应的 Source1

3.3 Source/Timer/Observer 的数据结构

CFRunLoopSource
struct __CFRunLoopSource {CFRuntimeBase _base;uint32_t _bits;                     // 标志位pthread_mutex_t _lock;CFIndex _order;                     // 优先级CFMutableBagRef _runLoops;          // 该 Source 被添加到哪些 RunLoopunion {CFRunLoopSourceContext version0; // Source0 的上下文CFRunLoopSourceContext1 version1; // Source1 的上下文} _context;
};

Source0 和 Source1 的区别:

  • Source0:_context.version0 包含回调函数,需要手动标记为待处理
  • Source1:_context.version1 包含 mach_port,可以主动唤醒 RunLoop
CFRunLoopTimer
struct __CFRunLoopTimer {CFRuntimeBase _base;uint16_t _bits;                     // 标志位pthread_mutex_t _lock;CFRunLoopRef _runLoop;              // 所属的 RunLoopCFMutableSetRef _rlModes;           // 添加到哪些 modesCFAbsoluteTime _nextFireDate;       // 下次触发时间CFTimeInterval _interval;           // 时间间隔CFTimeInterval _tolerance;          // 容差uint64_t _fireTSR;                  // 触发时间戳CFIndex _order;                     // 优先级CFRunLoopTimerCallBack _callout;    // 回调函数CFRunLoopTimerContext _context;    // 上下文
};
CFRunLoopObserver
struct __CFRunLoopObserver {CFRuntimeBase _base;uint32_t _bits;                     // 标志位pthread_mutex_t _lock;CFRunLoopRef _runLoop;              // 所属的 RunLoopCFMutableSetRef _rlModes;           // 添加到哪些 modesCFOptionFlags _activities;          // 监听哪些活动CFIndex _order;                     // 优先级CFRunLoopObserverCallBack _callout; // 回调函数CFRunLoopObserverContext _context;  // 上下文
};

3.4 关键函数实现分析

CFRunLoopRun
void CFRunLoopRun(void) {int32_t result;do {result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10,  // 超时时间(几乎无限)true);   // returnAfterSourceHandled} while (result != kCFRunLoopRunStopped);
}

说明:

  • 不断调用 CFRunLoopRunInMode,直到 RunLoop 被停止
  • 使用 kCFRunLoopDefaultMode 作为默认 Mode
  • 超时时间设置为很大,几乎不会超时
CFRunLoopRunInMode

这是 RunLoop 的核心函数,主要逻辑:

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {// 1. 获取或创建 RunLoopCFRunLoopRef rl = CFRunLoopGetCurrent();// 2. 根据 modeName 查找对应的 ModeCFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);// 3. 如果 Mode 不存在,返回错误if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {return kCFRunLoopRunFinished;}// 4. 设置当前 ModeCFRunLoopModeRef previousMode = rl->_currentMode;rl->_currentMode = currentMode;// 5. 通知 Observer:即将进入 RunLoop__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);// 6. 进入主循环result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);// 7. 恢复之前的 Moderl->_currentMode = previousMode;// 8. 通知 Observer:即将退出 RunLoop__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);return result;
}
__CFRunLoopRun(核心循环)

这是 RunLoop 真正执行循环的地方:

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {int32_t retVal = 0;do {// 1. 通知 Observer:即将处理 Timer__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);// 2. 通知 Observer:即将处理 Source__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);// 3. 处理 Source0__CFRunLoopDoSources0(rl, rlm, stopAfterHandle);// 4. 检查是否有 Source1 就绪Boolean hasSource1 = __CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL, rl, rlm);if (hasSource1) {// 5. 处理 Source1__CFRunLoopDoSource1(rl, rlm, msg, msg->msgh_size, &reply);}// 6. 处理主队列的 block__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);// 7. 通知 Observer:即将进入休眠__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);// 8. 进入休眠,等待消息__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy, rl, rlm);// 9. 通知 Observer:被唤醒__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);// 10. 处理唤醒时收到的事件if (MACH_PORT_NULL != livePort) {// 处理 Source1__CFRunLoopDoSource1(rl, rlm, msg, msg->msgh_size, &reply);} else if (timers_all_fired) {// 处理 Timer__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());}// 11. 处理主队列的 block__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);// 12. 检查是否需要退出if (__CFRunLoopIsStopped(rl)) {retVal = kCFRunLoopRunStopped;} else if (rlm->_stopped) {rlm->_stopped = false;retVal = kCFRunLoopRunStopped;} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {retVal = kCFRunLoopRunFinished;}} while (0 == retVal);return retVal;
}

3.5 Common Modes 的实现机制

Common Modes 是一个特殊的概念,它不是一个真正的 Mode,而是一个占位符。

实现原理:

  1. NSRunLoopCommonModes 是一个字符串数组,包含多个 Mode 名称
  2. 当向 Common Modes 添加 item 时,实际上会添加到所有 common modes 中
  3. 当切换 Mode 时,common mode items 会自动跟随切换

源码中的处理:

// 添加 item 到 common modes
void CFRunLoopAddItemToCommonModes(CFRunLoopRef rl, CFRunLoopModeItemRef item) {// 遍历所有 common modesCFSetApplyFunction(rl->_commonModes, ^(const void *value, void *context) {CFStringRef modeName = (CFStringRef)value;CFRunLoopModeRef mode = __CFRunLoopFindMode(rl, modeName, false);if (mode) {// 将 item 添加到该 mode__CFRunLoopModeAddItem(mode, item);}}, NULL);
}

3.6 自动释放池与 RunLoop

自动释放池的创建和释放与 RunLoop 密切相关:

时机:

  • kCFRunLoopEntry 时创建自动释放池
  • kCFRunLoopBeforeWaiting 时释放旧的池并创建新池
  • kCFRunLoopExit 时释放自动释放池

源码实现:

// 在 RunLoop 进入时
if (kCFRunLoopEntry == activity) {_objc_autoreleasePoolPush();
}// 在 RunLoop 即将休眠时
if (kCFRunLoopBeforeWaiting == activity) {_objc_autoreleasePoolPop();_objc_autoreleasePoolPush();
}// 在 RunLoop 退出时
if (kCFRunLoopExit == activity) {_objc_autoreleasePoolPop();
}

这样设计的好处是:

  • 在一次 RunLoop 循环中创建的对象,在循环结束时统一释放
  • 避免内存峰值过高
  • 及时释放不需要的对象

4. RunLoop 的实际应用

4.1 主线程 RunLoop 与 UI 渲染

主线程的 RunLoop 是 iOS 应用的核心,负责处理所有 UI 相关的事件。

主线程 RunLoop 的工作流程:

  1. 触摸事件处理

    • 用户触摸屏幕 → Source1 接收事件 → 唤醒 RunLoop
    • RunLoop 处理触摸事件 → 调用 UIResponder 的方法
    • 事件沿着响应链传递
  2. UI 更新

    • 修改 UI 属性(如 frame、backgroundColor)不会立即生效
    • 修改会在当前 RunLoop 循环结束时统一渲染
    • 通过 setNeedsDisplay 标记需要重绘的视图
  3. 布局更新

    • setNeedsLayout 标记需要重新布局
    • 在 RunLoop 的 kCFRunLoopBeforeWaiting 时执行布局计算
    • 然后执行绘制操作

代码示例:

// 修改视图属性
self.view.backgroundColor = [UIColor redColor];
// 此时视图不会立即更新,而是等到 RunLoop 休眠前统一更新// 强制立即更新(不推荐,会阻塞主线程)
[self.view layoutIfNeeded];

4.2 子线程中创建 RunLoop

默认情况下,子线程没有 RunLoop,线程执行完任务后就会退出。如果需要让子线程保持存活并处理任务,需要手动创建并运行 RunLoop。

创建常驻线程的步骤:

  1. 创建线程
  2. 在线程中获取 RunLoop(会自动创建)
  3. 添加 Source 或 Timer 到 RunLoop(否则 RunLoop 会立即退出)
  4. 运行 RunLoop

代码示例:

// 创建常驻线程
@property (nonatomic, strong) NSThread *backgroundThread;- (void)createBackgroundThread {self.backgroundThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadEntry) object:nil];[self.backgroundThread start];
}- (void)threadEntry {@autoreleasepool {// 获取当前线程的 RunLoop(会自动创建)NSRunLoop *runLoop = [NSRunLoop currentRunLoop];// 添加一个 Port 作为 Source,防止 RunLoop 退出NSPort *port = [NSPort port];[runLoop addPort:port forMode:NSDefaultRunLoopMode];// 运行 RunLoop(会一直运行,直到线程退出)[runLoop run];}
}// 在常驻线程上执行任务
- (void)performTaskOnBackgroundThread:(void (^)(void))task {[self performSelector:@selector(executeTask:) onThread:self.backgroundThread withObject:task waitUntilDone:NO];
}- (void)executeTask:(void (^)(void))task {if (task) {task();}
}

4.3 使用 RunLoop 实现常驻线程

常驻线程常用于后台任务处理,如网络请求、文件操作等。

应用场景:

  • 后台下载任务
  • 定时任务执行
  • 数据同步
  • 日志记录

注意事项:

  • 必须添加 Source 或 Timer,否则 RunLoop 会立即退出
  • 使用 Port 是最简单的方式
  • 线程退出前要停止 RunLoop

4.4 NSTimer 与 RunLoop 的关系

NSTimer 必须添加到 RunLoop 才能正常工作,这是很多开发者容易忽略的点。

常见问题:

  1. Timer 不触发

    • 原因:没有添加到 RunLoop 或添加到了错误的 Mode
    • 解决:确保添加到正确的 RunLoop 和 Mode
  2. 滑动时 Timer 暂停

    • 原因:Timer 添加到了 NSDefaultRunLoopMode,滑动时切换到 UITrackingRunLoopMode
    • 解决:添加到 NSRunLoopCommonModes

代码示例:

// 方式1:使用 scheduledTimerWithTimeInterval(自动添加到当前 RunLoop 的 DefaultMode)
NSTimer *timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];// 方式2:手动创建并添加到 RunLoop
NSTimer *timer2 = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
// 添加到 DefaultMode(滑动时会暂停)
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:NSDefaultRunLoopMode];// 添加到 CommonModes(滑动时不会暂停)
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:NSRunLoopCommonModes];// 方式3:使用 GCD Timer(不依赖 RunLoop,更精确)
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{// 定时任务
});
dispatch_resume(timer);

4.5 网络请求与 RunLoop

NSURLConnection 的代理方法默认在创建连接的线程的 RunLoop 中回调。

问题场景:

  • 在子线程中创建 NSURLConnection
  • 子线程没有运行 RunLoop
  • 代理方法不会被调用

解决方案:

// 方式1:在主线程创建连接
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[connection start];// 方式2:在子线程运行 RunLoop
dispatch_async(dispatch_get_global_queue(0, 0), ^{NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];NSRunLoop *runLoop = [NSRunLoop currentRunLoop];[connection scheduleInRunLoop:runLoop forMode:NSDefaultRunLoopMode];[connection start];[runLoop run]; // 运行 RunLoop,等待回调
});// 方式3:使用 NSURLSession(推荐,不依赖 RunLoop)
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {// 处理响应
}];
[task resume];

4.6 事件响应链与 RunLoop

触摸事件的处理流程与 RunLoop 密切相关:

  1. 事件产生:用户触摸屏幕
  2. 系统捕获:IOKit 捕获触摸事件
  3. 事件传递:通过 Source1 传递给主线程 RunLoop
  4. RunLoop 唤醒:RunLoop 被唤醒
  5. 事件分发:RunLoop 处理 Source1,调用 UIApplication 的 sendEvent:
  6. 响应链传递:事件沿着响应链传递(UIApplication → UIWindow → ViewController → View)
  7. 处理事件:找到合适的响应者处理事件

代码示例:

// 重写 hitTest:withEvent: 自定义事件响应
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {// 检查是否在视图范围内if (![self pointInside:point withEvent:event]) {return nil;}// 检查子视图for (UIView *subview in [self.subviews reverseObjectEnumerator]) {CGPoint convertedPoint = [self convertPoint:point toView:subview];UIView *hitView = [subview hitTest:convertedPoint withEvent:event];if (hitView) {return hitView;}}return self;
}// 重写 touchesBegan:withEvent: 处理触摸事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {// 处理触摸开始
}

4.7 performSelector 与 RunLoop

performSelector:onThread:withObject:waitUntilDone: 依赖于目标线程的 RunLoop。

工作原理:

  • 在目标线程的 RunLoop 中添加一个 Source0
  • 当 RunLoop 运行时,会处理这个 Source0
  • 调用指定的 selector

注意事项:

  • 目标线程必须有运行中的 RunLoop
  • waitUntilDone:YES 会阻塞当前线程
  • waitUntilDone:NO 不会阻塞,但需要目标线程的 RunLoop 运行

代码示例:

// 在主线程执行(waitUntilDone:NO,不阻塞)
[self performSelectorOnMainThread:@selector(updateUI) withObject:nil waitUntilDone:NO];// 在指定线程执行
[self performSelector:@selector(doWork) onThread:self.backgroundThread withObject:nil waitUntilDone:NO];// 延迟执行(依赖于当前线程的 RunLoop)
[self performSelector:@selector(delayedAction) withObject:nil afterDelay:2.0];// 取消延迟执行
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(delayedAction) object:nil];

5. 常见应用场景

5.1 性能监控(卡顿检测)

通过监控 RunLoop 的状态,可以检测主线程的卡顿情况。

原理:

  • 添加 Observer 监听 RunLoop 的状态
  • 记录 kCFRunLoopBeforeWaitingkCFRunLoopAfterWaiting 的时间间隔
  • 如果间隔过长,说明主线程被阻塞

实现代码:

@interface RunLoopMonitor : NSObject
@property (nonatomic, assign) BOOL isMonitoring;
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) CFRunLoopActivity activity;
@end@implementation RunLoopMonitor+ (instancetype)sharedInstance {static RunLoopMonitor *instance = nil;static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{instance = [[RunLoopMonitor alloc] init];});return instance;
}- (void)startMonitoring {if (self.isMonitoring) {return;}self.isMonitoring = YES;self.semaphore = dispatch_semaphore_create(0);// 创建 ObserverCFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);// 添加到主线程 RunLoopCFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);// 在子线程中监控dispatch_async(dispatch_get_global_queue(0, 0), ^{while (self.isMonitoring) {// 等待 50ms,如果超时说明主线程可能卡顿long st = dispatch_semaphore_wait(self.semaphore, dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC));if (st != 0) {if (!self.semaphore) {self.isMonitoring = NO;self.activity = 0;break;}// 检查 RunLoop 状态if (self.activity == kCFRunLoopBeforeSources || self.activity == kCFRunLoopAfterWaiting) {// 可能发生卡顿,记录堆栈信息[self logStackInfo];}}}});
}static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {RunLoopMonitor *monitor = (__bridge RunLoopMonitor *)info;monitor.activity = activity;dispatch_semaphore_signal(monitor.semaphore);
}- (void)logStackInfo {NSArray *callStack = [NSThread callStackSymbols];NSLog(@"主线程可能卡顿,堆栈信息:\n%@", callStack);
}- (void)stopMonitoring {self.isMonitoring = NO;self.semaphore = nil;
}@end

5.2 自动释放池的时机

了解 RunLoop 与自动释放池的关系,有助于优化内存管理。

自动释放池的创建和释放时机:

  • kCFRunLoopEntry:创建自动释放池
  • kCFRunLoopBeforeWaiting:释放旧池,创建新池
  • kCFRunLoopExit:释放自动释放池

优化建议:

  • 在 RunLoop 循环中创建大量临时对象时,考虑手动创建 @autoreleasepool
  • 避免在单次 RunLoop 循环中创建过多对象

代码示例:

// 在循环中处理大量数据时,使用 @autoreleasepool
for (int i = 0; i < 10000; i++) {@autoreleasepool {// 创建临时对象NSArray *tempArray = [self processData:i];// 使用 tempArray}// tempArray 在这里已经被释放
}

5.3 图片加载优化

利用 RunLoop 的特性,可以在合适的时机加载图片,避免阻塞主线程。

优化策略:

  • kCFRunLoopBeforeWaiting 时加载图片
  • 避免在用户交互时加载图片
  • 使用 Observer 监控 RunLoop 状态

代码示例:

@interface ImageLoader : NSObject
@property (nonatomic, strong) NSMutableArray *imageLoadTasks;
@end@implementation ImageLoader- (void)setupRunLoopObserver {CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,kCFRunLoopBeforeWaiting | kCFRunLoopExit,YES,0,^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {// 在 RunLoop 即将休眠时加载图片[self loadImagesIfNeeded];});CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);CFRelease(observer);
}- (void)loadImagesIfNeeded {if (self.imageLoadTasks.count == 0) {return;}// 每次只加载一张图片,避免阻塞ImageLoadTask *task = [self.imageLoadTasks firstObject];[self.imageLoadTasks removeObjectAtIndex:0];// 加载图片[self loadImage:task];
}@end

5.4 网络请求优化

虽然现在推荐使用 NSURLSession,但了解 RunLoop 与网络请求的关系仍然有用。

优化要点:

  • 确保网络请求的回调线程有运行中的 RunLoop
  • 使用 NSURLSession 可以避免 RunLoop 相关问题
  • 在后台线程处理网络请求时,注意 RunLoop 的生命周期

5.5 后台任务处理

使用 RunLoop 实现后台任务的持续处理。

应用场景:

  • 后台数据同步
  • 日志上传
  • 缓存清理

代码示例:

@interface BackgroundTaskManager : NSObject
@property (nonatomic, strong) NSThread *backgroundThread;
@property (nonatomic, strong) NSMutableArray *taskQueue;
@end@implementation BackgroundTaskManager- (void)startBackgroundThread {self.taskQueue = [NSMutableArray array];self.backgroundThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadEntry) object:nil];[self.backgroundThread start];
}- (void)threadEntry {@autoreleasepool {NSRunLoop *runLoop = [NSRunLoop currentRunLoop];// 添加 Port,保持 RunLoop 运行NSPort *port = [NSPort port];[runLoop addPort:port forMode:NSDefaultRunLoopMode];// 添加 Timer,定期检查任务队列NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(processTasks) userInfo:nil repeats:YES];[runLoop addTimer:timer forMode:NSDefaultRunLoopMode];[runLoop run];}
}- (void)processTasks {if (self.taskQueue.count > 0) {Task *task = [self.taskQueue firstObject];[self.taskQueue removeObjectAtIndex:0];[self executeTask:task];}
}- (void)addTask:(Task *)task {[self performSelector:@selector(addTaskToQueue:) onThread:self.backgroundThread withObject:task waitUntilDone:NO];
}- (void)addTaskToQueue:(Task *)task {[self.taskQueue addObject:task];
}@end

5.6 延迟执行优化

利用 RunLoop 的特性,实现更精确的延迟执行。

场景:

  • 需要在用户停止操作后执行任务
  • 避免频繁执行某些操作
  • 实现防抖(debounce)功能

代码示例:

@interface Debouncer : NSObject
@property (nonatomic, strong) NSTimer *timer;
@end@implementation Debouncer- (void)debounce:(void (^)(void))block delay:(NSTimeInterval)delay {// 取消之前的 Timer[self.timer invalidate];// 创建新的 Timerself.timer = [NSTimer scheduledTimerWithTimeInterval:delay target:self selector:@selector(executeBlock:) userInfo:block repeats:NO];// 添加到 CommonModes,确保滑动时也能执行[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}- (void)executeBlock:(NSTimer *)timer {void (^block)(void) = timer.userInfo;if (block) {block();}[self.timer invalidate];self.timer = nil;
}@end// 使用示例
Debouncer *debouncer = [[Debouncer alloc] init];
[debouncer debounce:^{// 用户停止操作 0.5 秒后执行NSLog(@"执行搜索");
} delay:0.5];

6. 最佳实践

6.1 何时需要手动管理 RunLoop

需要手动管理的情况:

  • 创建常驻后台线程
  • 在子线程中使用 NSURLConnection(旧 API)
  • 需要在特定时机执行任务
  • 实现自定义的事件处理机制

不需要手动管理的情况:

  • 主线程(系统自动管理)
  • 使用 GCD 和 NSOperationQueue(不依赖 RunLoop)
  • 使用 NSURLSession(不依赖 RunLoop)
  • 大多数日常开发场景

6.2 RunLoop 的常见陷阱和注意事项

陷阱1:Timer 在滑动时暂停

问题:

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
// 滑动 ScrollView 时,Timer 会暂停

解决:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
陷阱2:子线程 RunLoop 立即退出

问题:

dispatch_async(dispatch_get_global_queue(0, 0), ^{NSRunLoop *runLoop = [NSRunLoop currentRunLoop];[runLoop run]; // 立即退出,因为没有 Source 或 Timer
});

解决:

dispatch_async(dispatch_get_global_queue(0, 0), ^{NSRunLoop *runLoop = [NSRunLoop currentRunLoop];NSPort *port = [NSPort port];[runLoop addPort:port forMode:NSDefaultRunLoopMode];[runLoop run]; // 现在会持续运行
});
陷阱3:performSelector 在子线程不执行

问题:

dispatch_async(dispatch_get_global_queue(0, 0), ^{[self performSelector:@selector(doWork) withObject:nil afterDelay:1.0];// doWork 不会执行,因为子线程的 RunLoop 没有运行
});

解决:

dispatch_async(dispatch_get_global_queue(0, 0), ^{NSRunLoop *runLoop = [NSRunLoop currentRunLoop];[self performSelector:@selector(doWork) withObject:nil afterDelay:1.0];[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]];
});
陷阱4:RunLoop 无法停止

问题:

// 错误的方式
CFRunLoopStop(CFRunLoopGetCurrent()); // 可能无法立即停止

解决:

// 正确的方式:使用标志位
self.shouldStop = YES;
CFRunLoopStop(CFRunLoopGetCurrent());

6.3 性能优化建议

  1. 避免在主线程 RunLoop 中执行耗时操作

    • 耗时操作会阻塞主线程,导致 UI 卡顿
    • 使用后台线程处理耗时任务
  2. 合理使用 CommonModes

    • 只有需要在滑动时也执行的任务才添加到 CommonModes
    • 避免添加过多任务到 CommonModes,影响滑动性能
  3. 及时移除不需要的 Observer、Timer、Source

    • 避免内存泄漏
    • 减少 RunLoop 的负担
  4. 使用 GCD 替代 RunLoop(如果可能)

    • GCD 更简单,性能更好
    • 不依赖 RunLoop,更可靠

6.4 调试技巧

  1. 打印 RunLoop 信息
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
NSLog(@"Current RunLoop: %@", runLoop);
NSLog(@"Current Mode: %@", CFRunLoopCopyCurrentMode(runLoop));
  1. 监控 RunLoop 状态
// 添加 Observer 监控所有状态变化
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {switch (activity) {case kCFRunLoopEntry:NSLog(@"RunLoop 进入");break;case kCFRunLoopBeforeTimers:NSLog(@"即将处理 Timer");break;case kCFRunLoopBeforeSources:NSLog(@"即将处理 Source");break;case kCFRunLoopBeforeWaiting:NSLog(@"即将进入休眠");break;case kCFRunLoopAfterWaiting:NSLog(@"被唤醒");break;case kCFRunLoopExit:NSLog(@"RunLoop 退出");break;}});
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
  1. 检查 Timer 是否正确添加
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
NSLog(@"RunLoop modes: %@", [runLoop currentMode]);

7. 实际代码示例

7.1 创建常驻线程的完整示例

// .h 文件
@interface PersistentThread : NSObject
- (void)start;
- (void)stop;
- (void)performTask:(void (^)(void))task;
@end// .m 文件
@interface PersistentThread ()
@property (nonatomic, strong) NSThread *thread;
@property (nonatomic, strong) NSPort *port;
@property (nonatomic, assign) BOOL shouldStop;
@end@implementation PersistentThread- (void)start {if (self.thread && self.thread.isExecuting) {return;}self.shouldStop = NO;self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];[self.thread start];
}- (void)threadMain {@autoreleasepool {NSRunLoop *runLoop = [NSRunLoop currentRunLoop];// 添加 Port,保持 RunLoop 运行self.port = [NSPort port];[runLoop addPort:self.port forMode:NSDefaultRunLoopMode];// 运行 RunLoopwhile (!self.shouldStop) {@autoreleasepool {[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];}}// 清理[runLoop removePort:self.port forMode:NSDefaultRunLoopMode];self.port = nil;}
}- (void)stop {self.shouldStop = YES;if (self.port) {// 通过 Port 发送消息唤醒 RunLoop[self.port sendBeforeDate:[NSDate date] components:nil from:nil reserved:0];}
}- (void)performTask:(void (^)(void))task {if (!task || !self.thread || !self.thread.isExecuting) {return;}[self performSelector:@selector(executeTask:) onThread:self.thread withObject:task waitUntilDone:NO];
}- (void)executeTask:(void (^)(void))task {if (task) {task();}
}@end

7.2 使用 RunLoop Observer 监控性能

@interface PerformanceMonitor : NSObject
+ (instancetype)sharedInstance;
- (void)startMonitoring;
- (void)stopMonitoring;
@end@interface PerformanceMonitor ()
@property (nonatomic, assign) BOOL isMonitoring;
@property (nonatomic, strong) CFRunLoopObserverRef observer;
@property (nonatomic, assign) NSTimeInterval lastActivityTime;
@end@implementation PerformanceMonitor+ (instancetype)sharedInstance {static PerformanceMonitor *instance = nil;static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{instance = [[PerformanceMonitor alloc] init];});return instance;
}- (void)startMonitoring {if (self.isMonitoring) {return;}self.isMonitoring = YES;self.lastActivityTime = CACurrentMediaTime();// 创建 ObserverCFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};self.observer = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallback,&context);// 添加到主线程 RunLoopCFRunLoopAddObserver(CFRunLoopGetMain(), self.observer, kCFRunLoopCommonModes);
}static void runLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {PerformanceMonitor *monitor = (__bridge PerformanceMonitor *)info;NSTimeInterval currentTime = CACurrentMediaTime();NSTimeInterval interval = currentTime - monitor.lastActivityTime;if (interval > 0.016) { // 超过一帧的时间(60fps = 16.67ms)NSLog(@"⚠️ 主线程可能卡顿,间隔: %.3f 秒", interval);}monitor.lastActivityTime = currentTime;
}- (void)stopMonitoring {if (!self.isMonitoring) {return;}self.isMonitoring = NO;if (self.observer) {CFRunLoopRemoveObserver(CFRunLoopGetMain(), self.observer, kCFRunLoopCommonModes);CFRelease(self.observer);self.observer = NULL;}
}- (void)dealloc {[self stopMonitoring];
}@end

7.3 自定义 Source 的实现

@interface CustomSource : NSObject
- (void)fire;
@end@interface CustomSource ()
{CFRunLoopSourceRef _source;CFRunLoopRef _runLoop;
}
@end@implementation CustomSource- (instancetype)init {self = [super init];if (self) {[self setupSource];}return self;
}- (void)setupSource {// 创建 Source0 的上下文CFRunLoopSourceContext context = {0};context.info = (__bridge void *)self;context.perform = &sourcePerform;context.schedule = &sourceSchedule;context.cancel = &sourceCancel;// 创建 Source_source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
}static void sourcePerform(void *info) {CustomSource *source = (__bridge CustomSource *)info;NSLog(@"CustomSource 被触发");// 执行自定义逻辑
}static void sourceSchedule(void *info, CFRunLoopRef rl, CFRunLoopMode mode) {CustomSource *source = (__bridge CustomSource *)info;source->_runLoop = rl;NSLog(@"CustomSource 被调度到 RunLoop");
}static void sourceCancel(void *info, CFRunLoopRef rl, CFRunLoopMode mode) {CustomSource *source = (__bridge CustomSource *)info;source->_runLoop = NULL;NSLog(@"CustomSource 被取消");
}- (void)addToRunLoop:(NSRunLoop *)runLoop forMode:(NSString *)mode {CFRunLoopAddSource([runLoop getCFRunLoop], _source, (__bridge CFStringRef)mode);
}- (void)removeFromRunLoop:(NSRunLoop *)runLoop forMode:(NSString *)mode {CFRunLoopRemoveSource([runLoop getCFRunLoop], _source, (__bridge CFStringRef)mode);
}- (void)fire {// 标记 Source 为待处理CFRunLoopSourceSignal(_source);// 如果有 RunLoop,唤醒它if (_runLoop) {CFRunLoopWakeUp(_runLoop);}
}- (void)dealloc {if (_source) {CFRelease(_source);}
}@end

7.4 常见问题的解决方案

问题1:Timer 在后台不工作
// 解决方案:使用后台任务和本地通知
- (void)setupBackgroundTimer {// 注册后台任务[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{// 任务过期处理}];// 使用 NSTimer(在后台有限时间内有效)NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:60.0 target:self selector:@selector(backgroundTask) userInfo:nil repeats:YES];[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
问题2:网络请求在子线程回调失败
// 解决方案:确保子线程的 RunLoop 运行
- (void)networkRequestInBackground {dispatch_async(dispatch_get_global_queue(0, 0), ^{NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://api.example.com"]];// 使用 NSURLConnection(需要 RunLoop)NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];NSRunLoop *runLoop = [NSRunLoop currentRunLoop];[connection scheduleInRunLoop:runLoop forMode:NSDefaultRunLoopMode];[connection start];// 运行 RunLoop,等待回调[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:30.0]];});
}// 更好的方案:使用 NSURLSession(不依赖 RunLoop)
- (void)networkRequestWithSession {NSURLSession *session = [NSURLSession sharedSession];NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"https://api.example.com"]completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {// 处理响应}];[task resume];
}
问题3:performSelector 延迟执行不准确
// 问题:performSelector:withObject:afterDelay: 依赖于 RunLoop,可能不准确
[self performSelector:@selector(doSomething) withObject:nil afterDelay:1.0];// 解决方案1:使用 GCD(更准确)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{[self doSomething];
});// 解决方案2:使用 NSTimer(添加到 CommonModes)
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(doSomething) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

参考资料

  • Apple Developer Documentation - Run Loops
  • CFRunLoop 源码
http://www.dtcms.com/a/607088.html

相关文章:

  • 如何来建设网站青岛建设集团招工信息网站
  • 1688采购系统:批量下单自动下单功能实现
  • 网站服务器cpu占用多少要升级工业信息化部网站备案
  • 手机网站模块一直免费的服务器下载
  • 实战:爬取汽车之家车型参数对比的技术指南
  • 网站后台怎么控制护理专业简历制作
  • DP 转光纤:捷米特 JM-DP-FIBER-S-A/B-R 转换器汽车焊接产线应用案例
  • 驭见未来,服务致胜:2025中国汽车终端服务体验洞察报告
  • 京东商品评论 API 返回数据解析指南:从嵌套 JSON 到结构化评论信息
  • 给别人开发一个网站多少钱大型外贸商城网站建设
  • 对于数据结构:链式二叉树的超详细保姆级解析—上
  • 石家庄红酒公司 网站建设资讯门户网站 dede
  • 【MySQL】内外链接和数据库索引
  • HOT100题打卡第38天——贪心算法
  • 力扣热题100道前55道,内容和力扣官方稍有不同,记录了本人的一些独特的解法
  • RDP登录事件详细溯源分析脚本(兼容Windows PowerShell版本)
  • 贪心算法实验1
  • 怎样做一个网站电子商务平台的类型
  • 好的网站建设公司哪家好北京优化推广公司
  • 易语言模块反编译与破解技术解析 | 深入理解反编译的原理与应用
  • 网站开发是哪个营销方案策划书
  • 用ps做一份网站小程序在线制作模板
  • Vite 7 + React 19 升级清单
  • 微网站怎么建设wordpress餐饮
  • 中国建设银行社保卡网站wordpress看板猫
  • 动易网站中添加邮箱seo推广主要做什么
  • 网站建设教程吧百度收录入口提交
  • 密度估计与人群计数的深度学习方法综述
  • 坪地网站建设游戏网页设计
  • Spring Data JAP中Pageable对象如何从1开始分页,而不是从0开始