iOS 开发核心知识点全解析(面试必备)

26 阅读25分钟

iOS 开发的深度面试往往围绕运行时机制、内存管理、多线程、视图渲染、架构设计等核心领域展开。本文将系统梳理这些领域的高频问题,并提供清晰、完整、可直接用于面试的答案,帮助开发者构建扎实的知识体系。

一、Runtime 核心原理

Runtime(运行时)是 OC 的灵魂,负责对象的创建、方法调用、消息转发等底层操作。其开源源码可参考苹果官方的objc4仓库。

1. Runtime 内存模型(isa、对象、类、metaclass)

OC 的内存模型以isa 指针为核心,串联起实例对象、类对象、元类(metaclass)  三层结构,每层对应不同的结构体,存储不同信息。

(1)核心结构关系

plaintext

实例对象(Instance)-> 类对象(Class)-> 元类(Metaclass)-> 根元类(Root Metaclass)
  • 实例对象(Instance) :存储成员变量(ivar)  的值,其isa指针指向对应的类对象
    结构体简化:

    objc

    struct Instance {
        Class isa; // 指向类对象
        // 成员变量的值(如NSString *name; int age;)
    };
    
  • 类对象(Class) :存储实例方法(-method)、属性(property)、协议(protocol) ,其isa指针指向元类,同时包含指向父类的superclass指针。
    结构体核心依赖class_data_bits_t,内部通过data()方法获取class_rw_t(可读写数据):

    objc

    struct objc_class {
        Class isa;         // 指向元类
        Class superclass;  // 指向父类
        class_data_bits_t bits; // 存储类的核心数据
    };
    
  • 元类(Metaclass) :存储类方法(+method) ,其isa指针指向根元类(如NSObject的元类),superclass指向父类的元类。
    元类的本质是 “类的类”—— 因为类对象也是 OC 对象(可调用+method),需要元类来管理其方法。

  • 根元类(Root Metaclass) :所有元类的最终父类(如NSObject的元类),其isa指针指向自身superclass指向根类(如NSObject)。

(2)isa 指针的作用

  • 本质是Class类型的指针,用于定位对象的 “所属类”

    • 实例对象的isa → 类对象(确定实例能调用哪些实例方法);
    • 类对象的isa → 元类(确定类能调用哪些类方法)。
  • 64 位系统中,isa 指针通过位掩码存储额外信息(如对象是否在堆上、引用计数等),需通过ISA_MASK提取真实的类地址。

2. 为什么要设计 metaclass?

核心目的是解决 “类方法的存储归属” 问题

  • OC 中,实例方法的调用依赖实例对象的isa找到类对象,类对象存储实例方法列表;
  • 类方法(如+alloc)的调用者是 “类对象”,而类对象本身也是 OC 对象(可被isa指向),因此需要一个专门的 “类”(元类)来存储类方法列表。
  • 若没有 metaclass,类方法将无处存储,导致[NSObject alloc]这类调用无法实现。

3. class_copyIvarList & class_copyPropertyList 区别

两者均用于获取类的成员信息,但针对的对象和返回内容完全不同,核心区别如下:

对比维度class_copyIvarListclass_copyPropertyList
获取的内容成员变量(ivar)属性(property)
本质区别编译时定义的 “底层变量”(如_name封装后的 “属性”(含 setter/getter)
是否包含合成变量是(如@property合成的_name是(直接返回属性本身)
访问权限可获取私有 ivar(如类内部定义的int _age仅获取属性(私有 property 也可获取)
返回类型Ivar *(成员变量指针数组)objc_property_t *(属性指针数组)

示例
若类定义为@interface Person : NSObject { int _weight; } @property (nonatomic, copy) NSString *name; @end,则:

  • class_copyIvarList返回_weight_name(合成的 ivar);
  • class_copyPropertyList仅返回name(属性)。

4. class_rw_t 和 class_ro_t 的区别

两者均是类对象的核心数据结构,存储方法、属性、协议等信息,但核心区别在于读写权限和初始化时机

对比维度class_rw_t(Read-Write)class_ro_t(Read-Only)
读写权限可读写(运行时可修改)只读(编译时确定,不可修改)
初始化时机运行时(类第一次被使用时初始化)编译时(编译器生成,存储在 Mach-O 的__DATA段)
存储内容包含class_ro_t的指针 + 运行时添加的方法 / 属性 / 协议(如 Category 的内容)编译时确定的 “固定信息”:初始方法列表、属性列表、协议列表、成员变量信息
核心作用支持动态添加内容(如 Category、Method Swizzle)存储类的 “静态基础信息”,确保编译后不可篡改

关系class_rw_t内部有一个const class_ro_t *ro指针,指向类的只读基础数据;运行时动态添加的内容(如 Category 的方法)会直接存入class_rw_t

5. Category 加载流程 & 方法优先级

Category(分类)是 OC 中动态扩展类功能的核心机制,其加载和方法调用有严格的顺序规则。

(1)Category 加载流程(运行时阶段)

  1. 编译时:编译器将 Category 编译为category_t结构体,存储分类的方法列表、属性列表、协议列表,以及所属的类名。
    category_t结构体简化:

    objc

    struct category_t {
        const char *name;       // 所属类名
        classref_t cls;         // 所属类(运行时绑定)
        struct method_list_t *instance_methods; // 实例方法
        struct method_list_t *class_methods;    // 类方法
        struct protocol_list_t *protocols;      // 协议
        struct property_list_t *properties;     // 属性
    };
    
  2. 运行时(map_images 阶段)

    • dyld(动态链接器)加载完所有类和分类后,调用_objc_init初始化 Runtime;
    • Runtime 通过_processCatlist遍历所有category_t,将分类的方法、属性、协议合并到所属类的class_rw_t(实例方法合并到类的instance_methods,类方法合并到元类的class_methods)。
  3. 合并规则
    分类的方法会插入到类原有方法列表的前面(而非替换),因此分类方法会 “覆盖” 类的同名方法(实际是优先调用)。

(2)Category 的 load 方法加载顺序

+load方法是 Category 中特殊的方法,不遵循消息转发机制,由 Runtime 直接调用,顺序规则如下:

  1. 类的 load 先于分类的 load:先调用所有类的+load(父类 → 子类),再调用所有分类的+load
  2. 同类分类的 load 按编译顺序:Xcode 编译时,后添加到项目的分类,其+load先被调用(可通过 “Build Phases → Compile Sources” 调整顺序);
  3. 不同类分类的 load 按类的加载顺序:依赖类的加载顺序(如 A 类依赖 B 类,则 B 类的分类+load先调用)。

(3)Category 同名方法的调用顺序

当多个分类(或类与分类)有同名方法时,调用顺序遵循 “后编译的分类优先”:

  1. 分类方法覆盖类的同名方法(因分类方法在方法列表前面);

  2. 多个分类的同名方法,后编译的分类方法先被调用(编译顺序可通过 Xcode 调整);

  3. 父类分类的方法优先级低于子类的分类(因子类的类加载晚于父类)。

注意:分类无法覆盖+load+initialize方法(+initialize遵循消息转发,会先调用父类的)。

6. Category & Extension 区别 + 能否给 NSObject 添加 Extension?

(1)核心区别

对比维度Category(分类)Extension(扩展)
能否添加成员变量不能(仅能添加方法、属性、协议,属性不会自动合成 ivar,需手动关联)能(可添加私有成员变量、方法、属性)
可见性公开(需在.h 中声明,或匿名分类在.m 中)私有(仅在定义的.m 文件中可见)
编译时机运行时合并到类中编译时作为类的一部分(与类同时编译)
是否需要实现可单独实现(.m 文件)必须在类的.m 文件中实现(否则编译报错)
核心用途扩展已有类的功能(如给 UIView 加分类)给类添加私有成员(如在.m 中隐藏细节)

(2)能否给 NSObject 添加 Extension?

不能直接添加,原因如下:

  • Extension 是类的 “一部分”,必须在类的定义文件(.m)中声明和实现

  • NSObject 是系统类,开发者无法修改其.m 文件,因此无法直接为其添加 Extension;

  • 若强行在自己的文件中声明@interface NSObject () { int _myVar; } @end,编译时会报错(“Category is not allowed on 'NSObject'”)。

替代方案:若需给 NSObject 添加私有成员,可通过 “匿名分类 + 关联对象” 实现,或自定义 NSObject 的子类。

7. 消息转发机制 + 与其他语言对比

OC 的方法调用本质是 “发送消息”(objc_msgSend),当消息无法被接收者处理时,会触发消息转发机制,避免崩溃。

(1)消息转发三阶段(完整流程)

在进入转发前,会先进行方法查找

  1. 从接收者的类的缓存(cache_t)  中查找方法(快速查找);

  2. 缓存未命中,从类的class_rw_t的方法列表中查找,若未找到则递归查找父类(直到根类NSObject);

  3. 若所有父类均未找到,进入动态方法解析 → 快速转发 → 慢速转发三阶段。

具体转发流程:

  1. 动态方法解析(Resolve)

    • 调用+resolveInstanceMethod:(实例方法)或+resolveClassMethod:(类方法),允许开发者动态添加方法实现
    • 示例:若[person run]未实现,可在resolveInstanceMethod中用class_addMethod添加run的实现;
    • 若返回YES,则重新发起消息查找;若返回NO,进入下一阶段。
  2. 快速转发(Fast Forwarding)

    • 调用-forwardingTargetForSelector:,允许开发者将消息转发给其他对象(“替身”);
    • 示例:返回self.otherObject,则消息会转发给otherObject处理;
    • 若返回非nil,则消息转发给该对象;若返回nil,进入下一阶段。
  3. 慢速转发(Slow Forwarding)

    • 调用-methodSignatureForSelector:,获取方法签名(返回值类型、参数类型);
    • 若返回nil,则触发崩溃(unrecognized selector sent to instance);
    • 若返回有效签名,调用-forwardInvocation:,开发者可在该方法中自定义消息处理逻辑(如转发给多个对象、记录日志)。

(2)与其他语言(如 Java)的消息机制对比

对比维度OC(消息转发)Java(方法调用)
绑定时机运行时绑定(动态)编译时绑定(静态,除非用反射)
方法不存在的处理触发消息转发,可自定义处理(避免崩溃)编译时报错(若未声明)或运行时抛NoSuchMethodError
灵活性高(支持动态添加方法、转发消息)低(需提前声明方法,反射仅能绕过编译检查)
性能略低(运行时查找和转发有开销)高(编译时确定方法地址)
崩溃风险可通过转发避免崩溃无法避免(除非用 try-catch 捕获异常)

8. 方法调用前的准备:消息查找流程

在 “动态解析→消息转发” 之前,Runtime 会先执行消息查找流程(分为快速查找和慢速查找),这是方法调用的核心前置步骤:

  1. 快速查找(缓存查找)

    • 调用objc_msgSend时,先从接收者的类的cache_t(缓存)中查找方法;
    • cache_t是哈希表,key 为SEL(方法选择器),value 为IMP(方法实现指针);
    • 若找到IMP,直接跳转到实现执行;若未找到,进入慢速查找。
  2. 慢速查找(方法列表查找)

    • 从类的class_rw_tmethod_list中遍历查找SEL(按方法列表顺序);
    • 若未找到,递归查找父类的method_list(直到根类NSObject);
    • 若找到,将SELIMP存入当前类的cache_t(缓存,供下次快速查找),然后执行IMP
    • 若所有父类均未找到,进入动态方法解析和消息转发。

9. IMP、SEL、Method 的区别和使用场景

三者是 Runtime 中描述 “方法” 的核心概念,关系为:Method包含SELIMPSEL是方法标识,IMP是方法实现地址。

概念定义本质核心作用使用场景
SEL方法选择器(typedef const struct objc_selector *SEL;字符串(方法名的哈希值)唯一标识一个方法(如@selector(run)方法调用(objc_msgSend(person, @selector(run)))、判断方法是否存在([person respondsToSelector:@selector(run)]
IMP方法实现指针(typedef id (*IMP)(id, SEL, ...);函数指针指向方法的具体实现代码直接调用方法(跳过消息查找,如IMP imp = [person methodForSelector:@selector(run)]; imp(person, @selector(run));)、Method Swizzle
Method方法结构体(typedef struct objc_method *Method;包含 SEL、IMP、方法签名封装方法的完整信息获取方法详情(如method_getName(method)获取 SEL、method_getImplementation(method)获取 IMP)、动态添加方法(class_addMethod

关系示例
Method method = class_getInstanceMethod([Person class], @selector(run));
SEL sel = method_getName(method); // 获取 SEL
IMP imp = method_getImplementation(method); // 获取 IMP

10. load、initialize 方法的区别(含继承关系)

+load+initialize是类初始化时的两个特殊方法,但触发时机、调用逻辑、继承行为完全不同。

(1)核心区别

对比维度+load 方法+initialize 方法
触发时机类 / 分类被加载到内存时(dyld 阶段)类第一次接收消息时(如[Person alloc]
调用方式Runtime 直接调用(不经过 objc_msgSend)经过消息转发(objc_msgSend)
是否自动调用父类是(父类 load 先于子类 load)否(仅当子类未实现时,才调用父类)
分类是否覆盖否(类和分类的 load 都会调用)是(分类的 initialize 会覆盖类的)
调用次数仅一次(类加载时)仅一次(类第一次使用时)
线程安全是(Runtime 加锁,串行调用)否(需手动加锁,避免多线程调用)

(2)继承关系中的区别

  • +load
    父类的+load先于子类的+load调用,且所有类的 load 调用完后,才调用分类的 load
    示例:NSObject → Person(子类) → Person+Category1 → Person+Category2(按编译顺序)。

  • +initialize
    仅当子类未实现+initialize时,才会调用父类的+initialize(因消息转发会先查找子类,子类未实现则找父类)。
    示例:

    objc

    @interface Father : NSObject @end
    @implementation Father
    + (void)initialize { NSLog(@"Father initialize"); }
    @end
    
    @interface Son : Father @end
    @implementation Son
    // 未实现initialize
    @end
    
    // 调用 [Son alloc] 时,会先调用 Father 的 initialize(因Son未实现)
    

    若子类实现了+initialize,则仅调用子类的,父类的不会被调用(除非父类单独被使用)。

11. 消息转发机制的优劣

(1)优点

  1. 灵活性高:允许动态添加方法、转发消息,适配复杂场景(如 “多继承” 模拟、解耦);
  2. 容错性强:可捕获 “未实现的方法”,避免崩溃(如在forwardInvocation中记录日志或返回默认值);
  3. 支持 AOP(面向切面编程) :通过转发机制在方法调用前后插入逻辑(如埋点、权限校验)。

(2)缺点

  1. 性能开销:消息查找(缓存→方法列表→父类)+ 转发(三阶段)会增加运行时开销,频繁触发会影响性能;
  2. 调试难度大:方法调用链路长,崩溃时的调用栈可能不完整(如转发后崩溃,难以定位原始调用者);
  3. 可读性差:动态转发逻辑隐藏在底层,代码维护成本高(如新人难以理解 “为什么未实现的方法能执行”)。

二、内存管理

iOS 内存管理的核心是引用计数,Runtime 通过SideTableautoreleasepool等机制实现自动管理,ARC 则进一步简化了开发者的操作。

1. weak 的实现原理 + SideTable 结构

weak是 OC 中用于避免循环引用的弱引用机制,其核心是通过SideTable管理弱引用表,确保对象释放时自动将weak指针置为nil

(1)SideTable 结构

SideTable是 Runtime 中的全局哈希表,每个对象的引用计数和弱引用均由SideTable管理,结构简化如下:

objc

struct SideTable {
    spinlock_t slock;          // 自旋锁(保证线程安全)
    RefcountMap refcnts;       // 引用计数表(key:对象指针,value:引用计数)
    weak_table_t weak_table;   // 弱引用表(存储所有指向该对象的weak指针)
};
  • spinlock_t:轻量级锁,适用于短时间持有(如修改引用计数时),避免线程竞争;
  • RefcountMapstd::unordered_map<DisguisedPtr<objc_object>, size_t>,存储对象的引用计数;
  • weak_table_t:弱引用表,结构为std::unordered_map<DisguisedPtr<objc_object>, weak_entry_t>weak_entry_t内部存储所有指向该对象的weak指针数组。

(2)weak 实现原理

  1. weak 指针赋值时(如__weak Person *weakP = person;):

    • Runtime 通过objc_storeWeak(&weakP, person)weakP添加到person对应的SideTableweak_table中;
    • personnil,则直接将weakP置为nil(不操作SideTable)。
  2. 对象释放时dealloc阶段):

    • 调用objc_clear_deallocating,从SideTable中找到该对象的weak_entry_t
    • 遍历weak_entry_t中的所有weak指针,将其置为nil
    • weak_table中删除该weak_entry_t,并清空引用计数表中的条目。
  3. 核心优势weak指针不会增加对象的引用计数,且对象释放时自动置为nil,避免野指针访问。

2. 关联对象的应用 + 系统实现 + 内存管理

关联对象(Associated Object)是 Category 中 “间接添加成员变量” 的机制,通过 Runtime API 将对象与另一个对象关联。

(1)关联对象的应用

  • 给 Category 添加 “成员变量” :Category 不能直接添加 ivar,但可通过关联对象存储数据;

  • 解耦数据存储:如给 UIView 关联一个NSString *identifier,无需继承 UIView;

  • 临时存储上下文:如网络请求回调中,将请求参数与回调 block 关联。

示例

objc

// 给UIView添加分类,关联identifier
@interface UIView (Identifier)
@property (nonatomic, copy) NSString *identifier;
@end

@implementation UIView (Identifier)
static const void *kIdentifierKey = &kIdentifierKey;

- (void)setIdentifier:(NSString *)identifier {
    // 关联对象:key=kIdentifierKey,value=identifier,策略=OBJC_ASSOCIATION_COPY_NONATOMIC
    objc_setAssociatedObject(self, kIdentifierKey, identifier, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)identifier {
    return objc_getAssociatedObject(self, kIdentifierKey);
}
@end

(2)系统实现原理

关联对象的管理依赖 Runtime 内部的全局哈希表,核心结构如下:

  1. AssociationsManager:单例管理器,持有AssociationsHashMap,并通过自旋锁保证线程安全;

  2. AssociationsHashMapunordered_map<DisguisedPtr<objc_object>, ObjectAssociationMap>,key 是 “被关联的对象”(如 UIView 实例),value 是该对象的关联表;

  3. ObjectAssociationMapunordered_map<void *, ObjcAssociation>,key 是开发者定义的key(如kIdentifierKey),value 是ObjcAssociation(存储关联值和内存管理策略);

  4. ObjcAssociation:存储关联值(id _value)和内存管理策略(objc_AssociationPolicy)。

操作流程

  • objc_setAssociatedObject:通过AssociationsManager找到AssociationsHashMap,根据 “被关联对象” 找到ObjectAssociationMap,存入keyObjcAssociation
  • objc_getAssociatedObject:反向查找,根据 “被关联对象” 和key获取ObjcAssociation中的_value
  • objc_removeAssociatedObjects:删除 “被关联对象” 对应的ObjectAssociationMap

(3)关联对象的内存管理

关联对象的内存管理由objc_AssociationPolicy(关联策略)决定,策略对应 ARC 下的内存语义:

关联策略内存语义(ARC)作用
OBJC_ASSOCIATION_ASSIGNassign弱引用,不 retain,对象释放后变为野指针
OBJC_ASSOCIATION_RETAIN_NONATOMICstrong(非原子)retain 关联值,线程不安全
OBJC_ASSOCIATION_COPY_NONATOMICcopy(非原子)copy 关联值,线程不安全
OBJC_ASSOCIATION_RETAINstrong(原子)retain 关联值,线程安全
OBJC_ASSOCIATION_COPYcopy(原子)copy 关联值,线程安全

释放时机

  • 当 “被关联对象” 释放时(dealloc),Runtime 会自动调用objc_removeAssociatedObjects,根据关联策略释放关联值(如retain策略会release关联值);
  • 也可手动调用objc_removeAssociatedObjects移除所有关联值。

(4)关联对象如何实现 weak 属性

关联对象本身不支持weak策略(OBJC_ASSOCIATION_ASSIGNassign,非weak),但可通过弱引用容器实现:

  1. 自定义一个WeakContainer类,内部用__weak持有目标对象;

  2. WeakContainer实例作为关联值,策略设为OBJC_ASSOCIATION_RETAIN_NONATOMIC

  3. 访问时从WeakContainer中获取__weak对象,实现弱引用效果。

示例

objc

// 弱引用容器
@interface WeakContainer : NSObject
@property (nonatomic, weak) id value;
@end

@implementation WeakContainer
@end

// 关联时使用容器
- (void)setWeakValue:(id)weakValue {
    WeakContainer *container = [WeakContainer new];
    container.value = weakValue;
    objc_setAssociatedObject(self, kWeakValueKey, container, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)weakValue {
    WeakContainer *container = objc_getAssociatedObject(self, kWeakValueKey);
    return container.value; // 弱引用,对象释放后为nil
}

3. Autoreleasepool 原理 + 数据结构

Autoreleasepool(自动释放池)是 iOS 中管理临时对象内存的机制,通过延迟释放对象,避免频繁调用release

(1)核心原理

  • 作用:收集调用autorelease的对象,在Autoreleasepool销毁时,对池内所有对象调用release

  • 触发时机

    1. 主线程:RunLoop 的每个循环周期结束时(如kCFRunLoopBeforeWaiting),自动销毁并重建Autoreleasepool
    2. 子线程:需手动创建@autoreleasepool {},否则对象可能无法及时释放;
    3. 手动销毁:@autoreleasepool {}代码块执行完毕时,池内对象被release

(2)数据结构

Autoreleasepool基于双向链表实现,核心结构是AutoreleasePoolPage

  • AutoreleasePoolPage:每个 Page 是 4096 字节(一页内存),结构简化如下:

    objc

    class AutoreleasePoolPage {
        static const size_t SIZE = 4096; // 4KB
        AutoreleasePoolPage *next;       // 下一个Page(链表节点)
        AutoreleasePoolPage *prev;       // 上一个Page
        id *begin;                       // Page内存储对象的起始地址
        id *end;                         // Page内存储对象的结束地址
        id *top;                         // 当前存储对象的下一个位置(栈指针)
        pthread_t thread;                // 所属线程(每个线程对应一个Page链表)
    };
    
  • Page 链表:当一个 Page 装满(top == end)时,创建新的 Page 并加入链表;

  • POOL_BOUNDARY:哨兵对象,标记Autoreleasepool的边界。@autoreleasepool {}会在开始时压入POOL_BOUNDARY,结束时从top向下遍历,直到遇到POOL_BOUNDARY,对中间所有对象调用release,并将top重置到POOL_BOUNDARY之后。

(3)操作流程

  1. 创建Autoreleasepool:调用objc_autoreleasePoolPush(),压入POOL_BOUNDARY,返回其地址;
  2. 对象调用autorelease:调用objc_autorelease(),将对象指针存入当前 Page 的top位置,top自增;若当前 Page 满,创建新 Page 并继续存储;
  3. 销毁Autoreleasepool:调用objc_autoreleasePoolPop(POOL_BOUNDARY),从top向下遍历,对每个对象调用release,直到遇到POOL_BOUNDARY,并调整top指针。

4. ARC 实现原理 + 优化

ARC(Automatic Reference Counting)是编译器和 Runtime 协作的自动内存管理机制,核心是 “编译器自动插入引用计数操作代码”。

(1)ARC 实现原理

  1. 编译器层面

    • 分析代码中对象的生命周期,在合适的位置自动插入retainreleaseautorelease

    • 例如:

      objc

      // ARC代码
      - (void)test {
          Person *p = [[Person alloc] init]; // 编译器自动插入 [p retain](实际alloc返回的对象引用计数为1,无需retain)
      } // 函数结束时,编译器自动插入 [p release]
      
    • 遵循 “谁持有,谁释放” 原则:局部变量出作用域时释放,成员变量在对象dealloc时释放。

  2. Runtime 层面

    • 提供objc_retainobjc_releaseobjc_autorelease等 API,供编译器插入调用;
    • 通过SideTable管理引用计数,确保线程安全;
    • 处理weak指针(如对象释放时置为nil)。

(2)ARC 对 retain & release 的优化

ARC 通过编译器和 Runtime 优化,减少不必要的retain/release操作,提升性能:

  1. 返回值优化(NRVO - Named Return Value Optimization)
    若函数返回局部对象,编译器直接将对象的所有权转移给调用者,避免插入autoreleaseretain
    示例:

    objc

    - (Person *)createPerson {
        Person *p = [[Person alloc] init]; // 局部对象
        return p; // ARC优化:直接返回p,无需autorelease
    }
    // 调用者:Person *p = [self createPerson]; 无需retain
    
  2. Toll-Free Bridging 优化
    当 Core Foundation 对象(如CFStringRef)与 OC 对象(如NSString)桥接时,ARC 自动管理引用计数,避免手动调用CFRetain/CFRelease
    示例:NSString *str = (__bridge_transfer NSString *)CFStringCreateWithCString(...);__bridge_transfer让 ARC 接管 CF 对象的释放。

  3. 局部变量优化
    若局部变量仅在当前作用域使用,且无外部引用,编译器会省略retain/release(如循环内的临时对象)。

  4. 零成本异常处理
    MRC 中,异常抛出时需手动处理release;ARC 中,编译器通过@try/@finally自动插入release,且优化了异常处理的性能开销。

5. ARC 下的内存泄漏场景

ARC 虽自动管理内存,但仍存在以下常见泄漏场景:

  1. 循环引用

    • Block 与 self 循环引用self持有 block,block 持有self(如self.block = ^{ [self doSomething]; };);
      解决:用__weak typeof(self) weakSelf = self;打破循环。
    • ** delegate 循环引用 **:若delegatestrong修饰(如@property (nonatomic, strong) id<Delegate> delegate;),会导致委托方与被委托方循环引用;
      解决:delegateweak修饰。
    • 容器与对象循环引用:对象持有容器,容器存储对象(如self.array = @[self];);
      解决:用weak容器(如NSArray存储WeakContainer)。
  2. NSTimer 未 invalidate
    NSTimerretaintarget(如self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(tick) userInfo:nil repeats:YES];),若self持有timer,会形成循环引用;
    解决:在dealloc或页面销毁时调用[self.timer invalidate]; self.timer = nil;

  3. AFN 请求未取消
    AFN 的NSURLSessionDataTaskretain其回调 block,若 block 持有self,且请求未取消,self会一直被持有;
    解决:页面销毁时调用[task cancel];

  4. 缓存未清理
    全局缓存(如NSCache、单例中的NSDictionary)存储大量对象,且未设置过期策略,导致对象无法释放;
    解决:设置缓存上限(如cache.countLimit = 100;),或在内存警告时清理缓存。

  5. 子线程未退出
    子线程中开启 RunLoop 且未手动停止(如CFRunLoopRun();),导致子线程一直存活,持有其内部的对象;
    解决:调用CFRunLoopStop(CFRunLoopGetCurrent());停止 RunLoop。

三、NSNotification 机制

NSNotificationCenter(通知中心)是 iOS 中跨模块通信的核心机制,基于 “发布 - 订阅” 模式实现。

1. 实现原理(结构设计、存储关系)

NSNotificationCenter的核心是通知表,存储 “通知名 - 观察者 - 处理方法” 的映射关系,结构设计如下:

(1)核心结构

  • 通知表(_notificationTable)NSNotificationCenter内部维护一个哈希表,key 为**通知名(NSString * ,value 为NSMapTable(存储该通知名对应的所有观察者);
  • 观察者表(NSMapTable) :key 为观察者(id) ,value 为NSMutableArray(存储该观察者订阅该通知的所有 “处理条目”);
  • 处理条目(_NotificationObserver) :每个条目包含selector(处理方法)、object(通知发送者过滤条件)、queue(指定线程处理通知)、context(上下文)。

(2)name & observer & SEL 的关系

  • 多对多关系:一个通知名(name)可被多个观察者(observer)订阅,一个观察者可订阅多个通知名;
  • 过滤逻辑:订阅时指定object,则仅接收该object发送的通知;若objectnil,则接收所有发送者的该通知;
  • 处理逻辑:当通知被发布时,NSNotificationCenter根据通知名找到所有观察者,遍历处理条目,若object匹配,调用objc_msgSend(observer, selector, notification)

2. 通知的发送是同步还是异步?

默认是同步的

  • 调用postNotificationName:object:userInfo:时,NSNotificationCenter会在当前线程中立即遍历所有匹配的观察者,同步调用其selector

  • 若某个观察者的selector执行耗时,会阻塞当前线程(包括主线程,导致 UI 卡顿)。

异步发送方式
需手动将通知发布逻辑放入异步队列,例如:

objc

// 在子线程异步发布通知
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [[NSNotificationCenter defaultCenter] postNotificationName:@"TestNotification" object:nil];
});

// 或在主线程异步处理通知(观察者侧)
[[NSNotificationCenter defaultCenter] addObserverForName:@"TestNotification" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
    // 主线程异步处理
}];

3. NSNotificationCenter 接收与发送是否在同一线程?如何异步发送?

(1)线程一致性

  • 默认情况:接收通知的线程与发送通知的线程完全一致
    示例:在子线程 A 调用postNotification,则所有观察者的selector会在子线程 A 执行;在主线程调用,则在主线程执行。
  • 例外情况:若订阅时指定了queue(如addObserverForName:object:queue:usingBlock:),则通知会在指定的queue对应的线程执行;
    示例:指定queue:[NSOperationQueue mainQueue],则无论通知在哪个线程发布,都会在主线程执行 block。

(2)异步发送通知的两种方式

  1. 发布侧异步:将postNotification放入异步队列,让发布操作不阻塞当前线程;

    objc

    dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"AsyncPost" object:nil];
    });
    
  2. 接收侧异步:订阅时指定异步队列,让处理逻辑在后台线程执行;

    objc

    // 接收通知的block在全局队列执行(异步)
    [[NSNotificationCenter defaultCenter] addObserverForName:@"AsyncReceive" object:nil queue:[[NSOperationQueue alloc] init] usingBlock:^(NSNotification * _Nonnull note) {
        // 耗时处理(如解析数据)
    }];
    

4. NSNotificationQueue 是异步还是同步?在哪个线程响应?

NSNotificationQueue(通知队列)是对NSNotificationCenter的扩展,核心作用是延迟发送通知合并重复通知,其发送方式和线程规则如下:

(1)同步 / 异步特性

  • 默认是异步的NSNotificationQueue不会立即发送通知,而是将通知加入队列,在RunLoop 的特定模式下批量发送;

  • 支持三种发送模式(NSPostingStyle):

    • NSPostWhenIdle:RunLoop 空闲时发送(如无事件处理时);
    • NSPostASAP:RunLoop 下一次循环时发送(尽快发送,但不阻塞当前 RunLoop);
    • NSPostNow:立即发送(同步,等价于NSNotificationCenter的直接发布)。

(2)响应线程

  • 与发布通知的线程一致NSNotificationQueue的通知最终由NSNotificationCenter发送,因此响应线程与NSNotificationQueue所在的线程一致;
  • 示例:在主线程创建NSNotificationQueue并添加通知,则通知会在主线程的 RunLoop 空闲时发送,观察者在主线程响应;在子线程创建,则在子线程响应。

5. NSNotificationQueue 和 RunLoop 的关系

NSNotificationQueue完全依赖RunLoop实现延迟发送,核心交互如下:

  1. 通知入队:调用enqueueNotification:postingStyle:coalesceMask:forModes:时,NSNotificationQueue将通知存储在内部队列,并注册一个RunLoop 观察者(CFRunLoopObserverRef)
  2. RunLoop 触发:当 RunLoop 进入指定的模式(如NSDefaultRunLoopMode)且满足发送条件(如NSPostWhenIdle对应 RunLoop 空闲)时,RunLoop 观察者触发回调;
  3. 批量发送NSNotificationQueue从队列中取出所有符合条件的通知,调用NSNotificationCenterpostNotification批量发送;
  4. 合并通知:若设置了coalesceMask(如NSNotificationCoalescingOnName),则