iOS GCD

GCD

任务+队列

主队列: 任务在主线程执行,主队列是一个串行队列,它主要处理 UI 相关任务,也可以处理其他类型任务,但为了性能考虑,尽量让主队列执行 UI 相关或少量不耗时间和资源的操作。

系统全局并发队列:全局并发队列,存在 5 个不同的 QoS 级别,可以使用默认优先级,也可以单独指定

并行队列: 先进先出,多个任务可以并行执行

串行队列 : 先进先出,同一时间只能执行一个任务

基本操作:

sync 同步

任务一经提交就会阻塞当前线程(当前线程可以理解为下方代码示例中执行 sync 方法所在的线程 thread0),并请求队列立即安排其执行,执行任务的线程 thread1 默认等于 thread0,即同步任务直接在当前线程运行,任务完成后恢复线程原任务。

async 异步

任务提交后不会阻塞当前线程,会由队列安排另一个线程执行。

1. 同步(Synchronous)

​ • 定义:同步意味着任务是按顺序执行的,一个任务完成后,才会开始下一个任务。在执行过程中,程序会阻塞,等待当前任务完成之后才能继续执行下一步。

​ • 特征

​ • 阻塞:程序在执行某个任务时,会等待该任务完成,然后才会继续执行其他任务。

​ • 执行顺序:任务按照书写的顺序依次执行。

​ • 简单直观:容易理解,执行顺序清晰,但当任务较慢时会浪费时间,因为程序需要等待任务完成。

​ • 示例

假设你需要从数据库中获取数据,读取数据的过程是同步的:

复制代码
func fetchData() {
    let data = loadDataFromDatabase()  // 阻塞直到数据加载完成
    processData(data)  // 数据加载完成后才会执行
}

这意味着,如果 loadDataFromDatabase() 操作需要很长时间(比如网络请求、文件读取等),那么程序在此期间会被阻塞,不能执行其他任务。

2. 异步(Asynchronous)

​ • 定义:异步意味着任务的执行不阻塞主线程。任务在后台执行时,程序可以继续执行其他操作,直到任务完成时,通知程序来处理结果。

​ • 特征

​ • 非阻塞:程序不会等待当前任务完成,允许其他操作继续执行。

​ • 回调:当异步任务完成时,通常会通过回调函数(比如闭包、代理等)来通知主程序,处理结果。

​ • 并发:多个任务可以并行执行,从而提高效率,避免长时间的阻塞。

​ • 示例

假设你需要从数据库中获取数据,但希望不阻塞主线程,使用异步方法来加载数据:

复制代码
func fetchData() {
    loadDataFromDatabaseAsync { data in  // 异步加载数据,回调处理
        processData(data)  // 数据加载完成后执行回调
    }
}

在这个例子中,loadDataFromDatabaseAsync 会在后台加载数据,主线程不受阻塞,其他操作可以继续进行。当数据加载完成时,回调函数会被调用,随后执行 processData(data)。

同步和异步的区别总结

特性 同步 (Synchronous) 异步 (Asynchronous)
执行方式 按顺序执行,任务完成后再执行下一个 任务在后台执行,程序可以继续执行其他操作
程序等待 程序会等待任务完成,任务执行时会阻塞 程序不等待任务完成,可以继续执行其他任务
阻塞与非阻塞 阻塞,当前任务未完成无法继续执行其他任务 非阻塞,任务在后台执行,主线程不受阻塞
适用场景 适合任务简单、快速,或必须按顺序执行的场景 适合任务需要较长时间、可以并发处理的场景

举个例子:下载文件

假设你需要从互联网上下载一个文件:

​ 1. 同步下载:如果你使用同步方式下载文件,程序会等待文件下载完成后,才会继续执行后续的操作。在文件下载的过程中,程序会被"卡住",无法做其他事情,直到下载完成。

​ 2. 异步下载:如果你使用异步方式下载文件,程序会立即开始下载并跳转到下一步操作,文件下载则在后台进行。下载完成后,程序会通过回调或通知来处理下载结果,这样你可以在下载文件的同时执行其他任务。

优缺点对比

​ • 同步

​ • 优点:简单、直观,适合任务简单且必须顺序执行的场景。

​ • 缺点:如果一个任务耗时较长,会阻塞整个程序,导致性能瓶颈。

​ • 异步

​ • 优点:不会阻塞主线程,适合长时间运行的任务,可以提高程序效率和响应性。

