普通视图

发现新文章,点击刷新页面。
昨天 — 2025年12月20日iOS

Xcode 26还没有适配SceneDelegate的app建议尽早适配

作者 wvy
2025年12月19日 19:35

Xcode 26之前不需要多窗口的很多app没有适配SceneDelegate,升级到Xcode 26后运行没有问题,但是控制台有以下输出:

`UIScene` lifecycle will soon be required. Failure to adopt will result in an assert in the future.

UIApplicationDelegate 中的相关生命周期函数也有弃用标记:

/// Tells the delegate that the application has become active 
/// - Note: This method is not called if `UIScene` lifecycle has been adopted. 
- (void)applicationDidBecomeActive:(UIApplication *)application API_DEPRECATED("Use UIScene lifecycle and sceneDidBecomeActive(_:) from UISceneDelegate or the UIApplication.didBecomeActiveNotification instead.", ios(2.0, 26.0), tvos(9.0, 26.0), visionos(1.0, 26.0)) API_UNAVAILABLE(watchos);

建议尽早适配

方案举例

以下是我的适配方案,供大家参考

  • 兼容iOS13以下版本;
  • app只有单窗口场景。

1. 配置Info.plist

Delegate Class Name和Configuration Name 可自定义

image.png

2. 配置SceneDelegate

  • 创建SceneDelegate class 类名要和Info.plist中配置一致

image.png

  • appDelegate中实现代理
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options  API_AVAILABLE(ios(13.0)){
   //  name要和Info.plist中配置一致
  return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
}

- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions  API_AVAILABLE(ios(13.0)){
  // 释放资源,单窗口app不用关注
}

3. 新建单例 AppLifecycleHelper 实现AppDelegate和SceneDelgate共享的方法

  • iOS 13 及以上需要在scene: willConnectToSession: options: 方法中创建Window,之前仍然在 didFinishLaunchingWithOptions:

AppDelegate:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [AppLifecycleHelper sharedInstance].launchOptions = launchOptions;
     // ... 自定义逻辑
    if (@available(iOS 13, *)) {
 
    } else {
        [[AppLifecycleHelper sharedInstance] createKeyWindow];
    }
}

SceneDelgate:

URL冷启动APP时不调用openURLContexts方法,这里保存URL在DidBecomeActive处理

- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions  API_AVAILABLE(ios(13.0)){
    [[AppLifecycleHelper sharedInstance] createKeyWindowWithScene:(UIWindowScene *)scene];
    // 通过url冷启动app,一般只有一个url 
    for (UIOpenURLContext *context **in** connectionOptions.URLContexts) {
        NSURL *URL = context.URL;
        if (URL && URL.absoluteString.length > 0) {
            self.launchUrl = URL;
        }
    }
}

AppLifecycleHelper:

- (void)createKeyWindow {
    UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    [self setupMainWindow:window];
}

- (void)createKeyWindowWithScene:(UIWindowScene *)scene API_AVAILABLE(ios(13.0)) {
    UIWindow *window = [[UIWindow alloc] initWithWindowScene:scene];
    [self setupMainWindow:window];
}

- (void)setupMainWindow:(UIWindow *)window {
}
  • 实现SceneDelegate后appDelegate 中失效的方法

AppLifecycleHelper中实现,共享给两个DelegateClass

- (void)applicationDidBecomeActive:(UIApplication *)application {
    [[AppLifecycleHelper sharedInstance] appDidBecomeActive];
}

- (void)applicationWillResignActive:(UIApplication *)application {

}

- (void)applicationDidEnterBackground:(UIApplication *)application {

}

- (void)applicationWillEnterForeground:(UIApplication *)application {

}
  
 /// URL Scheme
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, **id**> *)options {

}

/// 接力用户活动
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<**id**<UIUserActivityRestoring>> * _Nullable))restorationHandler {

}

/// 快捷方式点击
- (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL))completionHandler API_AVAILABLE(ios(9.0)) {
}

SceneDelegate部分代码示例:


- (void)sceneDidBecomeActive:(UIScene *)scene  API_AVAILABLE(ios(13.0)){
    [[AppLifecycleHelper sharedInstance] appDidBecomeActiveWithLaunchUrl:self.launchUrl];
    // 清空冷启动时的url
    self.launchUrl = nil;
}

这个方法总结下来就是求同存异,由Helper提供SceneDelegate与AppDelegate相同或类似的方法,适合单窗口、且支持iOS 13以下的app;

另外注意URL Scheme冷启动app不会执行openURL需要记录URL,在合适的时机(一般是DidBecomeActive)处理。

UIWindowScene 使用指南:掌握 iOS 多窗口架构

作者 sweet丶
2025年12月19日 20:32

引言

在 iOS 13 之前,iOS 应用通常只有一个主窗口(UIWindow)。但随着 iPadOS 的推出和多任务处理需求的增加,Apple 引入了 UIWindowScene 架构,让单个应用可以同时管理多个窗口,每个窗口都有自己的场景(Scene)。本文将深入探讨 UIWindowScene 的核心概念和使用方法。

什么是 UIWindowScene?

UIWindowScene 是 iOS 13+ 中引入的新架构,它代表了应用程序用户界面的一个实例。每个场景都有自己的窗口、视图控制器层级和生命周期管理。

核心组件关系

UISceneSessionUIWindowSceneUIWindowUIViewControllerUISceneConfiguration

基础配置

1. 项目设置

首先需要在 Info.plist 中启用多场景支持:

<key>UIApplicationSceneManifest</key>
<dict>
    <key>UIApplicationSupportsMultipleScenes</key>
    <true/>
    <key>UISceneConfigurations</key>
    <dict>
        <key>UIWindowSceneSessionRoleApplication</key>
        <array>
            <dict>
                <key>UISceneConfigurationName</key>
                <string>Default Configuration</string>
                <key>UISceneDelegateClassName</key>
                <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
                <key>UISceneStoryboardFile</key>
                <string>Main</string>
            </dict>
        </array>
    </dict>
</dict>

2. SceneDelegate 实现

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    
    func scene(_ scene: UIScene, 
               willConnectTo session: UISceneSession, 
               options connectionOptions: UIScene.ConnectionOptions) {
        
        guard let windowScene = (scene as? UIWindowScene) else { return }
        
        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = YourRootViewController()
        window?.makeKeyAndVisible()
        
        // 处理深度链接
        if let userActivity = connectionOptions.userActivities.first {
            self.scene(scene, continue: userActivity)
        }
    }
    
    func sceneDidDisconnect(_ scene: UIScene) {
        // 场景被系统释放时调用
    }
    
    func sceneDidBecomeActive(_ scene: UIScene) {
        // 场景变为活动状态时调用
    }
    
    func sceneWillResignActive(_ scene: UIScene) {
        // 场景即将变为非活动状态时调用
    }
    
    func sceneWillEnterForeground(_ scene: UIScene) {
        // 场景即将进入前台
    }
    
    func sceneDidEnterBackground(_ scene: UIScene) {
        // 场景进入后台
    }
}

创建和管理多个场景

1. 动态创建新窗口

class SceneManager {
    static func createNewScene(with userInfo: [String: Any]? = nil) {
        let activity = NSUserActivity(activityType: "com.yourapp.newWindow")
        activity.userInfo = userInfo
        activity.targetContentIdentifier = "newWindow"
        
        let options = UIScene.ActivationRequestOptions()
        options.requestingScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
        
        UIApplication.shared.requestSceneSessionActivation(
            nil,
            userActivity: activity,
            options: options,
            errorHandler: { error in
                print("Failed to create new scene: \(error)")
            }
        )
    }
}

2. 场景配置管理

// 自定义场景配置
class CustomSceneDelegate: UIResponder, UIWindowSceneDelegate {
    static let configurationName = "CustomSceneConfiguration"
    
    func scene(_ scene: UIScene, 
               willConnectTo session: UISceneSession, 
               options connectionOptions: UIScene.ConnectionOptions) {
        
        guard let windowScene = scene as? UIWindowScene else { return }
        
        // 根据场景角色自定义配置
        if session.role == .windowApplication {
            configureApplicationWindow(scene: windowScene, 
                                      session: session, 
                                      options: connectionOptions)
        } else if session.role == .windowExternalDisplay {
            configureExternalDisplayWindow(scene: windowScene)
        }
    }
    
    private func configureApplicationWindow(scene: UIWindowScene,
                                          session: UISceneSession,
                                          options: UIScene.ConnectionOptions) {
        // 主窗口配置
        let window = UIWindow(windowScene: scene)
        
        // 根据用户活动恢复状态
        if let userActivity = options.userActivities.first {
            window.rootViewController = restoreViewController(from: userActivity)
        } else {
            window.rootViewController = UIViewController()
        }
        
        window.makeKeyAndVisible()
        self.window = window
    }
}

场景间通信与数据共享

1. 使用 UserActivity 传递数据

class DocumentViewController: UIViewController {
    var document: Document?
    
    func openInNewWindow() {
        guard let document = document else { return }
        
        let userActivity = NSUserActivity(activityType: "com.yourapp.editDocument")
        userActivity.title = "Editing \(document.title)"
        userActivity.userInfo = ["documentId": document.id]
        userActivity.targetContentIdentifier = document.id
        
        let options = UIScene.ActivationRequestOptions()
        UIApplication.shared.requestSceneSessionActivation(
            nil,
            userActivity: userActivity,
            options: options,
            errorHandler: nil
        )
    }
}

// 在 SceneDelegate 中处理
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    guard let windowScene = scene as? UIWindowScene,
          let documentId = userActivity.userInfo?["documentId"] as? String else {
        return
    }
    
    let document = fetchDocument(by: documentId)
    let editorVC = DocumentEditorViewController(document: document)
    windowScene.windows.first?.rootViewController = editorVC
}

2. 使用通知中心通信

extension Notification.Name {
    static let documentDidChange = Notification.Name("documentDidChange")
    static let sceneDidBecomeActive = Notification.Name("sceneDidBecomeActive")
}

class DocumentManager {
    static let shared = DocumentManager()
    private init() {}
    
    func updateDocument(_ document: Document) {
        // 更新数据
        NotificationCenter.default.post(
            name: .documentDidChange,
            object: nil,
            userInfo: ["document": document]
        )
    }
}

高级功能

1. 外部显示器支持

class ExternalDisplayManager {
    static func setupExternalDisplay() {
        // 监听外部显示器连接
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleScreenConnect),
            name: UIScreen.didConnectNotification,
            object: nil
        )
    }
    
    @objc private static func handleScreenConnect(notification: Notification) {
        guard let newScreen = notification.object as? UIScreen,
              newScreen != UIScreen.main else { return }
        
        let options = UIScene.ActivationRequestOptions()
        options.requestingScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
        
        let activity = NSUserActivity(activityType: "externalDisplay")
        UIApplication.shared.requestSceneSessionActivation(
            nil,
            userActivity: activity,
            options: options,
            errorHandler: nil
        )
    }
}

// 在 SceneDelegate 中配置外部显示器场景
func configureExternalDisplayWindow(scene: UIWindowScene) {
    let window = UIWindow(windowScene: scene)
    window.screen = UIScreen.screens.last // 使用外部显示器
    window.rootViewController = ExternalDisplayViewController()
    window.makeKeyAndVisible()
}

2. 场景状态保存与恢复

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
        // 返回用于恢复场景状态的 activity
        let activity = NSUserActivity(activityType: "restoration")
        if let rootVC = window?.rootViewController as? Restorable {
            activity.addUserInfoEntries(from: rootVC.restorationInfo)
        }
        return activity
    }
    
    func scene(_ scene: UIScene, 
               willConnectTo session: UISceneSession, 
               options connectionOptions: UIScene.ConnectionOptions) {
        
        // 检查是否有保存的状态
        if let restorationActivity = session.stateRestorationActivity {
            restoreState(from: restorationActivity)
        }
    }
}

最佳实践

1. 内存管理

class MemoryAwareSceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    func sceneDidEnterBackground(_ scene: UIScene) {
        // 释放不必要的资源
        if let vc = window?.rootViewController as? MemoryManageable {
            vc.releaseUnnecessaryResources()
        }
    }
    
    func sceneWillEnterForeground(_ scene: UIScene) {
        // 恢复必要的资源
        if let vc = window?.rootViewController as? MemoryManageable {
            vc.restoreResources()
        }
    }
}

2. 错误处理

enum SceneError: Error {
    case sceneCreationFailed
    case invalidConfiguration
    case resourceUnavailable
}

class RobustSceneManager {
    static func createSceneSafely(configuration: UISceneConfiguration,
                                completion: @escaping (Result<UIWindowScene, SceneError>) -> Void) {
        
        let options = UIScene.ActivationRequestOptions()
        
        UIApplication.shared.requestSceneSessionActivation(
            nil,
            userActivity: nil,
            options: options
        ) { error in
            if let error = error {
                completion(.failure(.sceneCreationFailed))
            } else {
                // 监控新场景创建
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                    if let newScene = UIApplication.shared.connectedScenes
                        .compactMap({ $0 as? UIWindowScene })
                        .last {
                        completion(.success(newScene))
                    } else {
                        completion(.failure(.sceneCreationFailed))
                    }
                }
            }
        }
    }
}

调试技巧

1. 场景信息日志

extension UIWindowScene {
    func logSceneInfo() {
        print("""
        Scene Information:
        - Session: \(session)
        - Role: \(session.role)
        - Windows: \(windows.count)
        - Screen: \(screen)
        - Activation State: \(activationState)
        """)
    }
}

// 在 AppDelegate 中监控所有场景
func application(_ application: UIApplication, 
               configurationForConnecting connectingSceneSession: UISceneSession,
               options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    
    print("Connecting scene: \(connectingSceneSession)")
    return UISceneConfiguration(
        name: "Default Configuration",
        sessionRole: connectingSceneSession.role
    )
}

2. 内存泄漏检测

class SceneLeakDetector {
    static var activeScenes: [String: WeakReference<UIWindowScene>] = [:]
    
    static func trackScene(_ scene: UIWindowScene) {
        let identifier = "\(ObjectIdentifier(scene).hashValue)"
        activeScenes[identifier] = WeakReference(object: scene)
        
        // 定期检查泄漏
        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            self.checkForLeaks()
        }
    }
    
    private static func checkForLeaks() {
        activeScenes = activeScenes.filter { $0.value.object != nil }
        print("Active scenes: \(activeScenes.count)")
    }
}

class WeakReference<T: AnyObject> {
    weak var object: T?
    init(object: T) {
        self.object = object
    }
}

兼容性考虑

1. 向后兼容 iOS 12

@available(iOS 13.0, *)
class ModernSceneDelegate: UIResponder, UIWindowSceneDelegate {
    // iOS 13+ 实现
}

class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    
    func application(_ application: UIApplication, 
                   didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        if #available(iOS 13.0, *) {
            // 使用场景架构
        } else {
            // 传统 UIWindow 设置
            window = UIWindow(frame: UIScreen.main.bounds)
            window?.rootViewController = UIViewController()
            window?.makeKeyAndVisible()
        }
        return true
    }
}

结语

UIWindowScene 架构为 iOS 应用带来了强大的多窗口支持,特别适合 iPadOS 和需要复杂多任务处理的应用。通过合理使用场景管理,可以:

  1. 提供更好的多任务体验
  2. 支持外部显示器
  3. 实现高效的状态保存与恢复
  4. 优化内存使用

虽然学习曲线较陡,但掌握 UIWindowScene 将显著提升应用的现代化水平和用户体验。


示例项目: 完整的示例代码可以在 GitHub 仓库 找到。

