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

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

一、Runtime 核心原理

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

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

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

(1)核心结构关系

plaintext

rust 复制代码
实例对象(Instance)-> 类对象(Class)-> 元类(Metaclass)-> 根元类(Root Metaclass)
  • 实例对象(Instance) :存储成员变量(ivar) 的值,其isa指针指向对应的类对象

    结构体简化:

    objc

    arduino 复制代码
    struct Instance {
        Class isa; // 指向类对象
        // 成员变量的值(如NSString *name; int age;)
    };
  • 类对象(Class) :存储实例方法(-method)、属性(property)、协议(protocol) ,其isa指针指向元类 ,同时包含指向父类的superclass指针。

    结构体核心依赖class_data_bits_t,内部通过data()方法获取class_rw_t(可读写数据):

    objc

    arduino 复制代码
    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_copyIvarList class_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

    arduino 复制代码
    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

    示例:NSObjectPerson(子类) → Person+Category1Person+Category2(按编译顺序)。

  • +initialize

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

    示例:

    objc

    less 复制代码
    @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

arduino 复制代码
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

less 复制代码
// 给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_ASSIGN assign 弱引用,不 retain,对象释放后变为野指针
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong(非原子) retain 关联值,线程不安全
OBJC_ASSOCIATION_COPY_NONATOMIC copy(非原子) copy 关联值,线程不安全
OBJC_ASSOCIATION_RETAIN strong(原子) retain 关联值,线程安全
OBJC_ASSOCIATION_COPY copy(原子) 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

less 复制代码
// 弱引用容器
@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

    arduino 复制代码
    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

      csharp 复制代码
      // 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

    ini 复制代码
    - (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

objectivec 复制代码
// 在子线程异步发布通知
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

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

    objc

    swift 复制代码
    // 接收通知的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),则
相关推荐
xrkhy3 小时前
SpringBoot面试
spring boot·后端·面试
Sherry0075 小时前
【译】CSS 高度之谜:破解百分比高度的秘密
前端·css·面试
用户095 小时前
更现代、更安全:Swift Synchronization 框架与 Mutex 锁
ios·面试·swift
用户095 小时前
Kotlin 将会成为跨平台开发的终极选择么?
android·面试·kotlin
沉默王二3 天前
金山还是小米,谁才是雷军的亲儿子?附小米线下一面面经(八股盛宴)
后端·面试
橙序员小站3 天前
搞定系统设计题:如何设计一个订单系统?
java·后端·面试
社会牛马也要做匹黑马3 天前
Recyclerview回收复用机制——图文详解
前端·面试
沐怡旸3 天前
【底层机制】std::move 解决的痛点?是什么?如何实现?如何正确用?
c++·面试
UrbanJazzerati4 天前
CSS选择器入门指南
前端·面试