RunLoop Mode 深度剖析:为什么滚动时 Timer 会“失效“?

引言

在 iOS 开发中,你一定遇到过这个经典问题:

页面上有一个 NSTimer,正常情况下每秒触发一次。但是当你滑动 UIScrollView 时,Timer 突然就不触发了,手指一松开又恢复正常。

这个现象背后的原因就是 RunLoop Mode 的切换机制。


一、什么是 RunLoop Mode?

1.1 Mode 的定义

从源码看 CFRunLoopMode 的结构:

c 复制代码
// CFRunLoop.c
struct __CFRunLoopMode {
    CFStringRef _name;              // Mode 名称,如 "kCFRunLoopDefaultMode"
    Boolean _stopped;               // 是否已停止
    
    CFMutableSetRef _sources0;      // Source0 集合(非端口事件)
    CFMutableSetRef _sources1;      // Source1 集合(端口事件)
    CFMutableArrayRef _observers;   // Observer 数组
    CFMutableArrayRef _timers;      // Timer 数组
    
    CFMutableDictionaryRef _portToV1SourceMap;  // 端口到 Source1 的映射
    mach_port_t _portSet;           // 端口集合
    CFIndex _observerMask;          // Observer 掩码
    
    // ... 其他字段
};

Mode 本质上是一个"容器",装着一组 Source、Timer、Observer。

1.2 RunLoop 与 Mode 的关系

c 复制代码
struct __CFRunLoop {
    CFMutableSetRef _modes;          // 所有 Mode 的集合
    CFRunLoopModeRef _currentMode;   // 当前正在运行的 Mode
    CFMutableSetRef _commonModes;    // Common Mode 名称集合
    CFMutableSetRef _commonModeItems; // Common Mode 中的所有 Item
    
    // ... 其他字段
};

关系图示:

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                           CFRunLoop                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   _currentMode ──▶ 当前只能运行一个 Mode                            │
│                                                                     │
│   _modes (集合):                                                    │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                                                             │   │
│   │  ┌─────────────────┐  ┌─────────────────┐                  │   │
│   │  │ DefaultMode     │  │ TrackingMode    │   ...            │   │
│   │  ├─────────────────┤  ├─────────────────┤                  │   │
│   │  │ • sources0      │  │ • sources0      │                  │   │
│   │  │ • sources1      │  │ • sources1      │                  │   │
│   │  │ • timers        │  │ • timers        │                  │   │
│   │  │ • observers     │  │ • observers     │                  │   │
│   │  └─────────────────┘  └─────────────────┘                  │   │
│   │                                                             │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│   _commonModes: { "kCFRunLoopDefaultMode", "UITrackingRunLoopMode" }│
│   _commonModeItems: { 被标记为 Common 的 Timer/Source/Observer }    │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

二、系统定义的 Mode

2.1 五种常见 Mode

Mode 名称 说明 使用场景
kCFRunLoopDefaultMode 默认模式 App 默认状态,无特殊交互
UITrackingRunLoopMode 追踪模式 滑动 ScrollView 时
kCFRunLoopCommonModes 通用模式(伪模式) 包含多个 Mode 的集合
UIInitializationRunLoopMode 启动模式 App 启动时,启动完成后不再使用
GSEventReceiveRunLoopMode 系统事件模式 接收系统内部事件

2.2 Mode 的代码定义

objc 复制代码
// Foundation
NSString * const NSDefaultRunLoopMode = @"kCFRunLoopDefaultMode";
NSString * const NSRunLoopCommonModes = @"kCFRunLoopCommonModes";

// UIKit
NSString * const UITrackingRunLoopMode = @"UITrackingRunLoopMode";

2.3 打印主线程 RunLoop 的所有 Mode

