阅读视图

发现新文章,点击刷新页面。

老司机 iOS 周报 #341 | 2025-07-07

ios-weekly
老司机 iOS 周报,只为你呈现有价值的信息。

你也可以为这个项目出一份力,如果发现有价值的信息、文章、工具等可以到 Issues 里提给我们,我们会尽快处理。记得写上推荐的理由哦。有建议和意见也欢迎到 Issues 提出。

新手推荐

🐎 Don ‘ t Liquid Glass All the Things

@阿权:iOS 26 的液体玻璃效果让大家眼前一亮,大家可能已经在重新设计自己的 App,恨不得给所有 UI 都加上液态玻璃效果。文章提到液态玻璃效果容易滥用,导致界面的不和谐。使用液态玻璃的场景应该是用于突出按钮下方的内容,例如地图预览上方的操作按钮。换句话说,液态玻璃效果的控件会让控件自身与下方内容悬浮出来,形成两个解耦的交互层级,如果控件本身与内容是嵌合的二维依赖关系,则并不适合添加液态玻璃效果。

文章

🐢 深入解析| Cursor 编程实践经验分享

@Cooper Chen:本文深度剖析 AI 编程助手 Cursor 的进阶使用方法,为开发者提供一套可落地的效率提升方案:

1.Prompt 设计黄金法则

  • 采用"目标-背景-约束"三段式结构
  • 技术方案设计阶段明确禁止生成代码
  • 单测生成时附带示例代码确保风格统一

2.Rules 规范引擎

  • 自动生成项目专属开发规范(支持 Go/Java 等)
  • 中间件调用错误率降低至 0.3%
  • 通过"/"命令快速适配团队规范

3.工具链整合方案

  • 复杂需求使用 AutoGPT 进行任务分解
  • 技术调研调用 Claude 深度研究模式
  • 钉钉文档直接解析免去格式转换

本文提供的技术方案设计模板和开发规范 Rules 可直接复用,帮助开发者快速建立 AI 辅助编程工作流。文中揭示的"代码生成 + 架构决策"分层协作模式,为现代软件开发提供了高效的智能解决方案。

🐕 Flutter 里的像素对齐问题,深入理解为什么界面有时候会出现诡异的细线?

@david-clang:Flutter 界面中出现的诡异细线,本质原因是:

  1. 逻辑像素到物理像素转换出现浮点值(非整数 DPR、布局误差)。
  2. Skia 默认开启 AAA(Analytic Anti-Aliasing)抗锯齿处理,处理相邻同色元素时各自计算的像素覆盖率总和可能不足 100%(如 40% + 50% = 90%)。
  3. 未被覆盖的剩余部分(如上例的 10%)会显露背景色,形成半透明的细线。

解决方案是:

  1. pixel_snap:提前将逻辑像素换算物理像素,根本上避免转换后出现物理像素不对齐。
  2. Impeller: MSAA(Multisample Anti-Aliasing)抗锯齿处理,通过在每个像素内部采样多个点来获得更准确的边缘渲染效果,使那些原本因浮点误差产生的“半像素边缘”更加平滑自然,从而视觉上弱化或隐藏了细线问题。

🐕 Rewriting a 12 Year Old Objective-C iOS App with Claude Code

@Smallfly: 这是一篇非常详实的 AI 辅助开发实践分享。作者用 Claude Code 将一个 12 年前的 Objective-C 应用 Vinylogue 重写为 Swift + SwiftUI,仅用 7 天时间就完成了从架构升级到 App Store 上架的全流程。

文章的价值在于:

  1. 真实的成本分析 - 详细记录了理论花费 $353 vs 实际花费 $20 的对比,以及每日开发进度
  2. 实用的最佳实践 - 总结了大量 Claude Code 使用技巧,如使用 --quiet 标志、合理管理上下文窗口、创建反馈循环等
  3. 架构升级经验 - 展示了如何利用 AI 工具进行大规模重构,从传统架构升级到现代的 swift-dependencies + swift-sharing 架构
  4. 完整的开发流程 - 涵盖了从数据迁移、UI 适配到自动化截图生成的全过程

对于想要尝试 AI 辅助开发的 iOS 开发者来说,这篇文章提供了一个很好的参考框架。特别是文章中提到的"保持在宏观层面评估代码库,让 AI 处理微观层面的工作"这一理念,对提高开发效率很有启发意义。

🐕 Understanding and Improving SwiftUI Performance

