阅读视图

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

Swift中的析构函数deinit

析构函数

在 Swift 中,deinit(析构函数)只适用于 class 类型。因为只有类是引用类型,而不像结构体 struct 或枚举 enum 是值类型。

虽然 Swift 自动管理内存(ARC,Automatic Reference Counting),你不需要经常写 deinit,但在某些关键场景中非常有用。以下是最常见且实用的使用场景:

常用场景一:移除通知监听者

使用 NotificationCenter 添加观察者时,如果不手动移除,会导致内存泄漏或重复响应事件

class MyObserver {

    init() {
        NotificationCenter.default.addObserver(self, selector: #selector(handleEvent), name: .someEvent, object: nil)
    }

    @objc func handleEvent() {
        print("Event received")
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
        print("Observer deinitialized")
    }
}

常用场景二:取消定时器、任务、订阅等异步资源

比如 Timer、DispatchSourceTimer、Combine 的 AnyCancellable、URLSession task 等等。

class TimerHandler {
    var timer: Timer?

    init() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            print("Tick")
        }
    }

    deinit {
        timer?.invalidate()
        print("Timer invalidated")
    }
}

常用场景三:断开委托(Delegate)或弱引用检查

有时候你需要在 deinit 中把自己从其他类的 delegate 中移除,以避免无用引用。

class Child: SomeDelegate {
    init(parent: Parent) {
        parent.delegate = self
    }

    deinit {
        print("Child released")
        // 这里通常不强制清空 delegate,但有时你想显式处理
    }
}

常用场景四:清理自定义资源或临时文件

当类负责写入临时文件或打开文件句柄等,需要在析构时清理。

class TempFileManager {
    let filePath: String

    init() {
        filePath = NSTemporaryDirectory() + UUID().uuidString
        FileManager.default.createFile(atPath: filePath, contents: nil)
    }

    deinit {
        try? FileManager.default.removeItem(atPath: filePath)
        print("Temporary file deleted")
    }
}

常用场景五:调试或资源释放追踪

打印 deinit 是常见的调试手段,能够帮助判断是否某对象已经被释放(尤其是在处理循环引用、Retain Cycle 的时候)。

deinit {
    print("MyViewController has been released!")
}

总结:deinit 适用的典型场景

场景 用途说明
通知监听移除 防止内存泄漏
定时器/任务取消 释放异步资源
文件或自定义资源清理 避免残留资源
delegate/数据源解绑(可选) 规范化资源管理
Combine/任务订阅取消 清理绑定或订阅
打印调试(检查是否释放) 非常实用的调试手段

在 SwiftUI 或 iOS 架构设计中,有些类如 ViewModel 或自定义对象,还是推荐善用 deinit 来做资源释放和问题定位



构造链

Swift 中 enum、struct 和 class 都可以拥有构造函数(initializer),但三者在构造行为和使用方式上有 明显差异,尤其是在以下几个地方:

• 默认构造器是否自动生成

• 成员变量初始化规则

• 继承与重写支持

• 可变性

• 引用与值语义

这里主要讨论下构造链。构造在 Swift 中,class 是引用类型,支持继承。而子类初始化时需要 保证父类的属性也被正确初始化,因此构造函数需要遵循一定的调用顺序,也就是所谓的“构造链”:

• 子类的 init 必须在某一阶段调用 super.init(...),以确保父类部分正确构造。

• 所有 stored property 初始化完后,才能调用 super.init()。

• Swift 编译器会严格检查构造顺序,避免未初始化状态下访问属性。

class Animal {
    var name: String
    init(name: String) {
        self.name = name
        print("Animal init")
    }
}

class Dog: Animal {
    var breed: String
    init(name: String, breed: String) {
        self.breed = breed              // 1️⃣ 先初始化子类属性
        super.init(name: name)         // 2️⃣ 调用父类构造器
        print("Dog init")
    }
}

// 调用
let dog = Dog(name: "Buddy", breed: "Labrador")

// 输出
# Animal init
# Dog init

对于 class类型:

1. 所有存储属性必须初始化。

2. 子类构造器必须在合适时机调用 super.init(...)。

3. 若父类没有默认构造器,子类必须手动调用父类指定构造器。

4. convenience init 只能调用本类的 designated init,不能直接调用 super.init。

5. 父类的 required init 必须被子类实现。

"struct 和 enum 没有继承, 它们只有构造器,不存在构造链,甚至构造器都可以用默认自动生成的

苹果3.2f倒计时结束后“死亡邮件”是啥样的?

前言