objc 复制代码
- (void)printMainRunLoopModes {
    CFRunLoopRef mainLoop = CFRunLoopGetMain();
    CFArrayRef allModes = CFRunLoopCopyAllModes(mainLoop);
    
    NSLog(@"=== 主线程 RunLoop 的所有 Mode ===");
    for (int i = 0; i < CFArrayGetCount(allModes); i++) {
        CFStringRef mode = CFArrayGetValueAtIndex(allModes, i);
        NSLog(@"Mode %d: %@", i, mode);
    }
    CFRelease(allModes);
}

输出:

复制代码
=== 主线程 RunLoop 的所有 Mode ===
Mode 0: UITrackingRunLoopMode
Mode 1: GSEventReceiveRunLoopMode
Mode 2: kCFRunLoopDefaultMode
Mode 3: kCFRunLoopCommonModes

三、Mode 切换机制

3.1 核心规则:同一时刻只能运行一个 Mode

看源码中的关键逻辑:

c 复制代码
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, 
                            CFStringRef modeName,  // 指定要运行的 Mode
                            CFTimeInterval seconds, 
                            Boolean returnAfterSourceHandled) {
    
    // 1. 根据 modeName 查找对应的 Mode 对象
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    
    // 2. 如果 Mode 不存在,或者 Mode 中没有任何 Source/Timer,直接返回
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        return kCFRunLoopRunFinished;
    }
    
    // 3. ★ 设置当前 Mode ★
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
    
    // 4. 运行这个 Mode
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    // 5. 恢复之前的 Mode
    rl->_currentMode = previousMode;
    
    return result;
}

关键点:rl->_currentMode = currentMode;

这意味着:RunLoop 在某一时刻只会处理当前 Mode 中的事件,其他 Mode 中的事件会被"忽略"。

3.2 Mode 切换时机

复制代码
┌──────────────────────────────────────────────────────────────────┐
│                      Mode 切换流程图                              │
└──────────────────────────────────────────────────────────────────┘

  用户操作 App                          系统自动切换 Mode
       │                                      │
       ▼                                      ▼
 ┌───────────┐                         ┌───────────────┐
 │  正常状态  │◀──────────────────────▶│ DefaultMode   │
 └───────────┘                         └───────────────┘
       │                                      │
       │ 手指触摸屏幕                           │ 系统检测到滚动手势
       │ 开始滑动 ScrollView                   │
       ▼                                      ▼
 ┌───────────┐                         ┌───────────────┐
 │  滑动状态  │◀──────────────────────▶│ TrackingMode  │
 └───────────┘                         └───────────────┘
       │                                      │
       │ 手指离开屏幕                           │ 滚动结束
       │                                      │
       ▼                                      ▼
 ┌───────────┐                         ┌───────────────┐
 │  正常状态  │◀──────────────────────▶│ DefaultMode   │
 └───────────┘                         └───────────────┘

四、Timer 失效问题详解

4.1 问题复现

objc 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建一个 Timer,添加到 DefaultMode
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 
                                                 repeats:YES 
                                                   block:^(NSTimer *timer) {
        static int count = 0;
        NSLog(@"Timer 触发: %d, 当前 Mode: %@", ++count, 
              [[NSRunLoop currentRunLoop] currentMode]);
    }];
    
    // 添加一个 ScrollView
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
    scrollView.contentSize = CGSizeMake(self.view.bounds.size.width, 2000);
    scrollView.backgroundColor = [UIColor lightGrayColor];
    [self.view addSubview:scrollView];
}

现象:

复制代码
Timer 触发: 1, 当前 Mode: kCFRunLoopDefaultMode
Timer 触发: 2, 当前 Mode: kCFRunLoopDefaultMode
Timer 触发: 3, 当前 Mode: kCFRunLoopDefaultMode
// 开始滑动... Timer 停止触发
// 停止滑动后
Timer 触发: 4, 当前 Mode: kCFRunLoopDefaultMode
Timer 触发: 5, 当前 Mode: kCFRunLoopDefaultMode

4.2 原因分析

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                     Timer 失效原因图解                               │
└─────────────────────────────────────────────────────────────────────┘