@AidenRao:Airbnb 的 SwiftUI 性能优化分享:通过为视图自定义 Equatable 协议实现,仅在实际数据变化时触发重绘,避免不必要的视图更新。将大型视图分解为小型可差异化组件,配合复杂度检测工具(如 SwiftLint 规则)预警重构时机,减少单次渲染计算量。

代码

container

@老驴:Apple 最近发布了一个新的开源项目叫 Container,本质上是一个运行在 Linux 上,基于 Swift 和 Virtualization framework 的容器库。它的重点是更好地支持 Apple Silicon 芯片跑容器。 个人猜测,这可能是 Apple 为将来在自家服务器上使用 Apple Silicon 做准备的一步。毕竟一直有传言说 Apple 想让自家数据中心的服务器用上自研芯片,而要做到这一点,一个完善的容器方案是少不了的。

内推

重新开始更新「iOS 靠谱内推专题」,整理了最近明确在招人的岗位,供大家参考

具体信息请移步:https://www.yuque.com/iosalliance/article/bhutav 进行查看(如有招聘需求请联系 iTDriverr)

关注我们

我们是「老司机技术周报」,一个持续追求精品 iOS 内容的技术公众号,欢迎关注。

关注有礼,关注【老司机技术周报】,回复「2024」,领取 2024 及往年内参

同时也支持了 RSS 订阅:https://github.com/SwiftOldDriver/iOS-Weekly/releases.atom

说明

🚧 表示需某工具,🌟 表示编辑推荐

预计阅读时间:🐎 很快就能读完(1 - 10 mins);🐕 中等 (10 - 20 mins);🐢 慢(20+ mins)

autorelease pool

  1. 有两个observer会监听runloop两个事件,一个observer监听runloop要进入的是时候entry,会调用pool push方法创建一个autorelease pool

  2. 另一个observer监听runloop的状态,当runloop要进入休眠状态时beforewaiting,会pop一个自动释放池,同时push创建一个新的自动释放池。

  3. AutoreleasePoolPage 结构

    class AutoreleasePoolPage
    {
    const magic
    id *next 指向下一个可以存放被释放对象的地址
    pthread_t const thread 当前所在的线程
    AutoreleasePoolPage *const parent 当前page的父节点
    AutoreleasePoolPage *child
    
    
    }
    
  4. 每个page占4096个字节也就是4kb,自身成员变量只占56个字节,也就是7个成员变量,每个成员变量占8个字节。其他四千多个字节都是用来存放被autorelease修饰的对象内存地址。

  5. pool_boundary的作用是区分不同自动释放,调用push时,会传入一个pool_boundary并返回一个地址,这个地址不存储@autorelease对象的地址,起到一个标识作用,用来分隔不同的autoreleasepool

  6. 调用pop的时候,会传入end地址,从后到前调用对象的release方法,直到pool_boundary为止。

  7. 如果存在多个page,会从child的page最末尾开始调用,直到pool_boundary

  8. page是一个栈结构,释放是从栈顶开始

  9. 多层嵌套会共用一个page,通过pool_boundary来分隔,优先释放在里层的pool,因为最里层的pool中的对象被放倒了栈顶,优先释放栈顶对象。

    @autoreleasepool {
         NSObject *p1 = [[NSObject alloc] init]
         NSObject *p2 = [[NSObject alloc] init]
              @autoreleasepool {
                     NSObject *p3 = [[NSObject alloc] init]
                            @autoreleasepool{
                                   NSObject *p4 = [[NSObject alloc] init]
    }
    }
    }
    

16476988032851.jpg

  1. 释放时机:如果通过代码添加一个autoreleasepool,在作用域结束时,随着pool的释放,就会释放pool中的对象,这种情况是几十释放的,并不依赖于runloop。另一个就是系统自动释放的,系统会在runloop开始的时候创建一个pool,结束的时候会对pool中对象执行release操作。
  2. autoreleasepool 和 runloop的关系

16509481525421.jpg

Xcode16报错: SDK does not contain 'libarclite' at the path '/Applicati

xcode 16运行项目报如下错误:

SDK does not contain 'libarclite' at the path '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a'; try increasing the minimum deployment target

解决方案:

  • 一、错误原因是在这个路径下边缺少一个libarclite_iphonesimulator.a文件,那就command + G打开这个路径看一下,结果发现这个目录下边没有arc这个文件夹。如下图所示:
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/

Snip20250705_1.png

  • 二、点击这里下载arc文件下载下来放到这个路径下边再次运行就不报错了。这里需要注意下,就是必须是command + C + command + V复制粘贴arc这个目录下,不能拖拽,拖拽的是快捷方式不是真实的文件。