进一步阅读:

swift中的知识总结(一)

2025年12月19日 15:23

一、associatedtype的用法

在swift中,泛型T是一个非常强大的特性,它允许我们编写灵活且可复用的代码。而当我们在 协议(Protocol) 中需要使用泛型时,associatedtype 就派上了用场。

在 Swift 的协议中,我们无法直接使用泛型 <T>,但可以使用 associatedtype 关键字来声明一个占位类型,让协议在不确定具体类型的情况下仍然能够正常使用。

1、让协议支持不同数据类型的

protocol SomeProtocol {
    associatedtype SomeType // 声明一个占位类型 SomeType,但不指定具体类型。
    func doSomething(with value: SomeType)
}

// Int类型
protocol SomeProtocol {
    associatedtype Item
    mutating func doSomething(with value: Item)
    func getItem(at index: Int) -> Item
}

struct ContainerDemo: SomeProtocol {

    typealias Item = Int // 指定Item为Int类型
    private var items: [Int] = []

    mutating func doSomething(with value: Int) {
        items.append(value)
        print(value)
    }

    func getItem(at index: Int) -> Int {
        return items[index]
    }
}

// String类型
struct StringContainer: SomeProtocol {

    typealias Item = String
    private var items: [String] = []

    mutating func doSomething(with value: String) {
        items.append(value)
    }

    func getItem(at index: Int) -> String {
        return items[index]
    }
}

protocol StackProtocol {
    associatedtype Element
    mutating func push(_ item: Element)
    mutating func pop() -> Element?
}

struct IntStack: StackProtocol {

    typealias Element = Int
    private var stacks: [Int] = []

    mutating func push(_ item: Int) {
        stacks.append(item)
    }

    mutating func pop() -> Int? {
        return stacks.popLast()
    }
}

2、使用where关键词限定类型

有时候希望assocaitedtype只能是某种类型的子类或实现了某个协议。可以使用where关键字进行类型约束

protocol Summable {
    associatedtype Number: Numeric // 限定Number必须是Numeric协议的子类型( Int、Double)
     func sum(a: Number,b: Number) -> Number
}

struct myIntergerAddr: Summable {
     func sum(a: Int, b: Int) -> Int {
        return a + b
    }
}

// 使用泛型结构体遵循协议
struct myGenericSatck<T>: StackProtocol {
    
    private var elements: [T] = []
    var isEmpty: Bool {return elements.isEmpty}
    var count: Int {return elements.count}

    mutating func push(_ item: T) {
        elements.append(item)
    }

    mutating func pop() -> T? {
        return elements.popLast()
    }
}

3、associatedtype 与泛型的区别

比较项 associatedtype (协议中的泛型) 普通泛型
适用范围 只能用于 协议 可用于 类、结构体、函数
作用 让协议支持不确定的类型,由实现者决定具体类型 让类型/函数支持泛型
例子 protocol Container { associatedtype Item } struct Stack {}
限制 只能用于协议,不能直接实例化 适用于所有类型

4、什么时候使用 associatedtype

  • 当你需要创建一个通用的协议,但不想限定某个具体类型时。
  • 当不同的实现类需要指定不同的数据类型时。
  • 当你希望协议中的某些类型参数具备类型约束时(如 where 关键字)。

二、Subscript下标的用法

  • 是一种访问集合、列表或序列中元素成员的快捷方式。它允许你通过下标语法(使用方括号 [])来访问实例中的数据,而不需要调用方法。

  • 使用Subscript可以给任意类型(枚举、结构体、类)增加下标功能。

  • subscript的语法类似于实例方法,计算属性,本质就是方法

// demo1
struct TimesTable {
    let multiplier: Int

    subscript(index: Int) -> Int {
        return multiplier * index
    }
}

let threeTimesTable = TimesTable(multiplier: 3)
print(threeTimesTable[6])  // 输出: 18
    
// demo2
class MyPoint {
    var x = 0.0
    var y = 0.0
    subscript(index: Int) ->Double {
        set {
            if index == 0 {
                x = newValue
            } else if index == 1 {
                y = newValue
            }
        }

        get {
            if index == 0 {
                return x
            } else if (index == 1) {
                return y
            }
            return 0
        }
    }
}
 var mmpoint = MyPoint()
  mmpoint[0] = 11.1
  mmpoint[1] = 22.2

  print(mmpoint.x)
  print(mmpoint.y)
  print(mmpoint[0])
  print(mmpoint[1])
    
  // dem3
    struct Container {
    var items: [Int] = []
    
    // 单个整数下标
    subscript(index: Int) -> Int {
        return items[index]
    }
    
    // 范围下标
    subscript(range: Range<Int>) -> [Int] {
        return Array(items[range])
    }
    
    // 可变参数下标
    subscript(indices: Int...) -> [Int] {
        return indices.map { items[$0] }
    }
}

1、subscript中定义的返回值类型决定了
2、get方法的返回值类型 set方法中的newvalue的类型

3、subscript可以接受多个参数,并且类型任意

4、subscript可以没有set方法,但是必须要有get方法,如果只有get方法,可以省略get关键字

5、可以设置参数标签

6、下标可以是类型方法

三、swift中的迭代机制Sequence、collection、Iterator、AsyncSequence

image.png

在swift中,Sequence是一个协议,表示可以被逐一遍历的有序集合。一个符合Sequence协议的类型可以使用for-in循环迭代其所有元素。

Sequence是swift集合类型(Array,Dictionary、set等)的基础协议,许多高级功能如:map、filter、 reduce都依赖于它

常见的 Sequence 类型

许多 Swift 标准库类型都符合 Sequence 协议,例如:

Array:一个有序的集合。

Set:一个无序、唯一的集合。

Dictionary:键值对集合。

Range:连续的整数范围。

String:一个字符序列。

/// Sequence的核心定义
public protocol Sequence {
    /// 表示序列中元素的类型。
    associatedtype Element
    associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
    /// 返回一个迭代器对象,该对象遵循 IteratorProtocol 协议,并提供逐一访问元素的功能。
    func makeIterator() -> Iterator
}

public protocol IteratorProtocol {
    associatedtype Element
    /// 每次调用时返回序列的下一个元素;如果没有更多元素可用,则返回 nil。
    mutating func next() -> Element?
}

总结:

1.Sequence只承诺“能生成迭代器”,不能保证反复便利,也不保证有count

2.迭代器几乎总是是struct:值语义保证“复制一份就从头开始”,不会意外共享状态

3.单趟序列完全合法;第二次makeIterator()可以返回空迭代器

// 可以创建自己的类型并使符合Sequence协议,只需要实现makeIterator()方法,并返回一个符合IteratorProtocol的迭代器
// 自定义一个从n倒数到0的序列
struct myCountDownDemo: Sequence {
    
    let start: Int
    func makeIterator() -> Iterator {
        Iterator(current: start)
    }

    struct Iterator: IteratorProtocol {
        var current: Int
    
        mutating func nex() -> Int? {
            guard current >= 0 else {return nil}
            defer {current -= 1}
            return current
        }
    }
}
// 调用了myArr.makeIterator()拿到一个迭代器 反复调用iterator.next() 返回的可选值解包后赋值给item
for n in myCountDownDemo(start: 3) {
     print(n)
}

let myArr = [1,5,6,8]
for item in myArr {
    print(item)
}
// for in 实际执行的是
var iterator = myArr.makeIterator()
while let element = iterator.next() {
    print(element)
}
    
// collection可以额外保证:多次遍历且顺序稳定,提供count、endIndex、下标访问,支持切片、前缀、后缀等默认实现
// 三种安全写法

// 方法一
todoItems.removeAll{$0 == "B"}

// 方法二 先记下索引,后删除
let indexsToRemove = todoItems.indices.filter{todoItems[$0] == "B"}
for i in indexsToRemove.reversed() {
    todoItems.remove(at: i)
}

// 方法三
todoItems = todoItems.filter{$0 != "B"}
//map
var numbersArr = [3,6,8]
let squares = numbersArr.map{$0 * $0}
print(squares) // 输出 [9,36,64]

// filter过滤列表中的元素
let eventNumbers = numbersArr.filter{ $0 % 2 == 0}
print(eventNumbers) // 输出[6,8]

// reduce将列表中所有元素组合成一个值
let sum = numbersArr.reduce(0 , +)
print(sum) // 输出17

// forEach对列表中的每个元素执行操作
numbersArr.forEach{print($0)}
协议 核心能力 特点与限制 常见实现
IteratorProtocol 通过 next() 方法单向、一次性地提供下一个元素 只进不退,遍历后即消耗。是所有迭代的基础。 通常作为 Sequence 的一部分实现,很少直接使用。
Sequence 可进行顺序迭代(如 for-in 循环),支持 mapfilterreduce 等操作 不一定可多次遍历,不保证通过下标访问元素 有限序列(如数组迭代器)、无限序列(如斐波那契数列生成器)
Collection 在 Sequence 基础上,可多次、非破坏性访问,并支持通过下标索引访问任意有效位置的元素 必须是有限的,并且索引操作的时间复杂度有明确规定(如 startIndexendIndex ArrayStringDictionarySet 以及自定义的集合类型。

AsyncSequence 是 Swift 并发模型的重要部分,特别适合处理:

  • 异步数据流(网络请求、文件读取)
  • 实时数据(传感器数据、消息推送)
  • 分页或懒加载数据
  • 长时间运行的数据生成任务

而 Sequence 更适合:

  • 内存中的集合操作
  • 同步数据处理
  • 简单的数据转换

选择依据:如果你的数据源是异步的或会产生延迟,使用 AsyncSequence;如果数据是同步可用的,使用 Sequence

// demo1
import Foundation

// 自定义异步序列
struct AsyncCountdown: AsyncSequence {
    typealias Element = Int
    
    let count: Int
    
    // 必须实现 makeAsyncIterator()
    func makeAsyncIterator() -> AsyncIterator {
        AsyncIterator(count: count)
    }
    
    // 异步迭代器
    struct AsyncIterator: AsyncIteratorProtocol {
        var count: Int
        
        // 注意:next() 是异步的!
        mutating func next() async -> Int? {
            guard count > 0 else { return nil }
            
            // 模拟异步等待
            await Task.sleep(1_000_000_000)  // 等待1秒
            
            let value = count
            count -= 1
            return value
        }
    }
}

// demo2
// 模拟从网络获取分页数据
struct PaginatedAPISequence: AsyncSequence {
    typealias Element = [String]
    
    let totalPages: Int
    let delay: UInt64
    
    func makeAsyncIterator() -> AsyncIterator {
        AsyncIterator(totalPages: totalPages, delay: delay)
    }
    
    struct AsyncIterator: AsyncIteratorProtocol {
        let totalPages: Int
        let delay: UInt64
        var currentPage = 0
        
        mutating func next() async throws -> [String]? {
            guard currentPage < totalPages else { return nil }
            
            // 模拟网络延迟
            await Task.sleep(delay)
            
            // 模拟获取数据
            let items = (0..<10).map { "Item \(currentPage * 10 + $0)" }
            currentPage += 1
            
            return items
        }
    }
}

// 使用
func fetchPaginatedData() async throws {
    let pageSize = 10
    let apiSequence = PaginatedAPISequence(totalPages: 5, delay: 500_000_000)
    
    for try await page in apiSequence {
        print("收到页面数据: \(page.count) 条")
        // 处理数据...
    }
}

GetX 状态管理实践

作者 feelingHy
2025年12月19日 14:30

下面内容只关注 GetxController / GetBuilder / Obx / 局部状态组件这些部分。


GetX 状态管理实践说明

本文介绍在项目中如何使用 GetxControllerGetBuilderObx / GetX 等组件来组织业务逻辑和控制 UI 更新。

GetxController 的角色与生命周期

GetxController 用来承载页面或模块的业务状态与逻辑,通常搭配 StatelessWidget 使用,无需再写 StatefulWidget。

  • 常用生命周期方法:
    • onInit:Controller 创建时调用,做依赖注入、初始请求、订阅等。
    • onReady:首帧渲染后调用,适合做需要 UI 已经渲染的操作(弹窗、导航等)。
    • onClose:Controller 销毁时调用,用于取消订阅、关闭 Stream、释放资源。

推荐习惯:

  • 把原来写在 StatefulWidget initState / dispose 里面的逻辑迁移到 Controller 的 onInit / onClose 中,UI 层尽量保持“傻瓜视图”。

GetX 中的两种状态管理方案

GetX 内置两类状态管理方式:简单状态管理(GetBuilder)与响应式状态管理(Obx / GetX)。

方案一:简单状态管理(GetBuilder + GetxController)

适用场景:不想使用 Rx 类型(.obs),希望显式控制刷新时机。

  • 写法示例:

    class CounterController extends GetxController {
      int count = 0;
    
      void increment() {
        count++;
        update(); // 手动触发使用该 controller 的 GetBuilder 重建
      }
    }
    
    class CounterPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final controller = Get.put(CounterController());
    
        return Scaffold(
          body: Center(
            child: GetBuilder<CounterController>(
              builder: (c) => Text('Count: ${c.count}'),
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: controller.increment,
          ),
        );
      }
    }
    
  • 特点:

    • 无需 .obs,状态是普通字段。
    • 只有调用 update() 的时候,使用该 Controller 的 GetBuilder 才会重建。
    • 适合页面级、大块 UI、不太频繁刷新场景。

方案二:响应式状态管理(Obx / GetX + Rx)

适用场景:已经在使用 .obs,或希望局部 UI 随状态变化自动刷新。

  • 写法示例:

    class CounterController extends GetxController {
      var count = 0.obs;
    
      void increment() => count++;
    }
    
    class CounterPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final controller = Get.put(CounterController());
    
        return Scaffold(
          body: Center(
            child: Obx(() => Text('Count: ${controller.count}')),
            // 或
            // child: GetX<CounterController>(
            //   builder: (c) => Text('Count: ${c.count}'),
            // ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: controller.increment,
          ),
        );
      }
    }
    
  • 特点:

    • 变量通过 .obs 变为 Rx 类型(如 RxInt、RxString)。
    • 一旦值变化,Obx / GetX 所在的小部件自动重建,无需写 update()
    • 适合高频、小区域更新,如计数器、进度、网络状态、Switch 等。

两种方案的混用

在同一个项目、同一个 Controller 中,可以同时使用:

  • 一部分状态使用普通字段 + GetBuilder
  • 一部分状态使用 .obs + Obx / GetX

经验规则:

  • 页面大块区域、刷新不频繁 → 优先 GetBuilder
  • 小范围、高频刷新 → 优先 Obx / GetX

GetBuilder 的生命周期回调

GetBuilder 本质上是一个 StatefulWidget,内部有完整的 State 生命周期,对外通过参数暴露部分回调:[1]

  • 常用回调参数:
    • initState:对应 State.initState,适合调用 Controller 方法、发请求等。
    • didChangeDependencies:父级依赖变化时触发,用得不多。
    • didUpdateWidget:父组件重建、参数改变时触发。
    • dispose:组件销毁时触发,适合释放本地资源。

示例:

GetBuilder<CounterController>(
  initState: (_) {
    // widget 创建时执行
  },
  dispose: (_) {
    // widget 销毁时执行
  },
  builder: (c) => Text('Count: ${c.count}'),
);

建议:

  • 页面 /模块的“生命周期逻辑”优先放在 Controller.onInit/onClose
  • 某个局部 Widget 有特别的创建 / 销毁逻辑时,再使用 GetBuilder 的 initState / dispose

局部状态组件:ValueBuilder 与 ObxValue