【正常状态】
┌─────────────────────────────────────────┐
│            DefaultMode (当前)            │
├─────────────────────────────────────────┤
│  timers: [Timer A] ◀── 会被处理         │
│  sources: [...]                         │
│  observers: [...]                       │
└─────────────────────────────────────────┘

【滑动状态】
┌─────────────────────────────────────────┐
│            DefaultMode                   │
├─────────────────────────────────────────┤
│  timers: [Timer A] ◀── 被忽略!         │  ← RunLoop 不再处理这个 Mode
│  sources: [...]                         │
│  observers: [...]                       │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│         TrackingMode (当前)              │
├─────────────────────────────────────────┤
│  timers: [] ◀── 空的,没有 Timer        │  ← RunLoop 只处理这个 Mode
│  sources: [滚动事件 Source]              │
│  observers: [...]                       │
└─────────────────────────────────────────┘

Timer A 在 DefaultMode 中,但 RunLoop 当前运行在 TrackingMode,
所以 Timer A 不会被触发!

4.3 源码层面的验证

__CFRunLoopDoTimers 的实现:

c 复制代码
static Boolean __CFRunLoopDoTimers(CFRunLoopRef rl, 
                                   CFRunLoopModeRef rlm,  // 当前 Mode
                                   uint64_t limitTSR) {
    Boolean timerHandled = false;
    CFMutableArrayRef timers = NULL;
    
    // ★ 只遍历当前 Mode 的 timers ★
    for (CFIndex idx = 0, cnt = rlm->_timers ? CFArrayGetCount(rlm->_timers) : 0; 
         idx < cnt; 
         idx++) {
        CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(rlm->_timers, idx);
        
        if (__CFIsValid(rlt) && !__CFRunLoopTimerIsFiring(rlt)) {
            if (rlt->_fireTSR <= limitTSR) {
                // 触发 Timer
                // ...
            }
        }
    }
    
    return timerHandled;
}

关键:rlm->_timers 只包含当前 Mode 的 Timer,其他 Mode 的 Timer 根本不会被遍历到!


五、解决方案:CommonModes

5.1 什么是 CommonModes?

kCFRunLoopCommonModes 并不是一个真正的 Mode,而是一个"占位符"或"标记"。

当你把 Timer/Source/Observer 添加到 CommonModes 时,实际上是把它添加到所有被标记为 "Common" 的 Mode 中。

c 复制代码
// 查看哪些 Mode 被标记为 Common
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
    // 将 modeName 添加到 _commonModes 集合
    CFSetAddValue(rl->_commonModes, modeName);
    
    // 将 _commonModeItems 中的所有 Item 同步到这个新的 Common Mode
    // ...
}

默认情况下,主线程的 _commonModes 包含:

objc 复制代码
- (void)printCommonModes {
    CFRunLoopRef mainLoop = CFRunLoopGetMain();
    NSLog(@"Common Modes: %@", (__bridge NSSet *)CFRunLoopCopyCommonModes(mainLoop));
}

// 输出:
// Common Modes: {
//     UITrackingRunLoopMode,
//     kCFRunLoopDefaultMode
// }

5.2 添加 Timer 到 CommonModes

方法一:使用 NSRunLoop API

objc 复制代码
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 
                                        repeats:YES 
                                          block:^(NSTimer *timer) {
    NSLog(@"Timer 触发");
}];

// 添加到 CommonModes
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

方法二:使用 scheduledTimer + 手动调整

objc 复制代码
// scheduledTimer 默认添加到 DefaultMode
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 
                                                 repeats:YES 
                                                   block:^(NSTimer *timer) {
    NSLog(@"Timer 触发");
}];

// 从 DefaultMode 移除,添加到 CommonModes
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

5.3 源码分析:添加到 CommonModes 的过程