Snip20250705_2.png

下载地址我已经放到github了,需要的可以自行下载。

谈一谈iOS线程管理

前言

iOS 线程管理是一个老生常谈的话题,也是新人绕不过去的一道难关。用好多线程不容易,下面我简单谈一谈我的体会,希望对屏幕前的你有所帮助。

一、什么时候需要多线程

首先,要知道线程不是越多越好,创建线程和切换线程都有一定的开销,线程使用的不当也容易造成崩溃。那么什么时候需要使用多线程呢?一个主要的衡量标准是这个操作是否耗时,比如读写文件、网络请求、加解密等。特别是IO密集的操作,一定是要多线程的,否则会阻塞当前线程。

其次,线程和队列有着紧密的联系(ios里面特指GCD队列),如果某些操作需要按照一定的时序来执行并且对执行的时间不是那么敏感的话,那么最好就是放在一个串行队列里,比如写缓存。如果这些操作对执行时间敏感,且不是很讲究顺序的话,那么放在并行队列里比较合适,比如从分批下载视频片段(例如dash和hls)。如果是对执行时间敏感,并且又有一定的执行顺序,那么可以考虑NSOperationQueue,或者用dispatch_group、dispatch_semaphore来管理多个线程及其依赖关系。如果对这些都不讲究,那就用不着多线程了。

二、同步还是异步

一般情况下,能用异步还是用异步,除非是需要等待结果返回的才用同步。这主要是因为同步操作会阻塞线程,弄的不好还会导致死锁。编写同步代码的话,主要是用在同步读取某些属性这种场景,比如以下这个方法

- (BOOL)hasSubscribeTopic:(NSString*)topic {

        __block BOOL subscribed = NO;

        dispatch_sync(self.syncQueue, ^{

            subscribed = [self.subscribedTopics containsObject:topic];

        });

        return subscribed;


}

但是这样写有一个问题,就是如果别的方法在syncQueue对应的线程上调用了hasSubscribeTopic这个方法,就会导致死锁,所以正确的方式应该是这样

static const void * kDispatchQueueSpecificKey = &kDispatchQueueSpecificKey;
//init方法中调用
dispatch_queue_set_specific(_syncQueue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);

- (BOOL)hasSubscribeTopic:(NSString*)topic {

    void* value = dispatch_get_specific(kDispatchQueueSpecificKey);

    if (value == (__bridge void *)(self)) {

        return [self.subscribedTopics containsObject:topic];

    }else{

        __block BOOL subscribed = NO;

        dispatch_sync(self.syncQueue, ^{

            subscribed = [self.subscribedTopics containsObject:topic];

        });

        return subscribed;

    }

}

有些第三方库没有注意这方面,比如SDWebImage的SDImageCache,使用的时候就需要尤其注意

- (void)storeImageDataToDisk:(nullable NSData *)imageData

                      forKey:(nullable NSString *)key {

    if (!imageData || !key) {

        return;

    }

    

    dispatch_sync(self.ioQueue, ^{

        [self _storeImageDataToDisk:imageData forKey:key];

    });

}

三、串行还是并行

这个如前所述,主要看对执行时间的敏感程度和有无顺序要求。一般使用dispatch_create创建的队列以串行为主(swift的dispatchQueue默认就是串行的)。并行队列使用global_queue就可以了,但是有一个需要特别注意的是,不管是dispatch_get_global_queue还是dispatch_create分配的线程都是有上限的,如果超出上限,系统要么就是等待前面的线程执行完成(iOS模拟器),要么就会因为分配资源过多而导致崩溃(iOS真机)。通过下面这段代码,可以测试出系统最多能分配多少个线程,在iphone 15的模拟器上我测试得到的是global_queue能分配64个左右线程,而dispatch_create相对多一点,100多不到200个。

dispatch_queue_t syncQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

    for (int i=0;i<1000;i++) {

        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            pthread_t tid = pthread_self();

            printf("1 tid=%lu\n",(unsigned long)tid);

            dispatch_sync(syncQueue, ^{

                NSLog(@"2");

            });

            NSLog(@"3");

        });

    }

还有一个问题就是,使用dispatch_get_global_queue创建的线程看似64个也够用了,但如果在这些线程里面使用了同步操作等待串行队列执行完成的话,就会造成阻塞,最终超出线程数量上限而崩溃。比如将上面代码中的NSLog(@"2") 改为一个写缓存之类的耗时操作。

四、线程池