​ • 缺点:实现较为复杂,通常需要回调、闭包、Promise、异步/并发框架等机制来处理。

实际应用

​ • 同步:适合计算密集型操作或那些不需要等待的任务(如简单的计算)。

​ • 异步:适合网络请求、文件 I/O、数据库查询等耗时操作,这样可以避免程序界面卡顿或响应迟缓。

理解同步与异步的差异有助于提高程序的效率,尤其是在面对网络请求或处理大规模数据时。

创建队列:

提供了两种队列,系统队列(全局并发队列),自定义队列

对于自定义队列,默认是串行的

  • 串行队列同一时间只会使用同一线程、运行同一任务,并严格按照任务顺序执行。

  • 并行队列同一时间可以使用多个线程、运行多个任务,执行顺序不分先后。

  • 同步任务会阻塞当前线程,并在当前线程执行。

  • 异步任务不会阻塞当前线程,并在与当前线程不同的线程执行。

  • 如何避免死锁:不要在串行或主队列中嵌套执行同步任务。

栏栅任务

栅栏任务的主要特性是可以对队列中的任务进行阻隔,执行栅栏任务时,它会先等待队列中已有的任务全部执行完成,然后它再执行,在它之后加入的任务也必须等栅栏任务执行完后才能执行。

这个特性更适合并行队列,而且对栅栏任务使用同步或异步方法效果都相同。

  • 创建方式,先创建 WorkItem,标记为:barrier,再添加至队列中:
swift 复制代码
let queue = DispatchQueue(label: "queueName", attributes: .concurrent)
let task = DispatchWorkItem(flags: .barrier) {
    // do something
}
queue.async(execute: task)
queue.sync(execute: task) // 与 async 效果一样

迭代任务

并行队列利用多个线程执行任务,可以提高程序执行的效率。而迭代任务可以更高效地利用多核性能,它可以利用 CPU 当前所有可用线程进行计算(任务小也可能只用一个线程)。如果一个任务可以分解为多个相似但独立的子任务,那么迭代任务是提高性能最适合的选择。

使用 concurrentPerform 方法执行迭代任务,迭代任务的后续任务需要等待它执行完成才会继续。本方法类似于 Objc 中的 dispatch_apply 方法,创建方式如下:

复制代码
DispatchQueue.concurrentPerform(iterations: 10) {(index) -> Void in // 10 为迭代次数,可修改。
    // do something
}

迭代任务可以单独执行,也可以放在指定的队列中:

swift 复制代码
let queue = DispatchQueue.global() // 全局并发队列
queue.async {
    DispatchQueue.concurrentPerform(iterations: 100) {(index) -> Void in
        // do something
    }
    //可以转至主线程执行其他任务
    DispatchQueue.main.async {
        // do something
    }
}

延迟加入队列

swift 复制代码
class AsyncAfter {

    typealias ExchangableTask = (_ newDelayTime: TimeInterval?,
        _ anotherTask:@escaping (() -> ())
        ) -> Void

    /// 延迟执行一个任务,并支持在实际执行前替换为新的任务,并设定新的延迟时间。
    ///
    /// - Parameters:
    ///   - time: 延迟时间
    ///   - yourTask: 要执行的任务
    /// - Returns: 可替换原任务的闭包
    static func delay(_ time: TimeInterval, yourTask: @escaping ()->()) -> ExchangableTask {
        var exchangingTask: (() -> ())? // 备用替代任务
        var newDelayTime: TimeInterval? // 新的延迟时间

        let finalClosure = { () -> Void in
            if exchangingTask == nil {
                DispatchQueue.main.async(execute: yourTask)
            } else {
                if newDelayTime == nil {
                    DispatchQueue.main.async {
                        print("任务已更改,现在是:\(Date())")
                        exchangingTask!()
                    }
                }
                print("原任务取消了,现在是:\(Date())")
            }
        }

        dispatch_later(time) { finalClosure() }

        let exchangableTask: ExchangableTask =
        { delayTime, anotherTask in
            exchangingTask = anotherTask
            newDelayTime = delayTime

            if delayTime != nil {
                self.dispatch_later(delayTime!) {
                    anotherTask()
                    print("任务已更改,现在是:\(Date())")
                }
            }
        }

        return exchangableTask
    }
}

delay 方法接收两个参数,并返回一个闭包:

  • TimeInterval:延迟时间
  • @escaping () -> (): 要延迟执行的任务
  • 返回:可替换原任务的闭包,我们去了一个别名:ExchangableTask

