iOS 底层原理 runloop

runloop

RunLoop 本质是一个事件循环,让线程不执行完就退出;没有 RunLoop 的子线程,任务执行完毕后立刻销毁。

每个线程都可以拥有一个 RunLoop,但它不是"自带"的,而是"首次访问时才创建"的。主线程的 RunLoop 由系统自动创建并启动,子线程的 RunLoop 需要开发者手动触发创建,并且必须手动调用 run 方法才能让它开始工作。

获取runloop对象。

ini 复制代码
    // OC对象
    NSRunLoop *mainLoop = [NSRunLoop mainRunLoop];
    NSRunLoop *mainLoop1 = [NSRunLoop currentRunLoop];
    // C语言的CoreFoundation
    CFRunLoopRef loop2 = [mainLoop getCFRunLoop];

RunLoop 5 个核心对象(CFRunLoop 源码结构体)

比如默认的定时器,不添加runloop的UITrackingMode,在滑动UIScrollView的时候,就不会执行。因为UIScrollView滑动的时候,渲染的同时,又需要针对每个像素的移动进行回调。

因此为了页面不卡顿,提供了函数的优先级功能。默认优先级就是kCFRunLoopDefaultMode。而要任何时刻都不被影响,则UITrackingRunLoopMode。

  1. CFRunLoopRef:RunLoop 本体,里面包含 mode 数组、当前 mode

  2. CFRunLoopModeRef :运行模式,同一时刻只能处于一个 Mode。切换 Mode = 退出当前循环,重新进入新 Mode

    • 常用 mode:

      • kCFRunLoopDefaultMode:默认模式,日常 App 运行(滑动 UIScrollView 时会被切走)
      • UITrackingRunLoopMode:滚动模式,滚动时进入这个 mode,保证滑动流畅
      • kCFRunLoopCommonModes不是真实 Mode,是 mode 集合标记,把 default+tracking 标记为 common,添加到 common 的 timer/observer 会在两个 mode 都生效
  3. CFRunLoopSourceRef:事件源(输入源)

    • Source0:非内核事件,App 内部事件(UI 点击、performSelector:onThread,需要手动标记唤醒 RunLoop)
    • Source1:内核端口事件,由内核触发,Mach 端口驱动,收到消息直接唤醒 RunLoop(端口通信、系统事件)
  4. CFRunLoopTimerRef :基于 CFAbsoluteTime 的定时器,NSTimer 底层就是它,绑定 mode。如果 mode 切换,timer 不触发。

  5. CFRunLoopObserverRef:观察者,监听 RunLoop 状态变化,可以在循环的各个节点回调。 7 个状态:

    markdown 复制代码
    1. kCFRunLoopEntry         进入runloop
    2. kCFRunLoopBeforeTimers  处理Timer前
    3. kCFRunLoopBeforeSources 处理Source前
    4. kCFRunLoopBeforeWaiting 准备休眠前(最常用)
    5. kCFRunLoopAfterWaiting  被唤醒,刚结束休眠
    6. kCFRunLoopExit          退出runloop
    7. kCFRunLoopAllActivities 全部状态

3. RunLoop 完整运行逻辑(源码简化流程)

就算没有开定时器,依然能监听kCFRunLoopBeforeTimers的回调。这个属于流程。