由于线程数量是有上限的,并且线程切换比较耗时,所以对于性能要求较高的程序需要有线程池来管理多线程。iOS是没有系统自带的线程池的,一般都是自己实现(推荐使用dispatch_semaphore或NSOperationQueue,具体实现可以参考java的executor相关代码。需要注意的是,什么时候切换到线程池是有讲究的,一般规则是逻辑层的代码尽早切换到线程池,特别是有些逻辑可能会创建多个线程的时候,比如多个图片的下载和缓存。

五、线程的同步

线程的同步也是一个比较经典的话题了,我在这里就不想赘述了,大家可以在网上随便搜一搜,我只提一下,一般线程间同步就几种方式:

  1. 加锁
  2. 条件变量 3.信号量
  3. 串行队列+同步读异步写
  4. 内存屏障
  5. CAS原子操作

个人比较推荐的是加锁(性能要求没那么高)和条件变量(性能要求较高,逻辑相对简单的场景)。串行队列如果管理不当可能会创建多个线程,因此不做推荐。内存屏障和CAS原子操作比较底层,使用起来也没那么方便,除非是对时序和性能要求极高。

六、线程间通信

除了使用C语言的pthrea_create和pthread_join来进行线程创建和销毁时的通信外,iOS还可以使用NSMachPort和NSThread的performSelectorOnThread来做线程间通信。前者跟runloop结合,在runloop的生命周期内注册一个特定的事件来定期检查并执行,后者类似于pthread_create,在创建线程时传递一个参数。 除了这种系统提供的方法外,还有一种通用的方式,就是在线程内维护一个事件队列,外部需要给这个线程发消息时,就往队列插入一个事件,然后该线程在一个循环内定时去取事件执行。有点类似runloop的感觉,如果要跨平台的话可以考虑使用libevent(一般用来做网络通信)来实现。

结语

不管是在iOS还是其他的平台上,多线程管理都是一个复杂的话题。要用好多线程,除了要掌握一些常见的方法外,最主要还是平时编程的时候多思考,什么时候应该用多线程,以及怎么样做好线程同步和队列的选择,在追求高性能的同时保证安全性。

iOS断点下载

断点下载:是指在下载一个较大文件过程中因网络中断等主动暂停下载,当重启任下载任务时,能够从上次停止的位置继续下载,而不用重新下载。

知识点:

1.URLSession及其任务管理

URLSessionDownloadTask:是实现断点下载的核心类,专门用于下载文件到临时位置,并原生支持断点续传:

相关代码:

let configuration = URLSessionConfiguration.default

var downloadTask : URLSessionDownloadTask?

let fileURL = URL(string: "http://vjs.zencdn.net/v/oceans.mp4")

任务下载

let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)

downloadTask = session.downloadTask(with: URLRequest(url: fileURL!))
downloadTask?.resume()

继续下载

let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)

downloadTask = session.downloadTask(withResumeData: data)
downloadTask?.resume()

取消下载

downloadTask?.cancel(byProducingResumeData: { [weak self] resumeData in
   self?.downloadTask = nil
   // 其他操作
}

2.数据持久化

下载的过程本身是不处理相关数据的存储的,需要我们自己来实现。数据持久化的方式很多但支持断点下载功能的多半都是比较大型的文件。因此选择沙盒(SandBox)来存储下载的文件是十分合适的。

获取文件目录:一般都是把文件存储到documentDirectoryuserDomainMask目录

let fileManager = FileManager.default

let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!

创建写入文件路径:这里表示把文件写入MyVideos文件,文件名为:oceans.mp4

let folderName = documentDaiectory.appendingPathComponent("MyVideos")

let videoURL = folderName.appendingPathComponent("oceans.mp4")

在上一步获取文件目录已经指定了一个根目录这个会沙盒系统的根目录下再创建一个MyVideos文件

// 创建需要的文件目录
do {
   try fileManager.createDirectory(at: folderName, withIntermediateDirectories: true, attributes: nil)
   // 写入文件
} catch {
   print("创建目录失败:\(error)")
}

写入文件

do {
    try data.write(to: videoURL)
    print("写入成功")
} catch {
    print("写入失败:\(error)")
}

下次继续下载时要去做一个判断,查看是否已经存储之前下载的内容,返回TRUE则是进行继续下载,返回FALSE则是重新开始下载

let fileManager = FileManager.default

guard let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
   return false
}

let documentsFileURL = documentDirectory.appendingPathComponent("MyVideos/oceans.mp4")