对于“只在一个小部件内部使用”的临时状态,可以使用局部状态组件,而不必放入 Controller:

  • ValueBuilder(简单本地状态):
    dart ValueBuilder<bool>( initialValue: false, builder: (value, update) => Switch( value: value, onChanged: update, // update(newValue) ), );

  • ObxValue(本地 Rx 状态):

    ObxValue<RxBool>(
      (data) => Switch(
        value: data.value,
        onChanged: data, // 相当于 (v) => data.value = v
      ),
      false.obs,
    );
    

使用建议:

  • 仅在该 Widget 内使用且与全局业务无关的状态 → 用 ValueBuilder / ObxValue
  • 会被多个 Widget 或页面共享的状态 → 放入 GetxController,再用 GetBuilder / Obx 监听。

快速选型表

需求场景 状态写法 UI 组件 刷新方式
不想用 Rx,页面级 / 大块区域 普通字段 GetBuilder 手动 update()
已使用 .obs,局部自动刷新 .obs(RxXX 类型) Obx / GetX 值变化自动刷新
单个小 widget 内部的临时简单状态 普通字段 ValueBuilder 调用 updateFn
单个小 widget 内部的临时响应式状态 .obs ObxValue 值变化自动刷新

在这种“页面加载时请求 API”的需求里,推荐把请求放在 GetxController 的生命周期 里做,而不是放在页面 Widget 里。

方案一:在 onInit 里请求

适合“只要创建了这个 Controller(进入页面)就立刻请求”的场景。

class ArticleController extends GetxController {
  int pageCount = 0;              // 简单状态
  var likeCount = 0.obs;          // 响应式状态
  var isFavorite = false.obs;
  var loading = false.obs;        // 加载状态
  var article = Rxn<Article>();   // 文章详情

  @override
  void onInit() {
    super.onInit();
    increasePageCount();  // 原来的逻辑
    fetchArticle();       // 页面加载时请求 API
  }

  Future<void> fetchArticle() async {
    loading.value = true;
    try {
      final data = await api.getArticleDetail(); // 这里调用你的 repository / service
      article.value = data;
      // article 是 Rx,使用 Obx 的地方会自动刷新
      // 如果你有依赖简单状态的 GetBuilder,需要的话再调用 update()
      // update();
    } finally {
      loading.value = false;
    }
  }

  void increasePageCount() {
    pageCount++;
    update(); // 刷新 GetBuilder
  }

  void like() => likeCount++;
  void toggleFavorite() => isFavorite.toggle();
}

页面里依然混用 GetBuilder + Obx:

class ArticlePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final controller = Get.put(ArticleController());

    return Scaffold(
      appBar: AppBar(title: const Text('Article Detail')),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // 顶部浏览次数(简单状态)
          GetBuilder<ArticleController>(
            builder: (c) => Text('页面浏览次数:${c.pageCount}'),
          ),

          const SizedBox(height: 16),

          // 中间部分:加载中 / 内容(响应式状态)
          Obx(() {
            if (controller.loading.value) {
              return const CircularProgressIndicator();
            }
            final article = controller.article.value;
            if (article == null) {
              return const Text('暂无数据');
            }
            return Text(article.title); // 文章标题
          }),

          const SizedBox(height: 16),

          // 点赞 + 收藏(响应式状态)
          Obx(
            () => Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('点赞:${controller.likeCount}'),
                const SizedBox(width: 16),
                Icon(
                  controller.isFavorite.value
                      ? Icons.favorite
                      : Icons.favorite_border,
                  color: controller.isFavorite.value ? Colors.red : null,
                ),
              ],
            ),
          ),

          const SizedBox(height: 24),

          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: controller.increasePageCount,
                child: const Text('增加浏览次数 (GetBuilder)'),
              ),
              const SizedBox(width: 16),
              ElevatedButton(
                onPressed: controller.like,
                child: const Text('点赞 (Obx)'),
              ),
              const SizedBox(width: 16),
              ElevatedButton(
                onPressed: controller.toggleFavorite,
                child: const Text('收藏切换 (Obx)'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

方案二:在 onReady 里请求(需要等页面渲染后)

如果你的 API 请求需要在“首帧 UI 出来之后”再做,比如要先弹一个对话框提示用户,将请求放在 onReady

@override
void onReady() {
  super.onReady();
  fetchArticle(); // 首帧渲染完成后请求
}

不再建议的做法

  • 不建议再在页面的 initState 里请求,而是优先放到 GetxController.onInit / onReady,这样视图层更干净,也更符合 GetX 推荐的结构。

Swift Array的写时复制

作者 Yakamoz
2025年12月19日 14:06

众所周知Swift中Array是值类型,如果其中元素为值类型和引用类型,分别会发生什么?

相关验证方法

检查不同层次的地址

// 1. 栈变量地址
withUnsafePointer(to: &array) {
    print("\(name) 栈地址: \($0)")
}

// 2. 堆缓冲区地址
array.withUnsafeBufferPointer {
    print("数组缓冲区地址: \(String(describing: $0.baseAddress))")
}
    
// 3. 元素地址(引用类型时比较)
if let first = array.first as? AnyObject {
    print("\(name)[0] 对象地址: \(ObjectIdentifier(first))")
    }
}

元素为引用类型

随便定义一个类,并创建列表1,然后直接赋值给列表2

class Person {
    var name: String
    init(name: String) { self.name = name }
}
var people1 = [Person(name: "Alice"), 
               Person(name: "Bob")]
var people2 = people1

withUnsafePointer打印此时两个数组的栈地址(指向数组的指针)

withUnsafePointer(to: &people1) { ptr in
    print("people1 地址: \(ptr)")
}

withUnsafePointer(to: &people2) { ptr in
    print("people2 地址: \(ptr)")

}
// 输出结果
// people1 地址: 0x000000010df001a0
// people2 地址: 0x000000010df001a8

确实是两个不同的数组指针(废话!),但是我们再通过withUnsafeBufferPointer获取数组缓冲区地址

people1.withUnsafeBufferPointer { buffer in
    if let baseAddress = buffer.baseAddress {
        print("people1缓冲区地址(堆): \(baseAddress)")
    }
}

people2.withUnsafeBufferPointer { buffer in
    if let baseAddress = buffer.baseAddress {
        print("people2缓冲区地址(堆): \(baseAddress)")
    }
}
// 输出结果
// people1缓冲区地址(堆): 0x000000014d2040c0
// people2缓冲区地址(堆): 0x000000014d2040c0

会发现指向的是同一块缓冲区

如果我们更改people2中元素的name,指针地址和缓冲区地址都没有任何变化(这里就不贴代码和打印结果了),但是如果新增元素

people2.append(Person(name: "newPerson"))
withUnsafePointer(to: &people2) { ptr in
    print("people2 地址: \(ptr)")
}

people2.withUnsafeBufferPointer { buffer in
    if let baseAddress = buffer.baseAddress {
        print("people2缓冲区地址(堆):\(baseAddress)")
    }
}
// 输出结果:
// people2 地址: 0x000000010df001a8
// people2缓冲区地址(堆): 0x000000014f404b10

指针地址没变,但是缓冲区地址变了!证明Swift中的数组是写时复制,新开辟了缓冲区。(删除同理)

但是缓冲区里存的是什么?打印下数组中的元素看看

/* people1
people1 元素对象地址:
[0]: 0x122b04570
[1]: 0x122b04590

people2 元素对象地址:
[0]: 0x122b04570
[1]: 0x122b04590
[2]: 0x122b05ea0
*/

得出结论。虽然缓冲区确实开了新的,但是引用类型的元素还是不会被复制,相当于只是开了一块新地址存引用类型元素的指针而已。

结论:

  1. Array是值类型
  2. 赋值副本Array时发生逻辑复制(新的数组指针 在栈上),修改副本中的元素也会更改到原Array中的元素
  3. 修复副本Array时才实际复制堆缓冲区

元素为值类型

如果真的能读到值类型,相信也能看懂直接用代码解释了

var array1 = ["AAA", "BBB", "CCC"]
var array2 = array1

// 输出结果:

// 栈地址验证,不同
// array1 栈地址: 0x00000001101d0058
// array2 栈地址: 0x00000001101d0060

// 缓冲区 暂时相同
// array1 缓冲区地址: 0x0000000129b04440
// array2 缓冲区地址: 0x0000000129b04440

此时修改元素再查看,array2已经开辟新的缓冲区,就不重复贴新增和删除的代码了,结果也是如此。

array2[0] = "new AAA"

// 输出结果:
// array1 缓冲区地址: 0x0000000129b04440
// array2 缓冲区地址: 0x0000000129b0d950

但是!修改了array2并没有像array1那样影响到同一个元素,现在用下面的方法验证下数组中的元素,打印修改后的结果

array1.withUnsafeBufferPointer { buffer in
    if let baseAddress = buffer.baseAddress {
        for i in 0..<buffer.count {
            let elementAddress = baseAddress + i
            print("array[\(i)] 地址: \(elementAddress), 值: \(elementAddress.pointee)")
        }
    }
}

array2.withUnsafeBufferPointer { buffer in
    if let baseAddress = buffer.baseAddress {
        for i in 0..<buffer.count {
            let elementAddress = baseAddress + i
            print("array[\(i)] 地址: \(elementAddress), 值: \(elementAddress.pointee)")
        }
    }
}
/* 输出结果:
array[0] 地址: 0x0000000127504170, 值: AAA
array[1] 地址: 0x0000000127504180, 值: BBB
array[2] 地址: 0x0000000127504190, 值: CCC


array[0] 地址: 0x000000012750ba70, 值: newAAA
array[1] 地址: 0x000000012750ba80, 值: BBB
array[2] 地址: 0x000000012750ba90, 值: CCC
*/

元素地址不同,值也不同

小总结

元素类型 值类型 引用类型
赋值 逻辑复制 逻辑复制
缓冲区共享 初始共享 初始共享
元素独立性 完全独立 共享对象
写时复制触发 修改时 修改结构时候(增删)
内存影响 元素复制 只复制指针

SwiftUI 中的 compositingGroup():真正含义与渲染原理

作者 汉秋
2025年12月19日 11:14

在学习 SwiftUI 的过程中,很多人第一次看到 compositingGroup() 都会被官方文档这句话绕晕:

Use compositingGroup() to apply effects to a parent view before applying effects to this view.

“让父 View 的效果先于子 View 的效果生效”  —— 这句话如果按字面理解,几乎一定会误解。

本文将从 渲染顺序、效果作用范围、实际示例 三个角度,彻底讲清楚 compositingGroup() 到底解决了什么问题。


一句话结论(先记住)

compositingGroup() 会创建一个“合成边界”:

  • 没有它:父 View 的合成效果会被「拆分」并逐个作用到子 View
  • 有了它:子 View 会先整体合成,再统一应用父 View 的合成效果

⚠️ 它改变的不是 modifier 的书写顺序,而是“效果的作用范围”。


SwiftUI 默认的渲染行为(最关键)

先看一个最简单的例子:

VStack {
    Text("A")
    Text("B")
}
.opacity(0.5)

看起来是对 VStack 设置了透明度

但 SwiftUI 实际做的是:

Text("A") -> opacity 0.5
Text("B") -> opacity 0.5
再进行叠加

也就是说:

  • opacity 并没有作为一个“整体效果”存在
  • 而是被 拆分后逐个应用到子 View

这就是很多「透明度叠加变脏」「blur 看起来不对劲」的根源。


compositingGroup() 做了什么?

加上 compositingGroup()

VStack {
    Text("A")
    Text("B")
}
.compositingGroup()
.opacity(0.5)

SwiftUI 的渲染流程会变成:

VStack
 ├─ Text("A")
 └─ Text("B")

先合成为一张离屏图像

对这张图像应用 opacity 0.5

关键变化只有一句话

父 View 的合成类效果不再下发到子 View。


那官方说的“父 View 的效果先于子 View 的效果”是什么意思?

这句话并不是时间顺序,而是:

父 View 的合成效果不会参与子 View 的内部计算。

换句话说:

  • 子 View 内部的 blur / color / mask 先完成
  • 父 View 的 opacity / blendMode 再整体生效

而不是交叉、叠加、重复计算。


一个典型示例:blur + opacity

❌ 没有 compositingGroup

ZStack {
    Text("Hello")
    Text("Hello")
        .blur(radius: 5)
}
.opacity(0.5)

实际效果:

  1. 第二个 Text 先 blur
  2. 两个 Text 分别被 opacity 影响
  3. 模糊区域再次参与透明度混合
  4. 结果:画面更糊、更脏

✅ 使用 compositingGroup

ZStack {
    Text("Hello")
    Text("Hello")
        .blur(radius: 5)
}
.compositingGroup()
.opacity(0.5)

渲染流程变为:

  1. 子 View 内部:blur 只影响指定的 Text
  2. ZStack 合成完成
  3. 整体统一 opacity 0.5

📌 blur 不再被“二次污染”


compositingGroup() 常见适用场景

1️⃣ 半透明容器(避免透明度叠加)

VStack {
    ...
}
.compositingGroup()
.opacity(0.8)

2️⃣ blendMode 视觉异常

ZStack {
    ...
}
.compositingGroup()
.blendMode(.multiply)

3️⃣ 动画 + blur / scale / opacity

.content
.compositingGroup()
.transition(.opacity)

可显著减少闪烁、重影问题。


compositingGroup vs drawingGroup

对比项 compositingGroup drawingGroup
是否离屏渲染
是否使用 Metal
主要目的 控制合成效果作用范围 性能 / 特效加速
常见问题 解决视觉叠加 解决复杂绘制性能

📌 compositingGroup 关注“视觉正确性”,drawingGroup 更偏向“性能”。


记忆口诀(非常实用)

要“整体效果”,用 compositingGroup;
不想被子 View 叠加污染,也用 compositingGroup。


总结

  • compositingGroup() 并不会改变 modifier 的书写顺序
  • 它创建了一个 合成边界(compositing boundary)
  • 阻止父 View 的合成效果被拆分并下发到子 View
  • 在 opacity、blur、blendMode、动画场景中极其重要

如果你在 SwiftUI 中遇到:

  • 透明度看起来“不对”
  • blur 过重
  • 动画时出现重影

👉 第一时间就该想到 compositingGroup()


希望这篇文章能帮你真正理解 SwiftUI 背后的渲染逻辑。

iOS 循环引用篇 菜鸟都能看懂

作者 dongczlu
2025年12月19日 10:50

iOS 内存管理完整补充知识

从对象到类、从结构体到元类、从 C++ 到内存分布区、到手机硬件内存的完整知识线


目录

  1. ARC 自动引用计数详细机制
  2. 内存对齐与对象大小
  3. Tagged Pointer 技术
  4. Mach-O 文件结构与内存映射
  5. AutoreleasePool 与 RunLoop 关系
  6. 堆分配策略与内存碎片
  7. 栈基础与栈溢出
  8. 类/元类查找链与方法缓存
  9. OC vs C++ 内存模型差异
  10. 虚拟内存与物理内存映射
  11. Weak 表实现与性能

0. 引用计数基础概念(小白必读)

0.1 什么是引用计数?

引用计数 = 记录"有多少个地方在使用这个对象"的数字

生活化比喻:图书馆借书

想象一下图书馆的书:

一本书(对象):
- 被借出时:借书人数 = 1
- 又有人借:借书人数 = 2
- 有人还书:借书人数 = 1
- 所有人还完:借书人数 = 0 → 书可以放回仓库(对象被释放)

OC 对象也是一样:

  • 对象被创建:引用计数 = 1
  • 有人强引用它:引用计数 +1
  • 有人不再引用:引用计数 -1
  • 引用计数 = 0:对象被释放(内存回收)

0.2 "引用计数加1"是什么意思?

"引用计数加1" = 又多了一个地方在使用这个对象

代码示例
// 步骤 1:创建对象
NSObject *obj = [[NSObject alloc] init];
// 此时:obj 指向的对象,引用计数 = 1
// 意思:有 1 个地方在使用这个对象(就是 obj 这个变量)

// 步骤 2:另一个变量也指向这个对象
NSObject *obj2 = obj;  // 强引用赋值
// 此时:obj 指向的对象,引用计数 = 2
// 意思:有 2 个地方在使用这个对象(obj 和 obj2)

// 步骤 3:obj 不再指向这个对象
obj = nil;
// 此时:obj 指向的对象,引用计数 = 1
// 意思:还有 1 个地方在使用(obj2 还在用)

// 步骤 4:obj2 也不再指向
obj2 = nil;
// 此时:引用计数 = 0
// 意思:没有地方在使用这个对象了 → 对象被释放!

0.3 "self 引用计数加1"具体指什么?

"self 引用计数加1" = 又多了一个地方在强引用 self 这个对象

示例 1:普通赋值
@interface ViewController : UIViewController
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // self 的引用计数 = 1(假设只有系统在引用它)
    
    // 创建一个强引用
    ViewController *anotherRef = self;  // 强引用赋值
    // 此时:self 的引用计数 = 2
    // 意思:有 2 个地方在强引用 self(系统 + anotherRef)
    
    // anotherRef 不再引用
    anotherRef = nil;
    // 此时:self 的引用计数 = 1(恢复)
}