ExchangableTask 类型定义的闭包,接收一个新的延迟时间,和一个新的任务。

如果不执行返回的闭包,则在delay 方法内部,通过 dispatch_later 方法会继续执行原任务。

如果执行了返回的 ExchangableTask 闭包,则会选择执行新的任务。

线程安全和线程同步

读写操作

swift 复制代码
let queue = DispatchQueue(label: "com.example.queue", attributes: .concurrent)
var array = [Int]()

// 写操作
queue.async(flags: .barrier) {
    array.append(1)
    array.append(2)
}

// 读操作
queue.async {
    print(array)
}

什么时候用串行队列?什么时候用并发队列 + 栅栏?

​ • 用串行队列:

​ • 当你的任务量不大,或对性能要求不高时。

​ • 任务的顺序性更重要,所有操作按严格顺序执行。

​ • 用并发队列 + 栅栏:

​ • 当你的任务中有大量的读操作,并且需要同时保证写操作的安全性时。

​ • 适用于性能要求较高的场景,例如数据库、多线程缓存读写操作。

所以,串行队列的实现虽然简单,但并发队列 + 栅栏在性能和灵活性上更有优势,特别是在读多写少的场景下。

多线程典型场景

  • 一个页面有三个网络请求,需要在三个网络请求都返回的时候刷新页面

    使用dispatch group

  • 实现一个线程安全的Array的读和写

    swift 复制代码
    import Foundation
    
    class ThreadSafeArray<T> {
        private var array: [T] = []
        private let queue = DispatchQueue(label: "com.example.ThreadSafeArray", attributes: .concurrent)
        
        // 写操作:追加元素
        func append(_ item: T) {
            queue.async(flags: .barrier) {
                self.array.append(item)
            }
        }
        
        // 写操作:移除元素
        func remove(at index: Int) {
            queue.async(flags: .barrier) {
                guard index < self.array.count else { return }
                self.array.remove(at: index)
            }
        }
        
        // 读操作:通过索引访问
        func get(at index: Int) -> T? {
            var result: T?
            queue.sync {
                guard index < self.array.count else { return }
                result = self.array[index]
            }
            return result
        }
        
        // 读操作:获取数组的所有内容
        func getAll() -> [T] {
            var result: [T] = []
            queue.sync {
                result = self.array
            }
            return result
        }
        
        // 读操作:获取数组的大小
        var count: Int {
            var result = 0
            queue.sync {
                result = self.array.count
            }
            return result
        }
    }

    代码的线程安全性

    ​ • 写操作

    ​ • 使用 async(flags: .barrier) 保证写操作互斥。

    ​ • 当有一个写操作在执行时,队列会阻止其他读和写操作,确保数据一致性。

    ​ • 读操作

    ​ • 通过 queue.sync 保证读操作线程安全。

    ​ • 即使多个读操作同时进行,也不会干扰,因为并发队列允许多个 sync 操作并行。

    代码执行逻辑示例

    1. 写操作的独占性

    假设同时有以下两个任务:

    ​ • array.append(1)

    ​ • array.remove(at: 0)

    因为写操作使用了 .barrier,它们不会同时执行,队列会保证第一个写操作完成后再执行下一个写操作。

    2. 读写互斥

    假设同时有以下任务:

    ​ • 读取元素 array.getAll()

    ​ • 添加新元素 array.append(1)

    在这种情况下:

    ​ 1. 读操作通过 sync 提交到队列中,可以立即并发执行。

    ​ 2. 写操作通过 barrier 提交时,会等待所有正在进行的读操作完成后再执行。

    3. 并发读

    假设同时有以下任务:

    ​ • array.getAll()

    ​ • array.get(at: 0)

    因为读操作是通过 sync 提交到并发队列中,它们可以并行执行,不会互相阻塞。

同步(Synchronous)和异步(Asynchronous)是编程中两种不同的执行方式,主要区别在于任务的执行顺序和程序的等待行为。

信号量

DispatchSemaphore,通常称作信号量,顾名思义,它可以通过计数来标识一个信号,这个信号怎么用呢,取决于任务的性质。通常用于对同一个资源访问的任务数进行限制。

例如,控制同一时间写文件的任务数量、控制端口访问数量、控制下载任务数量等。

信号量的使用非常的简单:

  • 首先创建一个初始数量的信号对象
  • 使用 wait 方法让信号量减 1,再安排任务。如果此时信号量仍大于或等于 0,则任务可执行,如果信号量小于 0,则任务需要等待其他地方释放信号。
  • 任务完成后,使用 signal 方法增加一个信号量。
  • 等待信号有两种方式:永久等待、可超时的等待。

