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
相关推荐
tangweiguo0305198715 小时前
SwiftUI布局完全指南:从入门到精通
ios·swift
T1an-119 小时前
最右IOS岗一面
ios
坏小虎1 天前
Expo 快速创建 Android/iOS 应用开发指南
android·ios·rn·expo
光影少年1 天前
Android和iOS原生开发的基础知识对RN开发的重要性,RN打包发布时原生端需要做哪些配置?
android·前端·react native·react.js·ios
北京自在科技1 天前
Find My 修复定位 BUG,AirTag 安全再升级
ios·findmy·airtag
Digitally1 天前
如何不用 USB 线将 iPhone 照片传到电脑?
ios·电脑·iphone
Sim14802 天前
iPhone将内置本地大模型,手机端AI实现0 token成本时代来临?
人工智能·ios·智能手机·iphone
Digitally2 天前
如何将 iPad 上的照片传输到 U 盘(4 种解决方案)
ios·ipad
报错小能手2 天前
ios开发方向——swift并发进阶核心 @MainActor 与 DispatchQueue.main 解析
开发语言·ios·swift
LcGero2 天前
Cocos Creator 业务与原生通信详解
android·ios·cocos creator·游戏开发·jsb