@end
示例 2:Block 捕获 self(关键!)
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // self 的引用计数 = 1
    
    // ❌ 情况 A:block 直接捕获 self
    self.block = ^{
        [self doSomething];  // block 强引用 self
    };
    // 此时:self 的引用计数 = 2
    // 原因:self 强引用 block,block 强引用 self
    // 形成循环:self → block → self(循环引用!)
    
    // ✅ 情况 B:block 捕获 weakSelf
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        [weakSelf doSomething];  // block 弱引用 self(不增加引用计数)
    };
    // 此时:self 的引用计数 = 1(没有增加!)
    // 原因:weakSelf 是弱引用,不会让引用计数 +1
}
示例 3:Weak-Strong Dance 中的引用计数变化
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 初始状态:self 引用计数 = 1
    
    __weak typeof(self) weakSelf = self;
    // 此时:self 引用计数 = 1(weakSelf 不增加引用计数)
    
    self.block = ^{
        // block 被创建,捕获了 weakSelf(弱引用)
        // 此时:self 引用计数 = 1(仍然没有增加)
        
        // block 执行时:
        __strong typeof(weakSelf) strongSelf = weakSelf;
        // 此时:self 引用计数 = 2(strongSelf 强引用,+1)
        // 意思:又多了一个地方在强引用 self(就是 strongSelf)
        
        [strongSelf doSomething];
        
        // block 执行完,strongSelf 作用域结束
        // 此时:self 引用计数 = 1(strongSelf 释放,-1)
        // 意思:strongSelf 不再引用 self,引用计数恢复
    };
    
    // 最终:self 引用计数 = 1(block 只弱引用 self,不增加引用计数)
}

0.4 引用计数的"加1"和"减1"是怎么实现的?

底层实现(简化理解)
// 伪代码:引用计数的实现
struct NSObject {
    int retainCount;  // 引用计数(实际可能不在对象里,在 side table)
};

// retain(加1)
- (id)retain {
    retainCount++;  // 引用计数 +1
    return self;
}

// release(减1)
- (void)release {
    retainCount--;  // 引用计数 -1
    if (retainCount == 0) {
        [self dealloc];  // 引用计数为 0,释放对象
    }
}
ARC 自动插入 retain/release
// 你写的代码
NSObject *obj = [[NSObject alloc] init];
NSObject *obj2 = obj;

// 编译器实际生成的代码(伪代码)
NSObject *obj = [[NSObject alloc] init];  // retainCount = 1
NSObject *obj2 = [obj retain];            // retainCount = 2(自动插入 retain)
// ... 使用 ...
[obj release];                             // retainCount = 1(自动插入 release)
[obj2 release];                            // retainCount = 0,对象释放

0.5 常见误区澄清

误区 1:指针变量本身不占引用计数
NSObject *obj = [[NSObject alloc] init];
// obj 这个指针变量本身不占引用计数
// 引用计数是对象自己的属性,不是指针的属性

// 多个指针指向同一个对象
NSObject *obj1 = [[NSObject alloc] init];  // 对象引用计数 = 1
NSObject *obj2 = obj1;                      // 对象引用计数 = 2(不是 obj2 的引用计数)
NSObject *obj3 = obj1;                      // 对象引用计数 = 3(不是 obj3 的引用计数)

// 所有指针都指向同一个对象,所以这个对象的引用计数 = 3
误区 2:weak 引用不增加引用计数
NSObject *obj = [[NSObject alloc] init];  // 引用计数 = 1
__weak NSObject *weakObj = obj;           // 引用计数 = 1(没有增加!)
__strong NSObject *strongObj = obj;        // 引用计数 = 2(增加了!)

// weak 引用不会让引用计数 +1
// 只有 strong 引用才会让引用计数 +1
误区 3:引用计数不是对象的"数量"
// ❌ 错误理解:引用计数 = 对象的数量
NSObject *obj1 = [[NSObject alloc] init];  // 1 个对象,引用计数 = 1
NSObject *obj2 = [[NSObject alloc] init];  // 2 个对象,引用计数 = 1(每个对象都是 1)

// ✅ 正确理解:引用计数 = 指向这个对象的强引用数量
NSObject *obj = [[NSObject alloc] init];  // 1 个对象
NSObject *ref1 = obj;                      // 对象引用计数 = 2(2 个强引用指向它)
NSObject *ref2 = obj;                      // 对象引用计数 = 3(3 个强引用指向它)

0.6 面试一句话总结

"引用计数加1" = 又多了一个强引用指向这个对象,对象的引用计数数值 +1

关键点:

  • 引用计数是对象的属性,不是指针的属性
  • 只有 strong 引用才会让引用计数 +1
  • weak 引用不会让引用计数 +1
  • 引用计数 = 0 时,对象被释放

1. ARC 自动引用计数详细机制

1.1 ARC 在编译时做了什么?

ARC 不是运行时技术,而是编译时技术!

编译器会在编译阶段自动插入 retainreleaseautorelease 调用。

示例代码对比

MRC 时代(手动):

// MRC 代码
- (void)example {
    NSObject *obj = [[NSObject alloc] init];  // 引用计数 = 1
    [obj retain];                             // 引用计数 = 2
    [obj release];                            // 引用计数 = 1
    [obj release];                            // 引用计数 = 0,对象被释放
}

ARC 时代(自动):

// ARC 代码(你写的)
- (void)example {
    NSObject *obj = [[NSObject alloc] init];
    // 编译器自动在方法结束前插入 [obj release];
}

编译器转换后的伪代码:

// 编译器实际生成的代码
- (void)example {
    NSObject *obj = [[NSObject alloc] init];  // 引用计数 = 1
    // ... 你的代码 ...
    [obj release];  // ← 编译器自动插入!
}

1.2 ARC 的 retain/release 插入规则

规则 1:赋值时自动 retain
NSObject *obj1 = [[NSObject alloc] init];  // 引用计数 = 1
NSObject *obj2 = obj1;                      // obj2 强引用,引用计数 = 2
// 编译器自动插入:obj2 = [obj1 retain];
规则 2:变量作用域结束时自动 release
- (void)example {
    NSObject *obj = [[NSObject alloc] init];  // 引用计数 = 1
    // ... 使用 obj ...
    // 编译器在方法结束前自动插入:[obj release];
}
规则 3:属性赋值时自动管理
@property (strong, nonatomic) NSObject *obj;

- (void)setObj:(NSObject *)obj {
    if (_obj != obj) {
        [_obj release];      // 编译器自动插入:释放旧值
        _obj = [obj retain]; // 编译器自动插入:持有新值
    }
}

1.3 什么是循环引用?(核心概念)

1.3.1 用生活例子理解"两个对象互相引用"

想象两个好朋友互相借钱:

小明 和 小红:

小明说:"我借了小红 100 元,小红必须还我,我才能还别人"
小红说:"我借了小明 100 元,小明必须还我,我才能还别人"

结果:两个人互相等待对方还钱,永远还不完!
这就是"互相引用"的问题。

在代码中:

对象 A 说:"我强引用了对象 BB 必须存在,我才能存在"
对象 B 说:"我强引用了对象 AA 必须存在,我才能存在"

结果:两个对象互相等待对方释放,永远释放不了!
这就是"循环引用"。

1.3.2 循环引用的图示

正常情况(没有循环):

对象 A(引用计数 = 1)
  ↑
  │ 强引用
  │
变量 a

对象 B(引用计数 = 1)
  ↑
  │ 强引用
  │
变量 b

结果:a = nil 时,A 被释放;b = nil 时,B 被释放 ✅

循环引用情况:

对象 A(引用计数 = 2)
  ↑              ↑
  │              │
  │ 强引用        │ 强引用(来自 B)
  │              │
变量 a        对象 B(引用计数 = 2)
                ↑              ↑
                │              │
                │ 强引用        │ 强引用(来自 A)
                │              │
              变量 b        对象 A(引用计数 = 2)
                              ↑
                              │
                              │(形成循环!)
                              │
                           对象 B(引用计数 = 2

问题:

  • 即使 a = nilb = nil,A 和 B 的引用计数都还是 1(因为互相引用)
  • 引用计数永远不会变成 0
  • 对象永远不会被释放 → 内存泄漏!

1.3.3 代码示例:两个对象互相引用
// 定义两个类
@interface PersonA : NSObject
@property (strong, nonatomic) PersonB *personB;  // A 强引用 B
@end

@interface PersonB : NSObject
@property (strong, nonatomic) PersonA *personA;  // B 强引用 A
@end

// 使用
PersonA *a = [[PersonA alloc] init];  // A 引用计数 = 1
PersonB *b = [[PersonB alloc] init];  // B 引用计数 = 1

a.personB = b;  // B 引用计数 = 2(A 强引用 B)
b.personA = a;  // A 引用计数 = 2(B 强引用 A)

// 此时:
// A 引用计数 = 2(变量 a + B.personA)
// B 引用计数 = 2(变量 b + A.personB)

a = nil;  // A 引用计数 = 1(还有 B.personA 在引用)
b = nil;  // B 引用计数 = 1(还有 A.personB 在引用)

// 问题:A 和 B 的引用计数都是 1,永远不会变成 0
// 结果:A 和 B 永远不会被释放 → 内存泄漏!

图示:

初始:
变量 a → PersonA(引用计数 = 1)
变量 b → PersonB(引用计数 = 1)

互相引用后:
变量 a → PersonA(引用计数 = 2)← PersonB.personA
         ↓ PersonA.personB
变量 b → PersonB(引用计数 = 2)← PersonA.personB
         ↑ PersonB.personA
         │
         └───────────┘(形成循环!)

a = nil, b = nil 后:
PersonA(引用计数 = 1)← PersonB.personA
         ↓ PersonA.personB
PersonB(引用计数 = 1)← PersonA.personB
         ↑ PersonB.personA
         │
         └───────────┘(循环还在,无法释放!)

1.3.4 如何打破循环引用?

方法:把其中一个强引用改成弱引用

// ✅ 正确:B 弱引用 A
@interface PersonA : NSObject
@property (strong, nonatomic) PersonB *personB;  // A 强引用 B
@end

@interface PersonB : NSObject
@property (weak, nonatomic) PersonA *personA;    // B 弱引用 A(关键!)
@end

// 使用
PersonA *a = [[PersonA alloc] init];  // A 引用计数 = 1
PersonB *b = [[PersonB alloc] init];  // B 引用计数 = 1

a.personB = b;  // B 引用计数 = 2(A 强引用 B)
b.personA = a;  // A 引用计数 = 1(B 弱引用 A,不增加引用计数)

// 此时:
// A 引用计数 = 1(只有变量 a)
// B 引用计数 = 2(变量 b + A.personB)

a = nil;  // A 引用计数 = 0 → A 被释放!
          // B.personA 自动变成 nil(weak 的特性)

b = nil;  // B 引用计数 = 1(还有 A.personB?不对,A 已经释放了)
          // 实际上,A 释放时,A.personB 也被释放
          // 所以 B 引用计数 = 0 → B 被释放!

// 结果:两个对象都能正常释放 ✅

图示(打破循环后):

变量 a → PersonA(引用计数 = 1)
         ↓ PersonA.personB(强引用)
变量 b → PersonB(引用计数 = 2)
         ↑ PersonB.personA(弱引用,不增加引用计数)

a = nil 后:
PersonA(引用计数 = 0)→ 被释放!
         ↓ PersonA.personB 也被释放
PersonB(引用计数 = 1)← 只有变量 b
         ↑ PersonB.personA = nil(自动置 nil)

b = nil 后:
PersonB(引用计数 = 0)→ 被释放!✅

1.3.5 循环引用的核心理解

循环引用 = 两个或多个对象互相强引用,形成闭环,导致都无法释放

关键点:

  1. 必须是"强引用" :weak 引用不会形成循环引用
  2. 必须是"互相" :A → B → A(闭环)
  3. 结果:引用计数永远不会变成 0,对象永远不会被释放

解决方法:

  • 把循环中的至少一个强引用改成弱引用
  • 或者手动断开循环(设置为 nil)

1.3.6 循环引用会导致什么?(重要!)

🚨 循环引用的后果

1. 内存泄漏(Memory Leak)

最直接的后果:对象永远不会被释放,占用内存越来越多

@interface ViewController : UIViewController
@property (copy, nonatomic) void (^block)(void);
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // ❌ 循环引用
    self.block = ^{
        [self doSomething];  // block 强引用 self
    };
    // self 强引用 block,block 强引用 self → 循环引用
}

@end

// 使用场景:
ViewController *vc = [[ViewController alloc] init];
[self.navigationController pushViewController:vc animated:YES];
// 用户返回上一页
[self.navigationController popViewControllerAnimated:YES];

// 问题:
// vc 应该被释放,但因为循环引用,vc 无法释放
// 内存泄漏!vc 占用的内存永远不会回收

影响:

  • 内存占用持续增长
  • 长时间运行后可能导致内存不足
  • 应用可能被系统杀死(OOM - Out of Memory)

2. dealloc 永远不会被调用

dealloc 方法不会被调用,清理代码不会执行

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // ❌ 循环引用
    self.block = ^{
        [self doSomething];
    };
}

- (void)dealloc {
    NSLog(@"ViewController 被释放");  // ❌ 永远不会打印!
    // 清理代码不会执行
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [self.timer invalidate];
    // 这些清理代码都不会执行!
}

@end

影响:

  • 资源无法释放(通知观察者、定时器、网络请求等)
  • 可能导致其他问题(通知重复接收、定时器继续运行等)

3. 通知观察者无法移除
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 添加通知观察者
    [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(handleNotification:)
                                                     name:@"SomeNotification"
                                                   object:nil];
    
    // ❌ 循环引用
    self.block = ^{
        [self doSomething];
    };
}

- (void)dealloc {
    // ❌ 永远不会执行!
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

// 问题:
// ViewController 无法释放
// 通知观察者无法移除
// 即使 ViewController 已经不在屏幕上,仍然会接收通知
// 可能导致崩溃或逻辑错误

4. 定时器无法停止
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // ❌ 循环引用
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                  target:self  // timer 强引用 self
                                                selector:@selector(timerAction)
                                                userInfo:nil
                                                 repeats:YES];
    // self 强引用 timer,timer 强引用 self → 循环引用
}