c 复制代码
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {
    
    if (modeName == kCFRunLoopCommonModes) {
        // ★ 如果是 CommonModes ★
        
        // 1. 获取所有 Common Mode 的名称
        CFSetRef set = rl->_commonModes;
        
        // 2. 把 Timer 添加到 _commonModeItems
        CFSetAddValue(rl->_commonModeItems, rlt);
        
        // 3. 遍历所有 Common Mode,把 Timer 添加到每个 Mode
        CFTypeRef modes[256];
        CFIndex cnt = CFSetGetCount(set);
        CFSetGetValues(set, modes);
        
        for (CFIndex i = 0; i < cnt; i++) {
            CFStringRef mName = (CFStringRef)modes[i];
            CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, mName, true);
            if (rlm) {
                // 添加到这个 Mode 的 timers 数组
                CFMutableArrayRef timers = rlm->_timers;
                CFArrayAppendValue(timers, rlt);
            }
        }
        
    } else {
        // 普通 Mode,只添加到指定的 Mode
        CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
        if (rlm) {
            CFMutableArrayRef timers = rlm->_timers;
            CFArrayAppendValue(timers, rlt);
        }
    }
}

5.4 图解 CommonModes 工作原理

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                  CommonModes 工作原理                                │
└─────────────────────────────────────────────────────────────────────┘

添加 Timer 到 CommonModes:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
                                              │
                                              ▼
                    ┌─────────────────────────────────────────┐
                    │      检测 modeName == CommonModes       │
                    └─────────────────────────────────────────┘
                                              │
                    ┌─────────────────────────┴─────────────────────────┐
                    │                                                   │
                    ▼                                                   ▼
    ┌───────────────────────────────┐           ┌───────────────────────────────┐
    │  添加到 _commonModeItems      │           │  遍历 _commonModes 集合        │
    │                               │           │  {DefaultMode, TrackingMode}  │
    └───────────────────────────────┘           └───────────────────────────────┘
                                                            │
                        ┌───────────────────────────────────┴───────────────────────────────────┐
                        │                                                                       │
                        ▼                                                                       ▼
          ┌─────────────────────────────┐                             ┌─────────────────────────────┐
          │       DefaultMode           │                             │      TrackingMode           │
          ├─────────────────────────────┤                             ├─────────────────────────────┤
          │  timers: [Timer] ◀── 添加   │                             │  timers: [Timer] ◀── 添加   │
          │  sources: [...]             │                             │  sources: [滚动事件]        │
          │  observers: [...]           │                             │  observers: [...]           │
          └─────────────────────────────┘                             └─────────────────────────────┘

现在无论 RunLoop 运行在哪个 Mode,Timer 都能被触发!

六、完整解决方案对比

6.1 方案一:添加到 CommonModes(推荐)

objc 复制代码
// ✅ 推荐方式
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 
                                        repeats:YES 
                                          block:^(NSTimer *timer) {
    NSLog(@"Timer 触发, Mode: %@", [[NSRunLoop currentRunLoop] currentMode]);
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

输出(滑动时也会触发):

复制代码
Timer 触发, Mode: kCFRunLoopDefaultMode
Timer 触发, Mode: kCFRunLoopDefaultMode
Timer 触发, Mode: UITrackingRunLoopMode  ← 滑动时
Timer 触发, Mode: UITrackingRunLoopMode  ← 滑动时
Timer 触发, Mode: kCFRunLoopDefaultMode

6.2 方案二:使用 GCD Timer

GCD Timer 不依赖 RunLoop:

objc 复制代码
// ✅ GCD Timer 不受 Mode 影响
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, ^{
    NSLog(@"GCD Timer 触发");
});

dispatch_resume(timer);

// 保持引用
self.gcdTimer = timer;

如果是与屏幕刷新相关的定时任务:

objc 复制代码
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self 
                                                         selector:@selector(update:)];
// 添加到 CommonModes
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

- (void)update:(CADisplayLink *)link {
    NSLog(@"帧刷新");
}