基础源码解析

队列和线程之间的关系

一个重要概念是overcommit,overcommit的队列在队列创建时会新建一个线程,非overcommit队列创建队列则未必创建线程。另外width=1意味着是串行队列,只有一个线程可用,width=0xffe则意味着并行队列,线程则是从线程池获取,可用线程数是64个。

可以看到全局队列是非overcommit的(flat保留字只能传0,如果默认优先级则是com.apple.root.default-qos,但是width=0xffe是并行队列);主队列是overcommit的com.apple.root.default-qos.overcommit,不过它是串行队列,width=1,并且运行的这个线程只能是主线程;自定义串行队列是overcommit的,默认优先级则是 com.apple.root.default-qos.overcommit,并行队列则是非overcommit的。

串行并行的判断

首先通过width判定是串行队列还是并发队列,如果是并发队列则调用_dispatch_sync_invoke_and_complete,串行队列则调用_dispatch_barrier_sync_f

死锁的判断
复制代码
_dispatch_lock_is_locked_by(dispatch_lock lock_value, dispatch_tid tid)
{
	// equivalent to _dispatch_lock_owner(lock_value) == tid
	return ((lock_value ^ tid) & DLOCK_OWNER_MASK) == 0;
}

通过判断队列的状态和线程的状态,把这两个进行异或操作,如果状态相同的话,就返回yes,产生死锁。

如何执行调度
复制代码
DISPATCH_NOINLINE
static void
_dispatch_sync_invoke_and_complete_recurse(dispatch_queue_class_t dq,
		void *ctxt, dispatch_function_t func, uintptr_t dc_flags
		DISPATCH_TRACE_ARG(void *dc))
{
	_dispatch_sync_function_invoke_inline(dq, ctxt, func);
	_dispatch_trace_item_complete(dc);
	_dispatch_sync_complete_recurse(dq._dq, NULL, dc_flags);
}

// 看一下 _dispatch_sync_function_invoke_inline

DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_sync_function_invoke_inline(dispatch_queue_class_t dq, void *ctxt,
		dispatch_function_t func)
{
	dispatch_thread_frame_s dtf;
	_dispatch_thread_frame_push(&dtf, dq);
	_dispatch_client_callout(ctxt, func);
	_dispatch_perfmon_workitem_inc();
	_dispatch_thread_frame_pop(&dtf);
}

// 看一下 _dispatch_client_callout

void
_dispatch_client_callout(void *ctxt, dispatch_function_t f)
{
	@try {
		return f(ctxt);
	}
	@catch (...) {
		objc_terminate();
	}
}
典型流程示例

假设通过 dispatch_sync(queue, ^{ ... }) 提交任务:

  1. 任务入队
    • 如果队列空闲,直接调用 _dispatch_sync_invoke_and_complete_recurse
  2. 执行任务
    • _dispatch_sync_function_invoke_inline 调用用户代码。
    • 线程帧压栈,记录当前队列。
  3. 异常处理
    • 用户代码若抛出异常,进程终止。
  4. 清理资源
    • 线程帧弹栈,递归更新队列状态,唤醒其他任务。