- (void)dealloc {
    // ❌ 永远不会执行!
    [self.timer invalidate];  // 定时器无法停止
    self.timer = nil;
}

// 问题:
// ViewController 无法释放
// 定时器继续运行,即使 ViewController 已经不在屏幕上
// 定时器回调可能访问已销毁的视图,导致崩溃

5. 网络请求回调可能继续执行
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // ❌ 循环引用
    [NetworkManager requestWithCompletion:^(NSData *data) {
        [self handleResponse:data];  // block 强引用 self
    }];
    // 如果 NetworkManager 也强引用这个 block,可能形成循环引用
}

- (void)dealloc {
    // ❌ 永远不会执行!
    // 清理代码不会执行
}

// 问题:
// ViewController 无法释放
// 网络请求完成后,回调可能访问已销毁的视图
// 可能导致崩溃或逻辑错误

6. KVO 观察者无法移除
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self.model addObserver:self
                 forKeyPath:@"value"
                    options:NSKeyValueObservingOptionNew
                    context:nil];
    
    // ❌ 循环引用
    self.block = ^{
        [self doSomething];
    };
}

- (void)dealloc {
    // ❌ 永远不会执行!
    [self.model removeObserver:self forKeyPath:@"value"];
}

// 问题:
// ViewController 无法释放
// KVO 观察者无法移除
// 如果 model 被释放,可能导致崩溃

📊 循环引用的影响总结

影响 说明 严重程度
内存泄漏 对象无法释放,内存持续增长 ⚠️⚠️⚠️ 严重
dealloc 不执行 清理代码不会执行 ⚠️⚠️⚠️ 严重
通知无法移除 继续接收通知,可能导致崩溃 ⚠️⚠️ 中等
定时器无法停止 定时器继续运行,可能访问已销毁对象 ⚠️⚠️ 中等
网络回调继续执行 回调可能访问已销毁对象 ⚠️⚠️ 中等
KVO 无法移除 可能导致崩溃 ⚠️⚠️ 中等

🔍 如何检测循环引用?

方法 1:检查 dealloc 是否被调用
- (void)dealloc {
    NSLog(@"✅ ViewController 被释放");  // 如果没打印,说明有循环引用
}
方法 2:使用 Instruments 的 Leaks 工具
  1. 打开 Xcode
  2. Product → Profile(或 Cmd + I)
  3. 选择 Leaks
  4. 运行应用,执行可能产生循环引用的操作
  5. 查看是否有内存泄漏
方法 3:使用 Xcode Memory Graph
  1. 运行应用
  2. 在 Debug Navigator 中点击 Memory Graph
  3. 查看对象是否正常释放
方法 4:使用 MLeaksFinder(第三方工具)

自动检测内存泄漏,在开发阶段就能发现问题。


✅ 如何避免循环引用?

  1. 使用 weak 引用:在 block、delegate、通知等场景使用 weak
  2. 及时断开引用:在不需要时手动设置为 nil
  3. 使用 weak-strong dance:在 block 中使用 weak-strong dance 模式
  4. 代码审查:定期检查代码,特别是 block、delegate、通知等场景

1.4 循环引用的典型场景

场景 1:self ↔ block
// ❌ 错误:循环引用
@interface ViewController ()
@property (nonatomic, copy) void (^block)(void);
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // self 强引用 block
    self.block = ^{
        // block 强引用 self(捕获了 self)
        [self doSomething];  // ← 形成循环引用!
    };
}
@end

// ✅ 正确:使用 weak-strong dance
- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (!strongSelf) return;
        [strongSelf doSomething];
    };
}

🔍 Weak-Strong Dance 详细解释:

第一步:__weak typeof(self) weakSelf = self;
__weak typeof(self) weakSelf = self;

作用:

  • 创建一个 weak 指针指向 self
  • 不增加 self 的引用计数
  • 如果 self 被释放,weakSelf 会自动变成 nil

内存状态:

self 的引用计数 = 1(假设只有这里引用)
weakSelf → 指向 self(但不增加引用计数)

为什么需要 weak?

  • 如果 block 里直接用 self,block 会强引用 self
  • 形成循环:selfblockself(循环引用!)
  • weakSelf 后,block 只弱引用 self,打破循环

第二步:在 block 内部使用 __strong typeof(weakSelf) strongSelf = weakSelf;
self.block = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    // ...
};

作用:

  • 在 block 执行时,把 weakSelf 转成 strongSelf(强引用)
  • 如果 weakSelfnilstrongSelf 也是 nil
  • 如果 weakSelf 不是 nilstrongSelf增加引用计数,保证执行期间 self 不会被释放

内存状态变化:

情况 A:block 执行时,self 还存在

执行前:
self 引用计数 = 1
weakSelf → self(弱引用)

执行时(进入 block):
strongSelf = weakSelf;  // strongSelf 强引用 self
self 引用计数 = 2  ← 增加了!

执行中:
[self doSomething];  // 安全!self 不会被释放

执行后(block 结束):
strongSelf 作用域结束,自动 release
self 引用计数 = 1  ← 恢复

情况 B:block 执行时,self 已经被释放

执行前:
self 引用计数 = 0,已被释放
weakSelf = nil(自动置 nil)

执行时(进入 block):
strongSelf = weakSelf;  // strongSelf = nil
if (!strongSelf) return;  // 直接返回,不执行后续代码

第三步:if (!strongSelf) return;
if (!strongSelf) return;

作用:

  • 安全检查:如果 self 已经被释放,weakSelfnilstrongSelf 也是 nil
  • 直接返回,避免后续代码访问已释放的对象

为什么需要这个检查?

  • 虽然访问 nil 对象在 OC 中是安全的(不会崩溃),但逻辑上不应该执行
  • 提前返回,避免执行无意义的代码

第四步:使用 strongSelf 而不是 weakSelf
[strongSelf doSomething];  // ✅ 正确
// [weakSelf doSomething];  // ⚠️ 理论上可以,但不推荐

为什么用 strongSelf

关键原因:防止执行中途被释放

// ❌ 危险:只用 weakSelf
self.block = ^{
    __weak typeof(self) weakSelf = self;
    if (!weakSelf) return;
    
    // 假设 doSomething 执行时间很长
    [weakSelf doSomething];  // 执行到一半...
    
    // 如果此时 self 被释放了(其他强引用都断了)
    // weakSelf 变成 nil,但代码还在执行!
    [weakSelf doAnotherThing];  // 可能访问 nil
};

// ✅ 安全:使用 strongSelf
self.block = ^{
    __weak typeof(self) weakSelf = self;
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) return;
    
    // strongSelf 强引用 self,保证整个 block 执行期间 self 不会被释放
    [strongSelf doSomething];      // self 引用计数 = 2,安全
    [strongSelf doAnotherThing];   // self 引用计数 = 2,安全
    // block 结束,strongSelf 释放,self 引用计数 = 1
};

完整执行流程示例

@interface ViewController ()
@property (nonatomic, copy) void (^block)(void);
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 步骤 1:创建 weak 引用
    __weak typeof(self) weakSelf = self;
    // 此时:self 引用计数 = 1,weakSelf → self(弱引用)
    
    // 步骤 2:创建 block(捕获 weakSelf,不是 self)
    self.block = ^{
        // 步骤 3:block 执行时,转为 strong 引用
        __strong typeof(weakSelf) strongSelf = weakSelf;
        
        // 步骤 4:安全检查
        if (!strongSelf) {
            NSLog(@"self 已被释放,不执行");
            return;
        }
        
        // 步骤 5:使用 strongSelf(保证执行期间 self 不会被释放)
        NSLog(@"执行任务,self 引用计数 = %lu", [strongSelf retainCount]);
        [strongSelf doSomething];
        
        // 步骤 6:block 结束,strongSelf 自动释放
        // self 引用计数恢复
    };
    
    // 步骤 7:viewDidLoad 结束,但 block 还在(被 self.block 持有)
}

- (void)doSomething {
    NSLog(@"执行任务");
}

- (void)dealloc {
    NSLog(@"ViewController 被释放");
    // 如果 block 还在,这里不会被调用(因为循环引用)
    // 如果用了 weak-strong dance,这里会被调用
}

@end

常见问题解答

Q1:为什么不能直接用 weakSelf

// ❌ 不推荐
self.block = ^{
    __weak typeof(self) weakSelf = self;
    [weakSelf doSomething];  // 执行中途 self 可能被释放
};

答案: 虽然不会崩溃(OC 对 nil 消息安全),但执行中途 self 可能被释放,导致逻辑错误。


Q2:strongSelf 会不会又造成循环引用?为什么 block 里用了 strong 修饰,不也是强引用 self 吗?

答案:不会! 这是最关键的理解点!

关键理解:block 捕获的是什么?

重要:block 捕获的是 weakSelf(弱引用),不是 strongSelf

__weak typeof(self) weakSelf = self;  // 步骤 1:创建 weak 引用

self.block = ^{
    // 步骤 2:block 捕获的是 weakSelf(弱引用)
    // block 内部结构(伪代码):
    // struct Block {
    //     __weak typeof(self) weakSelf;  // ← block 捕获的是这个!
    //     void (*invoke)(...);
    // };
    
    // 步骤 3:block 执行时,才创建 strongSelf(局部变量)
    __strong typeof(weakSelf) strongSelf = weakSelf;
    // strongSelf 是 block 执行时才创建的,不是 block 捕获的!
};
详细解释:为什么不会形成循环引用?

情况 A:如果 block 直接捕获 self(会形成循环引用)

// ❌ 错误:block 捕获 self(强引用)
self.block = ^{
    [self doSomething];  // block 捕获 self(强引用)
};

// 内存关系:
// self → block(强引用)
// block → self(强引用,因为捕获了 self)
// 形成循环:self → block → self ❌

情况 B:block 捕获 weakSelf,执行时创建 strongSelf(不会形成循环引用)

// ✅ 正确:block 捕获 weakSelf(弱引用)
__weak typeof(self) weakSelf = self;

self.block = ^{
    // block 捕获的是 weakSelf(弱引用),不是 self!
    // 所以:block → weakSelf(弱引用,不增加引用计数)
    
    // strongSelf 是 block 执行时才创建的局部变量
    __strong typeof(weakSelf) strongSelf = weakSelf;
    // strongSelf 不是 block 捕获的,是执行时的临时变量
};

// 内存关系:
// self → block(强引用)
// block → weakSelf(弱引用,不增加引用计数)✅
// block 执行时:strongSelf → self(临时强引用,执行完就释放)✅
// 没有循环!✅
用图示理解

错误情况(会循环引用):

self ──→ block ──→ self(强引用)
  ↑                  │
  └──────────────────┘(循环!)

正确情况(不会循环引用):

self ──→ block ──→ weakSelf ──→ self(弱引用,不形成循环)
  ↑
  └──────────────────────────────┘(没有循环!)

block 执行时:
self ──→ block ──→ weakSelf ──→ self(弱引用)
  ↑                              ↑
  │                              │
  └──────────────────────────────┘
                                 │
                            strongSelf(临时强引用,执行完就释放)
关键点总结
  1. block 捕获的是什么?

    • block 捕获的是 weakSelf(弱引用),不是 strongSelf
    • 所以 block 不强引用 self,不会形成循环
  2. strongSelf 是什么?

    • strongSelf 是 block 执行时才创建的局部变量
    • 不是 block 捕获的,是执行时的临时强引用
    • block 执行完,strongSelf 就释放了
  3. 为什么不会形成循环?

    • 循环引用的关键是:block 本身是否强引用 self
    • 因为 block 捕获的是 weakSelf(弱引用),所以 block 不强引用 self
    • strongSelf 只是执行时的临时强引用,不会形成持久的循环
完整的内存关系图
创建阶段:
self(引用计数 = 1)
  ↓ 强引用
block(捕获 weakSelf,弱引用 self)
  ↓ 弱引用(不增加引用计数)
weakSelf → self(引用计数 = 1,没有增加)

执行阶段(block 被调用):
self(引用计数 = 1)
  ↓ 强引用
block
  ↓ 弱引用
weakSelf → self(引用计数 = 1)
  ↓
strongSelf(局部变量,强引用 self)
  ↓ 强引用(临时)
self(引用计数 = 2,临时增加)

执行结束:
strongSelf 释放 → self(引用计数 = 1,恢复)
block 仍然存在,但只弱引用 self(不形成循环)

答案:不会! 因为:

  • block 捕获的是 weakSelf(弱引用),不是 strongSelf
  • strongSelf局部变量,只在 block 执行期间存在
  • block 执行完,strongSelf 自动释放
  • 不会形成持久的循环引用,因为 block 本身不强引用 self

Q3:什么时候 weakSelf 会变成 nil

答案:self 的所有强引用都断开时:

// 场景:ViewController 被 pop 或 dismiss
[self.navigationController popViewControllerAnimated:YES];
// 此时如果 self 没有其他强引用,会被释放
// weakSelf 自动变成 nil

Q4:可以简化成这样吗?

// ⚠️ 简化版(不推荐,但某些场景可用)
__weak typeof(self) weakSelf = self;
self.block = ^{
    [weakSelf doSomething];  // 直接使用 weakSelf
};

答案:

  • 简单场景可以:如果 doSomething 执行很快,且不涉及多步操作
  • 复杂场景不行:如果 block 执行时间长,或有多步操作,必须用 strongSelf 保证执行期间对象不被释放

面试标准答案(一句话总结)

Weak-Strong Dance 的作用:

  1. weakSelf:打破循环引用,让 block 不强持有 self
  2. strongSelf:在 block 执行期间强持有 self,防止执行中途被释放
  3. if (!strongSelf) return:安全检查,如果 self 已释放则提前返回

核心思想: 用弱引用打破循环,用临时强引用保证执行安全。


场景 2:NSTimer 循环引用
// ❌ 错误:循环引用
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // self 强引用 timer
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                  target:self  // ← timer 强引用 self
                                                selector:@selector(timerAction)
                                                userInfo:nil
                                                 repeats:YES];
    // 形成循环:self → timer → self
}

// ✅ 正确:使用中间对象或 block-based API
- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                 repeats:YES
                                                   block:^(NSTimer * _Nonnull timer) {
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (!strongSelf) return;
        [strongSelf timerAction];
    }];
}

- (void)dealloc {
    [self.timer invalidate];  // 必须手动停止
    self.timer = nil;
}
场景 3:通知观察者循环引用
// ⚠️ iOS 9+ 后通知中心会弱引用观察者,但业务代码仍需注意
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 如果 self 强引用通知,通知回调里又用 self,可能形成循环
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(handleNotification:)
                                                 name:@"SomeNotification"
                                               object:nil];
}