经历了苹果审核几轮清洗,有不少来询问3.2f破局以及被封号之后的情况,所以单独拿出来简单做个回复。

3.2f终止时间

从收到3.2f邮件到苹果正式封号,大概是30个自然日左右。正常情况下,未做任何解释以及回复一般就是足月嘎了。

如果是积极回复苹果,主动向苹果解释说明情况的大概率会比30天长一些。如果说也是30天到时候嘎了,那么就证明解释的内容完全没有被苹果认可,直接维持原判

3.2f死亡邮件

下面的图就是30天后收到死亡通牒,如图可见。

wechat_2025-05-16_174421_144.png

其中最关键的两点:

第一点:死刑立即执行!

本函作为您与Apple之间的《Apple开发者计划许可协议》(以下简称“ADP协议”)和《Apple开发者协议》(以下简称“开发者协议”)的终止通知,立即生效。

第二点:没救了不用挣扎了。

本函并非对此事事实的完整陈述,本函中的任何内容均不应被解释为放弃苹果可能拥有的任何权利或补救措施,所有这些权利或补救措施均在此保留

后续处理

最基本是测试设备不用想了,要贴标签做好设备隔离。避免重蹈覆辙!开发设备如果条件允许的情况下建议也处理掉了,以绝后患。

其次是将绑定的内购账号存档记录,不再使用。

最后就是代码了能不用就不能,能不从里面粘贴就别粘贴。苹果的算法已经不像之前那么简单了,别因为偷懒因小失大!也恳请各位管理层能多宽限些时间,避免技术编码时间不够不得不偷懒。

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

JSON Schema 表单规范

🧾 JSON Schema 表单规范 v1.0

该规范用于描述一个表单页面的 UI、字段规则、交互逻辑和校验要求。支持分组、动态显隐、联动、正则等高级能力。


JSON示例

{
  "title": "注册信息",
  "sections": [
    {
      "title": "基础信息",
      "fields": [
        {
          "key": "name",
          "label": "姓名",
          "type": "text",
          "required": true,
          "placeholder": "请输入姓名"
        },
        {
          "key": "gender",
          "label": "性别",
          "type": "radio",
          "required": true,
          "options": [
            { "label": "男", "value": "male" },
            { "label": "女", "value": "female" }
          ]
        },
        {
          "key": "militaryStatus",
          "label": "服兵役情况",
          "type": "text",
          "placeholder": "如已服役/未服役",
          "visibleIf": {
            "gender": "male"
          }
        }
      ]
    },
    {
      "title": "联系方式",
      "fields": [
        {
          "key": "phone",
          "label": "手机号码",
          "type": "text",
          "validators": [
            {
              "type": "regex",
              "pattern": "^1[0-9]{10}$",
              "message": "请输入有效手机号"
            }
          ]
        }
      ]
    }
  ]
}

Swift Demo

chatgpt.com/canvas/shar…

📌 顶层结构

{
  "title": "表单标题",
  "sections": [ FormSection, ... ]
}
字段 类型 含义
title string 整个表单的标题
sections array 分区数组,每个分区有一个标题与若干字段

📦 FormSection

{
  "title": "分区标题",
  "fields": [ FormField, ... ]
}
字段 类型 含义
title string 分区标题
fields array 表单字段列表

🧩 FormField 字段属性说明

{
  "key": "字段标识",
  "label": "显示名称",
  "type": "字段类型",
  "required": true,
  "placeholder": "提示文本",
  "editable": true,
  "hidden": false,
  "options": [ Option ],
  "visibleIf": { "gender": "male" },
  "validators": [ Validator ]
}
字段 类型 说明
key string 字段唯一标识(存值取值)
label string 显示名
type string 字段类型(见下表)
required boolean 是否必填
placeholder string 输入框提示文字
editable boolean 是否可编辑
hidden boolean 是否默认隐藏(不可见)
options array of Option 可选项,仅限 select/radio
visibleIf object {k: v} 显示条件(当 key==value 时显示)
validators array of Validator 校验规则

支持的字段类型(type):

类型 说明
text 普通文本输入框
textarea 多行文本
number 数字输入
select 下拉菜单
radio 单选组
checkbox 多选组(待扩展)
date 日期选择(可扩展)

🎛️ Option 配置(用于 select / radio)

{ "label": "男", "value": "male" }
字段 类型 含义
label string 用户可见文字
value string 实际提交值

✅ 校验 Validator