scss 复制代码
// CFRunLoop.c 简化伪代码
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFRunLoopModeRef mode, ...)
{
    // 1. 通知Observer:kCFRunLoopEntry 进入循环
    __CFRunLoopDoObservers(rl, mode, kCFRunLoopEntry);
    
    do {
        // 2. Observer:kCFRunLoopBeforeTimers
        __CFRunLoopDoObservers(rl, mode, kCFRunLoopBeforeTimers);
        // 3. Observer:kCFRunLoopBeforeSources
        __CFRunLoopDoObservers(rl, mode, kCFRunLoopBeforeSources);

        // 4. 处理Source0(待执行block、点击事件等)
        __CFRunLoopDoSources0(rl, mode, ...);

        // 5. 如果有Source1事件,直接跳转处理,不进入休眠
        if (__CFRunLoopHasSourcesPending(rl, mode)) {
            continue;
        }

        // 6. Observer:kCFRunLoopBeforeWaiting,准备休眠
        __CFRunLoopDoObservers(rl, mode, kCFRunLoopBeforeWaiting);

        // 7. 【核心】调用mach_msg,线程休眠,等待内核消息
        // 阻塞在这里,不占用CPU,直到收到mach端口消息
        mach_msg(..., MACH_MSG_OPTION_RCV_TIMEOUT);

        // 8. Observer:kCFRunLoopAfterWaiting,被唤醒
        __CFRunLoopDoObservers(rl, mode, kCFRunLoopAfterWaiting);

        // 9. 被唤醒后判断是什么事件唤醒:
        if (timer触发) {
            __CFRunLoopDoTimers();
        } else if (source1收到消息) {
            __CFRunLoopDoSource1();
        } else if (被手动wakeup) {
            // 只是唤醒标记,无事件
        }

    } while (满足继续循环条件);

    // 10. Observer:kCFRunLoopExit,退出runloop
    __CFRunLoopDoObservers(rl, mode, kCFRunLoopExit);
    return result;
}
  • 休眠依靠 mach_msg,是 Mach 内核的 IPC 通信,不是忙等
  • Source0 不会主动唤醒 RunLoop,需要手动调用CFRunLoopWakeUp唤醒
  • Source1、Timer、外部 wakeup 可以唤醒 RunLoop

验证source0不会主动runloop

scss 复制代码
- (void)viewDidLoad {
    
    [self testSource0WakeUp];
}
void sourceo_print(void *info)
{
    NSLog(@"this is a source0_print message %@",  info);
}

- (void)testSource0WakeUp {
    // 1. 注册一个 Observer,监听 RunLoop 的休眠和唤醒状态
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(
        kCFAllocatorDefault,
        kCFRunLoopBeforeWaiting | kCFRunLoopAfterWaiting,
        NO, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            if (activity == kCFRunLoopBeforeWaiting) {
                NSLog(@"🛌 [Observer] RunLoop 即将休眠 (Sleep)");
            } else if (activity == kCFRunLoopAfterWaiting) {
                NSLog(@"⏰ [Observer] RunLoop 被唤醒 (WakeUp)");
            }
        });
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode);
    
    // 2. 创建一个 Source0
    CFRunLoopSourceContext context = {0};
    context.info = @"我是传入的参数";
    context.perform =  sourceo_print;
    
    CFRunLoopSourceRef source0 = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetMain(), source0, kCFRunLoopDefaultMode);
    
    // 3. 测试场景:延迟 3 秒后触发 Source0
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
        NSLog(@"--- 开始测试:只调用 CFRunLoopSourceSignal ---");
        
        // 仅标记 Source0 待处理,不主动唤醒
//        CFRunLoopSourceSignal(source0);
        
        // 【关键验证点】:取消下面这行注释,观察控制台日志的区别
//         CFRunLoopWakeUp(CFRunLoopGetMain());
    });
}

另外一个线程标记了CFRunLoopSourceSignal(source0);,但是休眠后并不执行。除非执行CFRunLoopWakeUp(CFRunLoopGetMain());手动唤醒这个runloop。

对于定时器,每一次runloop循环都会取一个最近的定时器任务,将时间传递给底层的mach_port。这些定时器是用最小堆实现的。并不会查询所有的定时器,因为如果其中一个时间没有到,就没有必要取查找后续的任务了。

注意点

子线程 RunLoop 如果没有 source/timer/observer,run 会直接退出(空 RunLoop 自动退出)。

CFRunLoopStop 是立刻终止 RunLoop 循环吗?

不会立刻打断当前正在执行的事件,只会标记停止标记;本次循环执行完毕,下一轮循环才退出