- (void)dealloc {
    // 必须移除观察者
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

1.4 Weak 表的工作机制

Weak 表是什么?

Weak 表 = 一张全局的哈希表,记录所有 weak 指针

// 伪代码:Weak 表的结构
struct WeakTable {
    // key: 对象的地址
    // value: 所有指向这个对象的 weak 指针数组
    HashMap<对象地址, Array<weak指针地址>>;
};
Weak 指针的工作流程
NSObject *obj = [[NSObject alloc] init];  // 引用计数 = 1
__weak NSObject *weakObj = obj;            // 引用计数仍 = 1

// 步骤 1:weakObj 被注册到 Weak 表
// Weak 表记录:obj 的地址 → [weakObj 的地址]

obj = nil;  // 引用计数 = 0,对象即将被释放

// 步骤 2:对象释放时,系统遍历 Weak 表
// 找到所有指向这个对象的 weak 指针
// 步骤 3:把所有 weak 指针置为 nil
// weakObj 现在 = nil(安全!)
面试常问:Weak 表如何实现?

答案要点:

  1. 全局哈希表:以对象地址为 key,存储所有指向它的 weak 指针
  2. 对象释放时:遍历 Weak 表,找到所有相关 weak 指针,置为 nil
  3. 性能优化:使用哈希表,查找是 O(1) 平均时间复杂度

2. 内存对齐与对象大小

2.1 什么是内存对齐?

内存对齐 = 数据在内存中的起始地址必须是某个数的倍数

对齐规则(64 位系统)
  • 基本类型对齐

    • char:1 字节对齐
    • short:2 字节对齐
    • int:4 字节对齐
    • long / 指针:8 字节对齐
    • double:8 字节对齐
  • 结构体对齐

    • 结构体整体大小必须是最大成员对齐值的倍数
    • 结构体起始地址必须是最大成员对齐值的倍数
示例:结构体内存对齐
struct Example {
    char a;      // 1 字节,偏移 0
    // 填充 3 字节(padding)
    int b;       // 4 字节,偏移 4(必须是 4 的倍数)
    char c;      // 1 字节,偏移 8
    // 填充 7 字节(padding)
    double d;    // 8 字节,偏移 16(必须是 8 的倍数)
};
// 总大小 = 24 字节(必须是 8 的倍数)

// 验证
NSLog(@"Size: %lu", sizeof(struct Example));  // 输出:24

2.2 OC 对象的内存对齐

对象内存布局
@interface Person : NSObject {
    @public
    char _name;      // 1 字节
    int _age;        // 4 字节
    double _height;  // 8 字节
}
@end

// 内存布局(64 位系统):
// [isa 指针: 8 字节] [padding: 0] 
// [_name: 1 字节] [padding: 3 字节]
// [_age: 4 字节]
// [padding: 4 字节](为了 double 对齐)
// [_height: 8 字节]
// 总大小 = 8 + 4 + 4 + 8 = 24 字节(必须是 8 的倍数)
查看对象实际大小
Person *p = [[Person alloc] init];

// 方法 1:实例大小(对齐后)
size_t instanceSize = class_getInstanceSize([Person class]);
NSLog(@"Instance size: %zu", instanceSize);  // 输出:24

// 方法 2:实际分配大小(系统可能分配更多)
size_t mallocSize = malloc_size((__bridge const void *)p);
NSLog(@"Malloc size: %zu", mallocSize);  // 可能输出:32(系统额外分配)

2.3 编译器如何插入 Padding?

@interface Example : NSObject {
    char a;      // 偏移 8(isa 后),大小 1
    // 编译器插入 padding: 3 字节
    int b;       // 偏移 12,大小 4
    char c;      // 偏移 16,大小 1
    // 编译器插入 padding: 7 字节(为了 double 对齐)
    double d;    // 偏移 24,大小 8
}
@end

// 编译器优化:调整成员顺序可以减少 padding
@interface OptimizedExample : NSObject {
    double d;    // 偏移 8,大小 8(最大对齐值)
    int b;       // 偏移 16,大小 4
    char a;      // 偏移 20,大小 1
    char c;      // 偏移 21,大小 1
    // padding: 6 字节(为了整体 8 字节对齐)
}
@end
// 优化后总大小可能更小!

2.4 面试常问点

Q:为什么需要内存对齐?

答案要点:

  1. CPU 读取效率:未对齐的数据可能需要多次内存访问
  2. 硬件要求:某些 CPU 架构要求数据必须对齐,否则崩溃
  3. 缓存行优化:对齐的数据更容易放入 CPU 缓存行

3. Tagged Pointer 技术

3.1 什么是 Tagged Pointer?

Tagged Pointer = 把小数据直接编码进指针里,不占用堆内存

传统方式 vs Tagged Pointer
// 传统方式(64 位系统)
NSNumber *num1 = @(42);
// 内存布局:
// 指针变量(栈上,8 字节)→ 指向堆上的 NSNumber 对象(至少 16 字节)
// 总占用:8 + 16 = 24 字节

// Tagged Pointer 方式
NSNumber *num2 = @(42);
// 内存布局:
// 指针变量(栈上,8 字节),但指针里直接存了 42 的值!
// 总占用:8 字节(节省 16 字节!)

3.2 Tagged Pointer 的识别

NSNumber *num1 = @(42);
NSNumber *num2 = @(1000000);  // 大数字

// 判断是否是 Tagged Pointer
NSLog(@"num1 is Tagged: %d", _objc_isTaggedPointer((__bridge void *)num1));
// 输出:1(是 Tagged Pointer)

NSLog(@"num2 is Tagged: %d", _objc_isTaggedPointer((__bridge void *)num2));
// 输出:0(不是,因为数字太大)

3.3 哪些对象支持 Tagged Pointer?

  • NSNumber:小整数(通常 < 2^60)
  • NSDate:时间戳在某个范围内
  • NSString:短字符串(通常 < 7 个字符,ASCII)
  • NSIndexPath:某些 iOS 版本
示例:NSString 的 Tagged Pointer
NSString *str1 = @"abc";           // Tagged Pointer
NSString *str2 = @"abcdefghijkl";  // 普通对象(堆上)

// 验证
NSLog(@"str1 pointer: %p", str1);  // 指针值看起来很奇怪(有 tag 位)
NSLog(@"str2 pointer: %p", str2);  // 正常的堆地址

// 查看实际内容
NSLog(@"str1: %@", str1);  // 正常输出
NSLog(@"str2: %@", str2);  // 正常输出

3.4 Tagged Pointer 的优势

  1. 节省内存:不需要堆分配
  2. 提高性能:不需要引用计数管理
  3. 减少碎片:不占用堆空间

3.5 面试常问点

Q:Tagged Pointer 如何工作?

答案要点:

  1. 利用指针的未使用位:64 位指针只用 48 位,剩余位用来存 tag 和数据
  2. 特殊标记位:最低位通常是 1,表示这是 Tagged Pointer
  3. 类型编码:用几个位表示类型(NSNumber/NSString/NSDate 等)
  4. 数据编码:剩余位存实际数据

4. Mach-O 文件结构与内存映射

4.1 Mach-O 是什么?

Mach-O = macOS/iOS 的可执行文件格式

类似于:

  • Windows:.exe(PE 格式)
  • Linux:ELF 格式
  • macOS/iOS:.app(Mach-O 格式)

4.2 Mach-O 的基本结构

Mach-O 文件
├── Header(文件头)
│   ├── 魔数(标识文件类型)
│   ├── CPU 架构(arm64/x86_64)
│   └── 加载命令数量
│
├── Load Commands(加载命令)
│   ├── 代码段位置
│   ├── 数据段位置
│   └── 动态库依赖
│
└── Data(数据区)
    ├── __TEXT(代码段)
    │   ├── 可执行代码
    │   └── 常量字符串
    │
    └── __DATA(数据段)
        ├── 全局变量
        ├── 静态变量
        └── 类元数据

4.3 主要段(Segment)详解

__TEXT 段(代码段)

特点:只读(Read-Only)、可执行(Executable)

// 这些内容在 __TEXT 段:

// 1. 可执行代码
- (void)example {
    NSLog(@"Hello");  // 这行代码编译后的机器指令在 __TEXT 段
}

// 2. 常量字符串
NSString *str = @"Hello";  // @"Hello" 在 __TEXT 段

// 3. 常量数据
const int kValue = 100;  // 在 __TEXT 段
__DATA 段(数据段)

特点:可读写(Read-Write)

// 这些内容在 __DATA 段:

// 1. 全局变量
int globalVar = 10;  // 在 __DATA 段

// 2. 静态变量
static int staticVar = 20;  // 在 __DATA 段

// 3. 类元数据(运行时注册)
@interface MyClass : NSObject
@end
// MyClass 的类对象信息在 __DATA 段

4.4 类对象在 Mach-O 中的位置

@interface Person : NSObject
@end

// 编译后,Person 类的信息存储在:
// 1. __TEXT 段:方法实现(机器码)
// 2. __DATA 段:类对象结构
//    - isa 指针
//    - superclass 指针
//    - 方法列表指针
//    - 属性列表指针
//    - 协议列表指针

4.5 静态库 vs 动态库的内存映射

静态库(.a 文件)
// 静态库的代码被直接链接进主可执行文件
// 内存映射:
// 主可执行文件的 __TEXT 段包含静态库的代码
// 主可执行文件的 __DATA 段包含静态库的数据
动态库(.dylib / .framework)
// 动态库由 dyld(动态链接器)在运行时加载
// 内存映射:
// 1. dyld 读取动态库的 Mach-O 文件
// 2. 将 __TEXT 段映射到内存(只读、可执行)
// 3. 将 __DATA 段映射到内存(可读写)
// 4. 每个进程共享同一份 __TEXT 段(节省内存)
// 5. 每个进程有独立的 __DATA 段副本

4.6 面试常问点

Q:类对象在哪里?

答案要点:

  1. 编译时:类信息写在 Mach-O 的 __DATA 段
  2. 运行时:dyld 加载 Mach-O,将类信息注册到 runtime
  3. 内存位置:类对象在进程的虚拟地址空间中(具体地址由 ASLR 随机化)

5. AutoreleasePool 与 RunLoop 关系

5.1 AutoreleasePool 是什么?

AutoreleasePool = 延迟释放池,让对象"晚一点"释放

// 传统 release(立即释放)
NSObject *obj = [[NSObject alloc] init];
[obj release];  // 立即释放,引用计数 = 0

// Autorelease(延迟释放)
NSObject *obj = [[NSObject alloc] init];
[obj autorelease];  // 加入自动释放池,等池子结束时才 release

5.2 AutoreleasePool 的结构

// AutoreleasePool 是一个栈结构
@autoreleasepool {
    // Pool 1(外层)
    @autoreleasepool {
        // Pool 2(内层)
        NSObject *obj = [[NSObject alloc] init];
        // obj 被加入 Pool 2
    }
    // Pool 2 结束,obj 被释放
}
// Pool 1 结束

5.3 RunLoop 与 AutoreleasePool 的关系

主线程的隐式 AutoreleasePool
// 主线程的 RunLoop 结构(简化)
void mainRunLoop() {
    while (appIsRunning) {
        @autoreleasepool {  // ← 系统自动创建
            // 处理事件
            handleEvents();
            // 处理定时器
            handleTimers();
            // 处理 Source
            handleSources();
        }
        // 池子结束,释放所有 autorelease 的对象
    }
}

关键点:

  • 主线程的每个 RunLoop 周期都有一个隐式的 @autoreleasepool
  • 当 RunLoop 进入休眠或结束一个周期时,池子会 drain(释放所有对象)
子线程没有隐式 AutoreleasePool
// ❌ 错误:子线程大量创建对象
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    for (int i = 0; i < 10000; i++) {
        NSObject *obj = [[NSObject alloc] init];
        // obj 被 autorelease,但没有池子,会积压!
    }
});

// ✅ 正确:手动创建 AutoreleasePool
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    @autoreleasepool {
        for (int i = 0; i < 10000; i++) {
            NSObject *obj = [[NSObject alloc] init];
            // obj 在池子结束时释放
        }
    }
    // 或者更细粒度:
    for (int i = 0; i < 10000; i++) {
        @autoreleasepool {
            NSObject *obj = [[NSObject alloc] init];
            // 每次循环结束就释放
        }
    }
});

5.4 什么时候需要手动创建 AutoreleasePool?

场景 1:子线程大量创建对象
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    @autoreleasepool {
        // 大量临时对象
        for (int i = 0; i < 100000; i++) {
            NSString *str = [NSString stringWithFormat:@"%d", i];
            // 使用 str...
        }
    }
    // 池子结束,所有临时对象立即释放,降低峰值内存
});
场景 2:大循环中创建临时对象
// ❌ 不好:所有临时对象积压到外层池子
for (int i = 0; i < 10000; i++) {
    NSMutableArray *arr = [NSMutableArray array];  // autorelease
    // 使用 arr...
}

// ✅ 好:每次循环结束就释放
for (int i = 0; i < 10000; i++) {
    @autoreleasepool {
        NSMutableArray *arr = [NSMutableArray array];
        // 使用 arr...
    }
    // arr 立即释放
}

5.5 面试常问点

Q:为什么子线程需要手动创建 AutoreleasePool?

答案要点:

  1. 主线程:RunLoop 自动创建和销毁 AutoreleasePool
  2. 子线程:没有 RunLoop(或 RunLoop 不活跃),没有隐式池子
  3. 后果:autorelease 的对象会积压,直到线程结束才释放,导致内存峰值过高
  4. 解决:手动创建 @autoreleasepool,及时释放临时对象

6. 堆分配策略与内存碎片

6.1 堆内存分配器(malloc)

iOS 使用 jemalloc 或类似的分配器管理堆内存。

分配策略(简化)
堆内存分配器
├── Tiny 区(< 16 字节)
│   └── 快速分配,固定大小块
│
├── Small 区(16 字节 ~ 几 KB)
│   └── 按大小分类的块池
│
└── Large 区(> 几 KB)
    └── 直接 mmap 分配

6.2 内存碎片问题

什么是内存碎片?
// 场景:频繁分配和释放不同大小的对象

// 1. 分配 100 字节
void *p1 = malloc(100);

// 2. 分配 200 字节
void *p2 = malloc(200);

// 3. 释放 p1(100 字节的空洞)
free(p1);

// 4. 现在想分配 150 字节
void *p3 = malloc(150);
// 问题:p1 的空洞只有 100 字节,不够!
// 只能从其他地方分配,导致碎片
如何减少碎片?

策略 1:对象池(Object Pool)

// 复用对象,而不是频繁创建和销毁
@interface ObjectPool : NSObject
+ (instancetype)sharedPool;
- (id)getObject;
- (void)returnObject:(id)obj;
@end

// 使用
ObjectPool *pool = [ObjectPool sharedPool];
MyObject *obj = [pool getObject];
// 使用 obj...
[pool returnObject:obj];  // 归还,而不是释放

策略 2:批量分配

// 一次性分配大块内存,自己管理
void *buffer = malloc(1024 * 1024);  // 1MB
// 自己在这 1MB 里分配小对象
// 减少系统 malloc 调用次数

6.3 面试常问点

Q:如何优化内存分配性能?

答案要点:

  1. 对象池:复用对象,减少分配/释放次数
  2. 批量分配:一次性分配大块内存,自己管理
  3. 避免频繁小对象分配:合并小对象,或使用结构体
  4. 使用 AutoreleasePool:及时释放临时对象,降低峰值

7. 栈基础与栈溢出

7.1 栈的基本概念

栈 = 函数调用的"工作区"

void functionA() {
    int a = 10;  // 在栈上
    functionB();
}

void functionB() {
    int b = 20;  // 在栈上
    functionC();
}

void functionC() {
    int c = 30;  // 在栈上
}

// 调用栈(从下往上):
// [functionA 的栈帧: a = 10]
// [functionB 的栈帧: b = 20]
// [functionC 的栈帧: c = 30]  ← 栈顶

7.2 iOS 线程栈大小

// 主线程栈大小:通常 1MB
// 子线程栈大小:通常 512KB(可配置)

// 创建自定义栈大小的线程
NSThread *thread = [[NSThread alloc] initWithTarget:self
                                            selector:@selector(threadMain)
                                              object:nil];