{
  "type": "regex",
  "pattern": "^1[0-9]{10}$",
  "message": "请输入有效手机号"
}
字段 类型 说明
type string 当前仅支持 "regex"
pattern string 正则表达式
message string 错误提示文本

🎯 visibleIf 条件控制说明

用于控制字段是否显示,格式如下:

"visibleIf": {
  "gender": "male"
}
  • 当 gender == "male" 时,该字段显示
  • 支持多个条件(与关系)
  • 可扩展支持数组值(如某字段 ∈ 多个值)

🧪 示例片段

{
  "key": "militaryStatus",
  "label": "服兵役情况",
  "type": "text",
  "visibleIf": {
    "gender": "male"
  }
}

🏁 约定建议(团队协作)

建议 说明
key 唯一 必须唯一,用于表单存储
所有文本请支持 i18n 协议 label / placeholder / message
支持 UI 端自定义字段类型扩展 可在 type 扩展为 imageUploader, mapPicker
使用 camelCase 命名字段 推荐风格一致

LazyViewContainer-iOS中节省内存的UI创建方式

背景

iOS工程中,创建自定义UI组件时有一个常见的写法:

  1. UI组件初始化时,创建所有子组件实例;并将子组件添加到自定义组件层级中,再使用Autolayout做好约束布局
  2. 后续有数据时,直接为组件的每个子组件填充数据

这样的写法是会导致一定的内存浪费,比如

  • 有的场景可能仅在特定情况下才会拿到数据显示UI组件,如果没有数据,若仍然初始化了某个UI组件,则导致该组件占用了一部分长期不会用到的内存
  • 这样的浪费在像直播房等需要大量UI、非UI组件的场景下比较常见

当然,根本的解决方案也是很容易想到的:

  • 延迟加载:仅在需要某个UI组件时才去创建和使用

没错,这无疑可以从根本上解决该问题,但是,延迟加载写起来其实要更麻烦一些,所以为了方便快捷,最开始提到的写法其实更普遍

下面会提到一种折中的方案,能部分降低内存的浪费

LazyViewContainer

  • 红框为普通UI组件直接创建和内存分配过程;绿框中是LazyViewContainer工作原理
  • 普通UI组件初始化完后,内存中就有了该组件完整的对象
  • LazyViewContainer只是一个有很少轻量属性(3个)的普通类,所以LazyViewContainer初始化后,此时内存中的占用较小(约48 bytes)。仅当执行ensureView方法时,才会尝试创建实际的UI组件
  • 最右侧紫色框展示了不同情况下内存占用情况
  • 简单总结一下LazyViewContainer的工作原理,就是在实际UI组件创建之前添加了一个中间层(使用泛型和closure来做到这一点),尽可能延迟实际UI组件及子组件的创建

内存占用测试

使用了几个最常见的系统UI组件进行了一下内存占用的测试

  • 测试环境:
    • iPhone 13 mini/iOS 18.1.1, Release环境
  • 测试方法:
    • 使用不同UI组件,使用不同创建方式,验证对应情况下,看单个UI组件实例实际的内存占用

不同UI组件的创建逻辑代码如下所示:

// UILabel
let label = UILabel()
label.text = "Test Label"
label.font = .systemFont(ofSize: 14)
label.textColor = .red
label.backgroundColor = .blue
label.frame = CGRect(x: 100, y: 100, width: 100, height: 20)
// UIButton
let button = UIButton(type: .system)
button.setTitle("Test Button", for: .normal)
button.backgroundColor = .green
button.frame = CGRect(x: 100, y: 200, width: 100, height: 40)
// UIImageView
let imageView = UIImageView()
imageView.backgroundColor = .yellow
imageView.frame = CGRect(x: 100, y: 300, width: 100, height: 100)
组件类型 创建方式 单个实例内存(bytes) 节省内存(bytes) 节省比例
UILabel 直接创建 896 - -
Lazy创建 48 768 94.64%
Lazy Ensure 944 -48 -5.36%
UIButton 直接创建 768 - -
Lazy创建 48 1792 93.75%
Lazy Ensure 816 -48 -6.25%
UIImageView 直接创建 512 - -
Lazy创建 48 384 90.62%
Lazy Ensure 560 -48 -9.38%

根据测试结果,总结一下结论:

  1. LazyViewContainer单个实例(在没有通过ensureView触发实际UI组件创建时)占用的内存约为 48 bytes
  2. 基于结论1,所以当LazyViewContainer真的要创建实际UI组件时,会比直接创建组件占用的内存更多,但也仅多48 bytes,至于精确的内存占比可以参考表格中“节省比例”列的绿字数据
  3. 同时也能预见到,当自定义UI组件越复杂时,“仅Lazy创建”的方式下,带来的内存节省比例更显著

