阅读视图

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

移动应用上架到应用商店的完整指南:原理与详细步骤

随着智能手机的普及,移动应用程序(App)已经成为人们日常生活中必不可少的一部分。而将自己的App上架到应用商店则是许多开发者的梦想,因为这意味着他们的作品可以被更多人看到、下载和使用。本文将介绍App上架到应用商店的原理和详细步骤。

一、App上架的原理

App上架到应用商店的原理可以简单概括为:开发者将开发好的App上传到应用商店,应用商店审核通过后将App发布到应用商店。在这个过程中,开发者需要遵守应用商店的规定和要求,以确保App能够通过审核并成功上架。

具体来说,开发者需要准备好以下内容:

  1. 应用商店账号:开发者需要在目标应用商店注册一个账号,并遵守该应用商店的规定和要求。

  2. App信息:开发者需要提供App的名称、描述、图标、版本号、支持的设备类型等信息。

  3. App安装包:开发者需要将App打包成符合应用商店要求的安装包,并上传到应用商店。对于iOS应用,可以使用AppUploader等工具在Windows、Linux或Mac系统中上传IPA文件到App Store,无需Mac电脑即可操作,比传统方法更高效。

  4. 证书和签名:开发者需要使用证书和签名对App进行加密和验证,以确保App的安全性和可靠性。使用工具如AppUploader可以简化iOS证书的申请和签名过程,支持多电脑协同,无需钥匙串助手。

  5. 测试和调试:开发者需要对App进行测试和调试,以确保App的质量和稳定性。

二、App上架的详细步骤

  1. 注册应用商店账号

开发者需要在目标应用商店注册一个账号,以便上传App和管理App的信息。不同的应用商店可能有不同的注册流程和要求,开发者需要仔细阅读应用商店的注册指南,并提供必要的信息和证明文件。

  1. 准备App信息

开发者需要准备好App的名称、描述、图标、版本号、支持的设备类型等信息。这些信息将在应用商店中展示,并影响用户对App的印象和选择。

  1. 打包App安装包

开发者需要将App打包成符合应用商店要求的安装包,并上传到应用商店。不同的应用商店可能有不同的安装包要求,开发者需要仔细阅读应用商店的指南,并使用合适的工具和方法进行打包。AppUploader支持快速上传IPA文件,并内置工具查看和编辑相关文件内容。

  1. 证书和签名

开发者需要使用证书和签名对App进行加密和验证,以确保App的安全性和可靠性。证书和签名的获取和使用也可能因应用商店的不同而有所差异,开发者需要仔细阅读应用商店的指南,并按照要求进行操作。利用AppUploader,开发者可以直接创建和管理iOS证书,简化流程。

  1. 测试和调试

开发者需要对App进行测试和调试,以确保App的质量和稳定性。测试和调试的过程可能会涉及多个设备和操作系统,开发者需要尽可能模拟用户的使用场景,并记录和解决问题。AppUploader提供USB和二维码安装测试功能,方便在iOS设备上验证应用。

  1. 提交审核

开发者需要将准备好的App信息、安装包、证书和签名上传到应用商店,并提交审核。审核的过程可能需要几天甚至几周的时间,开发者需要耐心等待,并及时响应应用商店的反馈和要求。

  1. 上架发布

审核通过后,应用商店会将App发布到应用商店,供用户下载和使用。开发者需要及时更新App的信息和版本,并处理用户的反馈和问题。

总之,将App上架到应用商店需要开发者投入大量时间和精力,需要遵守应用商店的规定和要求,并保证App的质量和安全性。只有经过认真准备和审核,才能让自己的App在应用商店中脱颖而出,成为用户喜爱的产品。

Xcode SPM 太慢/报错?代理 + 缓存修复

SPM 加速:终端代理

在终端执行(端口如 7890 按自己改):

export https_proxy=http://127.0.0.1:7890
export http_proxy=http://127.0.0.1:7890
cd /path/to/your/project
xcodebuild -resolvePackageDependencies

报 fatalError 时

错误里会带类似 FloatingPanel-f92b491a 的路径,删掉该缓存再重试:

rm -rf ~/Library/Caches/org.swift.swiftpm/repositories/FloatingPanel-f92b491a

多个包都报错就清空整个缓存:

rm -rf ~/Library/Caches/org.swift.swiftpm/repositories/*

然后重新执行 xcodebuild -resolvePackageDependencies

isa 指针、元类、继承链


一、isa 不只是一个指针

在 64 位设备上,指针只需要 36~40 位就能表示所有内存地址。苹果觉得剩下的位浪费了,于是把 isa 设计成了一个 union(联合体) ,把类指针和一堆标志位都塞进了这 64 位里。

这叫 Tagged Pointer / Non-pointer ISA 技术。


二、isa_t 的完整源码

// 文件:objc-private.h
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits;         // 原始的 64 位值

private:
    Class cls;              // 类指针(只在 non-pointer isa 关闭时使用)

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;       // 展开后是一堆位域定义
    };
    ...
};

ISA_BITFIELD 展开(ARM64,iOS 真机)

// 这是 ARM64 的位域定义
uintptr_t nonpointer        : 1;   // bit 0
uintptr_t has_assoc         : 1;   // bit 1
uintptr_t has_cxx_dtor      : 1;   // bit 2
uintptr_t shiftcls          : 33;  // bit 3~35  ← 类指针在这里!
uintptr_t magic             : 6;   // bit 36~41
uintptr_t weakly_referenced : 1;   // bit 42
uintptr_t unused            : 1;   // bit 43
uintptr_t has_sidetable_rc  : 1;   // bit 44
uintptr_t extra_rc          : 19;  // bit 45~63

三、每一位的含义逐个解释

bit 0:nonpointer

uintptr_t nonpointer : 1;

含义: 这个 isa 是不是 "non-pointer isa"(优化过的 isa)。

  • 0:纯指针,整个 64 位就是类地址(老设备/某些特殊情况)
  • 1:non-pointer isa,64 位里藏了很多信息

现代 iOS 设备全是 1


bit 1:has_assoc

uintptr_t has_assoc : 1;

含义: 这个对象是否有关联对象(Associated Object)。

关联对象就是你用 objc_setAssociatedObject 给对象动态绑定的数据。

为什么需要这一位?

  • 对象 dealloc 时,runtime 需要清理关联对象
  • 用这一位做快速判断:has_assoc == 0 → 跳过关联对象清理,直接释放,更快

bit 2:has_cxx_dtor

uintptr_t has_cxx_dtor : 1;

含义: 这个类(或它的父类)是否有 C++ 析构函数,或者 OC 的 .cxx_destruct 方法。

.cxx_destruct 是编译器自动生成的方法,用来清理带有 __strong 修饰的成员变量(ARC 下自动 release)。

为什么需要这一位?

  • 对象 dealloc 时,如果没有需要清理的 C++ 对象,就跳过 .cxx_destruct 调用
  • 优化释放速度

bit 3~35:shiftcls(33位)

uintptr_t shiftcls : 33;

含义: 这 33 位才是真正的类指针(右移 3 位存储,取的时候左移 3 位还原)。

为什么只用 33 位?因为 ARM64 的内存对齐保证类地址的低 3 位永远是 0,可以省掉。

如何取出类指针?

// runtime 内部的取法
Class getClass() const {
    return (Class)(shiftcls << 3);  // 左移3位还原真实地址
}

bit 36~41:magic(6位)

uintptr_t magic : 6;

含义: 固定的魔数,值是 0b011010(十进制 26)。

用途: 调试用。当你看到一个 isa,如果 magic 值不对,说明这个对象已经被释放或内存被踩了(野指针)。Xcode 和 runtime 的断言会检查这个值。


bit 42:weakly_referenced

uintptr_t weakly_referenced : 1;

含义: 这个对象是否被弱引用__weak 指针)指向过。

为什么需要这一位?

  • 对象 dealloc 时,如果有弱引用指向它,需要去 SideTable(全局散列表)里把那些弱引用都清零(避免 dangling pointer)
  • 用这一位快速判断:weakly_referenced == 0 → 跳过 SideTable 查找,直接释放

bit 43:unused

uintptr_t unused : 1;

含义: 目前未使用,预留位。


bit 44:has_sidetable_rc

uintptr_t has_sidetable_rc : 1;

含义: 引用计数是否溢出到了 SideTable。

正常情况下,引用计数存在 isa 的 extra_rc 里(19位,最大能存 2^19 - 1 = 524287)。如果引用计数超过了这个值,has_sidetable_rc = 1,多出来的部分存在全局的 SideTable 里。


bit 45~63:extra_rc(19位)

uintptr_t extra_rc : 19;

含义: 存储对象的引用计数 - 1

为什么是减 1?因为对象存活时引用计数至少为 1,存 0 代表计数是 1,节省一点空间。

实际的引用计数 = extra_rc + 1(如果 has_sidetable_rc == 0)


四、如何取出 isa 里的类指针(实际代码)

// objc-object.h
inline Class objc_object::getIsa() {
    if (fastpath(!isTaggedPointer())) {
        return ISA();
    }
    // ... TaggedPointer 的特殊处理
}

inline Class objc_object::ISA(bool authenticated) {
    ASSERT(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    // 某些架构用索引
    ...
#else
    // ARM64 主路径:取 shiftcls 位,左移3位还原地址
    return (Class)(isa.bits & ISA_MASK);
#endif
}

其中 ISA_MASK 在 ARM64 是 0x0000000ffffffff8ULL,作用就是取 bit 3~35。


五、元类(Metaclass)是什么?

这是 OC 最难理解的概念之一,但其实逻辑非常自洽。

问题的由来

在 OC 里,"一切皆对象"——包括类本身也是对象。

[NSString class]  // 这返回的是一个对象
[NSString stringWithString:@"hello"]  // 这是给"类对象"发消息

既然类也是对象,那类对象的 isa 指向哪里?

答案就是:元类(metaclass)

元类的定义

元类是"类的类"。它存储的是类方法+ 方法),就像普通类存储实例方法(- 方法)一样。

对比:类 vs 元类

普通类(Class) 元类(Metaclass)
本质 objc_class 结构体 也是 objc_class 结构体
方法列表里存的 实例方法(- 类方法(+
isa 指向 元类 根元类(NSObject 的元类)
superclass 指向 父类 父类的元类

六、完整的 isa + 继承链图

这是 OC 里最经典的一张图,一定要理解它:

                 isa                  isa               isa
实例对象(inst) --------→ 类(MyClass) --------→ 元类(Meta-MyClass) ──→ 根元类
                                                                          │
              superclass              superclass             superclass    │ isa(自指)
         MyClass ───────→ NSObject     Meta-MyClass ──────→ Meta-NSObject─┘
                               │                                  │
                               │ superclass = nil                 │ superclass
                               ↓                                  ↓
                             (nil)                             NSObject(不是元类!)

用文字描述:

  1. 实例对象.isaMyClass(类)
  2. MyClass.isaMeta-MyClass(元类)
  3. Meta-MyClass.isaMeta-NSObject(根元类)
  4. Meta-NSObject.isaMeta-NSObject自指!根元类的 isa 指向自己

继承链:

  1. MyClass.superclassNSObject
  2. NSObject.superclassnil
  3. Meta-MyClass.superclassMeta-NSObject(元类也有继承链)
  4. Meta-NSObject.superclassNSObject元类继承链的终点是 NSObject 类,不是 nil!

七、为什么元类的继承链终点是 NSObject?

这个设计让你可以在任何类方法里调用 NSObject 的实例方法(比如 respondsToSelector:)。

// 这为什么能工作?
[MyClass respondsToSelector:@selector(doSomething)];

+respondsToSelector: 是 NSObject 的实例方法(- 方法),存在 NSObject 类里。
当你给 MyClass 发这个消息,runtime 查找路径:

Meta-MyClass(没有)
    → Meta-NSObject(没有)
        → NSObject(在这找到了!)

因为 Meta-NSObject.superclass = NSObject,所以元类链最终能访问到 NSObject 的实例方法。优雅!


八、TaggedPointer:特殊的对象

不是所有"对象"都是真正的对象(有 isa 的结构体)。

什么是 TaggedPointer?

对于一些小值对象(比如 NSNumberNSDate、小字符串),苹果直接把值编码进指针本身,不分配堆内存。

NSNumber *num = @42;
// 在 64 位下,这个指针可能长这样:
// 0xb000000000000162  (不是真实的堆地址!)
// 最高位 1 = TaggedPointer 标志
// 低位存了 42 这个值

判断是否是 TaggedPointer

static inline bool _objc_isTaggedPointer(const void * _Nullable ptr) {
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
// ARM64: _OBJC_TAG_MASK = (1UL<<63),最高位为1就是 TaggedPointer

TaggedPointer 的好处

  • 不需要堆分配:直接在指针里存值,alloc 时不走 malloc
  • 不需要引用计数:也不需要 release,直接丢弃
  • 更快:少了内存分配和释放的开销

九、SideTable:引用计数和弱引用的大本营

当 isa 的 extra_rc 不够用,或者有弱引用时,数据存在 SideTable 里。

struct SideTable {
    spinlock_t slock;           // 自旋锁,保证线程安全
    RefcountMap refcnts;        // 引用计数表(散列表)
    weak_table_t weak_table;    // 弱引用表
};

全局有 8 个(或 64 个)SideTable,通过对象地址取模来分配,减少锁竞争。

weak_table_t 弱引用表

struct weak_table_t {
    weak_entry_t *weak_entries;  // 弱引用条目数组
    size_t num_entries;
    ...
};

struct weak_entry_t {
    DisguisedPtr<objc_object> referent;  // 被指向的对象
    // 指向该对象的所有 __weak 指针地址的集合
    union {
        struct { weak_referrer_t *referrers; ... };
        struct { weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; };
    };
};

__weak 置零的过程

对象 dealloc
    ↓
检查 isa.weakly_referenced
    ↓(== 1)
去 SideTable 找 weak_entry_t
    ↓
遍历所有指向该对象的 __weak 指针
    ↓
全部置 nil
    ↓
从 weak_table 删除该条目

这就是为什么 __weak 指针在对象释放后自动变成 nil,而不会变成野指针——runtime 帮你清零了。


十、小结

概念 本质 存在哪里
isa 64位 union,含类指针+引用计数+标志位 每个对象的第一个字段
元类 存类方法的 objc_class 全局静态区
TaggedPointer 值直接编码进指针,无堆对象 栈/寄存器
extra_rc 引用计数(-1)的快速存储 isa 的高19位
SideTable 溢出引用计数 + 弱引用表 全局散列表

下一篇:延伸问题 Q&A——消息发送、方法查找、Swizzle、dealloc 全流程等

objc_class 结构体逐行解析

前言

objc_class 开始,是因为它是整个 Runtime 的基础数据结构。Runtime 管的事很多——消息发送、方法查找、内存管理、Category 加载……但这些行为最终都要落在"类长什么样"上面。搞清楚 objc_class,后面的东西才能接得上。

一、源码全貌(先看完整结构)

下面是从 Apple 开源的 objc4 里提取的核心结构体,我做了适度精简,保留所有关键字段。

建议先整体扫一遍,有个印象,后面逐个解释。

// ============================================================
// 文件:objc-runtime-new.h(objc4-818.2)
// 源码地址:https://opensource.apple.com/source/objc4/
// ============================================================

// -------------------- 1. objc_object --------------------
// 所有 OC 对象的基类,只有一个字段:isa
struct objc_object {
private:
    isa_t isa;  // 64位,包含类指针+引用计数+标志位

public:
    Class ISA(bool authenticated = false);
    Class getIsa();
    // ... 省略其他方法
};


// -------------------- 2. objc_class --------------------
// 这就是"类"的底层结构,继承自 objc_object
struct objc_class : objc_object {
    // 注意:isa 字段继承自 objc_object,这里不重复写
    
    Class superclass;           // 父类指针
    cache_t cache;              // 方法缓存(哈希表)
    class_data_bits_t bits;     // 指向 class_rw_t 的指针+标志位

    // 取出真正的数据
    class_rw_t *data() const {
        return bits.data();
    }
    // ... 省略其他方法
};


// -------------------- 3. class_data_bits_t --------------------
// 这是 objc_class.bits 的类型,用来存储指向 class_rw_t 的指针 + 几个标志位
struct class_data_bits_t {
private:
    uintptr_t bits;   // 就是一个 64 位整数,低位藏标志位,高位存指针

public:
    // 用掩码取出真正的 class_rw_t 指针
    class_rw_t *data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }

    // 各种标志位的读取方法
    bool isSwiftLegacy() const {
        return getBit(FAST_IS_SWIFT_LEGACY);
    }
    bool isSwiftStable() const {
        return getBit(FAST_IS_SWIFT_STABLE);
    }
    // ... 其他方法
};

// ARM64 下的掩码和标志位定义:
// FAST_DATA_MASK      = 0x00007ffffffffff8UL  (取 bit 3~46,即真正的指针)
// FAST_IS_SWIFT_LEGACY = 1 << 0  (bit 0: 是否是旧版 Swift 类)
// FAST_IS_SWIFT_STABLE = 1 << 1  (bit 1: 是否是新版 Swift 类)
// FAST_HAS_DEFAULT_RR  = 1 << 2  (bit 2: 是否有默认的 retain/release)


// -------------------- 4. cache_t --------------------
// 方法缓存,加速方法查找
struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;  // 桶数组地址
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;   // 桶数量-1(用于哈希取模)
            uint16_t                   _flags;
            uint16_t                   _occupied;    // 已使用的桶数
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
public:
    // ... 省略查找、插入方法
};

// 单个缓存桶
struct bucket_t {
private:
    explicit_atomic<SEL> _sel;       // 方法名(选择子)
    explicit_atomic<uintptr_t> _imp; // 函数指针(方法实现地址)
};


// -------------------- 5. class_rw_t --------------------
// 运行时可读写数据(Category 方法会合并到这里)
struct class_rw_t {
    uint32_t flags;
    uint16_t witness;
    uint16_t index;

    explicit_atomic<uintptr_t> ro_or_rw_ext;  // 指向 class_ro_t 或扩展数据

    Class firstSubclass;       // 第一个子类
    Class nextSiblingClass;    // 兄弟类(形成链表)

    // 获取方法/属性/协议列表
    const method_array_t methods() const;
    const property_array_t properties() const;
    const protocol_array_t protocols() const;

    // 获取只读数据
    const class_ro_t *ro() const;
};


// -------------------- 6. class_ro_t --------------------
// 编译期只读数据(源码里写死的方法、变量、属性)
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;      // 实例变量起始偏移
    uint32_t instanceSize;       // sizeof(实例),对象占多少字节

    const uint8_t * ivarLayout;  // 强引用 ivar 的内存布局

    const char * name;           // 类名字符串,如 "NSString"
    
    WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;  // 方法列表
    protocol_list_t * baseProtocols;     // 协议列表
    const ivar_list_t * ivars;           // 实例变量列表
    
    const uint8_t * weakIvarLayout;      // 弱引用 ivar 的内存布局
    property_list_t *baseProperties;     // 属性列表
};


// -------------------- 7. method_t --------------------
// 单个方法的描述
struct method_t {
    SEL name;              // 方法名(选择子),本质是 const char *
    const char *types;     // 类型编码,如 "v16@0:8"
    IMP imp;               // 函数指针(真正的代码地址)
};


// -------------------- 8. ivar_t --------------------
// 单个实例变量的描述
struct ivar_t {
    int32_t *offset;       // 偏移量指针(Non-Fragile ABI 用)
    const char *name;      // 变量名,如 "_name"
    const char *type;      // 类型编码,如 "@"NSString""
    uint32_t alignment_raw;// 对齐方式
    uint32_t size;         // 占多少字节
};


// -------------------- 9. isa_t --------------------
// isa 的真正定义(union,64位里塞了很多信息)
union isa_t {
    uintptr_t bits;        // 原始64位值

    // ARM64 位域展开(iOS 真机):
    struct {
        uintptr_t nonpointer        : 1;   // bit 0:  是否是优化过的 isa
        uintptr_t has_assoc         : 1;   // bit 1:  有关联对象?
        uintptr_t has_cxx_dtor      : 1;   // bit 2:  有 C++ 析构?
        uintptr_t shiftcls          : 33;  // bit 3-35:  类指针(右移3位存储)
        uintptr_t magic             : 6;   // bit 36-41: 固定值 0x1a,调试用
        uintptr_t weakly_referenced : 1;   // bit 42: 被弱引用?
        uintptr_t unused            : 1;   // bit 43: 未使用
        uintptr_t has_sidetable_rc  : 1;   // bit 44: 引用计数溢出到 SideTable?
        uintptr_t extra_rc          : 19;  // bit 45-63: 引用计数-1
    };
};

二、结构关系图

objc_class(一个类在内存里的样子)
┌─────────────────────────────────────┐
│  isa (继承自 objc_object)           │ ← isa_t union,64位
├─────────────────────────────────────┤
│  superclass                         │ ← 指向父类的 objc_class
├─────────────────────────────────────┤
│  cache                              │ ← cache_t 结构体
│    └── bucket_t[] 数组              │     每个桶存 { SEL, IMP }
├─────────────────────────────────────┤
│  bits                               │ ← class_data_bits_t(指针+标志位)
│    └── data() ───────────────────────────→ class_rw_t(运行时可写)
│                                     │        ├── methods()
│                                     │        ├── properties()
│                                     │        ├── protocols()
│                                     │        └── ro() ────────→ class_ro_t(只读)
│                                     │                             ├── name
│                                     │                             ├── baseMethods
│                                     │                             │     └── method_t[]
│                                     │                             ├── ivars
│                                     │                             │     └── ivar_t[]
│                                     │                             └── baseProperties
└─────────────────────────────────────┘

三、逐结构体解析

接下来按源码出现的顺序,逐个讲解每个结构体、每个字段的含义。


3.1 objc_object —— 所有对象的祖宗

struct objc_object {
private:
    isa_t isa;
};

这是什么?

这是 OC 里所有对象的底层表示。不管是 NSStringUIView、还是你自定义的 MyClass 实例,底层都是 objc_object

字段解析

字段 类型 含义
isa isa_t "is a" 的缩写,标识"这个对象是什么类型"。是一个 64 位的 union,里面藏了类指针 + 引用计数 + 各种标志位。isa_t 的详细结构会在第二篇展开讲解。

为什么只有一个字段?

因为 objc_object最小公共祖先。每个对象只需要知道"我是什么类型"(isa),其他的成员变量由具体的类定义,紧跟在 isa 后面存储。

内存布局示意

一个 MyClass 实例的内存:
┌────────────────┐ ← 对象起始地址
│     isa        │   8 字节(objc_object 的字段)
├────────────────┤
│    _name       │   8 字节(MyClass 自己的 ivar)
├────────────────┤
│    _age        │   4 字节(MyClass 自己的 ivar)
└────────────────┘

3.2 objc_class —— 类的完整定义

struct objc_class : objc_object {
    Class superclass;
    cache_t cache;
    class_data_bits_t bits;

    class_rw_t *data() const {
        return bits.data();
    }
};

这是什么?

这是 OC 里的底层表示。每个 @interface MyClass 在运行时都对应一个 objc_class 结构体实例。

注意它继承自 objc_object,所以"类也是对象"——类对象有自己的 isa(指向元类)。

字段逐个解析

字段 类型 含义
isa isa_t(继承来的) 类对象的 isa 指向它的元类(metaclass)。isa_t 的详细结构见第二篇。
superclass Class 父类指针。Classobjc_class * 的 typedef,即指向另一个 objc_class 的指针。NSObject 的 superclass 是 nil。;
cache cache_t 方法缓存,哈希表结构。最近调用的方法会缓存在这里,加速后续调用。
bits class_data_bits_t 一个 64 位整数,低 3 位是标志位,高位是 class_rw_t 指针

Class 是什么类型?

// objc.h
typedef struct objc_class *Class;

Class 就是 objc_class * 的别名,一个指向类对象的指针。你代码里写的所有 Class 都只是这个指针,没有额外结构:

Class cls = [MyClass class];       // 拿到 MyClass 的 objc_class * 指针
Class superCls = [cls superclass]; // 拿到父类的 objc_class * 指针

同理,id 也是:

typedef struct objc_object *id;    // id = objc_object *,指向任意实例对象

superclass 有什么用?

实现继承。当在当前类找不到方法时,runtime 会沿着 superclass 链往上找。

调用 [myObj doSomething]
    ↓
在 MyClass 的方法列表里找
    ↓ 找不到
通过 superclass 到 NSObject 里找
    ↓ 还找不到
触发消息转发

3.3 class_data_bits_t —— 指针 + 标志位的混合体

struct class_data_bits_t {
private:
    uintptr_t bits;   // 64 位整数

public:
    class_rw_t *data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
};

这是什么?

就是 objc_class.bits 的类型。它不是简单的指针,而是把 class_rw_t 指针几个标志位 打包进同一个 64 位整数里。

为什么能这样做?

因为 class_rw_t 在内存里是 8 字节对齐的,所以它的地址的低 3 位永远是 000。苹果就把这 3 位拿来存标志位,不浪费。

64 位的布局

class_data_bits_t.bits(64位)

 63                                3  2  1  0
┌────────────────────────────────┬──┬──┬──┐
│     class_rw_t 指针 (bit 3~63) │ 210│
└────────────────────────────────┴──┴──┴──┘
                                   │  │  │
                                   │  │  └─ FAST_IS_SWIFT_LEGACY (是旧版Swift类?)
                                   │  └──── FAST_IS_SWIFT_STABLE (是新版Swift类?)
                                   └─────── FAST_HAS_DEFAULT_RR  (有默认retain/release?)

取指针的掩码

// ARM64
#define FAST_DATA_MASK 0x00007ffffffffff8UL

// 二进制:...11111111111111111111111111111111111111000
// 作用:与运算后,低 3 位清零,剩下的就是真正的 class_rw_t 地址

data() 方法做了什么?

class_rw_t *data() const {
    return (class_rw_t *)(bits & FAST_DATA_MASK);
    // bits & 掩码 → 把低 3 位标志位清掉 → 得到纯净的 class_rw_t 指针
}

一句话总结

class_data_bits_tisa_t 的设计思路一样——充分利用内存对齐带来的空闲位,一个 64 位整数里塞多种信息,省内存


3.4 cache_t —— 方法缓存

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic<mask_t> _maybeMask;
            uint16_t _flags;
            uint16_t _occupied;
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
};

struct bucket_t {
private:
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
};

为什么需要缓存?

每次调用方法都去 class_rw_t 的方法列表里遍历查找,太慢了。cache_t 是一个哈希表,把最近调用过的方法缓存起来。

字段解析

字段 含义
_bucketsAndMaybeMask 哈希桶数组的起始地址
_maybeMask 桶数量 - 1,用于哈希取模(hash & mask
_occupied 当前已使用的桶数量

bucket_t 是什么?

单个缓存条目,存储 SEL(方法名)和 IMP(函数指针)的映射。

字段 类型 含义
_sel SEL 方法选择子(方法名),如 @selector(viewDidLoad)
_imp uintptr_t 方法实现的函数地址

查找流程

[obj doSomething]
    ↓
计算 @selector(doSomething) 的哈希值
    ↓
hash & _maybeMask → 得到桶的索引
    ↓
取出 bucket_t,比较 _sel 是否等于 @selector(doSomething)
    ↓
相等 → 直接调用 _imp,结束(命中缓存,极快)
不相等 → 去 class_rw_t 里慢速查找

缓存什么时候会失效?

  • 调用 method_exchangeImplementations(Method Swizzle)后
  • 动态添加方法后
  • 类第一次加载时

失效时 runtime 会调用 flushCaches() 清空缓存。


3.5 class_rw_t —— 运行时可读写数据

struct class_rw_t {
    uint32_t flags;
    uint16_t witness;
    uint16_t index;

    explicit_atomic<uintptr_t> ro_or_rw_ext;

    Class firstSubclass;
    Class nextSiblingClass;

    const method_array_t methods() const;
    const property_array_t properties() const;
    const protocol_array_t protocols() const;
    const class_ro_t *ro() const;
};

这是什么?

rw = read-write(可读写)。这里存放运行时可以修改的数据,比如 Category 添加的方法会合并到这里。

字段解析

字段 含义
flags 各种标志位(是否已初始化、是否有 C++ 构造函数等)
ro_or_rw_ext 指向 class_ro_t(只读数据),或扩展数据
firstSubclass 指向第一个子类,形成子类链表
nextSiblingClass 指向下一个兄弟类(同一个父类的其他子类)

获取方法/属性/协议

const method_array_t methods() const;     // 返回方法列表(含 Category 方法)
const property_array_t properties() const; // 返回属性列表
const protocol_array_t protocols() const;  // 返回协议列表

这些方法返回的是合并后的列表——源码里写的 + Category 加进来的。

ro() 方法

返回 class_ro_t 指针,取出编译期确定的只读数据。


3.6 class_ro_t —— 编译期只读数据

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;

    const uint8_t * ivarLayout;
    const char * name;
    
    WrappedPtr<method_list_t, ...> baseMethods;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
};

这是什么?

ro = read-only(只读)。这里存放编译时就确定的数据,运行时不能修改。

字段逐个解析

字段 类型 含义
flags uint32_t 标志位
instanceStart uint32_t 实例变量在对象内存中的起始偏移(通常是 8,跳过 isa)
instanceSize uint32_t 一个实例对象占多少字节(sizeof
ivarLayout const uint8_t * 描述哪些 ivar 是强引用(ARC 用)
name const char * 类名字符串,如 "UIViewController"
baseMethods method_list_t * 源码里定义的方法列表(不含 Category)
baseProtocols protocol_list_t * 源码里遵循的协议列表
ivars ivar_list_t * 实例变量列表
weakIvarLayout const uint8_t * 描述哪些 ivar 是弱引用
baseProperties property_list_t * 源码里定义的属性列表

class_ro_t vs class_rw_t 对比

class_ro_t class_rw_t
全称 read-only read-write
什么时候确定 编译期(写进 Mach-O 二进制文件) 运行时(启动时构造)
能修改吗 ❌ 不能 ✅ 能
存什么 源码里写死的方法、属性、变量 动态添加的方法、Category 合并的方法

为什么要分两层?

因为 Category 是运行时加载的。编译期不知道会有哪些 Category,所以:

  1. 编译期:把源码里写的方法存进 class_ro_t
  2. 运行时:遍历所有 Category,把它们的方法合并class_rw_t

查找方法时,先查 class_rw_t(含 Category),它内部会访问 class_ro_t


3.7 method_t —— 单个方法

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};

字段解析

字段 类型 含义 例子
name SEL 方法选择子(方法名) @selector(viewDidLoad)
types const char * 类型编码(返回值+参数的类型) "v16@0:8"
imp IMP 函数指针,指向方法的真正实现 0x100001234(代码段地址)

SEL 是什么?

typedef struct objc_selector *SEL;

本质是一个唯一化的 C 字符串。同名方法在整个程序里 SEL 值相同(指针相等),所以比较方法名只需要比较指针,极快。

SEL sel1 = @selector(doSomething);
SEL sel2 = @selector(doSomething);
// sel1 == sel2(指针相等,不是字符串比较)

IMP 是什么?

typedef void (*IMP)(id, SEL, ...);

函数指针,前两个参数固定是:

  • id self:消息接收者
  • SEL _cmd:方法选择子

这解释了为什么 OC 方法里能直接用 self_cmd——它们是函数的隐藏参数。

// 你写的:
- (void)doSomething {
    NSLog(@"%@", self);
}

// 编译器眼里的:
void doSomething(id self, SEL _cmd) {
    NSLog(@"%@", self);
}

types 字符串怎么读?

- (NSString *)nameWithPrefix:(NSString *)prefix 为例,types 是 @24@0:8@16

@    → 返回值是 id(对象)
24   → 所有参数总共占 24 字节
@    → 第1个参数是 id(self)
0    → 从第 0 字节开始
:    → 第2个参数是 SEL(_cmd)
8    → 从第 8 字节开始
@    → 第3个参数是 id(prefix)
16   → 从第 16 字节开始

这套编码叫 Type Encoding,runtime 靠它做方法签名校验。


3.8 ivar_t —— 单个实例变量

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    uint32_t alignment_raw;
    uint32_t size;
};

字段解析

字段 类型 含义 例子
offset int32_t * 偏移量的指针(不是值!) 指向存储偏移量的内存
name const char * 变量名 "_name"
type const char * 类型编码 "@"NSString""
alignment_raw uint32_t 内存对齐方式 通常是 3(2^3 = 8 字节对齐)
size uint32_t 占多少字节 指针占 8 字节

为什么 offset 是指针而不是值?

这是 Non-Fragile ABI(非脆弱 ABI)的设计。

假设父类 NSObject 有 8 字节的 isa,子类 MyClass_name 变量在 offset 8。

如果 Apple 在新系统里给 NSObject 加了一个成员变量(变成 16 字节),按老 ABI,MyClass_name 还在 offset 8,就会和 NSObject 新增的变量重叠——程序崩溃。

Non-Fragile ABI 的解决方案:

  1. offset 是指针,不是值
  2. App 启动时,runtime 检查父类大小是否变化
  3. 如果变化了,自动调整所有子类 ivar 的 offset 值
  4. 子类不需要重新编译
旧系统:NSObject 8字节,MyClass._name 在 offset 8
    ↓ Apple 升级系统
新系统:NSObject 16字节
    ↓ runtime 自动修正
MyClass._name 的 offset 从 8 改成 16

访问 ivar 的过程

// 伪代码
id value = *(id *)((char *)obj + *ivar->offset);
// 1. 取出 offset 指针指向的偏移值
// 2. 对象地址 + 偏移值 = ivar 的内存地址
// 3. 解引用得到 ivar 的值

3.9 isa_t —— 64 位里藏了很多东西

union isa_t {
    uintptr_t bits;

    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33;
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t unused            : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    };
};

为什么不直接存指针?

在 64 位系统上,指针只需要约 40 位就能表示所有内存地址。剩下的位"浪费"了,苹果就把引用计数和各种标志位塞进去,省内存。

这叫 Non-pointer ISA(优化过的 isa)。

每一位的含义

位域 位数 含义
nonpointer 1 是否是优化过的 isa(现代设备都是 1)
has_assoc 1 对象是否有关联对象(objc_setAssociatedObject
has_cxx_dtor 1 是否有 C++ 析构函数或 ARC 的 .cxx_destruct
shiftcls 33 类指针(右移 3 位存储,取时左移还原)
magic 6 固定值 0x1a,调试用(值不对说明内存被踩了)
weakly_referenced 1 是否被 __weak 指针指向过
unused 1 未使用,预留
has_sidetable_rc 1 引用计数是否溢出到 SideTable
extra_rc 19 存储引用计数 - 1(最大 2^19 - 1 = 524287)

如何取出类指针?

Class cls = (Class)(isa.bits & ISA_MASK);
// ISA_MASK = 0x0000000ffffffff8ULL
// 掩码取出 bit 3~35,然后隐含左移还原

四、完整内存布局示意

把所有结构体串起来,一个类在内存里长这样:

objc_class 实例(代表 MyClass 这个类)
┌─────────────────────────────────────────────────────┐
│  isa (64位 isa_t)                                   │ → 指向 Meta-MyClass(元类)
├─────────────────────────────────────────────────────┤
│  superclass (8字节)                                 │ → 指向 NSObject
├─────────────────────────────────────────────────────┤
│  cache (cache_t)                                    │
│    _bucketsAndMaybeMask → [ bucket_t, bucket_t... ] │ 每个桶: { SEL, IMP }
│    _maybeMask = N-1                                 │
│    _occupied = 已用桶数                              │
├─────────────────────────────────────────────────────┤
│  bits (class_data_bits_t)                           │ ← 低3位是标志位,高位是指针
│    data() ──────────────────────────────────────────│──→ class_rw_t
│                                                     │      ├── methods()   → [method_t, ...]
│                                                     │      ├── properties()→ [property_t, ...]
│                                                     │      ├── protocols() → [protocol_t, ...]
│                                                     │      └── ro() ───────→ class_ro_t
│                                                     │              ├── name = "MyClass"
│                                                     │              ├── instanceSize = 24
│                                                     │              ├── baseMethods
│                                                     │              │     ├── method_t { SEL, types, IMP }
│                                                     │              │     └── method_t { ... }
│                                                     │              └── ivars
│                                                     │                    ├── ivar_t { offset*, "_name", "@", 3, 8 }
│                                                     │                    └── ivar_t { offset*, "_age",  "i", 2, 4 }
└─────────────────────────────────────────────────────┘

五、小结

结构体 可否运行时修改 存放什么
objc_class 不直接改 类的容器,持有 superclass/cache/bits
isa_t 部分可改(引用计数位) 类指针 + 引用计数 + 标志位,全塞在 64 位里
class_data_bits_t 不直接改 class_rw_t 指针 + 3 个标志位,又一个"指针+标志"混合体
cache_t 是(每次调用方法后更新) 最近调用的方法 SEL → IMP 映射
class_rw_t 运行时合并后的方法、属性、协议
class_ro_t 编译期确定的方法、变量、属性,写死在二进制里
method_t IMP 可以换(Swizzle) 一个方法的名字、类型编码、实现地址
ivar_t offset 可改(Non-Fragile ABI) 一个实例变量的名字、类型、偏移量

下一篇:isa 指针深度解析、元类体系、完整继承链图

50 岁的苹果和 51 岁的我 -- 肘子的 Swift 周报 #127

issue127.webp

50 岁的苹果和 51 岁的我

再有不到半个月,Apple 将迎来 50 岁生日。Tim Cook 也发表了一篇短文,致敬过去半个世纪的历程。不过,由于苹果一直以来始终引领潮流的形象,很多人并没有意识到它已经是 IT 产业中名副其实的元老。与它年龄相当的 IT 巨头,如今仍留在一线牌桌上的寥寥无几。

作为一个只比苹果大一岁的科技爱好者,从 Apple II 到如今的 iPhone、MacBook,苹果的产品几乎伴随我走过了大半人生。严格来说,我并不算真正的果粉——不会因为没能第一时间买到新品而遗憾,也几乎不再熬夜看发布会,更说不出新产品的具体参数。但回顾过去,在每一个人生节点上,我都会很自然地选择苹果的产品,并在近几年成为了苹果开发生态中的一员。

其实我也没有完全想明白,苹果对我持久的吸引力究竟来自哪里。是因为很早就开始使用它的产品?是它的创新、体验和气质?还是 Jobs 的人格魅力?说实话,如今的选择已经完全出于习惯和本能,就像老友间的默契,早已不需要什么特别的理由。

当然,苹果的成长之路并非一帆风顺,其间也有过低谷。但有一点必须承认:它在过去 50 年间的企业定位几乎没变——为个人和社会创造强大的工具。即便在最新一轮 AI 浪潮中,苹果看似失去了先机,但作为连接人与数字世界的“最后一厘米”的核心参与者,它仍然具备在 AI 时代留在牌桌中央的资本。毕竟,我们生活在物质世界中,需要实打实的硬件设备和个人化服务来享受技术进步的成果。

50 岁的苹果或许能给更多企业带来启示:与其模仿它“炫酷”、“创新”的外表,不如学习它的专注与坚持。成为与用户长久互相陪伴的伙伴,或许才是它成功的真正密码。

大概率再过十年,当苹果 60 岁、我 61 岁的时候,我仍然用着一台苹果电脑。

生日快乐,苹果!

本期内容 | 前一期内容 | 全部周报列表

原创

2026 年,为什么我仍在思考 Core Data

到 2026 年,Core Data 已经问世 21 年,尽管仍有不少开发者在使用它,但在今天的 Swift 项目里,它越来越像个“时代遗留”。并发得靠 perform 一层层套,模型声明堆满样板代码,字符串谓词随时等你踩坑。这篇文章不是要为 Core Data 辩护,也不是要说服新的开发者回到 Core Data。它更像是一篇问题整理:在 2026 年,为什么仍有人坚持使用 Core Data;而如果要继续使用它,我们今天真正需要解决的问题又是什么。

近期推荐

原生 AI 聊天应用 — 极速、隐私优先、100+ 专业功能

一个原生应用,100+ AI 模型,支持 Mac、iOS 和 Android。极速响应、键盘驱动、非 Electron。使用码 FATBOBMAN25 立享 25% OFF。


苹果工程师谈应用安全与内存保护 (Fortify Your App: Essential Strategies to Strengthen Security Q&A)

在苹果开发者中心举办的一场安全专题活动中,多位苹果工程师围绕应用安全与内存安全进行了近六小时的分享与问答,内容涵盖现代应用面临的安全挑战,以及 Apple 平台提供的一系列防护技术。Anton Gubarenko 将这场活动中的大量开发者问答整理成文,讨论了第三方库安全评估、UserDefaults 与 plist 数据存储的风险、Keychain 与文件保护策略、Swift unsafe API 的使用边界,以及如何在 Xcode 中启用 Enhanced Security 等能力。对于希望了解 Apple 平台安全机制与实践建议的开发者来说,这是一份信息密度很高的问答整理,其中包含不少来自苹果工程师的一手信息。


用 CLI 与 MCP 自动化配置 iOS 订阅 (Faster iOS Subscriptions with ASC CLI and RevenueCat MCP)

为应用添加订阅功能本身并不复杂,但在 App Store Connect 与 RevenueCat 两个后台之间来回配置,过程往往相当繁琐。Rudrank Riyam 介绍了一种更高效的做法:使用 ASC CLI 在终端中一次性创建订阅产品,再让 AI 代理通过 RevenueCat 的 MCP Server 自动完成 entitlements、offerings 与 paywall 的配置,从而将原本依赖控制台点击的流程迁移到 CLI + AI Agent 的自动化工作流中。


JetBrains 面向 Swift 开发者的调查 (JetBrains Swift Developers Survey)

JetBrains 最近发布了一份面向 Swift 开发者的调研问卷,邀请开发者分享当前使用的开发工具、工作流程以及在 Swift 生态中的痛点。尽管官方并未说明调研的具体用途,但社区中已经出现不少猜测:这项调查可能与 JetBrains 重新评估 Swift 开发工具支持有关。

在 JetBrains 于 2022 年宣布停止维护 AppCode 之后,Swift 开发者基本回到了以 Xcode 为核心的工具链。此次调研也引发了一些讨论——有人期待 JetBrains 重新探索 Swift tooling 的可能性,也有人认为这更可能与 Kotlin Multiplatform 或 Swift 构建工具链相关。如果你对 Swift 开发工具生态的未来方向感兴趣,不妨参与这份调查。


不依赖编译器识别 Swift Protocol 的方法 (How Well Can You Detect a Swift Protocol Without the Compiler?)

在 Swift 项目中,Protocol 几乎无处不在,但如果不依赖编译器或完整构建环境,仅通过源码文本判断一个文件是否定义或使用了协议,结果会有多可靠?Xiangyu Sun 在这篇文章中系统评估了多种检测策略,例如使用 SourceKit/LSP、SwiftSyntax AST、关键字正则匹配,以及通过 extension Foo: Barany / some 等语法信号进行启发式判断,并对这些方法的准确率与适用场景进行了比较。

文章最有意思的部分在于作者发现:简单的命名约定可以显著提升静态分析效果。如果团队统一使用 *Protocol 后缀命名协议类型(如 PaymentServiceProtocol),很多原本存在歧义的检测方法都会变得更加可靠。作者还进一步讨论了这种约定在 AI 辅助开发中的价值:通过在文件级别预分类协议文件,可以在向 LLM 提供上下文时显著减少 token 消耗,并提高分析效率,这是一个颇具启发性的视角。


迁移到 Swift Concurrency 前需要注意的细节 (What you should know before Migrating from GCD to Swift Concurrency)

从 GCD 迁移到 Swift Concurrency 并非简单的语法替换。在这篇文章中,Soumya Ranjan Mahunt 指出:Swift Concurrency 在任务调度、执行顺序以及并发语义上与 GCD 存在一些关键差异,例如 Task 的调度并不保证与 GCD 相同的 FIFO 执行顺序,而 actor 也并不是 DispatchQueue 的直接替代,其执行行为可能受到任务优先级和调度策略的影响。此外,文中还讨论了一些在实际迁移过程中容易被忽视的问题,例如 DispatchGroup 在 Swift Concurrency 中并没有完全等价的 API,以及在旧系统版本中使用 assumeIsolated 可能遇到的兼容性问题。


选择 AI Agent Skill 的九步框架 (A 9-Step Framework for Choosing the Right Agent Skill)

随着 AI Agent 在开发工作流中的应用越来越广泛,如何为 Agent 设计合适的“技能”(Skill / Tool)也逐渐成为一个新的工程问题。Antoine van der Lee 提出了一个用于判断何时应该为 Agent 创建技能的九步框架,帮助开发者在自动化能力、可维护性以及系统复杂度之间取得平衡。Antoine 指出,并非所有任务都适合直接交给 LLM,也并非所有能力都需要实现为 Agent 工具。文章从任务确定性、执行成本、可复用性以及安全性等角度出发,提供了一套相对系统的评估思路。

工具

DataStoreKit

这是一个很有意思的开源项目,由 Anferne Pineda 开发。它基于 SwiftData 的自定义 store 能力,在保留 SwiftData 上层开发体验的同时,重新实现了一套面向 SQLite 的底层存储后端,包括从 SwiftData 模型、谓词到 SQLite schema、SQL、快照与持久化历史的映射和执行。

DataStoreKit 提供了一些值得关注的特性,例如支持对数组、字典等集合类型数据进行谓词查询,底层以 JSON 形式映射到 SQLite;同时也提供了 SQL 直通能力,让开发者在 #Predicate 之外,能够直接利用 SQLite 的能力完成查询或维护操作。

这是目前为数不多、且实现深度较高的 SwiftData DataStore 自定义实践,展示了 SwiftData 作为数据表现层而非完整持久化引擎的另一种可能性。项目目前仍处于较早期阶段,API 和能力边界可能还会继续调整,但已经非常值得持续关注。


Playwright for Swift

Miguel Piedrafita 开发的 swift-playwright,将 Playwright 这套成熟的浏览器自动化能力带入了 Swift 生态。开发者可以直接使用 Swift 代码驱动 Chromium、Firefox 和 WebKit,完成页面导航、点击、输入、截图、执行 JavaScript 等常见操作,整体 API 风格也尽量贴近官方 Playwright。

从实现方式上看,它并不是重新实现一套浏览器自动化框架,而是在 Swift 侧封装了 Playwright 协议,底层依然通过 Node.js 的 Playwright driver 与浏览器通信。对于希望使用 Swift 构建测试工具、CLI,甚至 AI Agent 的开发者来说,这个项目提供了一个颇具吸引力的切入点。

活动

LET'S VISION 2026 -- Born to Create · Powered by AI

  • 👀 70+ 展商现场体验
  • 🤖 AI 创新产品 / AI Agent
  • 🥽 XR / 空间计算沉浸体验
  • 🎤 创作者与开发者分享

如果你是开发者、设计师、产品经理、创作者,还是对 AI 和未来科技感兴趣的探索者,都很值得来逛逛。

  • 📅 2026.3.28 – 3.29
  • 📍 上海 · 漕河泾会议中心

15% OFF 门票 👇

往期内容

💝 支持与反馈

如果本期周报对你有帮助,请:

  • 👍 点赞 - 让更多开发者看到
  • 💬 评论 - 分享你的看法或问题
  • 🔄 转发 - 帮助同行共同成长

🚀 拓展 Swift 视野

用好你的 jj - 重新思考 Agent 时代的版本控制

过去大半年我一直在高强度地用 AI agent 写代码,用着用着发现一个问题:“怎么组织 agent 吐出来的东西”这件事,比我原来想的重要太多了。 这话听着可能有点奇怪。大家关心的一般都是模型能力、prompt 怎么写、上下文够不够长……但真的和 agent 密集配合过一阵子之后,你会发现有个更底层的东西一直在拖后腿:版本控制。说得再具体一点,就是你拿什么样的心智模型来管理本地的代码变更。 我现在的结论是:Git 作为远端协作和代码托管的标准还是没什么好说的,但在本地工作流这头,jj (Jujutsu) 明显更适合现在这种人和 agent 来回切着干活的开发方式。这篇文章就是来安利这个的。 Git 在 Agent 时代的摩擦 Git 是个伟大的工具,这一点没啥好争的。但它的很多设计假设,是建立在二十年前”人类手工编程”的时代背景上——一个人坐在编辑器前面,想清楚要改什么,改完检查一遍,然后 add、commit、push。这套流程是给人类的线性思维量身做的:staging area 给你一个”最后再看一眼”的机会,branch 帮你隔离不同的工作流,stash 让你...

50 岁的苹果和 51 岁的我 - 肘子的 Swift 周报 #127

再有不到半个月,Apple 将迎来 50 岁生日。Tim Cook 也发表了一篇短文,致敬过去半个世纪的历程。不过,由于苹果一直以来始终引领潮流的形象,很多人并没有意识到它已经是 IT 产业中名副其实的元老。与它年龄相当的 IT 巨头,如今仍留在一线牌桌上的寥寥无几。

iOS 学习笔记 - 创建第一个APP

创建项目

现在已经完成了对 Xcode 的安装,所以开发环境已经安装完成了,接下来就需要开始真正的 iOS 开发了。对于 iOS 开发,这里使用了目前苹果比较推荐初学者入门的方式,使用 Swift 语言进行编程,并且使用苹果推荐的 SwiftUI

1. 新建项目: 首先点击 Xcode 图标打开 Xcode,这时候会出现 Xcode 的初始界面,在这里点击 Create New Project... 创建一个新的项目。 截屏2026-03-15 23.15.00.png

2. 创建一个 iOS 项目: 之后需要选择开发什么类型的项目,需要开发的是 iOS 平台的 App,所以在对话框中,选择 Applocation 中的 App 选项,再点击 Next 按钮。 截屏2026-03-15 23.33.34.png

3. 选择项目的详细信息: 点击 Next 之后,就能看到配置具体的项目信息的选项,填写项目的基础信息。

  • Product Name: TestDemo
  • Organization Identifier: com.meohao 这个对则
  • Interface: 选择默认的 Swift UI
  • Language: 开发语言选择 Swift

然后点击 Next,之后选择保存的目录,就能创建出一个最基础的 iOS App 的项目了。 截屏2026-03-15 23.39.16.png

项目分析

完成了最基础的项目创建,可以得到一个简单的 iOS 的项目代码和目录结构,并且右侧会展示我们当前页面的预览。这是一个基于 Swift UIiOS App 项目。

image.png

下面可以分析一下创建出来的这个新项目项目。

目录结构

Xcode 中,左侧显示了项目目录结构,可以看到,我们的项目中包含三部分:

  1. Assets: 这里放了 iOS App 的相关资源文件,有图标,色彩定义等。后续使用到了会详细介绍。
  2. CotentView: 页面文件,整个 iOS 项目都由一个个 View 组成,每个 View 也能作为组件放到其他 View 中,这就很想前端现在的组件化。
  3. TestDemoApp: 这是整个项目的入口文件,这个文件里的代码定义了入口。

代码文件

接下来进代码可以分析一下这两个文件都做了些什么:

import SwiftUI // 导入 Swift UI 包

@main // 这里表示这里是main,作为入库,相当于我们其他编程中的 main 函数
struct TestDemoApp: App { // 定义了一个结构体
    var body: some Scene {
        WindowGroup {
            ContentView() // 把 ContentView 放进来执行
        }
    }
}

这个入口文件,定义使用 @main 定义了入口,并且调用了 ContentView。那接下来看看 ContentView 的代码。

import SwiftUI

struct ContentView: View { // 定义结构体
    var body: some View {
        VStack { // 使用布局方式 竖排布局
            Image(systemName: "globe") // 展示图片
                .imageScale(.large) // 设置大小
                .foregroundStyle(.tint) // 设置样式
            Text("Hello, world!") // 展示文字
        }
        .padding() // 这个布局外加上边距
    }
}

#Preview { // 通过设个代码,可以再右侧看到当前的预览
    ContentView()
}

这两段代码中,可以认识到 Swift 结构和一些知识。

Swift 结构体

Swift 结构体是一种通用且灵活的构造体,我们可以理解为一种组织代码的方式,如果有其他编程经验,相信对结构体并不陌生。

Swift结构体可以定义属性方法,这个和其他编程语言中的 class—— 很像。 Swift 中,也存在数据类型的定义,结构体有部分相同之处,也有一些不一样的地方,后续使用到的时候再详细介绍。

从这里可以看出,结构体可以定义一些变量,同时也能定义一些常量和方法。(这里不太懂的同学可以先不急着了解,下一节会详细介绍 Swift 语言编程的基础知识)例如,定义一个结构体:

struct TestStruct {
    var number: Int
}

这就定义了一个有属性结构体,同时,定义属性的时候,也可以对其初始化。

struct TestStruct {
    var number: Int = 1
}

定义好了结构体,就可以对结构体进行实例化,可以看到示例代码中,已经有对结构体进行实例化的例子,在 #Preview处。对于已经初始化变量的结构体,我们可以再初始化的时候,不带参数:

struct TestStruct {
    var number: Int = 1
}

TestStruct()

然而,在未初始化的结构体中,则必须在实例化的时候,带上参数,不然会发生报错:

struct TestStruct {
    var number: Int
}

TestStruct(number: 1)

结尾

这里,就完成创建出了一个最简单的 iOS App 项目。同时也对 Swift 的结构体有了一定的了解。

老司机 iOS 周报 #366 | 2026-03-16

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

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

新闻

对中国 App Store 的 iOS 及 iPadOS 的调整

26 年 3 月 15 日(消费者权益保护日)起,大陆 iAP 的抽成从 30% -> 25%,小开发者与小程序的抽成从 15% -> 12% 。抽成降低是大势所趋,毕竟其他地区还允许侧载等。同时 Google Play 商店的抽成也将从 30% -> 20% 。

Let's Vision 26 大会

今年的 Let's Vision 大会将在 3 月 28~29 日在上海漕河泾举办,会有大量嘉宾莅临,对详情感兴趣可以查看推送次条,通过链接购买有优惠券提供。我们也将在评论区抽出 5 张展览票(22 日开奖)。

文章

一文读懂 Agent Tools,拒绝复杂化、碎片化、黑盒化

@EyreFree:文章先从类型安全、LLM 友好接口、自我修复、人工介入、性能优化五大维度,讲解高可用 Agent Tools 的设计要点与方法;针对企业落地中工具碎片化、复杂化、黑盒化痛点,演示了依赖火山引擎 AgentKit 解决这类问题的方式,其 Gateway 可高效将存量 API 转为 MCP 工具,搭配企业级 Skills 管理与零信任鉴权体系,实现工具的高效治理与安全调用;还结合零售、金融科技行业实战案例。正在开发这类业务的同学可以参考。

🐎 Designing Swift Errors for an SDK

@Smallfly:这篇文章把 SDK 错误设计讲得很实用,提出用 struct + Code enum 组合来同时保证「错误码稳定」与「错误信息可读」。文中覆盖 LocalizedErrorCustomNSError、模式匹配 catch 和上下文 userInfo 的完整实践,既方便客户端做分支处理,也方便监控系统长期追踪。对团队来说,这是一套很适合沉淀为工程规范的错误治理方案。

🐎 Reverse Masking in SwiftUI Using Blend Modes

@Kyle-Ye: 文章介绍了在 SwiftUI 中实现反向遮罩的技巧——从视图中"挖出"形状以露出底层内容。核心方案是利用 .destinationOut blend mode 配合 .compositingGroup() 修饰符,将遮罩形状覆盖的区域从目标视图中擦除。文章封装了一个简洁的 reversedMask View 扩展,无需借助 Core Graphics 即可实现诸如毛玻璃镂空、网格卡片符号裁切等富有层次感的 UI 效果。对 SwiftUI 自定义视觉效果感兴趣的开发者值得一读。

🐎 vPhone 虚拟 iOS 环境解析与风控防御实践

@Damien: 研究员 wh1te4ever 利用苹果私有 Virtualization.framework 和 PCC 固件中的隐藏组件,在 Apple Silicon Mac 上实现了拥有 root 权限的 iOS 26 虚拟机(vphone)。与传统越狱不同,vphone 没有 Cydia 等文件特征,能透明绕过传统风控检测,且支持批量克隆和快照回滚,对黑产构成规模化威胁。为应对此挑战,作者提出 CloudPhoneRiskKit 3.0 四层防御体系,通过硬件指纹识别、多路径一致性校验、IMU 传感器行为熵分析和服务端动态策略进行纵深防御。该体系的核心思想是不追求单一"不可绕过"的检测,而是让攻击者的 patch 行为本身成为最强的风险信号,构建"绕过成本大于收益"的防御机制。

🐎 Apple 的 ANE 被挖掘,AI 硬件公开,宣传的 38 TOPS 居然是个"数字游戏"?

@david-clangmaderix/ANE 通过逆向私有 API,绕过 CoreML 框架,直接在 ANE 上进行神经网络的训练和推理调用,发现 ANE 宣传的 38 TOPS 实为 INT8 反量化至 FP16 的“数字游戏”。

maderix 经测试发现 ANE 本质上是一个内置约 32MB SRAM 的“卷积加速器”。1x1 卷积能走“专用超车道”,而矩阵乘法只能走低效的“备用通道”,导致两者吞吐量相差 3 倍。此外,ANE 的 16 个核心是基于深度流水线设计的,因此一次性打包提交包含数十个操作的“计算图”远比提交单个操作效率高。

另外,ANE 拥有极为强悍的硬件级电源门控(Power Gating)技术,它在空闲状态下,并非进入低功耗待机模式,而是做到完全断电、零泄漏,功耗为绝对的 0mW,这也解释了其在移动端设备上极佳的能效表现。有趣的是,可以看到 XNU 内核源码(osfmk/mach/coalition.h)也定义了 ane_energy_nj 字段统计 ANE 能耗。

代码

iOS-Accessibility-Agent-Skill

@含笑饮砒霜:这个仓库主要是把 iOS 无障碍开发经验,整理成一个可供 AI 调用的“专家型 Skill ”,帮助 AI 更专业地给出 UIKit / SwiftUI 的无障碍实现建议、测试建议和设计判断。 它最大的价值不是“自动修复 accessibility ”,而是让 AI 回答这类问题时更像一个有经验的 iOS Accessibility reviewer。

SwiftUI-Agent-Skill

@zhangferry:这个仓库是由 Swift 领域知名开发者 Paul Hudson(Hacking with Swift 博主)开源的 SwiftUI Pro Agent Skill 插件,主要功能是为 Agent 提供专业的 SwiftUI 开发能力。它主要包括这些能力:无障碍功能适配;使用规范 API 并能替换 deprecated API;基于 HIG 设计规范;导航功能设计规范;高性能的 SwiftUI 代码;数据流设计和管理的规范;视图结构和动画实现技巧。

如果使用 Claude 或 Codex 命令行工具仅执行安装指令就够了,如果是 Xcode(26.3 或以上) 还稍微有些特别。当前 Xcode 内的 Agent 仅 Codex 可以识别全局 Skill,Claude 的话还需要在 ~/Library/Developer/Xcode/CodingAssistant/ClaudeAgentConfig 这个目录下新建一个 skills 文件夹,手动把对应的 Skill 内容复制过去才能生效。

HealthQL

@阿权:HealthQL 是一款开源 Swift 库,简化 Apple HealthKit 数据查询:支持 SQL 语法和 Swift DSL(类型安全、链式调用)两种查询方式,覆盖步数、心率等常见健康数据,支持聚合、分组等操作,可通过 Swift Package Manager 安装,避免原生 HealthKit 的繁琐样板代码。

FabBar

@Barney:这是一个面向 iOS 26 Liquid Glass 风格的自定义 Tab Bar 组件,重点不只是“做得像”,而是补上系统 Tab Bar 无法优雅承载悬浮主操作按钮的问题。作者指出,把 FAB 硬塞成一个 .search tab 虽然实现简单,但会带来语义错误、VoiceOver 识别不准,以及状态切换时的交互竞态。这个库对外提供 SwiftUI 接口,内部借助 UISegmentedControl 复用原生玻璃态反馈,再补齐悬浮按钮、安全区适配、重复点击回调和按状态隐藏等能力。代价是实现依赖 UIKit 私有层级细节,未来系统更新后可能失效,部分原生无障碍行为也难做到完全一致。适合想跟进 iOS 26 视觉风格,同时又对底部主操作入口有更强控制需求的项目参考。

内推

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

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

关注我们

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

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

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

说明

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

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

大型 iOS 工程单元测试 — 变更驱动测试与跨模块 Mock

一、定位

本文是 为大型 iOS 工程补充单元测试方法论补充篇。前文提供了"画链路 → 选节点 → 写测试 → 融入迭代"的完整框架,覆盖了基础的 Mock 策略和用例设计原则。

本文聚焦以下问题:

  • 当测试目标不是"从零覆盖"而是"验证一次代码变更"时,应如何设计用例?
  • 当被测方法依赖的实验开关位于另一个模块、通过 Service Locator 解析时,如何 Mock?
  • 当同一份数据有两个来源、且实验开关决定是否去重时,如何构造测试场景?
  • 当方法的聚合语义不是"求和"而是"计数"时,如何防止未来开发者误改?

二、变更驱动测试设计(Change-Driven Test Design)

2.1 原则

传统的"从零覆盖"思路是:遍历方法的所有分支,为每个分支写用例。而在实际业务迭代中,更常见的场景是你刚修改了一段逻辑,需要快速验证变更的正确性

变更驱动测试的核心思路:

识别本次变更引入的新分支或行为差异
为新分支写正向用例(验证新行为)
为旧分支写回归用例(验证未被破坏)
如果变更引入了新的数据源排除/包含逻辑,为排除和包含各写至少一条用例

2.2 双态特性开关测试

当一次变更由特性开关(Feature Flag)控制时,同一个方法在 flag=true 和 flag=false 下有不同行为。此时必须成对测试

用例类型 目的 示例
flag=true 正向 验证新行为生效 shouldCountUnreadByCell=true 时 notice groups 被跳过,unreadCount 仅含 interactor 贡献
flag=false 回归 验证旧行为未被破坏 shouldCountUnreadByCell=false 时 notice groups 仍参与累加
flag=true 复合 新行为 + 多种过滤条件叠加 flag=true + muted groups + shop groups + redPoint groups → 全部被跳过,仅剩 interactor

关键原则:回归用例的重要性不低于正向用例。开发者常犯的错误是只测了新路径而遗漏了旧路径的回归验证,有可能改坏了旧路径的功能而未及时发现。

2.3 案例

BizScenarioItemInboxTabNumber.checkHasNoticeTabbarUnreadCount: 在引入 shouldCountUnreadByCell 分支后,新增了 4 条用例:flag=true 跳过 notice groups、flag=true 时 cellCount 不受影响、flag=false 包含 notice groups(回归)、flag=true 复合场景。每条用例的 assertion message 都明确标注了 flag 状态和预期计算过程。


三、Service Center Protocol Mock(跨模块协议依赖的 Mock 策略)

3.1 问题场景

在 Service Locator 架构中,模块间的依赖通过协议(Protocol)解耦。被测代码通过 GET_CLASS(IMModuleService) 获取另一个模块提供的 Class,再调用其类方法。这带来了一个 Mock 难题:

  • IMModuleService协议,不是类,无法直接 swizzle
  • 协议的实现类位于另一个模块,测试工程可能没有链接该模块
  • 在测试环境中,Service Center 默认为空,GET_CLASS 返回 nil

3.2 解法:注册 Mock Class 到测试 Service Center

SwiftTestCaseserviceBehavior = .newCenter 会为每个测试创建隔离的 Service Center。利用基类提供的 mockGetStatelessProtocolService(_:andReturn:) 方法,将一个轻量 Mock 类注册到该 Center 中:

// 定义只实现所需类方法的 Mock 类
private class MockIMModuleServiceTrue: NSObject {
    @objc class func shouldCountUnreadByCell() -> Bool { return true }
}

// 在测试中注册
if let proto = NSProtocolFromString("IMModuleService") {
    mockGetStatelessProtocolService(proto, andReturn: MockIMModuleServiceTrue.self)
}

工作原理

被测代码: [GET_CLASS(IMModuleService) shouldCountUnreadByCell]
         ↓
GET_CLASS 查询 Service Center → 返回 MockIMModuleServiceTrue.class
         ↓
[MockIMModuleServiceTrue shouldCountUnreadByCell] → YES

3.3 与 Runtime Swizzle 的对比

维度 Runtime Swizzle Service Center Mock
适用场景 目标类已知且已链接 目标是协议,实现类不可见或位于其他模块
隔离性 全局替换,需手动恢复 仅在测试的隔离 Service Center 内生效,自动还原
tearDown 负担 必须手动调用 restore() 无需手动清理
限制 需要已知类名和方法签名 仅适用于通过 GET_CLASS / Service Locator 解析的依赖

3.4 适用准则

当被测方法通过以下宏/方式获取依赖时,优先使用 Service Center Mock:

  • GET_CLASS(Protocol) / GET_PROTOCOL(Protocol)
  • ServiceCenter.defaultCenter.getStatelessProtocolService()
  • 任何通过 Service Locator 模式解析的跨模块协议依赖

四、Fake Environment 模式(Context Protocol Mock)

4.1 问题场景

某些组件通过一个宽接口的 "Context" 协议获取运行时环境(数据字典、配置管理器、事件分发器等)。直接构造真实 Context 需要初始化整个管理器链路,测试成本极高。

4.2 解法:实现仅含测试所需数据的 Fake Context

private class MockUnreadCountContext: NSObject, UnreadCountContext {
    var mockEntranceCountModelDict: [String: InboxEntranceUnreadCountModel] = [:]
    var checkNeedUpdateCalled = false
    var lastCheckScene: BizScenarioItemCheckScene = []

    func entranceCountModelDict() -> [String: InboxEntranceUnreadCountModel]? {
        return mockEntranceCountModelDict
    }
    func checkNeedUpdate(_ scene: BizScenarioItemCheckScene) {
        checkNeedUpdateCalled = true
        lastCheckScene = scene
    }
    // 其余方法空实现
}

4.3 设计要点

要点 说明
只实现被测路径依赖的方法 非必需方法留空实现,降低维护成本
var 暴露可控数据 测试通过直接修改 mockEntranceCountModelDict 来控制输入
Spy 能力 添加 checkNeedUpdateCalled / lastCheckScene 等标记,验证被测方法是否正确触发了 Context 上的副作用
弱引用安全 Context 属性通常为 weak,确保 Mock 对象在测试期间被持有(存为实例属性)

4.4 与协议 Mock 对象的区别

前文的"协议 Mock 对象"聚焦于数据提供者(如 NoticeUnreadCountItemProtocol),每个 Mock 只需返回数值。Fake Environment 聚焦于运行时环境,需要同时提供数据字典、触发副作用(如 checkNeedUpdate:)、并可能被多个被测方法共享。


五、聚合语义测试(Aggregation Semantics Testing)

5.1 问题

聚合方法有两种常见语义,外部签名几乎相同,但行为差异大:

语义 含义 示例方法
Sum 将所有符合条件的项的值相加 countForNumber: → 返回 5 + 3 = 8
Count 统计符合条件且值 > 0 的项的个数 countForUnreadCell: → 返回 2(有 2 项非零)

如果未来开发者误将 Count 语义改为 Sum 语义(或反过来),逻辑上仍然"能跑通",但业务含义错误。

5.2 方法:用数据设计锁定语义

构造让 Sum 和 Count 结果必然不同的测试数据,使得任何语义变更都会导致断言失败:

// count 为 5 和 3 → Sum = 8, Count = 2
// 如果断言 Count == 2,则改为 Sum 后结果变为 8,测试失败
func test_countForUnreadCell_countsEntrancesNotSumsCount() {
    mockContext.mockEntranceCountModelDict = [
        combinedKey(1): makeEntranceModel(entranceID: 1, count: 5),
        combinedKey(2): makeEntranceModel(entranceID: 2, count: 3),
    ]
    XCTAssertEqual(item.count(forUnreadCell: nil), 2,
                   "Cell count = number of entrances with unread, not sum of counts")
}

关键:选择每项 count > 1 的数据。如果所有 count 都为 1,则 Sum 和 Count 结果相同,无法区分语义。

5.3 推广

此方法适用于所有存在语义歧义的聚合操作:

  • Max vs Sum:确保数据中有多项,且各项值不同
  • Any vs All:确保数据中有 true 和 false 的混合
  • Distinct count vs Total count:确保数据中有重复项

六、数据源去重测试(Deduplication Testing)

6.1 问题

当同一份业务数据通过两个独立渠道到达聚合点时(如 notice_countentrance_count 都包含通知未读数),需要在特定条件下去重,否则会出现重复计算。

6.2 测试策略

构造"两个渠道都有数据"的场景,验证在去重开关开启时只有一路生效:

dataSource (notice groups) = { group100: 10, group200: 3 }
interactor (entrance items) = countForNumber: 5

shouldCountUnreadByCell=true  → result = 5  (只用 interactor,跳过 dataSource)
shouldCountUnreadByCell=false → result = 18 (dataSource 13 + interactor 5)

设计要点

  • 两路数据都给非零值,使得去重与不去重的结果有明显差异
  • 对去重路径和非去重路径各写至少一条用例
  • Assertion message 中明确标注"哪一路被跳过"以及"预期计算过程"

七、变更传播测试(Mutation-Aggregation Vertical Slice)

7.1 问题

底层数据的变更(标记已读、静音)需要正确传播到上层的聚合结果中。仅测试"model.count 被置 0"是不够的,因为聚合层可能因为缓存、过滤条件等原因未感知到变更。

7.2 方法:跨层断言

在一个测试用例中同时操作底层和观察上层,形成"垂直切片":

func test_updateMuteStatus_muteExcludesFromCount() {
    // 底层:构造 model
    let model = makeEntranceModel(entranceID: 1, count: 5)
    setUpEntranceModels([combinedKey(1): model])

    // 上层:构造 aggregation item,共享同一个 model
    let countItem = InboxEntranceUnreadCountItem()
    let ctx = MockUnreadCountContext()
    ctx.mockEntranceCountModelDict = [combinedKey(1): model]
    countItem.context = ctx

    XCTAssertEqual(countItem.count(forNumber: nil), 5, "Before mute")

    // 执行变更
    service.updateMuteStatus(true, forEntranceID: 1, subEntranceKey: nil)

    // 验证传播:上层聚合结果反映了底层变更
    XCTAssertEqual(countItem.count(forNumber: nil), 0, "After mute, excluded from count")
}

7.3 适用场景

  • 标记已读 → 未读数归零
  • 静音 → 从聚合计算中排除
  • 归档 → 从聚合计算中排除
  • 任何"底层状态变更应影响上层输出"的链路

7.4 与纯单元测试的关系

垂直切片测试严格来说介于单元测试和集成测试之间。在大型工程中,它的性价比很高:不需要启动完整的 Service 链路,但能验证两层之间的契约是否正确。推荐在以下情况使用:

  • 两层通过共享可变对象(同一个 model 实例)交互
  • 上层的聚合逻辑包含过滤条件(muted、archived 等),变更后的状态可能被过滤

八、短路行为测试(Short-Circuit Testing)

8.1 问题

某些遍历方法在找到第一个匹配项后会 stop*stop = YES),不再继续遍历。如果去掉 stop,方法签名和大部分行为不变,但在存在多个同类型 item 时会错误地累加。

8.2 方法:构造多个同类型 item,验证只取第一个

func test_countForNumberWithType_stopsAfterFirstMatch() {
    addMockItem(showType: .number, countForNumber: 10, itemType: .entranceCountItem)
    addMockItem(showType: .number, countForNumber: 5, itemType: .entranceCountItem)
    XCTAssertEqual(
        interactor.count(forNumber: nil, withUnreadCountItemType: .entranceCountItem), 10,
        "Should stop after first matching item"
    )
}

如果 stop 被移除,结果会变为 15,测试失败。

8.3 适用场景

  • 按类型过滤的方法(预期每种类型只有一个活跃实例)
  • 优先级查找方法(返回第一个满足条件的结果)
  • 任何使用 enumerateObjectsUsingBlock: + *stop = YES 的 ObjC 代码

九、总结:何时使用哪种模式

场景 推荐模式 本文章节
验证一次代码变更 变更驱动测试 + 双态 Flag 测试
被测方法依赖跨模块协议(通过 Service Locator) Service Center Protocol Mock
被测对象通过宽接口 Context 获取环境 Fake Environment 模式
聚合方法的 Sum/Count 语义容易被误改 聚合语义测试
同一数据有两个来源、需条件性去重 数据源去重测试
底层变更需传播到上层聚合结果 Mutation-Aggregation 垂直切片
遍历方法有 stop/短路行为 短路行为测试

这些模式与前文的基础方法论互补使用。基础方法论解决"测什么"和"怎么 Mock"的问题,本文解决"改了代码后怎么精准验证"和"复杂依赖场景下怎么构造可控环境"的问题。

运动的科学原理与健康价值 - 读《锻炼》

最近读完近期研读了哈佛大学进化生物学教授丹尼尔·利伯曼的著作《锻炼》,该书从进化生物学的视角,系统阐述了人类运动的本质及其对现代健康的重要性。本文将对书中核心观点进行梳理与总结。

锻炼是 “反人性” 的

利伯曼教授在书中开篇即指出,从进化角度看,锻炼在某种程度上是“反人性”的。人类基因在漫长的演化过程中,倾向于节约能量以应对生存挑战,如应对饥荒或繁殖需求,而非主动追求高强度体力活动。

然而,随着现代社会工具的普及,体力劳动显著减少,而人类的生理机制尚未完全适应这种快速变化的环境。因此,为了弥补体力活动不足带来的健康赤字,有意识的“锻炼”成为现代人维持健康的必要手段。值得注意的是,作者强调锻炼与娱乐性体育活动并非等同概念。

所以,我们需要接纳现在的自己,并意识到锻炼是反人性的。

静态下的身体

长期处于静态或低活动状态,可能引发慢性炎症反应,其机制主要包括:

    1. 脂肪细胞肥大: 当人体脂肪堆积过多时,脂肪细胞体积增大,可诱导白细胞聚集并释放炎症因子,进而引发慢性炎症。
    1. 久坐与代谢功能: 长时间久坐会降低身体从血液中吸收葡萄糖和脂肪的能力,这是导致全身性慢性轻度炎症的另一重要因素。
    1. 心理压力: 持续的心理压力导致皮质醇分泌增加。皮质醇不仅促使糖和脂肪进入血液循环,还可能增强对高糖高脂食物的渴望,从而促进内脏脂肪的储存。此外,过高的皮质醇水平还可能干扰睡眠周期,导致睡眠质量下降。
    1. 肌肉的抗炎作用: 肌肉不仅是运动器官,更兼具内分泌功能,能够合成并释放多种被称为“肌细胞因子”的蛋白质。这些因子具有多种生理作用,其中之一便是抑制炎症。适度的运动能够引发轻微的生理性炎症,进而刺激肌肉通过抗炎机制进行修复。

运动可以有效的抑制以上炎症反应。

人体内的能量反应

人体主要通过三磷酸腺苷(ATP)水解释放能量。ATP水解生成二磷酸腺苷(ADP)和磷酸,并释放能量和氢离子。ADP可通过“充电”过程,即利用糖分子和脂肪分子的化学反应,重新转化为ATP。

在运动过程中,能量供应遵循一定顺序:

  • ATP储备: 人体ATP储备量有限(不足100克),在运动初期迅速耗尽。
  • 磷酸原系统: 随后动用磷酸原系统,提供短暂的快速能量。
  • 糖酵解: 磷酸原耗尽后,启动糖酵解过程。此过程将一个糖分子分解为两个丙酮酸,并为两个ADP分子“充电”生成ATP。糖酵解无需氧气参与,在短时间高强度运动(如30秒冲刺)中贡献约一半的能量。然而,糖酵解会产生丙酮酸,进而分解为乳酸和氢离子。尽管乳酸本身无害,但氢离子累积会导致肌肉酸痛和疲劳,影响运动表现。
  • 有氧氧化: 在氧气充足条件下,一个糖分子通过有氧氧化产生的ATP是糖酵解的19倍。但有氧代谢过程复杂,涉及多步反应和大量酶。相比糖,脂肪燃烧产生能量所需时间更长。

在静息状态下,身体约70%的能量来源于脂肪的缓慢燃烧。然而,随着运动强度的增加,对糖的燃烧需求也随之增加。当运动强度超过有氧能力极限时,能量供应将完全依赖于糖的无氧分解。

肌肉的原理

肌肉由大量长而薄的细胞组成,称为肌纤维,每个肌纤维由数千个肌原纤维组成。再细分,肌原纤维包含数千个名为肌节的带状组织。肌节由两种重要蛋白质组成,一种细,一种粗,彼此交错,就像双手合十时手指那样。这种结构可以生成拉力,当神经向肌肉发出电信号时,就像两队拔河的人拉绳子一样,肌肉收缩的动作就发生了。

人体的肌肉纤维分为慢肌纤维和快肌纤维。

  • 慢肌纤维以有氧方式利用能量,不易产生疲劳,由于颜色暗淡,它又被称为红肌纤维。
  • 快肌纤维又分作白肌纤维和粉肌纤维。白肌纤维燃烧糖生成强烈而快速的力量,但是会很快疲劳。粉肌纤维以有氧的方式生成中等强度的力量,所以也不会很快产生疲劳。

人体很多肌肉的快肌纤维与慢肌纤维的比例大约都是 1:1。但是对于三头肌等用来发力的肌肉,快肌纤维比例就会达到 70%,而对于那些用来走路的肌肉,比如小腿的肌肉,慢肌纤维的比例就会到达 85%。

心脏健康与心血管疾病

多数心脏相关疾病源于心脏自身病变或血管问题。

动脉粥样硬化是动脉硬化的起始阶段,表现为动脉壁内斑块积聚。这些斑块由脂肪、胆固醇和钙等物质混合而成。为应对斑块对动脉壁的刺激和损伤,白细胞会启动炎症反应,将这些物质包裹并使其硬化,导致斑块逐渐增大。斑块若完全阻塞动脉或脱落后阻塞其他部位小动脉,均可导致严重后果。

高血压对心脏构成慢性损伤。长期高血压状态下,心脏为维持正常功能会增厚心肌壁,但增厚的心肌壁会逐渐硬化并被疤痕组织取代,最终导致心功能下降。

心肺训练被普遍认为是维护心血管系统的最佳运动方式。

胆固醇的生理意义

胆固醇检测通常测量血液中三种分子的水平:

  1. 低密度脂蛋白(LDL): 常被称为“坏胆固醇”。肝脏生成的气球状分子,负责在血液中运输脂肪和胆固醇。然而,某些LDL分子可能破坏并侵入动脉壁,尤其在高血压状态下,引发炎症反应并形成斑块。

  2. 高密度脂蛋白(HDL): 有时被称为“好胆固醇”。这些微小颗粒能清除LDL,并将其运回肝脏进行代谢。

  3. 甘油三酯: 自由漂浮在血液中的脂肪颗粒,是代谢综合征的重要标志物。

锻炼时长与强度建议

作者建议,成年人每周应至少进行5次,每次至少30分钟的中等强度至高强度有氧训练。

  • 中等强度训练: 心率维持在最大心率的50%~70%区间。
  • 高强度训练: 心率维持在最大心率的70%~85%区间。

最大心率的估算方法通常为220减去年龄。根据作者研究,达到上述锻炼时长可将全因死亡率降低一半。即使进一步延长锻炼时间,全因死亡率仍会下降,但下降幅度趋缓(如下图)。

此外,作者还建议每周进行两次肌肉力量增强训练,涵盖所有大肌肉群(包括腿、臀、背、核心、肩和臂),并确保每次训练后有足够的恢复时间。每个部位重复练习8~12次,进行2到3组。

小结

《锻炼》一书深刻阐明了运动对人体健康的科学益处,尤其强调了训练强度和时长的重要性。书中提出的每周150分钟有氧训练加两次力量训练的目标,为我们提供了长期健康管理的重要指引。期望读者能从中汲取知识,并将其融入日常生活中,以期实现更健康的生活方式。

Swift 从入门到精通-终篇

第14章:SwiftUI 进阶

14.1 动画

SwiftUI 动画非常简单,只需添加 .animation


struct AnimationView: View {

    @State private var isExpanded = false

    

    var body: some View {

        VStack {

            RoundedRectangle(cornerRadius: isExpanded ? 50 : 10)

                .fill(isExpanded ? Color.blue : Color.red)

                .frame(

                    width: isExpanded ? 300 : 100,

                    height: isExpanded ? 300 : 100

                )

                .animation(.spring(response: 0.3, dampingFraction: 0.5), value: isExpanded)

            

            Button("切换") {

                isExpanded.toggle()

            }

        }

    }

}

**动画类型: **

  • .default - 默认动画

  • .linear - 线性

  • .easeIn / .easeOut / .easeInOut - 缓动

  • .spring() - 弹性动画

  • .interactiveSpring() - 交互式弹性

14.2 手势


struct GestureView: View {

    @State private var offset: CGSize = .zero

    @State private var scale: CGFloat = 1.0

    @State private var rotation: Angle = .zero

    

    var body: some View {

        Image(systemName: "star.fill")

            .font(.system(size: 100))

            .foregroundColor(.yellow)

            .offset(offset)

            .scaleEffect(scale)

            .rotationEffect(rotation)

            .gesture(

                DragGesture()

                    .onChanged { gesture in

                        offset = gesture.translation

                    }

                    .onEnded { _ in

                        withAnimation {

                            offset = .zero

                        }

                    }

            )

            .gesture(

                MagnificationGesture()

                    .onChanged { scale = $0 }

            )

            .gesture(

                RotationGesture()

                    .onChanged { rotation = $0 }

            )

    }

}

14.3 数据持久化


import SwiftUI

  


struct TodoItem: Identifiable, Codable {

    let id: UUID

    var title: String

    var isCompleted: Bool

    var createdAt: Date

}

  


class TodoStore: ObservableObject {

    @Published var todos: [TodoItem] = []

    

    private let saveKey = "todos"

    

    init() {

        load()

    }

    

    func add(_ title: String) {

        let todo = TodoItem(

            id: UUID(),

            title: title,

            isCompleted: false,

            createdAt: Date()

        )

        todos.append(todo)

        save()

    }

    

    func toggle(_ todo: TodoItem) {

        if let index = todos.firstIndex(where: { $0.id == todo.id }) {

            todos[index].isCompleted.toggle()

            save()

        }

    }

    

    func delete(at offsets: IndexSet) {

        todos.remove(atOffsets: offsets)

        save()

    }

    

    // UserDefaults 存储

    private func save() {

        if let encoded = try? JSONEncoder().encode(todos) {

            UserDefaults.standard.set(encoded, forKey: saveKey)

        }

    }

    

    private func load() {

        if let data = UserDefaults.standard.data(forKey: saveKey),

           let decoded = try? JSONDecoder().decode([TodoItem].self, from: data) {

            todos = decoded

        }

    }

}


第15章:并发编程 - async/await

15.1 为什么需要并发?

想象一个场景:

  • 用户点击按钮

  • App 需要从网络加载图片

  • 如果用同步方式,界面会卡住,直到图片加载完成

  • 用户看到"假死",体验极差

**并发让 App 能同时做多件事: **

  • 主线程:响应用户操作、更新界面

  • 后台线程:网络请求、文件读写、复杂计算

15.2 旧的方式 - GCD


// 旧的写法(仍然有效,但不推荐新代码使用)

DispatchQueue.global().async {

    // 后台线程执行耗时操作

    let data = try! Data(contentsOf: url)

    

    DispatchQueue.main.async {

        // 主线程更新 UI

        imageView.image = UIImage(data: data)

    }

}

**问题: **

  • 嵌套层级深(回调地狱)

  • 错误处理麻烦

  • 代码难以阅读和维护

15.3 async/await - 新的方式


// 定义异步函数

func fetchImage(from url: URL) async throws -> UIImage {

    let (data, _) = try await URLSession.shared.data(from: url)

    guard let image = UIImage(data: data) else {

        throw ImageError.invalidData

    }

    return image

}

  


// 调用异步函数

func loadImage() async {

    do {

        let image = try await fetchImage(from: imageURL)

        imageView.image = image

    } catch {

        print("加载失败: \(error)")

    }

}

**代码看起来像同步的,实际上是异步执行的! **

15.4 在 SwiftUI 中使用


struct AsyncImageView: View {

    @State private var image: UIImage?

    @State private var isLoading = false

    

    let url: URL

    

    var body: some View {

        Group {

            if let image = image {

                Image(uiImage: image)

                    .resizable()

                    .aspectRatio(contentMode: .fit)

            } else if isLoading {

                ProgressView()

            } else {

                Image(systemName: "photo")

                    .font(.largeTitle)

                    .foregroundColor(.gray)

            }

        }

        .onAppear {

            loadImage()

        }

    }

    

    private func loadImage() {

        isLoading = true

        

        Task {

            do {

                let (data, _) = try await URLSession.shared.data(from: url)

                await MainActor.run {

                    self.image = UIImage(data: data)

                    self.isLoading = false

                }

            } catch {

                await MainActor.run {

                    self.isLoading = false

                }

            }

        }

    }

}

**Task: **创建异步任务的环境

**MainActor: **确保代码在主线程执行(用于 UI 更新)

15.5 结构化并发


// 顺序执行(慢)

func fetchSequentially() async throws -> [User] {

    let user1 = try await fetchUser(id: 1)

    let user2 = try await fetchUser(id: 2)

    let user3 = try await fetchUser(id: 3)

    return [user1, user2, user3]

}

  


// 并行执行(快!)

func fetchConcurrently() async throws -> [User] {

    async let user1 = fetchUser(id: 1)

    async let user2 = fetchUser(id: 2)

    async let user3 = fetchUser(id: 3)

    return try await [user1, user2, user3]

}

  


// 使用 TaskGroup 处理动态数量的任务

func fetchUsers(ids: [Int]) async throws -> [User] {

    try await withThrowingTaskGroup(of: User.self) { group in

        for id in ids {

            group.addTask {

                try await fetchUser(id: id)

            }

        }

        

        var users: [User] = []

        for try await user in group {

            users.append(user)

        }

        return users

    }

}

**async let: **启动并行任务

**withTaskGroup: **管理一组动态任务

15.6 异步序列


// 异步序列

let stream = AsyncStream<Int> { continuation in

    Task {

        for i in 1...5 {

            try? await Task.sleep(for: .seconds(1))

            continuation.yield(i)

        }

        continuation.finish()

    }

}

  


// 遍历

for await number in stream {

    print("收到: \(number)")

}


第16章:Actor 与数据竞争防护

16.1 数据竞争问题


class UnsafeCounter {

    var count = 0

    

    func increment() {

        count += 1  // 非线程安全!

    }

}

  


let counter = UnsafeCounter()

  


// 从多个线程同时增加

DispatchQueue.concurrentPerform(iterations: 1000) { _ in

    counter.increment()

}

  


// 结果可能小于 1000!

print(counter.count)

原因: count += 1 实际上分三步:

  1. 读取 count 的值

  2. 加 1

  3. 写回 count

如果两个线程同时执行,可能都读到 5,都变成 6,结果只增加了 1。

16.2 Actor - 隔离状态


actor SafeCounter {

    private var count = 0

    

    func increment() {

        count += 1  // 线程安全!

    }

    

    func getCount() -> Int {

        return count

    }

}

  


let counter = SafeCounter()

  


await withTaskGroup(of: Void.self) { group in

    for _ in 0..<1000 {

        group.addTask {

            await counter.increment()

        }

    }

}

  


let finalCount = await counter.getCount()

print(finalCount)  // 1000

**Actor 的特点: **

  • 同一时间只有一个线程能访问 actor 内部

  • 自动防止数据竞争

  • 访问 actor 的属性和方法需要 await

16.3 MainActor - 主线程 Actor


@MainActor

class ViewModel: ObservableObject {

    @Published var items: [Item] = []

    

    func load() async {

        items = await fetchItems()  // await 切到后台

        // 自动回到主线程

    }

}

  


// 或者只标记某个方法

class ViewModel2: ObservableObject {

    @Published var items: [Item] = []

    

    @MainActor

    func updateUI() {

        // 确保在主线程执行

        items.append(newItem)

    }

}

16.4 Sendable 协议


// 安全的值类型,可以跨 actor 传递

struct UserData: Sendable {

    let id: UUID

    let name: String

}

  


// 类也可以遵循 Sendable,但需要是 final 且属性都是 Sendable

final class SafeClass: Sendable {

    let value: Int

    init(value: Int) {

        self.value = value

    }

}

**Swift 6 会强制检查: **如果类型不是 Sendable,不能跨 actor 传递。


第一次做分享,期待更优秀的你~

Swift 从入门到精通-第四篇

第13章:SwiftUI 基础 - 声明式 UI

13.1 SwiftUI 是什么?

SwiftUI 是 Apple 在 2019 年推出的声明式 UI 框架,用来替代 UIKit。它的核心理念是:

**告诉 SwiftUI "你想要什么界面",而不是 "怎么创建界面"。 **

对比:

  • **UIKit(命令式) **:创建视图 → 添加到父视图 → 设置约束 → 手动更新

  • **SwiftUI(声明式) **:描述界面状态,SwiftUI 自动处理更新

13.2 你的第一个 SwiftUI 视图

在 Xcode 中创建 SwiftUI 项目,替换 ContentView.swift:


import SwiftUI

  


struct ContentView: View {

    var body: some View {

        Text("Hello, World!")

            .font(.largeTitle)

            .foregroundColor(.blue)

            .padding()

    }

}

  


#Preview {

    ContentView()

}

**关键概念: **

  • View 协议:所有 UI 元素都遵循这个协议

  • body:计算属性,返回视图层级

  • 修饰符(modifier):用 . 链式调用,修改视图样式

13.3 状态管理 - @State

SwiftUI 中,视图是函数:输入相同的状态,输出相同的界面。


struct CounterView: View {

    // @State 标记可变状态

    @State private var count = 0

    

    var body: some View {

        VStack(spacing: 20) {

            Text("Count: \(count)")

                .font(.largeTitle)

            

            Button("Increment") {

                count += 1  // 修改状态,自动刷新界面

            }

            .buttonStyle(.borderedProminent)

        }

    }

}

** @State 的特点: **

  • 用于视图内部状态

  • SwiftUI 自动管理存储

  • 值改变时自动重绘视图

13.4 布局系统 - Stacks


struct LayoutDemoView: View {

    var body: some View {

        VStack(spacing: 20) {      // 垂直堆栈

            Text("上方")

            

            HStack(spacing: 20) {  // 水平堆栈

                Text("左")

                    .frame(maxWidth: .infinity)

                    .background(Color.red.opacity(0.3))

                Text("中")

                    .frame(maxWidth: .infinity)

                    .background(Color.green.opacity(0.3))

                Text("右")

                    .frame(maxWidth: .infinity)

                    .background(Color.blue.opacity(0.3))

            }

            

            ZStack {                // 重叠堆栈

                Circle()

                    .fill(Color.yellow)

                    .frame(width: 100, height: 100)

                Text("Z")

                    .font(.largeTitle)

            }

            

            Text("下方")

        }

        .padding()

    }

}

**三种 Stack: **

  • VStack - 垂直排列

  • HStack - 水平排列

  • ZStack - 前后叠加

13.5 常用控件


struct ControlsView: View {

    @State private var text = ""

    @State private var isOn = false

    @State private var sliderValue = 50.0

    @State private var selectedDate = Date()

    @State private var selectedColor = Color.red

    

    var body: some View {

        Form// 表单,自动处理滚动和布局

            Section("输入") {

                TextField("请输入", text: $text)

                SecureField("密码", text: $text// 密码输入

                TextEditor(text: $text// 多行文本

            }

            

            Section("选择") {

                Toggle("开关", isOn: $isOn)

                

                Slider(value: $sliderValue, in: 0...100) {

                    Text("滑动条")

                }

                

                DatePicker("日期", selection: $selectedDate)

                

                ColorPicker("颜色", selection: $selectedColor)

            }

            

            Section("按钮") {

                Button("普通按钮") {}

                

                Button(action: {}) {

                    Label("带图标的按钮", systemImage: "star.fill")

                }

                

                Button("主要按钮") {}

                    .buttonStyle(.borderedProminent)

                

                Button("胶囊按钮") {}

                    .buttonStyle(.bordered)

                    .controlSize(.large)

                    .tint(.green)

            }

        }

    }

}

注意 **$** 符号: $texttext 的绑定(Binding),双向数据流。

13.6 列表与导航


// 数据模型

struct Restaurant: Identifiable {

    let id = UUID()

    let name: String

    let cuisine: String

    let rating: Double

}

  


struct RestaurantRow: View {

    let restaurant: Restaurant

    

    var body: some View {

        HStack {

            VStack(alignment: .leading) {

                Text(restaurant.name)

                    .font(.headline)

                Text(restaurant.cuisine)

                    .font(.subheadline)

                    .foregroundColor(.secondary)

            }

            

            Spacer()

            

            HStack {

                Image(systemName: "star.fill")

                    .foregroundColor(.yellow)

                Text(String(format: "%.1f", restaurant.rating))

            }

        }

    }

}

  


struct RestaurantListView: View {

    let restaurants = [

        Restaurant(name: "川味轩", cuisine: "川菜", rating: 4.5),

        Restaurant(name: "金鼎轩", cuisine: "粤菜", rating: 4.2),

        Restaurant(name: "日料屋", cuisine: "日料", rating: 4.8)

    ]

    

    var body: some View {

        NavigationView {

            List(restaurants) { restaurant in

                NavigationLink(destination: RestaurantDetailView(restaurant: restaurant)) {

                    RestaurantRow(restaurant: restaurant)

                }

            }

            .navigationTitle("餐厅列表")

        }

    }

}

  


struct RestaurantDetailView: View {

    let restaurant: Restaurant

    

    var body: some View {

        VStack(spacing: 20) {

            Image(systemName: "fork.knife.circle.fill")

                .resizable()

                .frame(width: 100, height: 100)

                .foregroundColor(.orange)

            

            Text(restaurant.name)

                .font(.largeTitle)

            

            Text(restaurant.cuisine)

                .font(.title2)

                .foregroundColor(.secondary)

            

            HStack {

                ForEach(0..<Int(restaurant.rating), id: \.self) { _ in

                    Image(systemName: "star.fill")

                        .foregroundColor(.yellow)

                }

            }

            

            Spacer()

        }

        .padding()

        .navigationTitle("详情")

    }

}

**关键概念: **

  • Identifiable 协议:让数据可以被列表唯一标识

  • List:自动处理行、分隔线、滑动删除

  • NavigationView + NavigationLink:页面跳转

13.7 数据绑定 - @Binding

当子视图需要修改父视图的状态时:


// 子视图

struct ToggleView: View {

    @Binding var isOn: Bool  // 绑定,不是自己的状态

    

    var body: some View {

        Toggle("开关", isOn: $isOn)

    }

}

  


// 父视图

struct ParentView: View {

    @State private var lightOn = false

    

    var body: some View {

        ToggleView(isOn: $lightOn// 传递绑定

    }

}

13.8 可观察对象 - @StateObject 和 @ObservedObject

对于复杂数据模型,使用 ObservableObject:


import Combine

  


class TaskStore: ObservableObject {

    @Published var tasks: [Task] = []

    @Published var filter: TaskFilter = .all

    

    var filteredTasks: [Task] {

        switch filter {

        case .all: return tasks

        case .active: return tasks.filter { !$0.isCompleted }

        case .completed: return tasks.filter { $0.isCompleted }

        }

    }

    

    func addTask(_ title: String) {

        tasks.append(Task(title: title))

    }

    

    func toggleTask(_ task: Task) {

        if let index = tasks.firstIndex(where: { $0.id == task.id }) {

            tasks[index].isCompleted.toggle()

        }

    }

}

  


struct TaskListView: View {

    @StateObject private var store = TaskStore()  // 创建可观察对象

    

    var body: some View {

        List {

            ForEach(store.filteredTasks) { task in

                TaskRow(task: task) {

                    store.toggleTask(task)

                }

            }

        }

    }

}

** @StateObject vs @ObservedObject: **

  • @StateObject:创建并持有对象(用这个视图创建的数据)

  • @ObservedObject:引用外部传入的对象

13.9 环境值 - @Environment


struct ContentView: View {

    @Environment(\.colorScheme) var colorScheme

    @Environment(\.dismiss) var dismiss

    

    var body: some View {

        VStack {

            Text(colorScheme == .dark ? "Dark Mode" : "Light Mode")

            

            Button("关闭") {

                dismiss()  // 关闭当前页面

            }

        }

    }

}


第14章:SwiftUI 进阶

14.1 动画

SwiftUI 动画非常简单,只需添加 .animation


struct AnimationView: View {

    @State private var isExpanded = false

    

    var body: some View {

        VStack {

            RoundedRectangle(cornerRadius: isExpanded ? 50 : 10)

                .fill(isExpanded ? Color.blue : Color.red)

                .frame(

                    width: isExpanded ? 300 : 100,

                    height: isExpanded ? 300 : 100

                )

                .animation(.spring(response: 0.3, dampingFraction: 0.5), value: isExpanded)

            

            Button("切换") {

                isExpanded.toggle()

            }

        }

    }

}

**动画类型: **

  • .default - 默认动画

  • .linear - 线性

  • .easeIn / .easeOut / .easeInOut - 缓动

  • .spring() - 弹性动画

  • .interactiveSpring() - 交互式弹性

14.2 手势


struct GestureView: View {

    @State private var offset: CGSize = .zero

    @State private var scale: CGFloat = 1.0

    @State private var rotation: Angle = .zero

    

    var body: some View {

        Image(systemName: "star.fill")

            .font(.system(size: 100))

            .foregroundColor(.yellow)

            .offset(offset)

            .scaleEffect(scale)

            .rotationEffect(rotation)

            .gesture(

                DragGesture()

                    .onChanged { gesture in

                        offset = gesture.translation

                    }

                    .onEnded { _ in

                        withAnimation {

                            offset = .zero

                        }

                    }

            )

            .gesture(

                MagnificationGesture()

                    .onChanged { scale = $0 }

            )

            .gesture(

                RotationGesture()

                    .onChanged { rotation = $0 }

            )

    }

}

14.3 数据持久化


import SwiftUI

  


struct TodoItem: Identifiable, Codable {

    let id: UUID

    var title: String

    var isCompleted: Bool

    var createdAt: Date

}

  


class TodoStore: ObservableObject {

    @Published var todos: [TodoItem] = []

    

    private let saveKey = "todos"

    

    init() {

        load()

    }

    

    func add(_ title: String) {

        let todo = TodoItem(

            id: UUID(),

            title: title,

            isCompleted: false,

            createdAt: Date()

        )

        todos.append(todo)

        save()

    }

    

    func toggle(_ todo: TodoItem) {

        if let index = todos.firstIndex(where: { $0.id == todo.id }) {

            todos[index].isCompleted.toggle()

            save()

        }

    }

    

    func delete(at offsets: IndexSet) {

        todos.remove(atOffsets: offsets)

        save()

    }

    

    // UserDefaults 存储

    private func save() {

        if let encoded = try? JSONEncoder().encode(todos) {

            UserDefaults.standard.set(encoded, forKey: saveKey)

        }

    }

    

    private func load() {

        if let data = UserDefaults.standard.data(forKey: saveKey),

           let decoded = try? JSONDecoder().decode([TodoItem].self, from: data) {

            todos = decoded

        }

    }

}


苹果谷歌纷纷调低官方抽成,苹果谷歌全球抽成比例汇总

一、苹果中国区抽成“紧急”下调

2026年3月12日,苹果突然宣布中国区AppStore官方抽成从 30% 改为 25%,小型开发者抽成从15% 改为 12%2026年3月15日生效来源

想必,今天大家都被这个截图刷屏了吧。

图片.png

为什么说“紧急”呢?
1、“根据与中国监管部门的沟通”,写得很清楚,是中国监管部门推动的;
2、“自3月15日起”,约等于立刻生效,对比谷歌的三个月后生效,凸显一个“急”;
3、“调整无需开发者在此之前签署新条款”,手续流程都免了,直接生效!
2、“更新版协议的简体中文版将于一个月内在 Apple开发者网站上线”,流程后面再补,先上线!

不知道苹果发生了什么,但是感觉很爽。有种苹果被工信部发了违规整改通知的感觉(DDDD),让苹果也尝尝工信部的厉害,马上整改,立刻上线!哈哈哈。

中国开发者什么都不用做,代码都不用改,就额外增(bai)加(piao) 3%~5% 的收益。

感谢那些为此做出贡献的人!

补充:有律师说出了苹果紧急“降税”的真相 ,有兴趣的可以点开看看。

二、谷歌将陆续降低全球抽成并开放三方支付

苹果紧急降低抽成除了迫于监管压力,估计也迫于竞争对手的压力。

早在3月4日,谷歌在安卓开发者网站发布了一篇博客《选择和开放的新时代》宣布将陆续在全球降低抽成,开放第三方支付,并且后续除了《小型开发者计划》外,还会新推出《应用体验计划》和《游戏升级计划》来让利开发者

《应用体验计划》《游戏升级计划》的本质:质量换费率。通过经济激励(降低费率)来引导开发者提升应用和游戏的整体品质。开发者必须达到相应的技术集成和体验标准,来满足计划条件,才能获得费率减免。举例说明,比如,游戏类必须集成 Play Games Services 功能(如成就系统、现代玩家个人资料认证)。Play Console 中的“Android Vitals”指标,确保应用在崩溃率、ANR(无响应)率等方面符合谷歌的健康度标准。

计划的具体内容,谷歌尚未公布。

谷歌将现有的抽成拆成了两部分:
Google商店服务费:标准20%、参加上述新计划15%、小型开发者10%、订阅10%(取最小值)
Google支付服务费:约5%(每个地区可能不一样)

在美国、英国和欧洲经济区 (EEA),支付服务费为 5%。其他地区的支付服务费详情谷歌后续公布。

商店服务费,只要你在谷歌商店上架就要交,不管你用谷歌支付还是三方支付;
支付服务费,用谷歌支付就要交,用三方支付不交。

谷歌最终抽成比例:
官方支付抽成:15%~25%
三方支付抽成:10%~20%

谷歌新政策全球上线后,官方支付和三方支付只差5%,三方支付还得加上3%左右的通道费,和官方支付相比,三方支付毫无竞争力,这也是为什么谷歌敢在全球开放三方支付的原因。

需要注意的是,这次费率变化并非即刻生效,而是将分时间、逐步在全球不同地区推广:

各区域的推出日期 地区 《应用体验计划》《游戏升级计划》上线地区
2026年6月30日 欧洲经济区、英国、美国  
2026年9月30日 澳大利亚 澳大利亚、欧洲经济区、英国、美国
2026年12月31日 日本、韩国 日本、韩国
2027年9月30日 世界其他地区 世界其他地区

三、苹果、谷歌全球抽成比例汇总

目前,谷歌和苹果,在全球都面临着反垄断、三方支付、三方商店的压力,革命一旦发起,就像星星之火一样会传递到全世界,一会这个国家闹,一会那个国家闹。面对这样的情况,谷歌和苹果却走出了不一样的应对路数。

1、谷歌全球统一标准

谷歌,将在2026年到2027年陆续在全球执行统一的新标准,开放三方支付、开放三方商店。

Google商店服务费:标准20%、参加上述新计划15%、小型开发者10%、订阅10%(取最小值)
Google支付服务费:约5%(每个地区可能不一样)

官方支付抽成:15%~25%
三方支付抽成:10%~20%

全球实行时间线:

各区域的推出日期 地区 《应用体验计划》《游戏升级计划》上线地区
2026年6月30日 欧洲经济区、英国、美国  
2026年9月30日 澳大利亚 澳大利亚、欧洲经济区、英国、美国
2026年12月31日 日本、韩国 日本、韩国
2027年9月30日 世界其他地区 世界其他地区

2、苹果按闹施政

从目前来看,苹果是按闹施政,谁闹我就便宜点,不闹就维持原样。但感觉不是长久之计,说不定苹果后续也会像谷歌那样统一标准。目前情况来看,谷歌还是眼光更长远一些,走在了前面,胸襟更大。

以下是苹果当前(2026.3.13)全球费率情况

地区 官方内购参考佣金 三方支付苹果抽成 备注
欧盟 13% - 20%,官方文档 15%~20% 欧盟计费很复杂,还会按安装量抽成
日本 15% - 26%,官方文档 10%~15%,外部链接购买 10%~21%,应第三方购买  
韩国 15% ~ 30% 11% ~ 26%  
美国 15% - 30% 0%,外部链接购买 海外公司可以申请;必须同时提供内购作为备选;仍然向苹果上报收入用于审计
中国 12% ~ 25%,官方文档 不允许三方支付  
其它 15% ~ 30% 不允许三方支付

和谷歌一样,苹果也把抽成拆了商店服务费+支付服务费,从上表可以看到三方支付和官方支付比也没有优势。

美国外链支付比较特殊,可以做到0%费率,但同样要满足三方支付的苛刻条件:必须接入官方内购作为备选、有苹果警告弹窗、仍然需要上报三方收入给苹果审计。

如果你对三方支付感兴趣可以看看我往期文档《三方支付真的香吗?日本iOS、Google三方支付调研报告 》,这篇虽然讲得是日本,但三方支付的接入流程和要求,全球都是一样的。

Flutter的状态管理工具

一、Provider

1.原理

Provider 本质上是基于 Flutter 的InheritedWidget 实现的,核心思想是数据自上而下传递,形成一个「数据提供者 - 消费者」的树形结构。

2、使用示例

2.1 定义可监听的状态模型(继承 ChangeNotifier) 核心:数据变化时调用 notifyListeners() 通知组件刷新


class LoginStatusModel extends ChangeNotifier {
  bool _isLogin = false;

  bool get isLogin => _isLogin;

  void updateLoginStatus(bool isLogin) {
    _isLogin = isLogin;
    notifyListeners();  // 关键:通知所有订阅的组件刷新
  }
}

2.2 使用 Provider或其子类,包裹 App实例,并将 状态模型实例作为值传递

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => LoginStatusModel(), 
      child: const MyApp()、
    ),
  );
}

2.3 使用状态数据,在需要监听数据变化的Widget中,使用Provider.of、Consumer获取数据:

  @override
  Widget build(BuildContext context) {
    // 使用 Consumer 监听 CounterModel
    return Consumer<LoginStatusModel>(
      builder: (context, loginStatus, child) {
        return Text('${loginStatus.isLogin}');
      },
    );
  }
}

// 也可以使用  Provider.of() 来获取:
Text('${Provider.of<LoginStatusModel>(context, listen: false).isLogin}')
特性 Provider.of<T>(context) Consumer<T> context.watch<T>() (推荐)
主要用途 灵活获取,常用于非 build 方法中 build 中获取并直接构建子 Widget build 中获取数据用于逻辑判断或属性赋值
是否监听变化 取决于 listen 参数 (默认 true) 是 (自动监听) 是 (自动监听)
代码位置 任意位置 (build 内/外,异步方法中) 只能在 build 方法的 return 树中 只能在 build 方法体内 (return 之前)
是否需要 builder 不需要 需要 (builder 回调) 不需要
典型场景 按钮点击事件、定时器、初始化逻辑 需要根据数据动态生成整个 Widget 时 需要根据数据决定 Widget 的属性 (颜色、文本) 时
性能优化 可设置 listen: false 避免不必要重绘 仅重建 Consumer 及其子节点 重建当前 Widget

A. Provider.of<T>(context)

这是最原始的方法。它的关键在于第二个参数 listen

  • listen: true (默认)

    • 行为:监听数据变化。如果数据变了,当前 Widget 会重建
    • 限制:只能在 build 方法中使用(因为重建需要触发 build)。
    • 缺点:如果在 build 中用默认值,会导致整个父 Widget 重绘,不够精细。
  • listen: false (常用)

    • 行为:不监听数据变化。只获取当前的实例对象。
    • 场景:在事件回调(如 onPressed)、initState、或者异步方法中调用修改数据的方法(如 increment())。
    • 优势:不会因为数据变化导致当前 Widget 无谓重绘。
Widget build(BuildContext context) {
  // ✅ 获取数据 (自动监听)
  final counter = context.watch<CounterModel>(); 
  
  // 可以在这里做逻辑处理
  final color = counter.count > 10 ? Colors.red : Colors.green;
  final text = counter.count > 10 ? '太多了!' : '正常';

  return Column(
    children: [
      // 使用处理后的数据
      Text(text, style: TextStyle(color: color)),
      
      // 按钮事件 (必须用 read 或 Provider.of(..., listen: false))
      ElevatedButton(
        onPressed: () => context.read<CounterModel>().increment(),
        child: Text('增加'),
      )
    ],
  );
}

B. Consumer 是一个 Widget。它的作用是将“获取数据”和“构建 UI”合二为一。

  • 特点:它提供了一个 builder 函数。只有当数据变化时,只有这个 Consumer 节点及其子节点会重绘,它的父兄弟节点不会重绘。
  • 场景:当你需要根据数据直接返回一个新的 Widget 结构时。

// ✅ 场景:只想让这段文字区域刷新,不影响周围的布局

Consumer<CounterModel>(
  builder: (context, counter, child) {
    // counter 就是 CounterModel 实例
    return Text(
      '当前计数: ${counter.count}',
      style: TextStyle(fontSize: 24, color: Colors.blue),
    );
  },
  // child 参数可用于优化:传递不变的子组件,避免每次重绘都重建它
  // child: Icon(Icons.star), 
)

C. context.watch<T>() —— Consumer 的语法糖 (现代推荐)

这是 provider 6.0+ 版本后最推荐的写法。它等价于 Provider.of<T>(context, listen: true),但写法更简洁。

  • 特点:直接在 build 方法体中使用,返回数据对象。
  • 场景:当你需要在 build 方法中获取数据,用来计算属性、做条件判断,或者组合多个数据源时。
  • 注意:调用 watch 的代码所在的 整个 Widget 的 build 方法 会在数据变化时重跑。如果该 Widget 很大,可能不如 Consumer 精准。
Widget build(BuildContext context) {
  // ✅ 获取数据 (自动监听)
  final counter = context.watch<CounterModel>(); 
  
  // 可以在这里做逻辑处理
  final color = counter.count > 10 ? Colors.red : Colors.green;
  final text = counter.count > 10 ? '太多了!' : '正常';

  return Column(
    children: [
      // 使用处理后的数据
      Text(text, style: TextStyle(color: color)),
      
      // 按钮事件 (必须用 read 或 Provider.of(..., listen: false))
      ElevatedButton(
        onPressed: () => context.read<CounterModel>().increment(),
        child: Text('增加'),
      )
    ],
  );
}

总结使用口诀

  • 改数据 (按钮/事件) ➡️ 用 read (或 of(..., listen: false))
  • 显数据 (局部刷新) ➡️ 用 Consumer
  • 显数据 (简单逻辑) ➡️ 用 watch
  • 初始化 (生命周期) ➡️ 用 of(..., listen: false)

二、RiverPod

1.原理

简洁表达:

  • 中心化管理:通过 ProviderContainerProviderScope)统一管理所有状态,状态封装在 Provider 中,脱离 Widget 上下文;

  • 精准订阅分发:基于 Ref 实现 Widget/Provider 对状态的订阅,状态变化时仅通知订阅者,最小化重建;

  • 无上下文 + 类型安全:解决了传统 Provider 的核心痛点,同时通过静态类型检查提升开发效率。

Ref (通常通过 WidgetRef 在 UI 中使用) 是整个状态管理系统的核心控制器上下文对象,是widget和provider,Provider和Provider沟通的唯一桥梁。

2、使用示例

  • 必须使用 ProviderScope 包裹整个应用。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(
    const ProviderScope( // 👈 必须包裹这里
      child: MyApp(),
    ),
  );
}
2.1 简单状态:@riverpod (替代 StateProvider)
// counter_provider.dart 文件,供后续订阅分发使用
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 使用 @riverpod 注解,运行 build_runner 后会自动生成 CounterProvider
@riverpod
class Counter extends _$Counter {
  @override
  int build() {
    // 初始值
    return 0;
  }

  // 定义修改状态的方法
  void increment() {
    state++; // 👈 直接修改 state 属性,自动通知监听者
  }

  void reset() {
    state = 0;
  }
}

2.2 复杂状态:AsyncNotifier (替代 FutureProvider + StateNotifier)

用于处理异步操作(如网络请求)并管理复杂状态。这是 Riverpod 最强大的部分。

import 'package:flutter_riverpod/flutter_riverpod.dart';

// 模拟用户模型
class User {
  final String name;
  final int age;
  User({required this.name, required this.age});
}

// 定义 AsyncNotifier
@riverpod
class CurrentUser extends _$CurrentUser {
  @override
  Future<User> build() async {
    // 模拟网络延迟
    await Future.delayed(const Duration(seconds: 2));
    
    // 模拟可能发生的错误
    // if (someCondition) throw Exception("Failed to load");

    return User(name: "Alice", age: 25);
  }

  // 修改用户信息的方法
  Future<void> updateAge(int newAge) async {
    state = const AsyncValue.loading(); // 手动设置加载状态
    
    try {
      await Future.delayed(const Duration(seconds: 1)); // 模拟 API 调用
      final user = state.value!; // 获取旧数据
      state = AsyncValue.data(User(name: user.name, age: newAge)); // 更新数据
    } catch (e, st) {
      state = AsyncValue.error(e, st); // 处理错误
    }
  }
}
2.3 组合状态:派生数据 (Derived State)

在一个 Provider 中读取另一个 Provider,实现数据联动。

@riverpod
String userNameRef(UserNameRef ref) {
  // 监听 CurrentUser Provider
  final userAsync = ref.watch(currentUserProvider);

  // 处理异步状态
  return userAsync.when(
    data: (user) => user.name,
    loading: () => "加载中...",
    error: (_, __) => "加载失败",
  );
}
A. 使用 ConsumerWidget (推荐)
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 继承 ConsumerWidget
class HomePage extends ConsumerWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 1. 监听简单状态 (Counter)
    // ref.watch 会自动订阅,数据变化时重建此 Widget
    final count = ref.watch(counterProvider); 

    // 2. 监听异步状态 (CurrentUser)
    final userAsync = ref.watch(currentUserProvider);

    return Scaffold(
      appBar: AppBar(title: const Text("Riverpod Demo")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 显示异步用户数据
            userAsync.when(
              data: (user) => Text("你好, ${user.name} (年龄: ${user.age})"),
              loading: () => const CircularProgressIndicator(),
              error: (err, stack) => Text("错误: $err"),
            ),
            
            const SizedBox(height: 20),

            // 显示计数
            Text("计数: $count", style: const TextStyle(fontSize: 24)),
            
            const SizedBox(height: 20),

            // 3. 修改状态 (使用 ref.read 或 ref.notifier)
            ElevatedButton(
              onPressed: () {
                // 方式 A: 直接调用生成的 notifier 方法 (推荐)
                ref.read(counterProvider.notifier).increment();
                
                // 方式 B: 如果是 AsyncNotifier
                // ref.read(currentUserProvider.notifier).updateAge(26);
              },
              child: const Text("增加计数"),
            ),
            
            ElevatedButton(
              onPressed: () {
                // 触发异步更新
                ref.read(currentUserProvider.notifier).updateAge(30);
              },
              child: const Text("更新用户年龄 (异步)"),
            ),
          ],
        ),
      ),
    );
  }
}

B. 在非 Widget 类中使用 (Riverpod 的杀手锏)

由于不依赖 Context,你可以在任何地方(如路由守卫、服务类、甚至 main 函数之后)访问状态。

// 例如:在一个普通的 Dart 类中
class AnalyticsService {
  final Ref ref; // 注入 Ref

  AnalyticsService(this.ref);

  void logCount() {
    // 直接读取当前值,不订阅变化 (类似 listen: false)
    final currentCount = ref.read(counterProvider);
    print("当前计数是: $currentCount");
  }
  
  void subscribeToCount() {
    // 也可以手动监听变化
    ref.listen(counterProvider, (previous, next) {
      print("计数从 $previous 变成了 $next");
    });
  }
}

特性 Provider (旧) Riverpod (新)
依赖 Context ✅ 强依赖 (BuildContext) ❌ 无依赖 (WidgetRefRef)
类型安全 ⚠️ 运行时检查 (容易崩溃) ✅ 编译时检查 (配合代码生成)
异步支持 🆗 需要 FutureProvider 🚀 原生强大 (AsyncValue, when)
状态组合 😐 较难,容易嵌套地狱 🤩 极简 (ref.watch 其他 Provider)
测试难度 😫 需要 Mock Context 😃 极易 (直接创建 ProviderContainer)
代码量 多 (样板代码) 少 (配合 @riverpod 宏)
学习曲线 低 (但精通难) 中 (概念多,但逻辑清晰)
  1. 始终使用代码生成 (@riverpod) :不要手动编写 Provider(...),让宏帮你处理类型安全和样板代码。

  2. 拆分小 Provider:不要试图用一个 Provider 管理所有状态。将计数器、用户信息、主题设置拆分成不同的 Provider,然后按需组合。

  3. 善用 AsyncValue:处理异步数据时,利用 .when() 方法优雅地处理 loadingdataerror 三种状态,避免大量的 if/else 判断。

  4. 区分 watch 和 read

    • 在 build 方法中需要重建 UI时用 ref.watch
    • 事件回调(如按钮点击)中修改数据时用 ref.read(...).notifier
    • 非 build 环境(如服务类)中用 ref.read 或 ref.listen

以下是自己的理解修正:

  1. watch:是订阅者。它监听 Provider 的数据变化,一旦变化,自动触发 UI 刷新(或 Provider 重算)。

  2. read:是获取动作

    • 事件回调(如按钮点击)中,我们使用 ref.read(provider.notifier) 来获取控制器,然后调用它的方法来修改数据
    • 修改数据后,Riverpod 会自动通知所有 watch 该数据的地方进行刷新。
  3. notifier:是控制器(遥控器)。它持有修改数据的方法(如 increment)。

你的目的 应该用什么? 结果
显示数据 (Text, Image, List) ref.watch() 数据变,UI 自动刷新 ✅
按钮点击/手势 (修改数据) ref.read(...).notifier 获取控制器,修改数据 ✅
按钮点击/手势 (读取参数) ref.read() 获取当前值,用于逻辑判断 ✅
定时器/异步回调 ref.read() 获取最新值,避免闭包旧值 ✅
纯 Dart 类/服务 ref.read() 访问全局状态 ✅
Build 中显示数据 ref.read() ❌ 界面不会更新 (Bug)

Swift 从入门到精通-第三篇

第9章:协议与扩展

9.1 协议 (Protocol) - 定义接口

协议定义了一组方法、属性或其他要求的蓝图:

protocol Greetable {
    var name: String { get }  // 只读属性要求
    func greet()              // 方法要求
}

protocol Describable {
    var description: String { get }
}

遵循协议:

struct User: Greetable, Describable {
    let name: String
    let email: String
    
    // 必须实现协议要求
    func greet() {
        print("Hi, I'm (name)!")
    }
    
    var description: String {
        return "User: (name) <(email)>"
    }
}

let user = User(name: "Alice", email: "alice@example.com")
user.greet()        // Hi, I'm Alice!
print(user.description)

9.2 协议扩展 - 提供默认实现

// 在扩展中提供默认实现
extension Greetable {
    func greet() {
        print("Hello, my name is (name)")
    }
}

struct Employee: Greetable {
    let name: String
    // 不需要实现 greet(),使用默认实现
}

let employee = Employee(name: "Bob")
employee.greet()  // Hello, my name is Bob

协议扩展的强大之处:

  • 可以给协议添加默认行为
  • 遵循者可以选择使用默认实现或自定义
  • 这是"面向协议编程"的核心

9.3 协议组合

protocol Named {
    var name: String { get }
}

protocol Aged {
    var age: Int { get }
}

struct Person: Named, Aged {
    var name: String
    var age: Int
}

// 函数接受同时遵循多个协议的类型
func wishHappyBirthday(to celebrator: Named & Aged) {
    print("Happy birthday (celebrator.name), you're (celebrator.age)!")
}

let person = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: person)

9.4 带关联类型的协议

protocol Container {
    associatedtype Item  // 关联类型
    var count: Int { get }
    mutating func append(_ item: Item)
    subscript(i: Int) -> Item { get }
}

struct IntStack: Container {
    // 自动推断 Item = Int
    private var items: [Int] = []
    
    var count: Int { items.count }
    
    mutating func append(_ item: Int) {
        items.append(item)
    }
    
    subscript(i: Int) -> Int {
        return items[i]
    }
}

// 也可以显式指定
double Stack: Container {
    typealias Item = Double  // 显式指定
    // ...
}

9.5 扩展系统类型

// 给 Int 添加方法
extension Int {
    var squared: Int {
        return self * self
    }
    
    func times(_ action: () -> Void) {
        for _ in 0..<self {
            action()
        }
    }
}

print(5.squared)  // 25
3.times {
    print("Hello!")
}
// Hello!
// Hello!
// Hello!

// 给 Collection 添加方法
extension Collection {
    var isNotEmpty: Bool {
        return !isEmpty
    }
}

[1, 2, 3].isNotEmpty  // true

第10章:泛型编程

10.1 为什么要用泛型?

想象你要写交换两个值的函数:

// 只能交换整数
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

// 只能交换字符串
func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let temporaryA = a
    a = b
    b = temporaryA
}

重复代码!用泛型可以写一个通用的:

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)

<T>类型参数,代表任意类型。

10.2 泛型类型

// 泛型栈
struct Stack<Element> {
    private var items: [Element] = []
    
    mutating func push(_ item: Element) {
        items.append(item)
    }
    
    mutating func pop() -> Element? {
        return items.popLast()
    }
    
    var topItem: Element? {
        return items.last
    }
    
    var isEmpty: Bool {
        return items.isEmpty
    }
}

var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
print(intStack.pop()!)  // 2

var stringStack = Stack<String>()
stringStack.push("hello")
stringStack.push("world")

10.3 类型约束

// 要求 T 必须遵循 Comparable 协议
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])

常用类型约束:

  • T: Equatable - 可以比较相等
  • T: Comparable - 可以比较大小
  • T: Hashable - 可以作为字典的 key
  • T: SomeProtocol - 遵循某个协议

10.4 关联类型与 where 子句

protocol Container {
    associatedtype Item
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

// 要求两个容器的 Item 相同且可比较
func allItemsMatch<C1: Container, C2: Container>(_ container1: C1, _ container2: C2) -> Bool
    where C1.Item == C2.Item, C1.Item: Equatable {
    
    if container1.count != container2.count {
        return false
    }
    
    for i in 0..<container1.count {
        if container1[i] != container2[i] {
            return false
        }
    }
    return true
}

10.5 不透明返回类型 (some)

protocol Shape {
    func draw() -> String
}

struct Triangle: Shape {
    var size: Int
    func draw() -> String {
        var result = [String]()
        for length in 1...size {
            result.append(String(repeating: "*", count: length))
        }
        return result.joined(separator: "\n")
    }
}

struct Square: Shape {
    var size: Int
    func draw() -> String {
        let line = String(repeating: "*", count: size)
        return Array(repeating: line, count: size).joined(separator: "\n")
    }
}

// 返回遵循 Shape 协议的某种类型,但隐藏具体是什么
func makeShape() -> some Shape {
    return Triangle(size: 3)
}

// 也可以返回不同的,只要都是 Shape
func makeRandomShape() -> some Shape {
    Bool.random() ? Triangle(size: 3) : Square(size: 3)
}

some 的好处:

  • 调用者不需要知道具体类型
  • 编译器可以进行类型优化
  • 在 SwiftUI 中非常常用(some View

第11章:错误处理

11.1 定义错误类型

enum VendingMachineError: Error {
    case invalidSelection                    // 选择无效
    case insufficientFunds(coinsNeeded: Int) // 金额不足,附带需要多少钱
    case outOfStock                          // 缺货
}

任何遵循 Error 协议的类型都可以表示错误。

11.2 抛出错误

class VendingMachine {
    var inventory = [
        "Candy Bar": Item(price: 12, count: 7),
        "Chips": Item(price: 10, count: 4),
        "Pretzels": Item(price: 7, count: 11)
    ]
    var coinsDeposited = 0
    
    func vend(itemNamed name: String) throws {
        guard let item = inventory[name] else {
            throw VendingMachineError.invalidSelection
        }
        
        guard item.count > 0 else {
            throw VendingMachineError.outOfStock
        }
        
        guard item.price <= coinsDeposited else {
            throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
        }
        
        // 执行购买逻辑
        coinsDeposited -= item.price
        var newItem = item
        newItem.count -= 1
        inventory[name] = newItem
        
        print("Dispensing (name)")
    }
}

throws 标记表示这个函数可能抛出错误。

11.3 处理错误

do-catch

let vendingMachine = VendingMachine()
vendingMachine.depositCoins(8)

do {
    try vendingMachine.vend(itemNamed: "Candy Bar")
} catch VendingMachineError.invalidSelection {
    print("Invalid selection.")
} catch VendingMachineError.outOfStock {
    print("Out of stock.")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
    print("Insufficient funds. Please insert an additional (coinsNeeded) coins.")
} catch {
    print("Unexpected error: (error)")
}

try? - 转换为可选值

// 成功返回结果,失败返回 nil
if let result = try? someThrowingFunction() {
    // 使用结果
} else {
    // 处理失败
}

// 等效于
do {
    let result = try someThrowingFunction()
} catch {
    // 忽略错误
}

try! - 强制解包(危险!)

// 确定不会失败时使用
let result = try! someThrowingFunction()
// 如果失败了,程序会崩溃

11.4 defer - 清理资源

func processFile(filename: String) throws -> String {
    let file = try openFile(filename)
    
    defer {
        closeFile(file)  // 无论如何都会执行
    }
    
    if filename.isEmpty {
        throw FileError.invalidName  // 先执行 defer,再抛出错误
    }
    
    return try readFile(file)
}  // 正常返回也会执行 defer

11.5 Result 类型

enum NetworkError: Error {
    case badURL
    case noData
    case decodingError
}

func fetchUser(completion: (Result<User, NetworkError>) -> Void) {
    // 模拟网络请求
    let success = Bool.random()
    if success {
        completion(.success(User(name: "Alice")))
    } else {
        completion(.failure(.noData))
    }
}

// 使用
fetchUser { result in
    switch result {
    case .success(let user):
        print("Got user: (user.name)")
    case .failure(let error):
        print("Error: (error)")
    }
}

第12章:内存管理

12.1 ARC (自动引用计数)

Swift 使用 ARC 自动管理内存:

  • 每次创建实例,引用计数 +1
  • 每次引用消失,引用计数 -1
  • 引用计数为 0,内存被释放

你不需要手动管理,但要理解引用关系。

12.2 强引用循环问题

class Person {
    let name: String
    var apartment: Apartment?  // 强引用
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("(name) is being deinitialized")
    }
}

class Apartment {
    let unit: String
    var tenant: Person?  // 强引用
    
    init(unit: String) {
        self.unit = unit
    }
    
    deinit {
        print("Apartment (unit) is being deinitialized")
    }
}

var john: Person? = Person(name: "John Appleseed")
var unit4A: Apartment? = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

john = nil
unit4A = nil
// 没有打印 deinit 消息!内存泄漏了!

问题: Person 持有 Apartment,Apartment 持有 Person,形成一个环,引用计数永远不会归零。

12.3 弱引用 (Weak Reference)

class Apartment {
    let unit: String
    weak var tenant: Person?  // 弱引用!
    
    init(unit: String) {
        self.unit = unit
    }
    
    deinit {
        print("Apartment (unit) is being deinitialized")
    }
}

// 现在当 john = nil,Person 实例会被释放
// 然后 Apartment 的 tenant 自动变成 nil

弱引用的特点:

  • 不增加引用计数
  • 指向的实例释放后自动变成 nil
  • 必须是可选类型(因为可能为 nil)

什么时候用弱引用?

  • 父子关系中的子(如 Apartment 和 tenant)
  • 委托模式 (Delegate pattern)

12.4 无主引用 (Unowned Reference)

class Customer {
    let name: String
    var card: CreditCard?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("(name) is being deinitialized")
    }
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer  // 无主引用
    
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    
    deinit {
        print("Card #(number) is being deinitialized")
    }
}

var alice: Customer? = Customer(name: "Alice")
alice!.card = CreditCard(number: 1234_5678_9012_3456, customer: alice!)

alice = nil
// 两个实例都被释放

无主引用的特点:

  • 不增加引用计数
  • 不是可选类型
  • 指向的实例释放后变成 dangling pointer(悬挂指针)

什么时候用无主引用?

  • 确定引用的实例永远比自己活得长
  • 不会造成循环引用的强关系

12.5 闭包中的循环引用

class HTMLElement {
    let name: String
    let text: String?
    
    // 闭包捕获 self,形成循环引用!
    lazy var asHTML: () -> String = {
        if let text = self.text {
            return "<(self.name)>(text)</(self.name)>"
        } else {
            return "<(self.name) />"
        }
    }
    
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
    
    deinit {
        print("(name) is being deinitialized")
    }
}

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "Hello")
let html = paragraph!.asHTML()
paragraph = nil
// 没有 deinit!循环引用!

解决:捕获列表

lazy var asHTML: () -> String = { [weak self] in
    guard let self = self else {
        return ""
    }
    if let text = self.text {
        return "<(self.name)>(text)</(self.name)>"
    } else {
        return "<(self.name) />"
    }
}

捕获列表语法:

  • [weak self] - 弱引用 self
  • [unowned self] - 无主引用 self
  • [x = someValue] - 捕获时复制值而不是引用

Buildable Folder & Group & Folder Reference in Xcode

深入理解代替单纯记忆

问题背景

  • 在开发iOS项目时,希望将一堆图片资源放入Main Bundle中,但又不希望资源在Bundle的最顶层目录中,希望自定义目录
  • 但一时想不到该如何解决,于是想到FolderGroup等概念
  • 经过简单搜索后,发现Xcode对于这两个概念的定义还是有些差异的
  • 于是继续查阅学习了一番,编写本文,方便后续查阅和分享

本文提到的内容,参考的Xcode版本为26.0(17A324)和26.3(17C529)

Buildable Folder

  • Buildable Folder是自Xcode 16(2024年6月)引入的概念,初衷是为了减少代码管理中的冲突问题
  • 后续新建的工程或者新建Folder时,默认都是Buildable Folder

官方原文如下:

Minimize project file changes and avoid conflicts with buildable folder references. Convert an existing group to a buildable folder with the Convert to Folder context menu item in the Project Navigator. Buildable folders only record the folder path into the project file without enumerating the contained files, minimizing diffs to the project when your team adds or removes files, and avoiding source control conflicts. To use a folder as an opaque copiable resource, the default behavior before Xcode 16, uncheck the Build Folder Contents option in the File Inspector.

Buildable Folder如何降低代码冲突

  1. 先添加1个普通Group--BuildableFolderTest,project文件的变化如下所示: image.png

  2. 然后向BuildableFolderTest Group中添加ABC.swift文件后,project文件的变化如下: image.pngimage.png

    这说明Group目录下的文件,都要在project文件中进行记录

  3. 继续,将BuildableFolderTest Group通过Convert to Folder选项转为Folder(Buildable Folder)后,project文件的变化如下: image.pngimage.png

  4. 然后再向BuildableFolderTest这个Folder中添加DEF.swift文件后,发现project文件没有任何变化

所以,project文件仅记录了Folder自身,至于目录中的文件是不会记录在project文件中,所以会减少因团队多人同时修改Project文件导致的代码冲突

Apply to Each File vs Apply Once to Folder

当创建Folder(Buildable Folder)后,选中Folder,在File inspector中会看到有个Build Rule,有两个选择:Apply to Each FileApply Once to Folder,默认是Apply to Each File

image.png

Apply Once to Folder

Apply Once to Folder开启后,project文件是什么样?

image.pngimage.pngimage.png

当开启该模式时,通过查看目录下的每个文件可以看出,文件是没有Target归属的概念的。同样,在该目录下创建新文件也不需要选择Target

再配合Xcode Buildable Folders中所提到的To use a folder as an opaque copiable resource, the default behavior before Xcode 16, uncheck the Build Folder Contents option in the File Inspector.

其实,Apply Once to Folder就是Xcode 16之前的Folder,之前叫Folder Reference (在Xcode 16之前,创建Folder时,官方名称就叫做Folder Reference)

  • Folder Reference一般是用作资源包,目录下不包含源代码
  • 另一个Folder Reference重要作用是可以在Bundle中自定义目录

Buildable Folder vs Folder Reference

Buildable Folder顾名思义,其中的内容是由编译系统参与的

  • 所以Buildable Folder中可以放源代码文件,并可以参与编译,打包到最终可执行文件中;也可以制定源文件的Target
  • Folder Reference则保留老的逻辑,不参与编译,用作资源包,即使放入源代码文件也无法选择Target,只能当做普通文件资源处理

Create Group with Folder

同样是在Xcode 16开始的另一个变化是,创建Group时由原来的不自动创建磁盘物理目录(Folder)变为自动创建。当然,仍可以创建没有FolderGroup,原文如下:

Create groups with associated folders by default when using the New Group and New Group from Selection commands in the Project Navigator. To create a group without a folder, hold the Option key in the context menu to reveal the New Group without Folder variant of the command.

[Group without Folder] vs [Group] vs [Folder(Buildable Folder)] vs [Folder Reference]

特性 Group without Folder Group Buildable Folder Folder Reference
Project Navigator 图标 image.png image.png image.png image.png
是否对应磁盘目录 ❌ 不必须 ✅ 必须 ✅ 必须 ✅ 必须
工程结构是否可与磁盘不同 ✅ 可以 ❌ 基本一致 ❌ 必须一致 ❌ 必须一致
.pbxproj 是否记录每个文件 ✅ 会 ✅ 会 ❌ 不会 ❌ 不会
新增文件是否修改 .pbxproj ✅ 会 ✅ 会 ❌ 不会 ❌ 不会
Git 冲突概率
是否参与编译系统
是否自动编译源码 ✅(自动发现目录中的源码)
Bundle 中是否保留目录结构 ❌ 通常不会 ❌ 通常不会 ❌ 通常不会 会保留(如果被加入 Bundle)
默认是否进入 Bundle ❌ 否 ❌ 否 ❌ 否 仅在选中 Target 时自动加入
典型用途 逻辑分组 常规项目结构 源码目录 资源目录
  • 当前(Xcode 26),默认的Group和Folder组合是Group with Folder + Buildable Folder。这可能也意味着这两项是日常最常用的

回答开始的问题

  • 既然是想打包资源放入Bundle,并自定义目录,那必然是Folder Reference

参考

iOS必看!Deepseek给的Runtime实现原理,通俗易懂~

iOS Runtime 消息转发机制完全解析

写在前面

在Objective-C的世界里,方法调用并不是像C++那样在编译时就确定要执行的函数地址,而是一个运行时动态绑定的过程。当我们写下 [receiver message] 这样的代码时,编译器实际上会将其转换为 objc_msgSend(receiver, @selector(message)) 的调用。这个 objc_msgSend 函数会负责在接收者所属的类及其父类的方法列表中查找对应的实现并执行。

那么问题来了:如果一直找到根类NSObject都没有找到这个方法的实现,会发生什么?

很多开发者都见过这样的崩溃信息:unrecognized selector sent to instance 0xXXXXXXXX。这正是因为消息发送失败,而Runtime也没有找到合适的方式处理这条消息,最终通过 doesNotRecognizeSelector: 抛出的异常。

但在这个崩溃发生之前,Objective-C的Runtime给了我们三次"拯救"的机会,这就是本文要详细讲解的消息转发机制


第一章:消息发送机制回顾

在深入探讨消息转发之前,有必要先回顾一下完整的消息发送流程,因为消息转发正是这个流程中处理失败情况的最后保障。

1.1 objc_msgSend的工作流程

当我们向一个对象发送消息时,Runtime系统会按照以下步骤查找方法的实现:

  1. 检查目标对象是否为nil:如果接收者为nil,Objective-C的特性是忽略该消息,程序不会崩溃(这在很多情况下简化了代码逻辑)。如果为nil且消息有返回值,基本数据类型的返回值为0,对象类型的返回值为nil。

  2. 查找缓存:每个类都有一个缓存(cache),用于存储最近使用过的方法。Runtime会首先在该类的缓存中查找方法的实现(IMP)。如果找到,直接调用该实现。

  3. 查找当前类的方法列表:如果在缓存中没有找到,Runtime会从当前类的方法列表中查找。方法列表以数组形式组织,查找过程会遍历整个列表(已排序的列表使用二分查找,否则线性查找)。

  4. 沿着继承链向上查找:如果在当前类中没有找到,Runtime会沿着继承链逐级向上查找父类的方法列表和缓存,直到根类NSObject为止。

  5. 动态方法解析:如果一直找到根类都没有找到方法的实现,Runtime会进入"动态方法解析"阶段,给类一个机会动态添加方法的实现。

  6. 消息转发:如果动态方法解析没有添加实现(或者添加后仍然无法处理),Runtime会进入"消息转发"流程。

  7. 抛出异常:如果所有转发尝试都失败,最终会调用 doesNotRecognizeSelector: 抛出异常,程序崩溃。

这个流程可以用下面的流程图清晰地展示:

flowchart TD
    A[向对象发送消息] --> B{接收者为nil?}
    B -->|是| C[忽略消息/返回0/nil]
    B -->|否| D[查找缓存]
    
    D --> E{缓存中找到IMP?}
    E -->|是| F[调用IMP]
    E -->|否| G[在当前类方法列表中查找]
    
    G --> H{当前类中找到?}
    H -->|是| F
    H -->|否| I[在父类方法列表中查找]
    
    I --> J{父类中找到?}
    J -->|是| F
    J -->|否| I
    
    J -->|一直查到NSObject仍未找到| K[动态方法解析]
    
    K --> L{动态添加了实现?}
    L -->|是| F
    L -->|否| M[消息转发流程]
    
    M --> N{转发成功?}
    N -->|是| F
    N -->|否| O[doesNotRecognizeSelector:\n抛出异常]

1.2 方法的本质:SEL、IMP与Method

要深入理解消息转发,我们需要先了解Objective-C中方法的三个核心概念:

SEL(选择器):是方法的名字,在Runtime中用 objc_selector 结构体表示。在运行时,不同类的同名方法的选择器是相同的。SEL在Runtime中会被唯一化,因此可以使用 == 来比较两个SEL是否相等。

IMP(函数指针):是方法的实现,本质上是一个函数指针,指向方法实现的首地址。它的定义如下:

typedef id (*IMP)(id self, SEL _cmd, ...);

每个IMP都至少包含两个参数:self(消息接收者)和_cmd(这个方法的SEL)。

Method(方法):是用于表示方法定义的结构体,包含三个成员:

struct method_t {
    SEL name;      // 方法名
    const char *types;  // 方法类型编码
    IMP imp;       // 方法实现
}

当我们调用一个方法时,就是从SEL到IMP的映射过程。Runtime维护了每个类的方法列表(method list),这个列表存储了该类定义的所有方法。消息转发机制本质上是在这个映射过程失败后的补救措施。


第二章:消息转发的三个阶段

当消息发送流程无法找到对应的IMP时,Runtime会启动消息转发机制。这个机制分为三个阶段,每个阶段都给开发者一次处理这条"无法识别"的消息的机会。

2.1 第一阶段:动态方法解析

这是消息转发的第一道防线。当Runtime在当前类和父类中都找不到方法的实现时,会首先调用 +resolveInstanceMethod:(对于实例方法)或 +resolveClassMethod:(对于类方法)。

2.1.1 resolveInstanceMethod的工作原理

这个方法的定义如下:

+ (BOOL)resolveInstanceMethod:(SEL)sel

当这个方法被调用时,Runtime给了我们一个机会:可以动态地为这个SEL添加一个实现。如果添加成功并返回YES,Runtime会重新启动消息发送流程,这次就能找到方法的实现了。

这个方法最典型的应用场景是处理 @dynamic 属性。@dynamic 告诉编译器不要自动生成属性的getter和setter方法,我们会在运行时动态提供它们。

2.1.2 实战:动态添加方法实现

让我们通过一个具体的例子来理解这个过程:

#import <objc/runtime.h>

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;  // 注意:我们使用@dynamic
@end

@implementation Person
@dynamic name;  // 告诉编译器不要自动生成getter/setter

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(name)) {
        // 动态添加getter方法
        class_addMethod(self, sel, (IMP)dynamicNameGetter, "@@:");
        return YES;
    }
    else if (sel == @selector(setName:)) {
        // 动态添加setter方法
        class_addMethod(self, sel, (IMP)dynamicNameSetter, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

// getter方法的实现
id dynamicNameGetter(id self, SEL _cmd) {
    // 通过关联对象获取存储的值
    return objc_getAssociatedObject(self, @selector(name));
}

// setter方法的实现
void dynamicNameSetter(id self, SEL _cmd, NSString *newName) {
    // 通过关联对象存储值
    objc_setAssociatedObject(self, @selector(name), newName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end

现在,当我们调用:

Person *p = [[Person alloc] init];
[p setName:@"张三"];
NSLog(@"%@", [p name]);  // 输出:张三

尽管Person类并没有真正实现name的getter和setter方法,但在消息发送过程中,Runtime调用了 resolveInstanceMethod:,我们动态添加了这两个方法的实现,因此程序能够正常运行。

2.1.3 方法签名的类型编码

在调用 class_addMethod 时,我们需要指定方法的类型编码(types)。这个编码字符串描述了方法的返回类型和参数类型。例如:

  • "v@:" 表示返回void,有两个参数:id和SEL(即标准的实例方法)
  • "@@" 表示返回id,有两个参数:id和SEL(标准的getter方法)
  • "v@:@" 表示返回void,有三个参数:id、SEL和id(标准的setter方法)

完整的类型编码表:

编码 含义
c char
i int
s short
l long
q long long
C unsigned char
I unsigned int
S unsigned short
L unsigned long
Q unsigned long long
f float
d double
B BOOL/C++ bool
v void
* char* (字符串)
@ id (对象)
# Class (类对象)
: SEL (选择器)
^type 指向type的指针

2.1.4 类方法的动态解析

对于类方法,我们需要重写 +resolveClassMethod:

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(classMethod)) {
        // 注意:这里添加方法的目标是元类(metaclass)
        Class metaClass = objc_getMetaClass(class_getName(self));
        class_addMethod(metaClass, sel, (IMP)dynamicClassMethodImp, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

需要注意的是,类方法是存储在元类(metaclass)中的,因此我们需要获取元类来添加类方法的实现。

2.1.5 动态方法解析的时机

动态方法解析发生在消息发送流程失败之后,但在消息转发之前。如果你希望每次调用这个方法时都能走动态解析,注意这个方法只会被调用一次(因为一旦添加了实现,后续调用就能直接找到IMP了)。

2.2 第二阶段:快速消息转发

如果动态方法解析没有添加实现(或者返回NO),Runtime会进入消息转发的第二阶段:快速消息转发。

这个阶段的核心是 forwardingTargetForSelector: 方法。Runtime会调用这个方法,期望它能返回一个能够处理这条消息的对象。

2.2.1 forwardingTargetForSelector的定义

- (id)forwardingTargetForSelector:(SEL)aSelector

这个方法的职责是:当对象无法处理某个消息时,返回一个能够处理该消息的对象。Runtime会将原始消息转发给这个返回的对象,就好像它才是原始的消息接收者一样。

这个机制非常高效,因为它只是简单地改变消息的接收者,不需要创建 NSInvocation 对象,也没有复杂的参数处理。

2.2.2 实战:将消息转发给备用对象

假设我们有一个 Person 类,它不包含 run 方法,但我们有一个 Car 类实现了 run 方法:

@interface Car : NSObject
- (void)run;
@end

@implementation Car
- (void)run {
    NSLog(@"Car is running");
}
@end

@interface Person : NSObject
@end

@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(run)) {
        // 返回一个可以处理run消息的Car对象
        return [[Car alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

现在执行以下代码:

Person *person = [[Person alloc] init];
[person run];  // 输出:Car is running

尽管 Person 对象并没有 run 方法,但通过 forwardingTargetForSelector:,我们将 run 消息转发给了 Car 对象,程序能够正常运行。

2.2.3 模拟多重继承

Objective-C不支持多重继承,但通过快速消息转发,我们可以实现类似多重继承的效果。一个对象可以将自己没有实现的方法转发给其他对象,从外部看就像这个对象继承了多个类的功能。

例如,我们可以创建一个类,它能够处理来自多个不同类的方法:

@interface MultiClass : NSObject
@property (nonatomic, strong) Car *car;
@property (nonatomic, strong) House *house;
@end

@implementation MultiClass
- (instancetype)init {
    if (self = [super init]) {
        _car = [[Car alloc] init];
        _house = [[House alloc] init];
    }
    return self;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([_car respondsToSelector:aSelector]) {
        return _car;
    } else if ([_house respondsToSelector:aSelector]) {
        return _house;
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

这样,MultiClass 的实例就能同时响应 CarHouse 的方法,达到了类似多重继承的效果。

2.2.4 注意事项

使用 forwardingTargetForSelector: 时有几点需要注意:

  1. 不要返回self:如果在这个方法中返回self,会造成无限循环,因为Runtime会再次尝试向self发送消息。
  2. 这个方法主要用于转发给其他对象,不适合修改消息本身。
  3. 返回的对象不必与原始接收者有继承关系,任何对象都可以。
  4. 如果返回nil或self,则进入下一阶段:完整消息转发。

2.3 第三阶段:完整消息转发

如果前两个阶段都无法处理消息,Runtime会进入最后一个阶段:完整消息转发。这是消息转发机制中最强大、最灵活但也最复杂的阶段。

这个阶段涉及两个方法:

  • methodSignatureForSelector::获取方法的签名(参数类型和返回类型)
  • forwardInvocation::转发封装了消息的 NSInvocation 对象
flowchart TD
    A[消息转发第二阶段返回nil] --> B[调用methodSignatureForSelector:]
    
    B --> C{返回有效的方法签名?}
    C -->|否| D[调用doesNotRecognizeSelector:\n抛出异常]
    C -->|是| E[创建NSInvocation对象]
    
    E --> F[调用forwardInvocation:\n并将NSInvocation传入]
    
    F --> G{在forwardInvocation:中\n处理消息?}
    G -->|否| D
    G -->|是| H[消息处理成功]
    
    H --> I[将返回值传递给\n原始消息发送者]

2.3.1 methodSignatureForSelector: 的作用

methodSignatureForSelector: 的定义如下:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

Runtime调用这个方法的目的是获取方法的签名信息,包括方法的返回类型和参数类型。有了这些信息,Runtime才能创建 NSInvocation 对象。

如果这个方法返回nil,Runtime会直接调用 doesNotRecognizeSelector: 并抛出异常,程序崩溃。因此,在实现完整消息转发时,我们必须为无法处理的消息提供一个有效的方法签名。

2.3.2 创建方法签名

方法签名可以通过多种方式创建:

// 方式1:使用字符串创建(类型编码)
NSMethodSignature *signature1 = [NSMethodSignature signatureWithObjCTypes:"v@:"];

// 方式2:从已有方法获取
NSMethodSignature *signature2 = [self methodSignatureForSelector:@selector(existingMethod)];

// 方式3:从协议获取
struct objc_method_description desc = protocol_getMethodDescription(protocol, selector, YES, YES);
NSMethodSignature *signature3 = [NSMethodSignature signatureWithObjCTypes:desc.types];

类型编码字符串的格式和之前 class_addMethod 中使用的格式一致。

2.3.3 forwardInvocation: 的核心作用

forwardInvocation: 的定义如下:

- (void)forwardInvocation:(NSInvocation *)anInvocation

methodSignatureForSelector: 返回了有效的方法签名后,Runtime会创建一个 NSInvocation 对象,该对象封装了这条消息的所有信息:

  • 消息的目标(target)
  • 消息的选择器(selector)
  • 所有的参数
  • 等待填充的返回值

然后将这个 NSInvocation 对象作为参数传递给 forwardInvocation: 方法。在这个方法中,我们可以:

  1. 将消息转发给其他对象
  2. 修改消息的选择器、参数或目标
  3. 直接处理消息并设置返回值
  4. 甚至"吃掉"消息,什么都不做(这样就不会崩溃)

2.3.4 实战:完整消息转发的实现

下面是一个完整的示例,演示如何实现完整消息转发:

@interface Person : NSObject
@end

@interface Car : NSObject
- (void)run;
@end

@implementation Car
- (void)run {
    NSLog(@"Car is running");
}
@end

@implementation Person
// 第一步:提供方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(run)) {
        // 返回run方法的签名:"v@:" 表示返回void,两个参数:id, SEL
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

// 第二步:转发调用
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    
    // 创建备用对象
    Car *car = [[Car alloc] init];
    
    // 检查备用对象是否能响应这个选择器
    if ([car respondsToSelector:selector]) {
        // 将消息转发给备用对象
        [anInvocation invokeWithTarget:car];
    } else {
        // 如果备用对象也不能处理,调用父类实现(最终会抛出异常)
        [super forwardInvocation:anInvocation];
    }
}
@end

执行测试代码:

Person *person = [[Person alloc] init];
[person run];  // 输出:Car is running

2.3.5 修改消息内容后转发

完整消息转发的一个强大之处在于,我们可以在转发前修改消息的内容。例如,我们可以修改方法的选择器:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL originalSelector = [anInvocation selector];
    
    if (originalSelector == @selector(run)) {
        // 修改选择器为drive
        [anInvocation setSelector:@selector(drive)];
        
        Car *car = [[Car alloc] init];
        if ([car respondsToSelector:@selector(drive)]) {
            [anInvocation invokeWithTarget:car];
            return;
        }
    }
    
    [super forwardInvocation:anInvocation];
}

我们也可以修改参数:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    
    if (selector == @selector(setAge:)) {
        // 获取原始参数
        int age;
        [anInvocation getArgument:&age atIndex:2]; // 前两个参数是self和_cmd
        
        // 修改参数值(例如:限制年龄范围)
        if (age < 0) age = 0;
        if (age > 150) age = 150;
        
        // 设置修改后的参数
        [anInvocation setArgument:&age atIndex:2];
    }
    
    // 转发给实际处理的对象
    if ([_realObject respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:_realObject];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

2.3.6 处理返回值

NSInvocation 也能处理返回值。我们可以从 anInvocation 中获取返回值,修改它,或者设置自己的返回值:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 先尝试转发给备用对象
    if ([_backup respondsToSelector:[anInvocation selector]]) {
        [anInvocation invokeWithTarget:_backup];
        
        // 获取返回值
        char returnType[10];
        strcpy(returnType, [[anInvocation methodSignature] methodReturnType]);
        
        if (returnType[0] == '@') { // 返回对象类型
            id result = nil;
            [anInvocation getReturnValue:&result];
            
            // 可以修改返回值
            if (result == nil) {
                result = @"Default Value";
                [anInvocation setReturnValue:&result];
            }
        }
        return;
    }
    
    [super forwardInvocation:anInvocation];
}

2.3.7 转发给多个对象

完整消息转发甚至可以将一个消息转发给多个对象。这在某些设计模式中很有用,例如观察者模式或责任链模式:

@interface MessageChain : NSObject
@property (nonatomic, strong) NSArray *handlers;
@end

@implementation MessageChain
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    BOOL handled = NO;
    
    for (id handler in self.handlers) {
        if ([handler respondsToSelector:selector]) {
            [anInvocation invokeWithTarget:handler];
            handled = YES;
            // 可以选择是否继续转发给下一个处理器
            // break;
        }
    }
    
    if (!handled) {
        [super forwardInvocation:anInvocation];
    }
}
@end

2.4 三个阶段的关系与选择

这三个阶段是递进的关系:如果第一阶段处理了,第二阶段就不会触发;如果第二阶段处理了,第三阶段就不会触发。

选择使用哪个阶段取决于你的需求:

  • 动态方法解析:适合在运行时动态添加方法实现,例如处理 @dynamic 属性、实现轻量级的代理模式。
  • 快速消息转发:适合简单地将消息转发给另一个对象,性能最好,但不能修改消息内容。
  • 完整消息转发:最强大、最灵活,可以修改消息内容、参数、返回值,甚至可以将消息转发给多个对象,但性能开销也最大。

第三章:深入源码分析

了解理论之后,让我们深入Runtime的源码,看看消息转发机制究竟是如何实现的。这里我们基于苹果开源的objc4源码进行分析。

3.1 从消息发送到消息转发的转折点

objc_msgSend 的核心实现中,如果方法查找失败,会调用 lookUpImpOrForward 函数。这个函数的简化逻辑如下:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver) {
    IMP imp = nil;
    bool triedResolver = NO;
    
    // 尝试从缓存和方法列表中查找
    // ...
    
    // 如果没有找到实现
    if (resolver && !triedResolver) {
        // 调用动态方法解析
        _class_resolveMethod(cls, sel, inst);
        triedResolver = YES;
        // 重新尝试查找
        goto retry;
    }
    
    // 动态解析失败,返回转发IMP
    imp = (IMP)_objc_msgForward_impcache;
    
    return imp;
}

关键点在于:当动态方法解析失败后,lookUpImpOrForward 会返回一个特殊的IMP:_objc_msgForward_impcache。这个IMP指向的是消息转发的入口函数。

3.2 消息转发的入口:__objc_msgForward

_objc_msgForward_impcache 最终会调用到 __objc_msgForward 函数。在x86_64架构的汇编实现中,这个函数的逻辑大致是:

ENTRY __objc_msgForward
    // 跳转到消息转发的核心实现
    jmp __objc_forward_handler
END_ENTRY __objc_msgForward

__objc_forward_handler 是一个C函数,它会调用到CoreFoundation框架中的 __forwarding__ 函数。这就是消息转发的真正核心实现。

3.3 CoreFoundation中的__forwarding__函数

__forwarding__ 函数是消息转发机制的心脏。虽然苹果没有开源CoreFoundation的全部代码,但通过反汇编和分析,我们可以还原其大致逻辑:

int __forwarding__(void *frameStackPointer, int isStret) {
    // 获取消息的接收者和选择器
    id receiver = *(id *)frameStackPointer;
    SEL sel = *(SEL *)(frameStackPointer + sizeof(id));
    
    // 尝试快速转发
    id forwardingTarget = nil;
    if ([receiver respondsToSelector:@selector(forwardingTargetForSelector:)]) {
        forwardingTarget = [receiver forwardingTargetForSelector:sel];
        if (forwardingTarget != nil && forwardingTarget != receiver) {
            // 转发给目标对象
            return objc_msgSend(forwardingTarget, sel, ...);
        }
    }
    
    // 快速转发失败,尝试完整转发
    NSMethodSignature *signature = nil;
    if ([receiver respondsToSelector:@selector(methodSignatureForSelector:)]) {
        signature = [receiver methodSignatureForSelector:sel];
    }
    
    if (signature == nil) {
        // 没有方法签名,无法继续
        [receiver doesNotRecognizeSelector:sel];
        return 0;
    }
    
    // 创建NSInvocation对象
    NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:signature frame:frameStackPointer];
    
    // 调用forwardInvocation:
    if ([receiver respondsToSelector:@selector(forwardInvocation:)]) {
        [receiver forwardInvocation:invocation];
    } else {
        [receiver doesNotRecognizeSelector:sel];
    }
    
    // 获取返回值
    // ...
    return 0;
}

从这个伪代码可以看出,__forwarding__ 函数完整地实现了我们之前讨论的消息转发流程:

  1. 尝试快速转发
  2. 如果快速转发没有返回合适的对象,尝试获取方法签名
  3. 如果方法签名有效,创建 NSInvocation 并调用 forwardInvocation:
  4. 如果所有步骤都失败,调用 doesNotRecognizeSelector: 抛出异常

3.4 日志调试技巧

Runtime提供了一个调试函数 instrumentObjcMessageSends,可以让我们查看消息发送和转发的详细过程:

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 开启消息发送日志
        instrumentObjcMessageSends(YES);
        
        Person *person = [[Person alloc] init];
        [person run];
        
        // 关闭日志
        instrumentObjcMessageSends(NO);
    }
    return 0;
}

运行程序后,在 /tmp/msgSends- 目录下会生成日志文件,内容类似于:

+ Person NSObject initialize
+ Person NSObject new
- Person NSObject init
- Person forwardingTargetForSelector: run
- Person methodSignatureForSelector: run
- Person forwardInvocation:
- Person doesNotRecognizeSelector: run

通过这个日志,我们可以清楚地看到消息转发的每一步调用过程,对于理解和调试消息转发非常有帮助。


第四章:消息转发的应用场景

消息转发机制不仅仅是理论上的知识点,它在实际开发中有很多实用的应用场景。

4.1 防止崩溃:安全的消息调用

一个常见的应用场景是防止因为调用未实现方法而导致的崩溃。例如,我们可以创建一个安全的代理对象,当目标对象不能响应某个消息时,不是崩溃而是返回一个默认值:

@interface SafeProxy : NSObject
@property (nonatomic, weak) id target;
@end

@implementation SafeProxy
- (id)forwardingTargetForSelector:(SEL)aSelector {
    // 如果target可以响应,直接转发
    if ([_target respondsToSelector:aSelector]) {
        return _target;
    }
    return self; // 让完整转发来处理
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    // 为任何方法提供默认签名(返回对象类型)
    return [NSMethodSignature signatureWithObjCTypes:"@@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 不处理消息,只设置返回值为nil
    id nilValue = nil;
    [anInvocation setReturnValue:&nilValue];
}
@end

使用这个SafeProxy,我们可以安全地调用任何方法:

Person *person = [[Person alloc] init];
SafeProxy *proxy = [[SafeProxy alloc] init];
proxy.target = person;

// 如果person实现了run方法,正常执行
[proxy run]; 

// 如果person没有实现fly方法,不会崩溃,而是返回nil
id result = [proxy fly]; // result = nil,没有崩溃

4.2 模拟多继承

如前所述,通过消息转发可以实现类似多继承的效果。这在某些设计模式中非常有用,例如"装饰器"模式或"代理"模式。

4.3 API兼容性处理

在开发中,我们经常会遇到iOS系统版本升级导致API变化的情况。通过消息转发,我们可以优雅地处理这种变化:

@interface CompatibilityHandler : NSObject
@end

@implementation CompatibilityHandler
+ (void)load {
    // 交换forwardInvocation:方法
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class cls = [UIDevice class];
        SEL originalSelector = @selector(forwardInvocation:);
        SEL swizzledSelector = @selector(compatibility_forwardInvocation:);
        
        // 方法交换的实现...
    });
}

- (void)compatibility_forwardInvocation:(NSInvocation *)invocation {
    SEL selector = [invocation selector];
    
    if (selector == @selector(isLowPowerModeEnabled)) {
        // 低电量模式是iOS 9.0引入的
        if (@available(iOS 9.0, *)) {
            // 如果系统支持,转发给原始实现
            [invocation invoke];
        } else {
            // 如果不支持,返回默认值NO
            BOOL defaultValue = NO;
            [invocation setReturnValue:&defaultValue];
        }
    } else {
        // 其他消息正常转发
        [self compatibility_forwardInvocation:invocation];
    }
}
@end

4.4 实现AOP(面向切面编程)

通过消息转发,我们可以实现简单的AOP编程,在不修改原有类的情况下添加额外的逻辑:

@interface AspectProxy : NSObject
@property (nonatomic, strong) id target;
@property (nonatomic, copy) void (^beforeBlock)(SEL);
@property (nonatomic, copy) void (^afterBlock)(SEL);
@end

@implementation AspectProxy
- (id)forwardingTargetForSelector:(SEL)aSelector {
    // 必须返回nil才能进入完整转发
    return nil;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [_target methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    
    // 执行前置逻辑
    if (_beforeBlock) {
        _beforeBlock(selector);
    }
    
    // 转发给目标对象
    if ([_target respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:_target];
    }
    
    // 执行后置逻辑
    if (_afterBlock) {
        _afterBlock(selector);
    }
}
@end

4.5 实现动态代理

在RxSwift等响应式编程框架中,消息转发被广泛用于实现动态代理,拦截方法调用并将它们转换为信号流:

// RxSwift中拦截方法的简化实现
@interface RXMessageSentObserver : NSObject
// ... 
@end

@implementation _RXObjCRuntime
- (void)interceptMethod:(SEL)selector ofClass:(Class)cls {
    // 1. 创建子类
    // 2. 重写forwardInvocation:
    // 3. 在forwardInvocation:中创建信号
}
@end

4.6 JSPatch等热修复框架的实现原理

热修复框架如JSPatch利用消息转发机制来实现动态替换OC方法的实现。基本原理是:

  1. 将要修复的类的 forwardInvocation: 方法替换为自己的实现
  2. 将原方法的IMP指向 _objc_msgForward,强制进入消息转发流程
  3. forwardInvocation: 中,执行JavaScript代码

第五章:性能考量与最佳实践

消息转发机制虽然强大,但使用不当可能会带来性能问题。

5.1 性能开销分析

不同阶段的消息转发性能开销不同:

阶段 性能开销 主要原因
正常消息发送 极小 直接查找IMP并调用
动态方法解析 较小 只执行一次,后续调用正常
快速消息转发 中等 需要调用Cocoa方法,但流程简单
完整消息转发 较大 需要创建NSInvocation对象,处理参数和返回值

为什么完整消息转发开销大

  1. 需要调用 methodSignatureForSelector: 获取方法签名
  2. Runtime需要根据方法签名创建 NSInvocation 对象
  3. NSInvocation 需要拷贝参数和设置返回值
  4. 整个流程涉及多次Objective-C方法调用

5.2 性能优化建议

根据性能开销,我们应遵循以下最佳实践:

  1. 优先使用快速消息转发:如果只是简单地将消息转发给另一个对象,尽量使用 forwardingTargetForSelector:,避免使用完整转发。

  2. 缓存方法签名:如果在完整转发中经常处理同一类消息,可以缓存方法签名,避免每次调用 methodSignatureForSelector:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    static NSMutableDictionary *signatureCache;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        signatureCache = [NSMutableDictionary dictionary];
    });
    
    NSString *selString = NSStringFromSelector(aSelector);
    NSMethodSignature *signature = signatureCache[selString];
    if (!signature) {
        signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        signatureCache[selString] = signature;
    }
    return signature;
}
  1. 避免频繁触发转发:如果一个方法经常被调用,最好不要依赖消息转发来处理它。考虑在 resolveInstanceMethod: 中动态添加IMP,这样后续调用就和正常方法一样快了。

5.3 调试消息转发

当遇到与消息转发相关的bug时,可以使用以下调试技巧:

  1. 使用instrumentObjcMessageSends:开启日志,查看消息转发的每一步。

  2. 添加日志输出:在转发方法中添加日志:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"📱 Forwarding %@ to another target", NSStringFromSelector(aSelector));
    // ...
}
  1. 使用断点调试:在 forwardInvocation: 中设置断点,检查 NSInvocation 的内容。

  2. 检查方法签名:常见的崩溃原因是 methodSignatureForSelector: 返回了不正确的签名。可以使用以下代码验证签名:

NSMethodSignature *sig = [self methodSignatureForSelector:@selector(someMethod:)];
NSLog(@"Signature: %s", [sig methodReturnType]); // 检查返回类型
for (NSUInteger i = 0; i < [sig numberOfArguments]; i++) {
    NSLog(@"Arg %lu: %s", i, [sig getArgumentTypeAtIndex:i]);
}

5.4 与其他动态特性的比较

消息转发与Objective-C的其他动态特性既有联系又有区别:

特性 目的 触发时机
消息转发 处理无法识别的消息 方法查找失败后
方法交换 交换两个方法的IMP 运行时主动执行
动态添加方法 为类添加新方法 运行时主动执行
KVO 监听属性变化 创建子类并重写setter

重要区别

  • 消息转发是被动的,只有在正常消息发送失败后才会触发
  • 方法交换、动态添加方法是主动的,我们可以在任何时候执行
  • KVO是利用Runtime创建子类并重写方法,本质上也是动态特性的一种应用

第六章:面试深度解析

消息转发是iOS面试中的高级话题。下面梳理一些常见的面试题和深度解析。

6.1 基础问题

Q1:OC中给nil对象发送消息会发生什么?

解析:给nil发送消息是安全的,不会崩溃。Runtime在 objc_msgSend 中会首先检查接收者是否为nil,如果是nil,直接返回。返回值的类型取决于方法声明的返回类型:

  • 如果返回对象类型,返回nil
  • 如果返回整型,返回0
  • 如果返回结构体,返回的结构体各字段都是0
  • 如果返回浮点类型,返回0.0

Q2:unrecognized selector sent to instance 这个异常是怎么产生的?

解析:当向一个对象发送它无法处理的消息,且消息转发机制也无法处理时,Runtime最终会调用 doesNotRecognizeSelector: 方法。NSObject 中该方法的默认实现就是抛出这个异常。也就是说,这个异常是消息转发流程失败的最后结果。

Q3:消息转发分哪几个阶段?每个阶段的作用是什么?

解析:消息转发分为三个阶段:

  1. 动态方法解析:调用 resolveInstanceMethod:/resolveClassMethod:,允许开发者动态添加方法实现。

  2. 快速消息转发:调用 forwardingTargetForSelector:,允许将消息转发给另一个对象。

  3. 完整消息转发:调用 methodSignatureForSelector: 获取方法签名,然后创建 NSInvocation 对象并调用 forwardInvocation:,允许修改消息内容或转发给多个对象。

6.2 进阶问题

Q4:如何在运行时动态添加方法?

解析:在 resolveInstanceMethod: 中使用 class_addMethod 函数:

void dynamicMethodIMP(id self, SEL _cmd) {
    NSLog(@"动态添加的方法被调用");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(dynamicMethod)) {
        class_addMethod(self, sel, (IMP)dynamicMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

Q5:快速转发和完整转发有什么区别?如何选择?

解析:主要区别在于:

  1. 需要重载的方法数量:快速转发只需重载 forwardingTargetForSelector:,完整转发需要重载 methodSignatureForSelector:forwardInvocation: 两个方法。

  2. 功能强大程度:快速转发只能简单地改变消息接收者,不能修改消息内容;完整转发可以修改消息的参数、选择器、返回值等。

  3. 性能开销:快速转发性能更好,完整转发需要创建 NSInvocation 对象,开销较大。

选择建议

  • 如果只是想将消息转发给另一个对象,且不需要修改消息内容,优先使用快速转发
  • 如果需要修改消息内容、参数、返回值,或者需要将消息转发给多个对象,使用完整转发

Q6:消息转发可以用来实现多重继承吗?和真正的多重继承有什么区别?

解析:可以通过消息转发实现类似多重继承的效果。区别在于:

  • 真正的多重继承是将多个类的功能合并到一个对象中
  • 通过消息转发实现的"伪多继承",功能仍然分散在不同的对象中,只是通过转发机制让外部看起来像一个对象处理了所有消息

Q7:如果消息转发的方法本身也找不到实现会怎样?

解析:这是一个容易忽略的细节。如果消息转发的方法(如 forwardingTargetForSelector:)本身没有实现,Runtime也会按照同样的流程查找它的实现。如果找不到,同样会触发消息转发。但通常情况下,这些方法在 NSObject 中都有默认实现,所以不会出现这种情况。

Q8:如何调试消息转发过程?

解析:可以使用以下方法:

  1. 使用 instrumentObjcMessageSends(YES) 开启日志
  2. 查看 /tmp/msgSends- 目录下的日志文件
  3. 在转发方法中添加断点和日志输出
  4. 使用反汇编工具分析 __forwarding__ 函数

6.3 高级问题

Q9:消息转发和method swizzling有什么关系?能结合使用吗?

解析:消息转发和method swizzling是两种不同的动态特性,但可以结合使用。例如,可以实现一个通用的方法拦截机制:

// 1. 先将原方法的IMP替换为_objc_msgForward
Method method = class_getInstanceMethod(cls, originalSelector);
method_setImplementation(method, _objc_msgForward);

// 2. 再添加一个转发方法
class_addMethod(cls, @selector(customForward:), (IMP)customForwardIMP, "v@:@");

// 3. 交换forwardInvocation:方法
Method originalForwardMethod = class_getInstanceMethod(cls, @selector(forwardInvocation:));
Method swizzledForwardMethod = class_getInstanceMethod(cls, @selector(customForwardInvocation:));
method_exchangeImplementations(originalForwardMethod, swizzledForwardMethod);

这种技术被用于RxSwift等框架的方法拦截功能。

Q10:如何实现一个通用的消息转发中心,能够记录所有无法识别的消息?

解析:可以创建一个基类,所有需要日志功能的类都继承自这个基类:

@interface LoggingBase : NSObject
@property (nonatomic, strong) NSMutableArray *unrecognizedMessages;
@end

@implementation LoggingBase
- (instancetype)init {
    if (self = [super init]) {
        _unrecognizedMessages = [NSMutableArray array];
    }
    return self;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    // 记录无法识别的消息
    NSString *message = [NSString stringWithFormat:@"%@: %@", self, NSStringFromSelector(aSelector)];
    [_unrecognizedMessages addObject:message];
    
    // 可以选择转发给默认处理对象
    return [DefaultHandler sharedHandler];
}

// 可以添加一个方法来导出日志
- (void)exportUnrecognizedMessages {
    NSLog(@"Unrecognized messages: %@", _unrecognizedMessages);
}
@end

Q11:消息转发机制在ARC下有什么特别需要注意的地方?

解析:ARC下使用消息转发时需要注意:

  1. 内存管理:在 forwardInvocation: 中处理对象参数时,ARC会自动处理内存管理,但要注意不要造成循环引用。

  2. 方法签名:方法签名的类型编码必须准确,特别是在有对象参数或返回值时。错误的类型编码可能导致ARC下的内存管理错误。

  3. 返回值处理:当从 forwardInvocation: 返回时,Runtime会根据方法签名自动处理返回值的retain/release。如果方法签名不准确,可能导致内存泄漏或崩溃。

  4. 使用 __unsafe_unretained:在某些情况下,可能需要使用 __unsafe_unretained 来避免ARC自动插入的retain/release操作干扰转发逻辑。

Q12:从源码层面分析,消息转发和消息发送的性能差异主要体现在哪些方面?

解析:从源码层面看,性能差异主要体现在:

  1. 正常消息发送:汇编实现,查找缓存后直接跳转,几条指令就能完成。

  2. 动态方法解析:需要调用Objective-C方法,但只执行一次,后续调用恢复正常。

  3. 快速转发:需要调用 forwardingTargetForSelector:,这是一个完整的Objective-C方法调用,涉及消息发送流程。但无需创建复杂的对象。

  4. 完整转发

    • 需要调用 methodSignatureForSelector: 获取签名
    • Runtime需要遍历方法签名,解析每个参数的类型
    • 创建 NSInvocation 对象需要分配内存
    • NSInvocation 需要拷贝参数值
    • 调用 forwardInvocation: 方法
    • 转发后需要处理返回值

这些步骤加起来,完整转发的性能开销可能是正常消息发送的几十倍甚至上百倍。


第七章:总结与展望

7.1 消息转发机制的核心价值

Objective-C的消息转发机制是其动态性的集中体现,它给了开发者三次机会来处理无法识别的消息:

  1. 动态方法解析:让我们能够在运行时动态添加方法实现
  2. 快速消息转发:让我们能够将消息简单地转发给其他对象
  3. 完整消息转发:让我们能够完全掌控消息的处理过程

这三次机会形成了一个从简单到复杂的递进结构,开发者可以根据需求选择合适的层次进行干预。

7.2 设计思想解读

消息转发机制的设计体现了几个重要的软件工程思想:

  1. 容错性:系统提供了容错机制,允许程序在出现问题时尝试恢复,而不是直接崩溃。

  2. 渐进式干预:提供了三个层次的干预机会,每个层次都有不同的复杂度和能力,开发者可以根据需要选择。

  3. 开闭原则:通过消息转发,我们可以在不修改原有类的情况下,扩展类的功能,符合开闭原则。

  4. 责任链模式:消息转发本质上是一个责任链模式的实现,每个阶段都有机会处理消息,如果处理不了就传递给下一阶段。

7.3 与其他语言动态特性的对比

与其他动态语言相比,Objective-C的消息转发机制有独特之处:

语言 类似特性 特点
Objective-C 消息转发 分三个阶段,功能强大,与Runtime紧密结合
Ruby method_missing 类似forwardInvocation:,但更简洁
Python getattr 属性访问的fallback机制
JavaScript Proxy 可以拦截对象的各种操作

其中,Ruby的 method_missing 和Objective-C的 forwardInvocation: 最为相似。不同之处在于,Objective-C提供了更细粒度的控制(三个阶段),而Ruby只提供了一个统一的入口。

7.4 未来展望

随着Swift的兴起,Objective-C的使用场景在减少,但消息转发机制的设计思想仍然值得学习:

  1. Swift的动态特性:Swift虽然强调静态类型安全,但也提供了反射机制和 @objc 动态特性。理解消息转发有助于理解Swift中与Objective-C交互的部分。

  2. 跨平台开发:像Flutter这样的跨平台框架,在实现平台通道时也借鉴了消息转发的思想。

  3. AOP编程:面向切面编程在现代开发中越来越重要,消息转发是实现AOP的基础技术之一。

7.5 最后的思考

消息转发机制是Objective-C Runtime皇冠上的明珠,它展示了动态语言的强大能力。掌握消息转发,不仅能帮助我们写出更健壮的代码,还能让我们更深入地理解Objective-C的设计哲学。

在实际开发中,我们应当合理使用消息转发机制:

  • 在需要的地方使用,但不要滥用
  • 优先考虑性能更好的方案(如快速转发优先于完整转发)
  • 做好日志和调试,确保转发逻辑正确

最终,消息转发机制体现了编程语言设计中的一个重要思想:给予开发者更多的控制权,同时也赋予更多的责任。当我们决定使用消息转发时,我们实际上是在说:"我知道这条消息可能无法被正常处理,但我有办法解决这个问题。"

这种思想超越了具体的编程语言,是每个优秀程序员都应该具备的能力——在系统无法自动处理的情况发生时,能够提供优雅的降级方案。


参考资料

  1. Apple官方文档:forwardInvocation:
  2. Objective-C Runtime源码 (objc4-818.2)
  3. 《Effective Objective-C 2.0》 - Matt Galloway
  4. 《Objective-C Runtime Programming Guide》 - Apple Inc.

iOS NotificationCenter Observer 的隐性性能代价

引言

iOS 9 之后,Apple 为 NSNotificationCenter 的 target-action 模式引入了 zeroing weak reference。当 observer 对象被释放后,系统自动将内部的 weak reference 置为 nil,不再向其投递通知,也不会产生野指针 crash。

Apple 文档对此也有明确说明:

If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its dealloc method.

这条规则被广泛接受,许多团队因此在代码规范中不再严格要求管理 observer 生命周期。然而,"不会 crash"并不等于"没有性能影响"。本文通过一个真实的线上卡死案例,探讨 NotificationCenter observer 管理不当可能带来的隐性性能问题。

一个线上卡死案例

在一次线上卡死监控中,我们发现一类卡死的比例明显上升。主线程被卡住 13 秒,CPU 占用 98.8%,处于 running 状态(不是锁等待)。堆栈顶部如下:

_weak_unregister_no_lock
_objc_moveWeak
__CFXNotificationRegistrarAddObserver
SomeTimeViewModel.componentMount()     ← 调用 NotificationCenter.default.addObserver(...)

卡死发生在一个 ViewModel 的初始化阶段——调用 addObserver(self, selector:, name:, object:) 注册通知时。注册一个通知 observer 本身应该是一个非常轻量的操作,为什么会导致 13 秒的卡死?

经过排查,这个问题的根因并不是"忘记 removeObserver 导致 dead entries 累积"(事实上代码在 dealloc 链路中已经正确调用了 removeObserver),而是一个更容易被忽视的问题:同一个通知名下积累了过多的 live observers

NotificationCenter 的内部机制

要理解这个问题,需要了解 NSNotificationCenter 在 iOS 9+ 中的内部机制。

注册表结构

当调用 addObserver:selector:name:object: 时,NotificationCenter 在内部维护一个注册表(registrar),按通知名称索引,存储所有注册信息。每条注册信息大致包括:

  • 一个指向 observer 的 weak reference
  • selector
  • notification name
  • object filter

这些信息存储在 CoreFoundation 内部的数据结构(类似于哈希表 + 数组)中。

扩容与 weak reference 迁移

和 HashMap 类似,NotificationCenter 的内部存储在容量不足时会扩容——分配更大的存储空间,将现有条目迁移到新位置。

对于包含 weak reference 的条目,迁移过程需要通过 ObjC runtime 的 objc_moveWeak 将 weak reference 从旧内存地址搬迁到新地址。这个操作涉及:

  1. _weak_unregister_no_lock:从 runtime 的 side table 中注销旧地址
  2. _weak_register_no_lock:在 side table 中注册新地址

单次操作很快,但如果某个通知名下积累了大量条目,扩容时需要逐个迁移所有 live entries 的 weak reference,累积耗时就可能达到秒级。

两种问题模式

NotificationCenter 的 entries 膨胀来自两个方面,它们可以独立存在,也可以叠加:

模式一:dead entries 累积(不调 removeObserver 的短生命周期对象)

当 observer 被释放时,其 weak reference 自动置 nil,但注册条目本身不会被移除。对于频繁创建和销毁的对象(如 Feed 中的各类 Component),如果不在 deinit 中 removeObserver,NotificationCenter 会持续累积 dead entries。

模式二:live entries 过多(长生命周期对象大量注册同一通知)

即使每个 observer 都正确管理了 removeObserver,如果大量长生命周期对象同时注册同一个通知,live entries 的数量本身就可能很大。

案例分析

回到开头的卡死案例。我们排查后发现,触发卡死的通知名 .tabBarDidChangeSelectedIndex 在整个 App 中有 31 个文件 注册了 observer,涵盖 Feed、社交、个人资料页、IM、Notice、电商 等几乎所有主要模块。

在 IM 模块的 会话 列表中,架构设计如下:

  • 每个会话对应一个持久化的 ViewModel(存储在字典中,不会被频繁销毁)
  • 每个 ViewModel 在 init 时创建一棵包含 50+ 子组件的组件树
  • 其中 ViewModel 本身和 TimeViewModel 各注册了一次 .tabBarDidChangeSelectedIndex

对于一个有 200 个会话的用户,仅 会话 列表就贡献了 400 个 live observers。这些 observer 的生命周期管理是正确的(dealloc 时通过组件树的 unmount 链路移除),但它们的数量本身就是问题。

再叠加 App 其他模块的 observer(包括可能存在的 dead entries),这个通知名下的总 entries 数量相当可观。当新增一个 observer 触发内部存储扩容时,迁移所有 entries 的累积耗时就造成了 13 秒的卡死。

被忽视的关键点

这个案例有一个容易被忽视的教训:即使 observer 生命周期管理完全正确(deinit 中有 removeObserver),也不意味着没有性能风险。 问题不在于单个 observer 的正确性,而在于同一个通知名下的 observer 总量。

哪些场景容易踩坑

1. 热门通知名 + 大量模块共同注册

像 Tab 切换、App 前后台、网络状态变化这类全局通知,往往被 App 中大量模块同时监听。每个模块的注册看起来都合理,但总量可能超出预期。

2. 持久化对象在 init 阶段无差别注册

如果一个对象会存在很久(如 1:1 对应数据模型的 ViewModel),且在 init 阶段就注册通知,那么所有实例的 observer 都会持续存在。即使只有屏幕上可见的几个实例真正需要响应通知,其余实例的注册也在白白增加 entries 总量。

3. 短生命周期对象不调 removeObserver

对于频繁创建和销毁的对象(如 Feed 滑动过程中的各种 Component),如果不在 deinit 中 removeObserver,每次销毁都会留下一个 dead entry。随着用户使用时间增长,dead entries 不断累积。

4. 组件树放大效应

在 TTKC 等组件化框架中,一个容器可能包含数十个子组件,每个子组件可能独立注册通知。容器的数量 × 子组件的数量 = 总 observer 数量,放大效应显著。

建议

按需注册:只为可见的实例注册 observer

对于列表中的 ViewModel,如果通知只用于更新 UI 展示(如刷新时间文本),那么只有屏幕上可见的实例才需要注册。可以在 Cell 即将显示时注册,在不可见时移除:

override func cellWillDisplay() {
    super.cellWillDisplay()
    NotificationCenter.default.addObserver(self, selector: #selector(onTabBarChange(_:)),
                                           name: .tabBarDidChangeSelectedIndex, object: nil)
}

override func cellDidEndDisplay() {
    super.cellDidEndDisplay()
    NotificationCenter.default.removeObserver(self, name: .tabBarDidChangeSelectedIndex, object: nil)
}

对于 200 个会话的用户,这将 observer 数量从 200 减少到 ~10-20(可见 Cell 数量)。

集中式 observer:N 个独立注册 → 1 个集中处理

如果多个同类对象都需要响应同一个通知,考虑用一个集中式 observer 替代 N 个独立注册:

// 在 DataController 中注册一次
NotificationCenter.default.addObserver(self, selector: #selector(onTabBarChange(_:)),
                                       name: .tabBarDidChangeSelectedIndex, object: nil)

@objc func onTabBarChange(_ notification: NSNotification) {
    for viewModel in viewModelDict.values {
        viewModel.handleTabBarChange()
    }
}

N 个 observer 注册变为 1 个,彻底消除了这个通知名下的数量问题。

对于短生命周期对象:在 deinit 中 removeObserver

deinit {
    NotificationCenter.default.removeObserver(self)
}

这一行代码的作用不是防 crash(iOS 9+ 不需要),而是及时清理 NotificationCenter 内部的注册条目,避免 dead entries 累积。

考虑使用 block-based API + 显式 token 管理

block-based API 返回一个 opaque token,移除时通过 token 精确定位,语义更清晰:

private var observerToken: NSObjectProtocol?

func setup() {
    observerToken = NotificationCenter.default.addObserver(
        forName: .someNotification, object: nil, queue: .main
    ) { [weak self] _ in
        self?.handleNotification()
    }
}

deinit {
    if let token = observerToken {
        NotificationCenter.default.removeObserver(token)
    }
}

需要注意的是,block-based API 不使用 zeroing weak reference——如果 block 中 strong capture 了 self,会导致循环引用。block 中必须使用 [weak self],且必须在合适时机 remove token。

在 Code Review 中关注

建议在 Code Review 中对以下模式保持敏感:

  • 这个通知名在 App 中有多少处注册?是否是"热门通知"?
  • 注册 observer 的对象有多少个实例同时存在?
  • 是否在 init 阶段就注册,但实际上只在可见时才需要?
  • 短生命周期对象是否在 deinit 中调了 removeObserver?

小结

NotificationCenter 的性能问题有两个维度:

  1. 单个 observer 的生命周期管理:短生命周期对象不调 removeObserver,导致 dead entries 累积
  2. 同一通知名下的 observer 总量:即使每个 observer 都正确管理了生命周期,大量 live observers 本身就是性能风险

第一个问题比较符合直觉,容易在 Code Review 中发现。第二个问题更隐蔽——每个模块的注册看起来都合理,但当一个大型 App 中有几十个模块同时注册同一个通知时,总量就可能超出 NotificationCenter 内部数据结构的性能安全边界。

Apple 的"不需要 removeObserver"是关于正确性的保证,不是关于性能的保证。在大型 App 中,NotificationCenter observer 需要像内存一样被视为一种有限资源来管理。

❌