thread.stackSize = 1024 * 1024;  // 1MB
[thread start];

7.3 栈溢出的常见原因

原因 1:无限递归
// ❌ 错误:无限递归
- (void)recursive {
    int localVar[1000];  // 大局部变量
    [self recursive];    // 无限递归,栈帧不断增长
    // 最终:栈溢出(Stack Overflow)
}

// ✅ 正确:有终止条件
- (void)recursiveWithDepth:(int)depth {
    if (depth <= 0) return;  // 终止条件
    
    int localVar[1000];
    [self recursiveWithDepth:depth - 1];
}
原因 2:大局部变量
// ❌ 危险:大数组在栈上
- (void)example {
    int hugeArray[1000000];  // 4MB 在栈上!
    // 可能栈溢出
}

// ✅ 安全:大数组在堆上
- (void)example {
    int *hugeArray = malloc(1000000 * sizeof(int));  // 堆上
    // 使用...
    free(hugeArray);
}

7.4 面试常问点

Q:栈溢出如何避免?

答案要点:

  1. 避免无限递归:确保递归有终止条件
  2. 大变量用堆:大数组、大结构体用 malloc 或对象
  3. 限制递归深度:设置最大递归深度
  4. 增加栈大小pthread_attr_setstacksize(不推荐,治标不治本)

8. 类/元类查找链与方法缓存

8.1 方法查找流程(完整版)

// 调用:[obj methodName]

// 步骤 1:通过 isa 找到类对象
Class cls = object_getClass(obj);  // obj->isa

// 步骤 2:在类对象的方法列表中查找
Method method = class_getInstanceMethod(cls, @selector(methodName));

// 步骤 3:如果没找到,沿 superclass 链向上查找
while (cls && !method) {
    cls = class_getSuperclass(cls);
    method = class_getInstanceMethod(cls, @selector(methodName));
}

// 步骤 4:如果找到,调用 method->imp(函数指针)

8.2 方法缓存(Method Cache)

为什么需要缓存?

方法查找需要遍历类的方法列表,如果每次都查找,性能很差。

缓存机制:

// 伪代码:方法缓存结构
struct MethodCache {
    // 哈希表:selector → IMP
    HashMap<Selector, IMP> cache;
};

// 查找流程(带缓存):
IMP imp = cache.get(selector);
if (imp) {
    return imp;  // 缓存命中,直接返回
} else {
    // 缓存未命中,查找方法列表
    imp = findMethodInClass(selector);
    cache.set(selector, imp);  // 加入缓存
    return imp;
}

8.3 类方法 vs 实例方法

@interface Person : NSObject
- (void)instanceMethod;  // 实例方法
+ (void)classMethod;     // 类方法
@end

// 调用实例方法
Person *p = [[Person alloc] init];
[p instanceMethod];
// 查找路径:p->isa(Person 类)→ 查找实例方法列表

// 调用类方法
[Person classMethod];
// 查找路径:Person 类对象->isa(Person 元类)→ 查找类方法列表

8.4 元类链(完整)

// 元类链(简化)
Person 实例
  └─ isa → Person 类对象
           ├─ isa → Person 元类
           │        ├─ isa → NSObject 元类
           │        │        └─ isa → NSObject 元类(根元类指向自己)
           │        └─ superclass → NSObject 元类
           └─ superclass → NSObject 类对象
                            └─ isa → NSObject 元类

8.5 面试常问点

Q:方法查找的完整流程?

答案要点:

  1. 实例方法:对象 isa → 类对象 → 方法列表 → superclass 链向上查找
  2. 类方法:类对象 isa → 元类 → 方法列表 → 元类的 superclass 链向上查找
  3. 缓存优化:查找结果缓存到 MethodCache,下次直接命中
  4. 消息转发:如果最终没找到,进入消息转发机制(forwardingTargetForSelector: 等)

9. OC vs C++ 内存模型差异

9.1 对象创建位置

Objective-C
// OC 对象总是在堆上
NSObject *obj = [[NSObject alloc] init];
// obj 是指针(栈上),指向堆上的对象
C++
// C++ 对象可以在栈上
class MyClass {
public:
    int value;
};

void example() {
    MyClass obj;  // 栈上对象
    obj.value = 10;
}  // obj 自动析构

// 也可以在堆上
MyClass *obj = new MyClass();  // 堆上对象
delete obj;  // 手动释放

9.2 内存管理方式

Objective-C:引用计数
NSObject *obj1 = [[NSObject alloc] init];  // 引用计数 = 1
NSObject *obj2 = obj1;                      // 引用计数 = 2
obj1 = nil;                                 // 引用计数 = 1
obj2 = nil;                                 // 引用计数 = 0,对象释放
C++:RAII(资源获取即初始化)
class MyClass {
public:
    MyClass() { /* 构造 */ }
    ~MyClass() { /* 析构,自动调用 */ }
};

void example() {
    MyClass obj;  // 构造
    // 使用 obj...
}  // 自动析构(栈上对象)

// 堆上对象需要手动管理
MyClass *obj = new MyClass();
delete obj;  // 手动析构

9.3 多态实现方式

Objective-C:isa 指针 + 消息发送
@interface Animal : NSObject
- (void)speak;
@end

@interface Dog : Animal
- (void)speak;  // 重写
@end

Animal *animal = [[Dog alloc] init];
[animal speak];  // 运行时查找,调用 Dog 的 speak
// 通过 isa 指针找到实际类型
C++:虚函数表(vtable)
class Animal {
public:
    virtual void speak() { /* 基类实现 */ }
    // 有虚函数,对象有 vptr(虚函数表指针)
};

class Dog : public Animal {
public:
    void speak() override { /* 派生类实现 */ }
};

Animal *animal = new Dog();
animal->speak();  // 通过 vptr 找到虚函数表,调用 Dog::speak

9.4 Objective-C++ 混编注意事项

// Objective-C++ 文件(.mm)

// OC 对象
NSObject *obj = [[NSObject alloc] init];

// C++ 对象
std::vector<int> vec;
vec.push_back(1);

// ⚠️ 注意:C++ 异常不能穿越 OC 代码
// 如果 C++ 代码抛异常,必须在 C++ 代码里捕获

9.5 面试常问点

Q:OC 和 C++ 的内存管理有什么区别?

答案要点:

  1. OC:引用计数(ARC),对象在堆上,通过 isa 实现多态
  2. C++ :RAII,对象可在栈/堆,通过虚函数表实现多态
  3. OC:自动管理(ARC),但需注意循环引用
  4. C++ :手动管理(new/delete)或智能指针(shared_ptr/unique_ptr)

10. 虚拟内存与物理内存映射

10.1 什么是虚拟内存?

虚拟内存 = 进程看到的"假地址空间"

进程视角(虚拟地址):
0x00000000 ──────────┐
                     │
0x10000000 ──────────┤ 代码段
                     │
0x20000000 ──────────┤ 数据段
                     │
0x30000000 ──────────┤ 堆
                     │
0x40000000 ──────────┤ 栈
                     │
0x7FFFFFFF ──────────┘

实际物理内存:
[物理地址 0x1000] ← 可能映射到虚拟地址 0x10000000
[物理地址 0x2000] ← 可能映射到虚拟地址 0x20000000
...

10.2 页(Page)的概念

页 = 内存管理的最小单位(通常 4KB 或 16KB)

// 虚拟地址空间被分成页
虚拟地址:0x10000000 - 0x10000FFF  → 页 1
虚拟地址:0x10001000 - 0x10001FFF  → 页 2
虚拟地址:0x10002000 - 0x10002FFF  → 页 3

// 每页可以独立映射到物理内存1 → 物理页 A2 → 物理页 B3 → 未映射(访问会触发缺页异常)

10.3 页表(Page Table)

页表 = 虚拟地址到物理地址的映射表

虚拟地址:0x10000000
         ↓
    页表查找
         ↓
物理地址:0x50000000

10.4 写时复制(Copy-On-Write, COW)

// 场景:fork 进程或复制大对象

// 1. 父进程有数据
NSMutableArray *arr = [NSMutableArray arrayWithObjects:@1, @2, nil];

// 2. 子进程 fork(或复制)
// 此时:父子进程共享同一份物理内存(只读)

// 3. 子进程修改数据
[arr addObject:@3];

// 4. 触发写时复制
// 系统复制物理页,子进程有自己的副本
// 现在:父子进程有独立的物理内存

10.5 代码段页共享

// 多个进程运行同一个 App
进程 A:加载 MyApp
进程 B:加载 MyApp

// 代码段(__TEXT)的物理页被共享
// 节省物理内存!

10.6 ASLR(地址空间布局随机化)

// 没有 ASLR(固定地址)
代码段起始:0x10000000(固定)

// 有 ASLR(随机地址)
进程 1 代码段起始:0x10001234(随机)
进程 2 代码段起始:0x10005678(随机)

// 目的:防止攻击者预测地址

10.7 面试常问点

Q:虚拟内存的作用?

答案要点:

  1. 地址空间隔离:每个进程有独立的虚拟地址空间
  2. 内存保护:不同段有不同的读写执行权限
  3. 按需加载:只有访问的页才映射到物理内存
  4. 共享内存:多个进程可以共享代码段的物理页
  5. 安全性:ASLR 随机化地址,防止攻击

11. Weak 表实现与性能

11.1 Weak 表的底层结构

// 伪代码:Weak 表结构
struct WeakTable {
    // 全局哈希表
    // key: 对象的地址(作为弱引用目标)
    // value: 指向这个对象的所有 weak 指针的数组
    HashMap<void *, Array<void **>> weakReferences;
};

// 示例:
NSObject *obj = [[NSObject alloc] init];
__weak NSObject *weak1 = obj;
__weak NSObject *weak2 = obj;

// Weak 表记录:
// obj 的地址 → [weak1 的地址, weak2 的地址]

11.2 Weak 指针注册流程

NSObject *obj = [[NSObject alloc] init];  // 对象创建
__weak NSObject *weakObj = obj;            // weak 指针赋值

// 系统内部操作(伪代码):
void weak_assign(id *location, id newObj) {
    // 1. 如果之前有 weak 指针,先移除
    if (*location) {
        removeWeakReference(*location, location);
    }
    
    // 2. 设置新的 weak 指针
    *location = newObj;
    
    // 3. 如果新对象不为 nil,注册到 Weak 表
    if (newObj) {
        addWeakReference(newObj, location);
    }
}

11.3 对象释放时的 Weak 清理

// 对象释放流程(伪代码)
void object_release(id obj) {
    // 1. 引用计数减 1
    if (retainCount(obj) > 1) {
        retainCount(obj)--;
        return;
    }
    
    // 2. 引用计数为 0,准备释放
    // 3. 查找 Weak 表,找到所有指向这个对象的 weak 指针
    Array<void **> weakRefs = getWeakReferences(obj);
    
    // 4. 把所有 weak 指针置为 nil
    for (void **weakPtr in weakRefs) {
        *weakPtr = nil;
    }
    
    // 5. 从 Weak 表中移除记录
    removeWeakTableEntry(obj);
    
    // 6. 释放对象内存
    free(obj);
}

11.4 Weak 表的性能考虑

优势
  1. 哈希表查找:O(1) 平均时间复杂度
  2. 批量清理:对象释放时一次性清理所有 weak 指针
潜在开销
// 场景:大量 weak 指针指向同一个对象
NSObject *obj = [[NSObject alloc] init];

for (int i = 0; i < 10000; i++) {
    __weak NSObject *weak = obj;  // 每个 weak 都注册到 Weak 表
}

// 对象释放时,需要清理 10000 个 weak 指针
// 虽然还是 O(n),但 n 可能很大

11.5 面试常问点

Q:Weak 表如何实现?性能如何?

答案要点:

  1. 数据结构:全局哈希表,key 是对象地址,value 是 weak 指针数组
  2. 注册:weak 指针赋值时,注册到 Weak 表
  3. 清理:对象释放时,遍历 Weak 表,把所有 weak 指针置 nil
  4. 性能:哈希表查找 O(1),但大量 weak 指针时清理可能较慢
  5. 优化:系统有优化机制,实际性能通常可接受

总结:完整知识线回顾

从对象到硬件内存的完整路径

1. 代码层面
   └─ OC 对象(Person *p = [[Person alloc] init])
       ├─ isa 指针 → 类对象
       ├─ 成员变量(内存对齐)
       └─ 引用计数管理

2. 运行时层面
   └─ 类对象 / 元类
       ├─ 方法列表
       ├─ 属性列表
       └─ 方法缓存

3. 内存布局层面
   └─ 虚拟地址空间
       ├─ 代码段(__TEXT):类的方法实现
       ├─ 数据段(__DATA):类对象、全局变量
       ├─ 堆:对象实例
       └─ 栈:局部变量、函数调用

4. 系统层面
   └─ 虚拟内存 → 物理内存映射
       ├─ 页表映射
       ├─ ASLR 随机化
       └─ 写时复制

5. 硬件层面
   └─ 物理内存(RAM)
       └─ CPU 缓存(L1/L2/L3)

面试重点检查清单

  • ARC 的 retain/release 插入规则
  • 循环引用的典型场景和解决方案
  • Weak 表的工作机制
  • 内存对齐规则和对象大小计算
  • Tagged Pointer 的原理和优势
  • Mach-O 文件结构和段的作用
  • AutoreleasePool 与 RunLoop 的关系
  • 堆分配策略和内存碎片
  • 栈溢出原因和避免方法
  • 类/元类查找链和方法缓存
  • OC vs C++ 内存模型差异
  • 虚拟内存到物理内存的映射
  • Weak 表的实现和性能

祝你面试顺利! 🚀

昨天以前iOS

SwiftUI 中的 @ViewBuilder 全面解析

作者 汉秋
2025年12月19日 10:17

SwiftUI 中的 @ViewBuilder 全面解析

在 SwiftUI 的世界里,@ViewBuilder 是一个你每天都在用,却可能从未认真了解过的核心机制

很多 SwiftUI 看起来“像写 DSL 一样优雅”的代码,其实都离不开它。

本文将从为什么需要它、它解决了什么问题、如何使用、常见坑点几个维度,系统性地介绍 @ViewBuilder,适合 SwiftUI 初学者到中级开发者 阅读。


一、问题的起点:Swift 只能返回一个值

在 Swift 中,函数或计算属性只能返回一个值

但在 SwiftUI 中,我们却经常写出这样的代码:

var body: some View {
    Text("Hello")
    Image(systemName: "star")
    Button("Tap") { }
}

表面看起来像是“返回了多个 View”,这在普通 Swift 函数里是不可能的

那 SwiftUI 是怎么做到的?

答案就是: @ViewBuilder


二、@ViewBuilder 是什么

@ViewBuilder 是 Swift 的一种 Result Builder(结果构建器)

它的核心职责只有一个:

把多行 View 表达式,组合成一个 View 返回。

你写的代码是这样:

Text("A")
Text("B")
Text("C")

编译器在背后会帮你组合成类似:

TupleView<(Text, Text, Text)>

但这些具体类型对开发者是隐藏的,你只需要关心:

可以像写布局一样写 View,而不是手动拼装结构。


三、为什么你很少看到 @ViewBuilder

因为 SwiftUI 已经帮你加好了。

例如:

struct ContentView: View {
    var body: some View {
        Text("Hello")
        Text("World")
    }
}

实际上等价于:

struct ContentView: View {
    @ViewBuilder
    var body: some View {
        Text("Hello")
        Text("World")
    }
}

👉 body 天生就支持多 View 与条件语法


四、@ViewBuilder 支持哪些能力

1️⃣ 多个 View