if fileManager.fileExists(atPath: documentsFileURL.path) {
  do {
      // 存在 
      // 同时获取当一已下载文件的Data
      self.currentDownloadData = try Data(contentsOf: documentsFileURL)
      return true
  } catch {
      return false
  }
} else {
  return true
}

在对返回的状态做相应的处理

if isFileExist() == true {
   // 继续下载
} else {
  // 重新下载
}

3.URLSessionDownloadDelegate

除了相关下载存储操作外还要实现 URLSessionDownloadDelegate 相关代理方法

下载完成:通过URLSessionDownloadTask下载完成的文件并不会存储到指定的文件夹,而是存储在sandbox的tmp目录下的临时文件夹内。该文件夹内的数据随时都会被系统清理,因此要在适当的时候把文件转移到我们需要的文件下。

这里我们把文件存储到""MyVideos"文件下并使用"oceans.mp4"为文件名

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
   let fileManager = FileManager.default
   let documentDirectory = FileManager.default.urls(for:.documentDirectory, in: .userDomainMask).first!
   let fileURL = documentDirectory.appendingPathComponent("MyVideos/oceans.mp4")

   do {
      if isFileExist() == true {
         // 还是对文件是否存在做一个判断并做一个删除处理,因为沙盒系统本身不会自动覆盖同名文件的处理
         try fileManager.removeItem(at: fileURL)
      }
      
      // 移动到指定目录
      try fileManager.moveItem(at: location, to: fileURL)
   } catch {
      print("删除文件出错:\(error)")
   }
}

下载过程中方法:可以从该方法获取到下载的进度

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
   self.currentProgressValue = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
}

核心代码

import UIKit
import Foundation

typealias DownloadProgressBlock = (_ progreee : Float) -> ()
typealias DownloadFileBlock = (_ fileURL : URL) -> ()

class WZGVideoDownloader : NSObject {
    static var shard = WZGVideoDownloader()

    var progressBlock : DownloadProgressBlock?
  
    var fileBlock : DownloadFileBlock?

    let configuration = URLSessionConfiguration.default
    var downloadTask : URLSessionDownloadTask?
    let fileURL = URL(string: "http://vjs.zencdn.net/v/oceans.mp4")
    
    // 存储已下载data
    var currentDownloadData : Data?

    // 当前文件大小
    var currentProgressValue : Float = 0.0
    
    func startDownload(_ fileSize : Data) {
        let fileManager = FileManager.default
        let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
        let documentFileURL = documentDirectory.appendingPathComponent("MyVideos/oceans.mp4")

        let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)

        // 判断是继续下载还是重新下载
        if isFileExist() == true {
            if let data = self.currentDownloadData {
                if data == fileSize {
                    self.progressBlock?(1)
                    self.fileBlock?(documentFileURL)
                    return
                }
                self.progressBlock?(self.currentProgressValue)

                // 继续下载
                print("继续下载")
                downloadTask = session.downloadTask(withResumeData: data)
                downloadTask?.resume()
            }

        } else {
            // 重新下载
            print("重新下载")
            downloadTask = session.downloadTask(with: URLRequest(url: fileURL!))
            downloadTask?.resume()
        }
    }
    
    func stopDownload() {
        downloadTask?.cancel(byProducingResumeData: { [weak self] resumeData in
            guard let resumeData = resumeData else {
                return
            }
            self?.writeSandBox(resumeData)
            self?.downloadTask = nil
        })
    }
    
    // 判断是否有下载的文件

    func isFileExist() -> Bool {
        let fileManager = FileManager.default
        guard let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
            return false
        }

        let documentsFileURL = documentDirectory.appendingPathComponent("MyVideos/oceans.mp4")

        if fileManager.fileExists(atPath: documentsFileURL.path) {
            do {
                self.currentDownloadData = try Data(contentsOf: documentsFileURL)
                print("currentDownloadData:\(currentDownloadData)")
                return true
            } catch {
                return false
            }
        } else {
            return false
        }
    }
    
    // 写入sandbox

    func writeSandBox(_ data : Data) {
        let fileManager = FileManager.default
        let documentDaiectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!

        //创建目录几写入文件名
        let folderName = documentDaiectory.appendingPathComponent("MyVideos")

        //设置写入文件名称
        let videoURL = folderName.appendingPathComponent("oceans.mp4")

        // 创建目录
        do {
            try fileManager.createDirectory(at: folderName, withIntermediateDirectories: true, attributes: nil)
            // 写入文件
            do {
                try data.write(to: videoURL)
                print("写入成功")
            } catch {
                print("写入失败:\(error)")
            }
        } catch {
            print("创建目录失败:\(error)")
        }
    }
}