异步操作:
复制代码
static void
_dispatch_root_queue_poke_slow(dispatch_queue_global_t dq, int n, int floor)
{
    int remaining = n;
    int r = ENOSYS;
    
    _dispatch_root_queues_init();
    _dispatch_debug_root_queue(dq, __func__);
    _dispatch_trace_runtime_event(worker_request, dq, (uint64_t)n);
    #if !DISPATCH_USE_INTERNAL_WORKQUEUE
    #if DISPATCH_USE_PTHREAD_ROOT_QUEUES
    if (dx_type(dq) == DISPATCH_QUEUE_GLOBAL_ROOT_TYPE)
    #endif
    {
    	_dispatch_root_queue_debug("requesting new worker thread for global "
    			"queue: %p", dq);
    	r = _pthread_workqueue_addthreads(remaining,
    			_dispatch_priority_to_pp_prefer_fallback(dq->dq_priority));
    	(void)dispatch_assume_zero(r);
    	return;
    }
    #endif // !DISPATCH_USE_INTERNAL_WORKQUEUE
    #if DISPATCH_USE_PTHREAD_POOL
    dispatch_pthread_root_queue_context_t pqc = dq->do_ctxt;
    if (likely(pqc->dpq_thread_mediator.do_vtable)) {
    	while (dispatch_semaphore_signal(&pqc->dpq_thread_mediator)) {
    		_dispatch_root_queue_debug("signaled sleeping worker for "
    				"global queue: %p", dq);
    		if (!--remaining) {
    			return;
    		}
    	}
    }
    
    bool overcommit = dq->dq_priority & DISPATCH_PRIORITY_FLAG_OVERCOMMIT;
    if (overcommit) {
        // 串行队列
    	os_atomic_add2o(dq, dgq_pending, remaining, relaxed);
    } else {
    	if (!os_atomic_cmpxchg2o(dq, dgq_pending, 0, remaining, relaxed)) {
    		_dispatch_root_queue_debug("worker thread request still pending for "
    				"global queue: %p", dq);
    		return;
    	}
    }
    
    // floor 为 0,remaining 是根据队列任务的情况处理的
    int can_request, t_count;
    // 获取线程池的大小
    t_count = os_atomic_load2o(dq, dgq_thread_pool_size, ordered);
    do {
        // 计算可以请求的数量
        can_request = t_count < floor ? 0 : t_count - floor;
        if (remaining > can_request) {
    	    _dispatch_root_queue_debug("pthread pool reducing request from %d to %d",
    	    		remaining, can_request);
    	    os_atomic_sub2o(dq, dgq_pending, remaining - can_request, relaxed);
    	    remaining = can_request;
        }
        if (remaining == 0) {
            // 线程池满了,就会报出异常的情况
            _dispatch_root_queue_debug("pthread pool is full for root queue: "
    				"%p", dq);
                return;
    	}
    } while (!os_atomic_cmpxchgvw2o(dq, dgq_thread_pool_size, t_count,
    		t_count - remaining, &t_count, acquire));
    
    pthread_attr_t *attr = &pqc->dpq_thread_attr;
    pthread_t tid, *pthr = &tid;
    #if DISPATCH_USE_MGR_THREAD && DISPATCH_USE_PTHREAD_ROOT_QUEUES
    if (unlikely(dq == &_dispatch_mgr_root_queue)) {
    	pthr = _dispatch_mgr_root_queue_init();
    }
    #endif
    do {
    	_dispatch_retain(dq); 
    	// 开辟线程
    	while ((r = pthread_create(pthr, attr, _dispatch_worker_thread, dq))) {
    		if (r != EAGAIN) {
    			(void)dispatch_assume_zero(r);
    		}
    		_dispatch_temporary_resource_shortage();
    	}
    } while (--remaining);
    #else
    (void)floor;
    #endif // DISPATCH_USE_PTHREAD_POOL
}

这段代码是 GCD(Grand Central Dispatch)中用于管理全局队列(Global Queue)线程池 的核心函数 _dispatch_root_queue_poke_slow,其主要职责是根据任务需求动态创建或唤醒线程。以下是对代码的分层解析:


一、函数背景与核心目标
  • 调用场景 :当全局队列(dispatch_queue_global_t)中有新任务提交,但当前线程池中没有足够空闲线程处理任务时,触发此函数。
  • 核心目标:通过创建新线程或唤醒休眠线程,确保任务能够被及时执行。
  • 关键参数
    • dq:目标全局队列(可能是并发队列或特定优先级的队列)。
    • n:需要唤醒或创建的线程数量。
    • floor:线程池的最小保留线程数(避免过度销毁)。

二、代码逻辑分层解析
1. 初始化与调试(Initialization & Debugging)
c 复制代码
_dispatch_root_queues_init();       // 确保全局根队列初始化
_dispatch_debug_root_queue(dq, __func__);  // 调试日志
_dispatch_trace_runtime_event(worker_request, dq, (uint64_t)n); // 性能追踪
  • 作用:确保根队列已初始化,记录调试信息和性能事件。

2. 内核级 Workqueue 处理(XNU Kernel Workqueue)
c 复制代码
#if DISPATCH_USE_PTHREAD_ROOT_QUEUES
r = _pthread_workqueue_addthreads(remaining, _dispatch_priority_to_pp_prefer_fallback(dq->dq_priority));
  • 条件 :当使用 pthread 工作队列(如 macOS 或 iOS 的默认配置)。
  • 行为 :直接通过 _pthread_workqueue_addthreads 向 XNU 内核请求线程。
  • 机制
    • 内核根据优先级(dq_priority 转换为 pthread_priority_t)动态调度线程。
    • 内核管理的线程生命周期更高效,避免用户态频繁创建/销毁。