@ViewBuilder
var content: some View {
    Text("Title")
    Text("Subtitle")
}

2️⃣ if / else 条件渲染(非常重要)

没有 @ViewBuilder,下面代码是非法的:

func makeView(flag: Bool) -> some View {
    if flag {
        Text("Yes")
    } else {
        Text("No")
    }
}

使用 @ViewBuilder 后:

@ViewBuilder
func makeView(flag: Bool) -> some View {
    if flag {
        Text("Yes")
    } else {
        Text("No")
    }
}

👉 这正是 SwiftUI 条件 UI 渲染的基础能力


3️⃣ 只有 if(没有 else

@ViewBuilder
var body: some View {
    Text("Always Visible")

    if isLogin {
        Text("Welcome")
    }
}

当条件不成立时,SwiftUI 会自动插入一个 EmptyView


4️⃣ switch

@ViewBuilder
func stateView(_ state: LoadState) -> some View {
    switch state {
    case .loading:
        ProgressView()
    case .success:
        Text("Success")
    case .error:
        Text("Error")
    }
}

五、最常见的使用场景

1️⃣ 自定义组件的内容闭包

struct Card<Content: View>: View {
    let content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        VStack(spacing: 8) {
            content
        }
        .padding()
        .background(.gray.opacity(0.2))
        .cornerRadius(12)
    }
}

使用时:

Card {
    Text("Title")
    Text("Subtitle")
}

👉 这正是 SwiftUI 组件化体验优秀的原因之一。


2️⃣ 模仿系统 API(如 .sheet / .toolbar

func customOverlay<Content: View>(
    @ViewBuilder content: () -> Content
) -> some View {
    overlay {
        content()
    }
}

六、常见坑点(非常容易踩)

❌ 1. 不能写普通逻辑代码

@ViewBuilder
var body: some View {
    let count = 10 // ❌ 编译错误
    Text("(count)")
}

原因是:

@ViewBuilder 只接受 生成 View 的表达式

✅ 正确方式:

var count: Int { 10 }

@ViewBuilder
var body: some View {
    Text("(count)")
}

❌ 2. 不能直接使用 for 循环

@ViewBuilder
var body: some View {
    for i in 0..<3 { // ❌
        Text("(i)")
    }
}

✅ 正确方式:

ForEach(0..<3, id: .self) { i in
    Text("(i)")
}

七、什么时候需要主动使用 @ViewBuilder

当你遇到以下情况时,就该考虑它:

  • 希望一个函数 / 闭包返回 多个 View
  • 需要在返回 View 时使用 if / else / switch
  • 编写 可组合的自定义组件

简单判断法则:

“这个 API 是否应该像 SwiftUI 一样写 UI?”

如果答案是「是」,那基本就需要 @ViewBuilder


八、总结

  • @ViewBuilder 是 SwiftUI 的核心基础设施
  • 它让 Swift 支持 声明式 UI 语法
  • 条件渲染、多 View 组合、本质都依赖它
  • 写组件时,合理使用 @ViewBuilder 能极大提升 API 体验

一句话总结:

没有 @ViewBuilder,就没有今天的 SwiftUI。


如果你觉得这篇文章有帮助,欢迎点赞 / 收藏 / 交流 🙌

后续也可以深入聊:

  • ViewBuilder 源码实现
  • @ViewBuilder 与 @ToolbarContentBuilder 的区别
  • SwiftUI 新数据流(@Observable / @Bindable)下的最佳实践

1V1 社交精准收割 3.6 亿!40 款马甲包 + 国内社交难度堪比史诗级!

作者 iOS研究院
2025年12月18日 21:06

背景

“她说明年就结婚,转头就把我拉黑了!”2024 年 9 月,山东鱼台县居民王某攥着手机账单冲进警局,声音颤抖。这位常年打工攒下 5 万积蓄的单身汉,从未想过自己在 “念梦”“冬梦” 两款交友 App 上邂逅的 “化妆品店老板娘”,竟是一场精心设计的骗局。

三个月里,这位昵称 “为你而来” 的 “女神” 温柔体贴,频频描绘二人未来的家,却以 “解锁视频聊天”“线下见面需充值刷亲密度” 为由,分三次榨干了他的全部积蓄。当王某停止充值后,昔日热情的恋人瞬间蒸发,只留下 27177 元、9592 元、13794 元三笔冰冷的充值记录。他不知道的是,自己只是这场 3.6 亿诈骗大案中,上千名受害者之一。

40 款马甲包背后:堪比上市公司的诈骗 “工厂”

山东济宁公安破获特大网络交友诈骗案,40余款App全是陷阱。王某的报警,像一把钥匙打开了潘多拉魔盒。警方顺着涉诈 App 的线索深挖,一个隐藏在合法公司外壳下的犯罪集团逐渐浮出水面。团伙头目王某某是正规大学毕业生,曾因运营 “来遇” App 涉诈被查处,却在 2023 年卷土重来,注册多家空壳公司,一口气推出 40 余款交友 App,形成 “换汤不换药” 的马甲矩阵。

这个诈骗团伙的运作模式堪称 “产业化”:运营部负责招募培训 5000 余名女聊手,定制从 “初遇暧昧” 到 “诱导充值” 的全套话术;客服部专门安抚投诉用户,用 “系统维护”“亲密度未达标” 等借口掩盖骗局;甚至设立法务部,钻法律空子规避监管。女聊手们则按照统一剧本,虚构 “单身富婆”“温柔贤妻” 等人设,精准瞄准三、四线城市的大龄单身男性,用暧昧言语和虚假承诺编织情感牢笼。

更令人咋舌的是平台设计的 “吸血机制”:文字消息 10-100 金币 / 条,视频通话 100-2000 金币 / 分钟,充值 1 元仅能兑换 100 金币。女聊手与公司按 4:6 分成,为了多赚钱,她们会用平台发放的免费金币给用户刷礼物,制造 “双向奔赴” 的假象,引诱受害者不断充值。警方后续查获的聊天记录显示,团伙内部流传着 “养鱼玩法拉高点,大哥刷一你刷两” 的黑暗话术。

62 亿条数据剥茧:千人跨省追缉 15 天破局

“这不是零散诈骗,是有组织、有预谋的犯罪网络。” 济宁市公安局迅速成立 “10.14” 专案组,抽调百余名警力攻坚。面对团伙设置的多层数据加密、定期删除证据、核心骨干分散办公等障碍,民警自主编写分析程序,从 8T 容量、超 62 亿条聊天记录和资金明细中抽丝剥茧。

合规化势在必行

立足当前行业大环境,存量社交产品必须将合规化置于开发工作的核心首位。

若不存在关键性的功能迭代需求,建议尽量减少版本更新频次,甚至暂停更新,以此规避审核环节可能出现的风险,避免给产品运营增添不必要的阻碍。

当前国内市场的恶性竞争态势,必然会导致社交类产品在App Store平台面临更严峻的监管压力与发展困境。因此,尽早布局出海业务、开拓海外新市场,已成这类产品突破发展瓶颈的关键方向

合规化的价值懂的无需多言,不懂得多说无益。

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

相关推荐

# 苹果开发者续费大坑及成功续费方案!亲测有效

# AppStore敏感词排查手册,多维度分析Guideline 2.3.1隐藏功能,轻松过审。

# 如何主动提防苹果3.2f的进攻,自查防御手册(代码篇)

# 如何主动提防苹果3.2f的进攻,自查防御手册(ASO篇)

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

【iOS】如何在 iOS 26 的UITabBarController中使用自定义TabBar

2025年12月18日 16:19

Demo地址:ClassicTabBarUsingDemo(主要实现代码可搜索“📌”查看)

前言

苹果自 iOS 26 起就使用全新的UI --- Liquid Glass,导致很多系统组件也被迫强制使用,首当其冲就是UITabBarController,对于很多喜欢使用自定义TabBar的开发者来说,这很是无奈:

  • 强行给你套个玻璃罩子

那如何在 iOS 26UITabBarController继续使用自定义TabBar呢?这里介绍一下两种方案。

方案一

来自大佬网友分享的方案 💪

  1. 自定义TabBar使用UITabBar,通过KVC设置(老方法):
setValue(customTabBar, forKeyPath: "tabBar")
  1. 重写UITabBaraddSubviewaddGestureRecognizer方法:
- (void)addSubview:(UIView *)view {
    if ([view isKindOfClass:NSClassFromString(@"UIKit._UITabBarPlatterView")]) {
        view.hidden = YES;
    }
    [super addSubview:view];
}

- (void)addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {
    if ([gestureRecognizer isKindOfClass:NSClassFromString(@"_UIContinuousSelectionGestureRecognizer")]) {
        gestureRecognizer.enabled = NO;
    }
    [super addGestureRecognizer:gestureRecognizer];
}

解释一下:

_UITabBarPlatterView这个是显示当前Tab的玻璃罩子:

  • 把它隐藏掉就行了

_UIContinuousSelectionGestureRecognizer这个是系统用来处理TabBar切换时的动画手势,触发时会在TabBar上添加_UIPortalView这个跟随手势的玻璃罩子:

  • 同样把它禁止掉就行了

这样就相当于把UITabBar的液态玻璃“移除”掉了,是可以实现以往的显示效果👏。

只不过这个方案在pop手势滑动时,TabBar会被「置顶」显示:

  • 这是苹果新UI的显示逻辑,暂时无法改动

这跟我的预期还差了一点,我是希望连pop手势也能像以前那样:

接下来介绍另一个方案,虽然麻烦很多,但能兼顾pop手势。

方案二

经观察,以往TabBar的显示效果,个人猜测系统是把TabBar放到当前子VC的view上:

按照这个思路可以这么实现:

  1. 首先自定义TabBar要使用UIView(如果使用的是私自改造的UITabBar,得换成UIView了),并且隐藏系统TabBar。
class MainTabBarController: UITabBarController {
    ......
    
    /// 自定义TabBar
    private let customTabBar = WLTabBar()
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // 隐藏系统TabBar
        setTabBarHidden(true, animated: false)
    }
    
    ......
}

  1. TabBarController及其子VC都创建一个专门存放自定义TabBar的容器,且层级必须是最顶层(之后添加的子视图都得插到TabBar容器的下面)。
class BaseViewController: UIViewController {
    /// 专门存放自定义TabBar的容器
    private let tabBarContainer = TabBarContainer()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        tabBarContainer.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tabBarContainer)
        NSLayoutConstraint.activate([
            tabBarContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tabBarContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tabBarContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            tabBarContainer.heightAnchor.constraint(equalToConstant: Env.tabBarFullH) // 下巴+TabBar高度
        ])
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        // 层级必须是最顶层
        view.bringSubviewToFront(tabBarContainer)
    }
    
    // 将自定义TabBar放到自己的TabBar容器上
    func addTabBar(_ tabBar: UIView) {
        tabBar.superview?.isUserInteractionEnabled = false
        tabBarContainer.addSubview(tabBar)
        tabBarContainer.isUserInteractionEnabled = true
    }
}
  1. 最后,TabBarController当前显示哪个子VC,就把自定义TabBar放到对应子VC的TabBar容器上,这样则不会影响pushpresent其他VC。

OK,完事了😗。

注意点

核心实现就是以上3点,接下来讲一下注意点。

上面说到,TabBarController也得创建一个TabBar容器,这主要是用来切换子VC的:

在切换子VC前,自定义TabBar必须先放到TabBarController的TabBar容器上,切换后再放到目标子VC的TabBar容器上。

🤔为什么?

一般子VC的内容都是懒加载(看到才构建),如果是很复杂的界面,不免会有卡顿的情况,如果直接把自定义TabBar丢过去,TabBar会闪烁一下,效果不太好;另外自 iOS 18 起切换子VC会带有默认的系统动画,其动画作用于子VC的view上,即便该子VC早就构建好,立马转移TabBar也会闪烁一下。

因此个人建议先把自定义TabBarTabBarControllerTabBar容器上(层级在所有子VC的view之上),延时一下(确保子VC完全构建好且已完全显示,同时避免被系统动画影响)再放到目标子VC的TabBar容器上,这样就能完美实现切换效果了。

核心代码如下:

// MARK: - 挪动TabBar到目标子VC
private extension MainTabBarController {
    func moveTabBar(from sourceIdx: Int, to targetIdx: Int) {
        guard Env.isUsingLiquidGlassUI else { return }
        
        // #1 取消上一次的延时操作
        moveTabBarWorkItem?.cancel()
        moveTabBarWorkItem = nil
        
        guard let viewControllers, viewControllers.count > 0 else {
            addTabBar(customTabBar)
            return
        }
        
        guard sourceIdx != targetIdx else {
            _moveTabBar(to: targetIdx)
            return
        }
        
        // #2 如果「当前子VC」现在不是处于栈顶,就把tabBar直接挪到「目标子VC」
        let sourceNavCtr = viewControllers[sourceIdx] as? UINavigationController
        if (sourceNavCtr?.viewControllers.count ?? 0) > 1 {
            _moveTabBar(to: targetIdx)
            return
        }
        
        // #3 能来这里说明「当前子VC」正处于栈顶,如果「目标子VC」此时也处于栈顶,就把tabBar放到层级顶部(不受系统切换动画的影响)
        let targetNavCtr = viewControllers[targetIdx] as? UINavigationController
        if (targetNavCtr?.viewControllers.count ?? 0) == 1 {
            addTabBar(customTabBar)
        } else {
            _moveTabBar(to: sourceIdx)
        }
        
        // #3.1 延迟0.5s后再放入到「目标子VC」,给VC有足够时间去初始化和显示(可完美实现旧UI的效果;中途切换会取消这个延时操作#1)
        moveTabBarWorkItem = Asyncs.mainDelay(0.5) { [weak self] in
            guard let self, self.selectedIndex == targetIdx else { return }
            self.moveTabBarWorkItem = nil
            self._moveTabBar(to: targetIdx)
        }
    }
    
    func _moveTabBar(to index: Int) {
        let tab = MainTab(index: index)
        switch tab {
        case .videoHub:
            videoHubVC.addTabBar(customTabBar)
        case .channel:
            channelVC.addTabBar(customTabBar)
        case .live:
            liveVC.addTabBar(customTabBar)
        case .mine:
            mineVC.addTabBar(customTabBar)
        }
    }
}

如果想移除系统切换动画可以这么做:

// MARK: - <WLTabBarDelegate>
extension MainTabBarController: WLTabBarDelegate {
    func tabBar(_ tabBar: WLTabBar!, didSelectItemAt index: Int) {
        moveTabBar(from: selectedIndex, to: index)
        // 想移除系统自带的切换动画就👇🏻
        UIView.performWithoutAnimation {
            self.selectedIndex = index
        }
    }
}

小结

方案一是比较激进的魔改方案,直接把系统的玻璃罩子和手势给移除掉了,缺点是如果苹果以后改动了这些私有类名或行为,可能会导致失效。

方案二是我能想到最完美的方案了,起码不用自定义UITabBarController,简单粗暴,个人感觉能应付80%的应用场景吧,除非你有非常特殊的过场动画需要挪动TabBar的。

以上就是我的方案了,起码不用自定义UITabBarController,简单粗暴,个人感觉能应付80%的应用场景吧,除非你有非常特殊的过场动画需要挪动TabBar的。

更多细节可以参考Demo,以上两种方案都有提供,只需要在WLTabBar.h中选择使用哪一种父类并注释另一个即可:

@interface WLTabBar : UITabBar // 方案一
@interface WLTabBar : UIView // 方案二

希望苹果以后能推出兼容自定义TabBar的API,那就不用这样魔改了😩。

❌
❌