extension WZGVideoDownloader : URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        let fileManager = FileManager.default
        let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        let fileURL = documentDirectory.appendingPathComponent("MyVideos/oceans.mp4")
        do {
            if isFileExist() == true {
                // 文件存在则删除
                try fileManager.removeItem(at: fileURL)
            }
            // 下载完会保存在temp零食文具目录 转移至需要的目录
            try fileManager.moveItem(at: location, to: fileURL)
            self.fileBlock?(fileURL)
        } catch {
            print("删除文件出错:\(error)")
        }
    }

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        self.currentProgressValue = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
        DispatchQueue.main.async {
            self.progressBlock?(self.currentProgressValue)
        }
    }
}

关于OC与Swift内存管理的解惑

  • 在Swift中,如何解决闭包的循环引用?
myClosure = { [weak self] in
    guard let self = self else { return }
}
  • 那么如果是多个闭包嵌套呢?
// ✅ 只需要在最外层写一次 
myClosure1 = { [weak self] in [weak self]
    // 在顶部进行一次安全解包
    guard let self = self else { return }
    // 在这个作用域里,`self` 是一个临时的强引用,可以安全使用
    // 不用担心它被提前释放
    myClosure2 = {
        // 它会捕获上面 guard let 创建好的、非可选的强引用 self
        myClosure3 = {
            // 你可以直接、安全地调用 self 的方法或属性
        }
    }
}
  • 那么又有一个问题:在OC中的block,就需要每一层都要弱引用,第二层要先强引用再弱引用,第三层再先强引用再弱引用吧?为什么Swift不是这个逻辑? 这是因为Swift 的逻辑确实和 OC 不一样,它在这方面做了极大的简化和安全性提升。

OC 的核心问题在于:   @strongify(self) 创建的那个临时强引用 strongSelf 的生命周期会持续到包含它的那个 block 执行完毕。如果这个 block 内部又启动了一个长时间的异步任务(第二个 block),那么 strongSelf 会被第二个 block 捕获,导致 self 实例的生命周期被不必要地延长。因此,为了追求最精细的内存管理,开发者才会在每一层异步调用前都重复进行“弱化 -> 强化”的操作。

Swift 的优势在于:  guard let self = self 创建的强引用 self 是一个全新的、仅存在于外层闭包作用域内的局部变量。当 anotherAsyncTask 的闭包(内层闭包)创建时,它捕获的是这个局部的、新的 self。一旦外层闭包执行完毕(someAsyncTask 的回调结束),这个局部的 self 变量就会被销毁。anotherAsyncTask 的闭包对它的持有也就自然解除了,完全不会影响到原始 self 实例的生命周期。

特性 / 行为 Objective-C (block) Swift (closure)
弱引用声明 @weakify(self) 或 __weak typeof(self) weakSelf = self; 在捕获列表 [weak self]
临时强引用 @strongify(self) 或 __strong typeof(weakSelf) strongSelf = weakSelf; guard let self = self else { return }
嵌套捕获 内层 block 捕获由 @strongify 创建的 strongSelf,其生命周期可能过长,导致需要“再次弱化”。 内层闭包捕获由 guard let 创建的局部强引用 self。该局部变量生命周期很短,因此无需再次弱化
开发者操作 需要警惕并可能在每一层异步调用前都重复“弱化-强化”的模式。 只需要在最外层做一次“弱化-强化” ,内部可以完全放心使用。
  • 但是在实际开发中,swift代码提示引用报错,往往xcode是这样解决的,为什么?
someClosure = { [self] in }

那是因为Xcode 的首要任务是解决编译错误,而不是帮你分析内存管理。  而 [self] in 正是解决这个特定编译错误的“最直接”的语法。它解决了语法问题,但它没有解决循环引用的问题。它只是把一个隐式的强引用,变成了一个显式的强引用。所以解决循环引用问题还是需要[weak self]

  • 那么在实际开发中[self]对于内存泄露来说是错误的呗?

这个说法不完全准确,但您的警惕性非常对!更精确的说法是: 在【会】产生循环引用的场景下使用 [self],是绝对错误的,它会直接导致内存泄漏。 但是,在【不会】产生循环引用的场景下,[self] 则是安全、甚至是被推荐的写法。 所以,[self] 本身不是“错误”,它只是一个工具。错误的是在不合适的场景下使用了这个工具。

比如下列两种情况