runloop的应用

  1. NSTimer / CADisplayLink(依赖 RunLoop Mode,滚动失效坑)
  2. performSelector:onThread:afterDelay: 底层就是往 RunLoop 添加 Source0
  3. 子线程保活(常驻线程,开启 RunLoop,持续处理任务)
  4. 异步渲染、监控卡顿(Observer 监听BeforeWaiting,检测长时间阻塞)
  5. AutoReleasePool:RunLoop 每次循环的 beforeWaiting、afterWaiting 自动创建 / 释放自动释放池

NSTime/CADisplayLink

objectivec 复制代码
- (void)viewDidLoad{
    [super viewDidLoad];
    
    // 自定添加到runloop
    NSTimer *t = [NSTimer scheduledTimerWithTimeInterval:3 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"scheduledTimerWithTimeInterval function");
    }];
    //
    [[NSRunLoop currentRunLoop] addTimer:t forMode:UITrackingRunLoopMode];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(7 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 会自动的将所有的模式都停掉
        //        [t invalidate];
        
        // 默认模式还能继续用
        CFRunLoopRemoveTimer(CFRunLoopGetCurrent(), (__bridge CFRunLoopTimerRef)t, (__bridge CFStringRef)UITrackingRunLoopMode);
    });
    
    
    
    NSTimer *timer = [NSTimer timerWithTimeInterval:3 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"time function");
    }];
    // 这里default模式,滑动的时候失效
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    CADisplayLink *d = [CADisplayLink displayLinkWithTarget:self selector:@selector(display)];
    [d addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        //CADisplayLink比较特殊,不能从某个模式中删除
        [d invalidate];
    });
    
}

- (void)display{
    NSLog(@"display");
}