LazyViewContainer使用场景

首先要明确两点:

  1. 这并不是创建UI组件的最节省内存的方式,只是一种在编码便捷性和内存性能之间的一种折中手段
  2. 不是所有情况下都推荐使用LazyViewContainer

那么,什么场景下推荐使用?

  • 用户可能长时间进行操作的、复杂UI层级的页面(如语音、视频、直播房间)
    • 这样的页面支持的功能比较多,但又不是需要同时运行这些功能,当多数用户、多数情况下仅使用一小部分功能时,那么如果仍然在进入页面时创建所有功能的业务或UI组件,就是会造成比较严重的内存浪费
  • 现有代码中业务逻辑太复杂,不宜短时间做大重构时,适合这样的折中方案
  • 复杂的自定义UI组件
    • 子组件越多,UI层级越复杂,当该UI组件使用率低的时候,所带来的内存节省效果越明显

什么场景下不推荐使用?

  • 列表页面
    • 列表滚动时,伴随多个cell的复用,虽然每个cell的数据不尽相同,有的UI元素可能不需要显示。但可能会被复用,所以此情况下,cell初始化时将所有子UI组件都创建,合理性更强,这样会更方便滚动时的复用

源码与使用方式

源码:

//
//  LazyViewContainer.swift
//
//  Created by songgeb on 2025/1/1.
//  Copyright © 2025 songgeb. All rights reserved.
//

import UIKit
/// 将创建自定义UIView逻辑延迟到数据到来时,同时避免当数据无效时仍创建自定义UIView,导致无意义的内存占用问题
final class LazyViewContainer<T: UIView> {
    private(set) var view: T?
    private let createView: () -> T
    private weak var parentView: UIView?

    init(createView: @escaping () -> T) {
        self.createView = createView
    }

    init() {
        createView = { T() }
    }

    /// 获取或创建视图,交给调用方处理布局
    /// - Parameters:
    ///   - parent: 父视图
    ///   - customAddition: 自定义添加视图逻辑
    ///   - onFirstAddition: 首次创建、同时添加到parentView的时机
    /// - Returns: 自定义视图
    @discardableResult
    func ensureView(
        in parent: UIView,
        customAddition: ((T) -> Void)? = nil,
        onFirstAddition: ((T) -> Void)? = nil) -> T
    {
        if let existingView = view {
            return existingView
        }

        let newView = Thread.isMainThread ? createView() : DispatchQueue.main.sync { createView() }

        // 使用自定义添加方式或默认 addSubview
        if let customAddition {
            customAddition(newView)
        } else {
            parent.addSubview(newView)
        }

        onFirstAddition?(newView)
        view = newView
        parentView = parent
        return newView
    }

    /// 移除视图
    /// - Parameter customRemoval: 自定义移除视图逻辑
    func removeView(using customRemoval: ((T) -> Void)? = nil) {
        guard let view = view, view.superview != nil else { return }
        func doRemoving() {
            if let customRemoval {
                customRemoval(view)
            } else {
                view.removeFromSuperview()
            }
        }
        if Thread.isMainThread {
            doRemoving()
        } else {
            DispatchQueue.main.async { doRemoving() }
        }
        self.view = nil
        parentView = nil
    }

    // 检查视图是否创建
    var isCreated: Bool {
        view != nil
    }

    deinit {
        removeView()
    }
}

使用方式:

class TestViewController: UIViewController {
    private lazy var leftVerticalStack: UIStackView = {
        let vstack = UIStackView(frame: .zero)
        vstack.axis = .vertical
        vstack.alignment = .leading
        vstack.distribution = .equalSpacing
        return vstack
    }()
    
    private lazy var detailLabelLoader = LazyViewContainer<UILabel> {
        let label = UILabel()
        label.font = .systemFont(withSize: 14)
        label.textColor = .bSecondary
        return label
    }

    private func updateDetailLabel(_ shouldShowDetail: Bool) {
        if shouldShowDetail {
            detailLabelLoader.ensureView(in: leftVerticalStack, customAddition: { [weak leftVerticalStack] label in
                guard let leftVerticalStack else { return }
                leftVerticalStack.addArrangedSubview(label)
            })
            detailLabelLoader.view?.text = "abc"

        } else {
            detailLabelLoader.removeView { [weak self] label in
                guard let self else { return }
                leftVerticalStack.removeArrangedSubview(label)
            }
        }
    }
}
❌