6.4 方案对比

方案 优点 缺点 适用场景
CommonModes 简单直接 稍微影响滑动性能 大多数场景
GCD Timer 精度高,不受 Mode 影响 需要手动管理生命周期 精确计时
CADisplayLink 与屏幕刷新同步 回调频率固定 动画、游戏

七、深入:自定义 Mode

7.1 创建自定义 Mode

objc 复制代码
// 定义自定义 Mode 名称
static NSString * const MyCustomRunLoopMode = @"com.example.MyCustomMode";

- (void)testCustomMode {
    // 创建 Timer
    NSTimer *timer = [NSTimer timerWithTimeInterval:0.5 
                                            repeats:YES 
                                              block:^(NSTimer *timer) {
        NSLog(@"自定义 Mode 中的 Timer 触发");
    }];
    
    // 添加到自定义 Mode
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:MyCustomRunLoopMode];
    
    // 显式运行自定义 Mode
    // 注意:这会阻塞当前线程,直到 Mode 退出
    [[NSRunLoop currentRunLoop] runMode:MyCustomRunLoopMode 
                             beforeDate:[NSDate dateWithTimeIntervalSinceNow:5]];
}

7.2 将自定义 Mode 加入 CommonModes

objc 复制代码
// 将自定义 Mode 标记为 Common
CFRunLoopAddCommonMode(CFRunLoopGetCurrent(), 
                       (__bridge CFStringRef)MyCustomRunLoopMode);

// 现在添加到 CommonModes 的 Item 也会被添加到 MyCustomRunLoopMode

八、实际应用案例

8.1 案例:图片轮播器不受滑动影响

objc 复制代码
@interface ImageCarouselView : UIView
@property (nonatomic, strong) NSTimer *autoScrollTimer;
@property (nonatomic, strong) UIScrollView *scrollView;
@end

@implementation ImageCarouselView

- (void)setupAutoScroll {
    // 使用 CommonModes,滑动时也能自动轮播
    self.autoScrollTimer = [NSTimer timerWithTimeInterval:3.0 
                                                  repeats:YES 
                                                    block:^(NSTimer *timer) {
        [self scrollToNextPage];
    }];
    
    [[NSRunLoop currentRunLoop] addTimer:self.autoScrollTimer 
                                 forMode:NSRunLoopCommonModes];
}

- (void)scrollToNextPage {
    CGFloat nextOffset = self.scrollView.contentOffset.x + self.scrollView.bounds.size.width;
    if (nextOffset >= self.scrollView.contentSize.width) {
        nextOffset = 0;
    }
    [self.scrollView setContentOffset:CGPointMake(nextOffset, 0) animated:YES];
}

@end

8.2 案例:滑动时暂停 Timer(有时这才是需求)

有时候你希望滑动时 Timer 暂停,使用 DefaultMode 反而是正确的选择:

objc 复制代码
// 只在 DefaultMode 运行,滑动时自动暂停
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 
                                                 repeats:YES 
                                                   block:^(NSTimer *timer) {
    // 这个 Timer 在滑动时会自动暂停
    // 适用于:视频播放进度更新等场景
    [self updatePlaybackProgress];
}];

8.3 案例:监听 Mode 切换

objc 复制代码
- (void)observeRunLoopModeChange {
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(
        kCFAllocatorDefault,
        kCFRunLoopEntry | kCFRunLoopExit,
        YES,
        0,
        ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
            
            if (activity == kCFRunLoopEntry) {
                NSLog(@"🟢 进入 Mode: %@", mode);
            } else if (activity == kCFRunLoopExit) {
                NSLog(@"🔴 退出 Mode: %@", mode);
            }
            
            CFRelease(mode);
        }
    );
    
    // 添加到 CommonModes 以观察所有 Mode 的切换
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}

滑动 ScrollView 时的输出:

复制代码
🟢 进入 Mode: kCFRunLoopDefaultMode
// 开始滑动
🔴 退出 Mode: kCFRunLoopDefaultMode
🟢 进入 Mode: UITrackingRunLoopMode
// 停止滑动
🔴 退出 Mode: UITrackingRunLoopMode
🟢 进入 Mode: kCFRunLoopDefaultMode

九、总结

核心要点

复制代码
┌────────────────────────────────────────────────────────────────────┐
│                    RunLoop Mode 核心要点                            │
├────────────────────────────────────────────────────────────────────┤
│                                                                    │
│  1. Mode 是什么?                                                   │
│     • 一个容器,装着 Source、Timer、Observer                        │
│     • RunLoop 同一时刻只能运行在一个 Mode                           │
│                                                                    │
│  2. 为什么滑动时 Timer 失效?                                        │
│     ┌────────────────────────────────────────┐                     │
│     │  滑动时 Mode 切换:                       │                     │
│     │  DefaultMode ──▶ TrackingMode           │                     │
│     │                                         │                     │
│     │  Timer 在 DefaultMode 中                 │                     │
│     │  RunLoop 只处理 TrackingMode 的事件      │                     │
│     │  所以 Timer 不触发                       │                     │
│     └────────────────────────────────────────┘                     │
│                                                                    │
│  3. 如何解决?                                                      │
│     • 添加到 CommonModes(推荐)                                    │
│     • 使用 GCD Timer                                               │
│     • 使用 CADisplayLink + CommonModes                             │
│                                                                    │
│  4. CommonModes 原理                                               │
│     • 不是真正的 Mode,是一个"标记"                                 │
│     • 添加到 CommonModes = 添加到所有被标记为 Common 的 Mode        │
│     • 默认 Common Modes: DefaultMode + TrackingMode                │
│                                                                    │
│  5. 主要 Mode 列表                                                  │
│     • kCFRunLoopDefaultMode  - 默认状态                            │
│     • UITrackingRunLoopMode  - 滑动状态                            │
│     • kCFRunLoopCommonModes  - 通用标记                            │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

一句话总结

RunLoop 在同一时刻只能运行在一个 Mode,滑动时会切换到 TrackingMode,导致 DefaultMode 中的 Timer 不触发。解决方案是将 Timer 添加到 CommonModes,这样它会被自动添加到所有 Common Mode 中。


参考资料

  1. CFRunLoop.c 源码
  2. Threading Programming Guide - Run Loops
  3. 深入理解 RunLoop - ibireme
相关推荐
QuantumLeap丶2 小时前
《Flutter全栈开发实战指南:从零到高级》- 21 -响应式设计与适配
android·javascript·flutter·ios·前端框架
2501_915106322 小时前
Charles抓包怎么用 Charles抓包工具详细教程、网络调试方法、HTTPS配置与手机抓包实战
网络·ios·智能手机·小程序·https·uni-app·webview
00后程序员张3 小时前
Fastlane 结合 开心上架,构建跨平台可发布的 iOS 自动化流水线实践
android·运维·ios·小程序·uni-app·自动化·iphone
wjm0410063 小时前
秋招ios面试 -- 真题篇(三)
ios·面试·职场和发展
ii_best3 小时前
ios脚本开发工具安装按键精灵uncOver越狱教程ios14以及以下系统
ios·自动化·编辑器
游戏开发爱好者83 小时前
iOS 性能测试的工程化方法,构建从底层诊断到真机监控的多工具测试体系
android·ios·小程序·https·uni-app·iphone·webview
2501_916008894 小时前
iOS App 混淆的真实世界指南,从构建到成品 IPA 的安全链路重塑
android·安全·ios·小程序·uni-app·cocoa·iphone
1024小神4 小时前
使用AVFoundation实现二维码识别的角点坐标和区域
开发语言·数码相机·ios·swift
Sheffi664 小时前
iOS Crash 本质与捕获修复方案
macos·ios·cocoa