performSelector:onThread:afterDelay:` 底层就是往 RunLoop 添加 Source0

这里虽然是source0。但是内部还有一个Timer,所以时间到了内核会自动激活,不需要手动wakeup。

子线程保活(常驻线程,开启 RunLoop,持续处理任务)

objectivec 复制代码
@interface MyThread : NSObject
- (void)excuteTask:(void (^)(void))block;
- (void)stop;
@end

runloop和线程保活

为了避免频繁创建和销毁线程。可以让一个线程一直存在。

MyThread.h 复制代码
@interface MyThread : NSObject
- (void)excuteTask:(void (^)(void))block;
- (void)stop;
@end
MyThread.m 复制代码
@interface MyThread()
@property (nonatomic,strong) NSThread *thread;
@property (nonatomic, assign) CFRunLoopRef threadRunLoop; // 保存子线程runloop
@property (nonatomic, assign) BOOL isStop; // 保存子线程runloop
@end

@implementation MyThread


- (void)setupRunLoopObserver {
    // 监听全部关键状态
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(
        NULL,
        kCFRunLoopAllActivities,
        YES,  // repeats=YES,每一轮循环都触发
        0,
        ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            switch (activity) {
                case kCFRunLoopEntry:
                    NSLog(@"RunLoop: 进入循环");
                    break;
                case kCFRunLoopBeforeTimers:
                    NSLog(@"RunLoop: 即将处理Timer");
                    break;
                case kCFRunLoopBeforeSources:
                    NSLog(@"RunLoop: 即将处理Source");
                    break;
                case kCFRunLoopBeforeWaiting:
                    NSLog(@"RunLoop: 准备进入休眠(mach_msg阻塞)");
                    break;
                case kCFRunLoopAfterWaiting:
                    NSLog(@"RunLoop: 被唤醒,离开mach_msg");
                    break;
                case kCFRunLoopExit:
                    NSLog(@"RunLoop: RunLoop即将退出");
                    break;
                default:
                    break;
            }
        }
    );
    // 添加到DefaultMode
    CFRunLoopAddObserver(self.threadRunLoop, observer, kCFRunLoopDefaultMode);
    CFRelease(observer); // create出来需要释放
}



- (instancetype)init
{
    self = [super init];
    if (self) {
        self.isStop = false;
        self.thread = [[NSThread alloc] initWithBlock:^{
            self.threadRunLoop = [[NSRunLoop currentRunLoop] getCFRunLoop];
            [self setupRunLoopObserver];
            NSLog(@"runloop begin");
            
            // run之前必须有任务。否则[[NSRunLoop currentRunLoop] run];直接退出,这种方式就算调用了stop也不会退出
            //            [self performSelector:@selector(keepAlive) onThread:self.thread withObject:nil waitUntilDone:NO];
            //            [[NSRunLoop currentRunLoop] run];
            
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
            while (!self.isStop) {
                /*
                 此时线程挂起,等待唤醒
                 - source0 事件:`performSelector:onThread`投递任务(执行 task)
                 - Source1 事件:NSPort 收到端口消息
                 - Timer 到点
                 - `CFRunLoopWakeUp(rl)` 手动唤醒 RunLoop
                 */
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:NSDate.distantFuture];
            }
            
            
            NSLog(@"runloop end");
            
            
        }];
        self.thread.name = @"MyKeepAliveThread";
        
        [self.thread start];
    }
    return self;
}

- (void)keepAlive{
    NSLog(@"keepAlive");
    
}

- (void)excuteTask:(void (^)(void))block{
    [self performSelector:@selector(runBlock:) onThread:self.thread withObject:block waitUntilDone:NO];
}
// 私有方法,真正在子线程执行block
- (void)runBlock:(void (^)(void))block {
    if (block) {
        block();
    }
}
- (void)stop{
    // 如果创建了当前对象init了,线程对象存在,但是子线程的代码还没有开始执行,这个runloop可能还是空的
    self.isStop = true;
    if (!self.threadRunLoop){
        //        如果初始化刚完成调用stop.可能导致这个threadRunLoop还没有初始化完成
        // 这里sleep,虽然会卡,但是收到的影响忽略不计。且大部分情况不会进来这个方法
        
        [NSThread sleepForTimeInterval:0.001];
    }
    if (self.threadRunLoop){
        NSLog(@"stop runloop");
//         这里只是标记要停止,如果当前没有任务,就算激活了runloop也不会停止。只有当还有任务的时候.
        // 因为不调用这段代码,runloop循环的时候,啥也不干,又继续等待。
        CFRunLoopStop(self.threadRunLoop);
        
        // 这里激活,也有可能会被其它激活,这里保底写一个。
        CFRunLoopWakeUp(self.threadRunLoop);
    }
    
}

-(void)dealloc{
    NSLog(@"MyThread dealloc");
}
ViewController.m 复制代码
@interface ViewController()
@property (nonatomic, strong) MyThread *myThread;

@end

@implementation ViewController


- (void)viewDidLoad{
    [super viewDidLoad];
    
    self.myThread = [[MyThread alloc] init];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self.myThread excuteTask:^{
            //        sleep(1);
            //        usleep(10) 微秒
            NSLog(@"task1 %@ begin", [NSThread currentThread]);
            [NSThread sleepForTimeInterval:3];
            NSLog(@"task1 %@ finished", [NSThread currentThread]);
        }];
    });
    

}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"touhbegin");
    [self.myThread stop];
    self.myThread = nil;
}


@end

runloop实现异步渲染、监控卡顿(Observer 监听BeforeWaiting,检测长时间阻塞)

RunLoop 在 iOS 性能优化中扮演着极其核心的角色。你提到的"异步渲染"和"卡顿监控"正是利用了 RunLoop 的两个关键特性:任务调度时机状态流转机制

结合底层原理,我为你深度拆解这两个场景的实现机制:

1. RunLoop 如何实现异步渲染?

在 iOS 中,UI 渲染(如 drawRect:、文本排版、图片解码等)默认是在主线程同步执行的。如果渲染任务过重,就会阻塞主线程,导致掉帧卡顿。RunLoop 在这里提供了两个层面的优化:

① 利用 RunLoop 的"渲染提交时机"进行异步绘制

当你在主线程调用 setNeedsDisplay 时,系统并不会立刻绘制,而是给 Layer 打上"脏标记"。苹果在 RunLoop 中注册了一个优先级很晚的 Observer,专门监听 kCFRunLoopBeforeWaiting(即将休眠)和 kCFRunLoopExit(即将退出)事件58。

  • 系统机制 :当 RunLoop 准备休眠时,Observer 被触发,系统会遍历所有带有脏标记的 Layer,调用 [CALayer display] 将生成的 Bitmap 提交给 GPU18。
  • 异步绘制切入点 :我们可以重写 displayLayer: 方法接管这个流程。在 kCFRunLoopBeforeWaiting 阶段,把耗时的绘制任务(如使用 Core Graphics 绘制文字或图片)派发到子线程 执行。子线程生成 Bitmap 后,再回到主线程赋值给 layer.contents110。这样就彻底避免了主线程在 RunLoop 休眠前被绘制任务阻塞。

② 利用 RunLoop 的"空闲时机"进行延迟加载

对于启动时不需要立即显示的大图或复杂视图,可以利用 performSelector:withObject:afterDelay:0 将任务挂载到下一次 RunLoop 循环中。这样既保证了不阻塞当前的启动流程,又能在主线程空闲时平滑地完成加载。

2. RunLoop 如何监控卡顿?

iOS 的卡顿本质上是主线程 RunLoop 在处理某个事件时耗时过长,导致无法及时进入休眠状态或处理下一帧的 VSync 信号4。利用 RunLoop 监控卡顿是目前线上最主流的方案(如微信的 Matrix 框架),其核心原理如下:

① 注册状态监听(Observer)

在主线程注册一个 CFRunLoopObserver,重点监听两个状态:

  • kCFRunLoopBeforeSources :RunLoop 即将开始处理事件(如触摸、网络回调等)4。此时记录下开始时间
  • kCFRunLoopBeforeWaiting :RunLoop 处理完所有事件,准备进入休眠。此时记录下结束时间

② 旁路监控线程(独立判断)

如果在主线程自己判断是否超时,一旦主线程卡死,判断逻辑也会跟着卡死。因此,必须创建一个独立的子线程(监控线程) 来进行旁路检测:

  • 监控线程会持续检查主线程记录的时间差(当前时间 - 开始时间)2。
  • 如果主线程长时间停留在 BeforeSourcesBeforeWaiting 之间,且时间差超过了预设的阈值(例如 200ms 或 2s,取决于业务对卡顿的定义),监控线程就会判定主线程发生了卡顿。

③ 抓取卡顿堆栈

一旦监控线程判定卡顿,它会立刻通过系统 API(如 backtrace)抓取主线程当前的函数调用栈,并将堆栈信息上报到服务器。这样开发者就能精准知道"主线程到底在干什么导致了卡顿"(比如是复杂的 JSON 解析,还是主线程的同步锁竞争)29。

总结

  • 异步渲染 :利用 RunLoop 的 BeforeWaiting 时机,将耗时的 CPU 绘制工作从主线程剥离到子线程,实现"主线程只负责发号施令,子线程负责画图,主线程再负责贴到屏幕上"的流水线作业。
  • 卡顿监控:利用 RunLoop 的状态流转作为"心跳",通过旁路线程监控"心跳"是否超时,从而精准捕获主线程的阻塞行为。
objectivec 复制代码
@interface ViewController ()
@property (nonatomic, assign) BOOL isRunLoopProcessing;
@property (nonatomic, assign) CFTimeInterval lastActivityTime;
@end

// 1. 模拟卡顿(在 ViewController 的 touchesBegan 或按钮点击事件中调用)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"🔥 主线程开始模拟卡顿(阻塞3秒)...");
    
    // 制造一个 3 秒的耗时操作(比如死循环或复杂计算)
    sleep(3); 
    
    NSLog(@"✅ 主线程卡顿结束,恢复响应");
}

// 2. 启动卡顿监控(在 viewDidLoad 中调用)
- (void)viewDidLoad {
    [super viewDidLoad];
    [self startANRMonitor];
}

- (void)startANRMonitor {
    // 1. 注册主线程的 RunLoop Observer,监听状态变化
    CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL, NULL};
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(
        kCFAllocatorDefault,
        kCFRunLoopAllActivities, // 监听所有状态
        YES,                     // 重复监听
        0,                       // 优先级最高
        &runLoopObserverCallback, // C 语言回调函数
        &context
    );
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    CFRelease(observer);
    
    // 2. 开启一个独立的监控子线程
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 监控线程:持续检查主线程的状态
        while (YES) {
            @autoreleasepool {
                // 休眠 200 毫秒检查一次(可根据需要调整,通常 100~250ms 比较合理)
                [NSThread sleepForTimeInterval:0.2];
                
                // 如果主线程当前处于 "处理事件" 或 "处理 Source" 阶段,且持续时间超过阈值
                // 这里我们简单用 1 秒作为卡顿判定标准(实际业务中通常是 200ms 掉帧)
                if (self.isRunLoopProcessing && self.lastActivityTime > 0) {
                    CFTimeInterval duration = CFAbsoluteTimeGetCurrent() - self.lastActivityTime;
                    if (duration > 1.0) {
                        // 发现卡顿!抓取主线程堆栈
                        NSLog(@"🚨 警告:检测到主线程卡顿 %.2f 秒!正在抓取堆栈...", duration);
                        [self dumpMainThreadCallStack];
                        // 重置时间,避免重复报警
                        self.lastActivityTime = 0; 
                    }
                }
            }
        }
    });
}

// 3. RunLoop 状态回调(C 函数)
static void runLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    ViewController *vc = (__bridge ViewController *)info;
    
    switch (activity) {
        case kCFRunLoopBeforeSources:
        case kCFRunLoopBeforeWaiting:
            // 即将处理事件,记录开始时间
            vc.lastActivityTime = CFAbsoluteTimeGetCurrent();
            vc.isRunLoopProcessing = YES;
            break;
            
        case kCFRunLoopAfterWaiting:
        case kCFRunLoopEntry:
        case kCFRunLoopExit:
            // 进入休眠或退出,说明当前事件处理完毕,重置标记
            vc.isRunLoopProcessing = NO;
            vc.lastActivityTime = 0;
            break;
            
        default:
            break;
    }
}

// 4. 抓取主线程堆栈
- (void)dumpMainThreadCallStack {
    NSArray<NSInvocation *> *callStack = [NSThread callStackSymbols];
    // 打印主线程的前 10 个堆栈帧,找出罪魁祸首
    NSLog(@"📜 主线程卡顿堆栈:");
    for (int i = 0; i < MIN(10, callStack.count); i++) {
        NSLog(@"%@", callStack[i]);
    }
}
相关推荐
2501_915909066 小时前
SwiftUI 和 UIKit 怎么选?两代 UI 框架的适用范围
vscode·ios·objective-c·个人开发·swift·敏捷流程
for_ever_love__8 小时前
iOS: GCD高级API
macos·ios·objective-c·cocoa·多线程·gcd
2501_915918418 小时前
在Windows10上使用VSCode和Code Runner搭建Swift开发环境详细步骤
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
用户2181697049309 小时前
iOS 底层原理 runtime
objective-c
for_ever_love__1 天前
iOS: GCD进阶,全局队列与自定义队列
macos·ios·objective-c·cocoa·多线程·gcd
用户2181697049302 天前
iOS 底层原理 block
objective-c
2501_915106322 天前
Swift 4开发环境设置教程:xCode安装与Playground入门
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
9765033352 天前
iOS 上架/审核 4.3a全面解读-最新
flutter·ios·objective-c·uniapp