// 这个闭包被传递给 UIView.animate,执行完动画后就会被销毁。 
// self 并没有一个属性来持有这个闭包。 
// 所以 self -> 闭包 这条强引用链不存在。 
UIView.animate(withDuration: 0.5) { [self] in 
    // 在这里使用 [self] in 是【完全正确】的。 
    // 它明确地告诉编译器:“我知道我在强引用 self,且我确定这是安全的。” 
}

// DispatchQueue.main.asyncAfter 的闭包同样是执行完就销毁。 
DispatchQueue.main.asyncAfter(deadline: .now() +1.0) { [self] in 
    // 这里使用 [self] in 也是【完全正确】的。 
}

简单的可以理解为“一次性”的工作 (用 [self] 是安全的),“长期”的规则 (必须用 [weak self])。

  • 那么又有一个场景,在对网络请求进行二次封装的情况下,在调用网络请求时,是否需要弱引用?

答案是不需要的。为什么呢?因为Alamofire临时持有了您的闭包,由于 ViewController 没有持有任何东西,所以闭包无论如何强引用 ViewController,都构不成一个闭环。因此,这里使用 [self] 来显式强引用是完全安全的。

  • 为什么感觉 OC 的 AFNetworking 封装调用时不需要弱引用?

无论是在 OC 的 AFNetworking 还是 Swift 的 Alamofire,它们【内部都没有,也不可能】自动处理您在闭包中捕获 self 导致的循环引用。防止循环引用的责任始终在调用者(也就是您)身上。

  • 那为什么您会有“AFN不需要弱引用”的印象呢?

原因和我们上面分析的完全一样:因为您在 OC 中调用 AFN 封装的场景,很可能也属于“一次性”的调用,本身就不会产生循环引用。由于AFHTTPSessionManager的实例 manager 是一个局部变量,方法执行完就释放了,success block 被 manager 临时持有,执行完也就释放了。所以,当时您在 OC 里不写弱引用是正确的,不是因为 AFNetworking 内部处理了,而是因为您的【用法】决定了它根本没有循环引用!

[MyOCNetworkManager requestWithURL:@"" parameters:nil success:^(id responseObject) { 
    // 您在这里直接使用 self,比如 [self.tableView reloadData]; 
    // 并没有写 __weak typeof(self) weakSelf = self; 
} failure:^(NSError *error) { 
    // ... 
}];
  • 那么在OC+AFNetworking的二次封装回调时,如果我依然写弱引用的话,会有问题吗?

完全没有问题,这样做在内存上是绝对安全的,但同样,它也是不必要的,并且有轻微的副作用。 在那个场景下,编写弱引用代码(即 weak/strong dance)是“安全但多余的”。

我们来对比一下两种写法:

  1. 在您调用一个“一次性”的网络请求封装时,闭包不会被您的 ViewController 持有,因此不存在循环引用。
[MyOCNetworkManager requestWithURL:@"" 
                        parameters:nil 
                           success:^(id responseObject) {
    // 直接使用 self,闭包会强引用 self。
    // 因为没有循环引用,self 会在闭包执行完后被正常释放,这是安全的。
    [self.tableView reloadData];
} 
                           failure:nil];

这种写法简洁、清晰,并且正确地表达了意图:“这是一个一次性的任务,我需要 self 在任务执行时是存在的。”

  1. 如果您坚持使用弱引用,代码会是这样:
__weak typeof(self) weakSelf = self;
[MyOCNetworkManager requestWithURL:@"/some/path" 
                        parameters:nil 
                           success:^(id responseObject) {
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) {
        return;
    }
    // 使用 strongSelf 来确保在闭包执行期间 self 不会被释放
    [strongSelf.tableView reloadData];
} 
                           failure:nil];
  1. 内存安全吗?

是的,100%安全。弱引用永远不会增加引用计数,所以它绝不会“创造”出一个循环引用。从这个角度看,它没有任何“问题”。

  1. 有什么副作用或缺点吗?  

有,和 Swift 的情况完全相同:

代码变得冗余:为了一个没有必要的安全措施,您多写了三行样板代码 (__weak__strongif)。这降低了代码的简洁性。

意图变得模糊:当其他开发者读到这段代码时,他们会看到 weak/strong dance,这通常是一个强烈的信号,表示“这里有循环引用的风险”。他们可能会因此花时间去寻找一个实际并不存在的风险点,增加了维护成本。

极端情况下的功能差异:如果 self (比如一个 UIViewController) 在网络请求发出后、但在回调执行前被释放了(例如用户快速返回上一个页面),那么:

写法一(强引用) :self 会被闭包“续命”,直到闭包执行完毕。[self.tableView reloadData] 会被执行。

写法二(弱引用) :self 会被立即释放。当回调执行时,strongSelf 会是 nil,代码会直接 return[strongSelf.tableView reloadData] 不会被执行。

[MyOCNetworkManager requestWithURL:@"" 
                        parameters:nil 
                           success:^(id responseObject) {
    // 直接使用 self,闭包会强引用 self。
    // 因为没有循环引用,self 会在闭包执行完后被正常释放,这是安全的。
    [self.tableView reloadData];
} 
                           failure:nil];
 __weak typeof(self) weakSelf = self;
[MyOCNetworkManager requestWithURL:@"" 
                        parameters:nil 
                           success:^(id responseObject) {
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) {
        return;
    }
    // 使用 strongSelf 来确保在闭包执行期间 self 不会被释放
    [strongSelf.tableView reloadData];
} 
                           failure:nil];
  • 极端情况下的功能差异:如果 self (比如一个 UIViewController) 在网络请求发出后、但在回调执行前被释放了(例如用户快速返回上一个页面),写法二(弱引用):self 会被立即释放。当回调执行时,strongSelf 会是 nil,代码会直接 return,[strongSelf.tableView reloadData] 不会被执行。 但是实际中的这种写法很常见,我以前就是这么写,但是没有发现什么问题啊?返回页面还是会执行dealloc‌,为什么?
  1. 大多数情况下,行为差异“无关紧要”

我们回到最常见的场景:网络请求回来后,更新界面。

Objective-C

// 弱引用写法
__weak typeof(self) weakSelf = self;
[MyNetworkManager request:^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) {
        return; // 如果 self 销毁了,就直接返回
    }
    [strongSelf.tableView reloadData]; // 更新界面
}];

设想一下用户快速返回,self (也就是 ViewController) 被销毁了。这时网络回调回来了,因为 strongSelf 是 nil,所以 [strongSelf.tableView reloadData] 这行代码没有被执行**。

这对用户来说是问题吗?完全不是。  因为界面都已经消失了,tableView 也不存在了,去刷新一个不存在的界面本来就是一件没有意义的事情。代码不执行,反而更干净利落。

所以,在 99% 的 UI 更新场景中,弱引用导致的“代码不执行”这个行为差异,不仅不是问题,反而是我们期望的、最合理的结果。

  1. “快速返回”的极端情况发生概率低

要触发这个“功能差异”,需要满足一个条件:self 的销毁发生在“网络请求发出后”和“回调执行前”这个短暂的时间窗口内。

对于大多数响应速度很快的 API 来说,这个窗口可能只有几百毫秒。用户需要操作得非常快才能正好卡在这个时间点上。因此,在日常测试和使用中,这个情况本身就不容易遇到。

  1. 即使代码不执行,也无可见负面影响

假设回调里做的事情是 [self hideLoadingIndicator]。如果用户已经返回了上一个页面,那个加载指示器 loadingIndicator 本来就已经随着页面消失了,所以 hideLoadingIndicator 这行代码执不执行,用户根本感知不到任何区别。

iOS图片编辑项目推荐

在 GitHub 上确实有不少优秀且实用的 iOS 图片编辑相关的开源项目和 Demo。这些项目覆盖了基础编辑(裁剪、旋转、调整)、滤镜应用、涂鸦、贴纸添加、高级特效等功能。以下是一些值得关注的项目,适合学习和集成:


🛠 一、功能较全的图片编辑框架

  1. TOCropViewController

  2. YPImagePicker


🎨 二、滤镜 & 特效处理

  1. MetalPetal
    • 简介:基于 Metal 的高性能图像处理框架,支持滤镜链、实时渲染。
    • 链接https://github.com/MetalPetal/MetalPetal
    • 特点:替代 GPUImage,性能优异,适合复杂滤镜开发。

✏️ 三、涂鸦 & 标注工具

  1. SignatureView

  2. PencilKitExample


🧩 四、完整图片编辑 App Demo

  1. PhotoEditDebug


✅ 选择建议:

  • 需要快速集成裁剪功能TOCropViewController
  • 开发完整图片编辑 App → 参考 PhotoEditDebug
  • 实现高性能滤镜MetalPetal
  • 添加手绘涂鸦PencilKitSignatureView

💡 学习资源:

这些项目大多持续维护,代码质量较高。建议根据需求先尝试 Demo,再选择性集成模块到项目中!如果遇到具体实现问题,可以深入查看其 Issues 或源码实现逻辑。

❌