3. 用户态线程池管理(User-Level Thread Pool)

当不使用内核 Workqueue 时(如某些定制化配置),进入用户态线程池管理逻辑:

3.1 唤醒休眠线程
c 复制代码
while (dispatch_semaphore_signal(&pqc->dpq_thread_mediator)) {
    if (!--remaining) return;
}
  • 目的 :尝试通过信号量唤醒线程池中休眠的线程
  • 信号量机制 :休眠线程通过 dispatch_semaphore_wait 挂起,信号量 signal 会唤醒一个线程。
  • 优化:避免创建新线程的开销,优先复用休眠线程。
3.2 处理 Overcommit(超发线程)
c 复制代码
if (overcommit) {
    os_atomic_add2o(dq, dgq_pending, remaining, relaxed);
} else {
    os_atomic_cmpxchg2o(dq, dgq_pending, 0, remaining, relaxed);
}
  • Overcommit 标志:表示队列允许超出发起线程数限制(如串行队列的特殊处理)。
  • 原子操作
    • os_atomic_add2o:直接增加挂起的线程请求数。
    • os_atomic_cmpxchg2o:通过 CAS(Compare-and-Swap)确保线程请求数正确。
3.3 动态调整线程请求数
c 复制代码
do {
    can_request = t_count < floor ? 0 : t_count - floor;
    if (remaining > can_request) {
        remaining = can_request;
    }
} while (!os_atomic_cmpxchgvw2o(dq, dgq_thread_pool_size, t_count, t_count - remaining, &t_count, acquire));
  • 目标 :根据当前线程池大小(dgq_thread_pool_size)和 floor 值,计算实际可创建的线程数。
  • 策略
    • 若当前线程数 t_count 小于 floor,不允许创建新线程。
    • 否则,可创建 t_count - floor 个线程。
  • 原子操作 :通过 os_atomic_cmpxchgvw2o 更新线程池大小,避免竞态条件。
3.4 创建新线程
c 复制代码
while ((r = pthread_create(pthr, attr, _dispatch_worker_thread, dq))) {
    if (r != EAGAIN) (void)dispatch_assume_zero(r);
    _dispatch_temporary_resource_shortage();
}
  • 入口函数_dispatch_worker_thread 是线程的入口,负责从队列中获取并执行任务。
  • 错误处理
    • pthread_create 返回 EAGAIN(资源不足),调用 _dispatch_temporary_resource_shortage 等待后重试。
    • 其他错误直接触发断言(dispatch_assume_zero)。

到了这里可以清楚的看到对于全局队列使用_pthread_workqueue_addthreads开辟线程,对于其他队列使用pthread_create开辟新的线程。


三、代码流程图
plaintext 复制代码
开始
│
├─ 初始化根队列 & 记录调试信息
│
├─ 如果使用内核 Workqueue:
│   ├─ 调用 _pthread_workqueue_addthreads 请求内核创建线程
│   └─ 返回
│
├─ 否则(用户态线程池):
│   ├─ 尝试通过信号量唤醒休眠线程
│   ├─ 处理 Overcommit 标志
│   ├─ 动态调整线程请求数(基于线程池大小和 floor)
│   ├─ 循环创建新线程(处理 EAGAIN 错误)
│   └─ 更新线程池状态
│
└─ 结束
相关推荐
watersink6 小时前
基于大模型的pc版语音对话问答
ide·macos·xcode
Alger_Hamlet6 小时前
OmniGraffle Pro for Mac思维导图
macos
Alger_Hamlet6 小时前
Ae After Effects2024 for Mac 视频处理
macos
Python之栈6 小时前
Python 3.13 正式支持 iOS:移动开发的新篇章
python·macos·objective-c·cocoa
白熊1887 小时前
Mac 本地化部署 dify
macos·dify
鸿蒙布道师7 小时前
鸿蒙NEXT开发Base64工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
美狐美颜sdk9 小时前
美颜SDK兼容性挑战:如何让美颜滤镜API适配iOS与安卓?
android·深度学习·ios·美颜sdk·第三方美颜sdk·视频美颜sdk
Invisible_He9 小时前
iOS自定义collection view的page size(width/height)分页效果
ui·ios·swift·collection
小橙子207712 小时前
一条命令配置移动端(Android / iOS)自动化环境
android·ios·自动化
yidahis12 小时前
iOS启动优化 - 1分钟让你的启动速度降低 1s
ios·客户端