iOS知识点汇总

93 阅读25分钟

一、基础知识类****

1. strong 和 copy 的区别?在使用 NSString 时应该用哪个?****

答案

• strong 表示对象的强引用,会增加引用计数。

• copy 会创建对象的副本,适用于不可变对象防止外部修改。

• 对于 NSString,推荐使用 copy,尤其是在属性中声明(如 @property (nonatomic, copy) NSString *name),因为如果传入的是 NSMutableString,copy 可确保内部保存的是不可变副本。

2. assign 和 weak 有什么区别?****

答案

• assign 适用于基本数据类型,不参与对象生命周期管理。

• weak 适用于对象类型,在引用对象被释放时自动置为 nil,防止野指针。

• 如果用于 UIViewController 中的 delegate,应使用 weak 防止循环引用。

📌 二、进阶技术类****

3. 如何避免 Block 导致的循环引用?****

答案

• 使用 __weak 或 __block 关键字,避免在 Block 中强引用 self。

self.completion = ^{
    [weakSelf doSomething];
};

• 注意在 GCD、NSTimer、CADisplayLink 中也需要弱引用 self。

4. 你如何处理主线程卡顿的问题?****

答案

• 使用 Instruments 的 Time Profiler 或 Main Thread Checker 分析阻塞。

• 避免在主线程进行大量计算或 IO 操作(比如图片解码、数据库读写)。

• 可以将耗时任务放入子线程,用 dispatch_async,并在需要更新 UI 时用 dispatch_main 回主线程。

5. 请介绍下你对 RunLoop 的理解,以及应用场景。****

答案

• RunLoop 是事件循环机制,处理输入源(如触摸、定时器、网络等)。

• 主线程的 RunLoop 自动启动,子线程需要手动开启。

• 应用场景:

• 保持线程存活(如后台常驻线程)

• 精确控制定时器(比如 CADisplayLink)

• 监控卡顿(通过 beforeWaiting / afterWaiting) 📌 三、系统设计 / 架构类****

6. 如何设计一个组件化的 App 架构?****

答案

• 使用模块拆分、按功能独立开发。

• 通过协议(Protocol)和中间件解耦模块之间的依赖。

• 各模块可以使用 Pod 管理,或独立仓库维护。

• 可以用 Router 做页面跳转与参数传递,如 CTMediator、MGJRouter。

7. 请设计一个图片缓存系统,支持内存和磁盘缓存。****

参考思路

• 内存缓存用 NSCache。

• 磁盘缓存保存到沙盒路径,建议使用文件名的 MD5 哈希防止命名冲突。

• 设置过期时间或最大容量,清理策略使用 LRU。

• 异步读写,避免阻塞主线程。 📌 四、项目经验类****

8. 你在项目中遇到过最难的问题是什么?你是怎么解决的?****

答案参考结构

• 问题背景:如动画卡顿 / 闪退 / 内存暴涨

• 分析过程:使用了什么工具(Instruments / Xcode Crash / 日志分析)

• 最终解决方案:比如优化图片加载、减少内存峰值

• 结果及反思:如何防止再次发生 📌 六、软技能 & 团队协作****

11. 如何看待 Code Review?****

答案

• Code Review 是保证代码质量、统一风格的重要手段。

• 能帮助发现潜在 Bug、优化逻辑。

• 应该客观、中肯、及时反馈,避免人身批评。

12. 遇到产品或设计频繁变更,你会怎么处理?****

答案

• 沟通变更原因,是否有明确价值。

• 技术评估是否影响已有结构,提出最优实现路径。

• 做好代码抽象和模块隔离,提升适配灵活度。

🧠 一、基础知识(Objective-C & Swift)****

1. Objective-C 和 Swift 的区别?什么时候用哪种更合适?****

参考答案:****

• Swift 是类型安全的,语法更简洁,支持更多现代特性(如值类型、多模式匹配、闭包等)。

• Objective-C 兼容性好,适用于维护老项目,Runtime 灵活。

• 新项目推荐用 Swift,老项目或需要 Runtime 特性的部分可继续使用 Objective-C。

2. 描述 ARC 的工作原理。__strong、__weak、__unsafe_unretained 有何区别?****

参考答案:****

• ARC 自动插入 retain/release 代码,避免内存泄漏。

• __strong:默认强引用。

• __weak:弱引用,引用对象释放后自动置为 nil。

• __unsafe_unretained:弱引用,但不会自动置为 nil,容易造成野指针。

🧩 二、进阶知识点****

3. iOS 中 RunLoop 的作用是什么?如何与 Timer、Gesture 等配合?****

参考答案:****

• RunLoop 管理事件循环,实现线程的持续运行。

• Timer/Touch/GCD/MainQueue 等依赖 RunLoop。

• 子线程需要手动启动 RunLoop 来处理 Timer 或 Port 事件。

4. performSelector 和 GCD/Block 的区别是什么?****

参考答案:****

• performSelector 是基于 Selector 的延迟和异步执行,有语法限制(最多带一个参数)。

• GCD 更现代、灵活、支持异步并发。

• Block 支持闭包式编程,代码更清晰。

🏗️ 三、架构与设计模式****

5. iOS 中常见的设计模式有哪些?你实际用过哪些?****

参考答案:****

• 单例(Singleton):如 NSUserDefaults

• 观察者(Notification、KVO)

• 委托(Delegate):如 UITableViewDelegate

• 策略、工厂、MVVM、MVC、MVP 等

• MVVM 是常用架构,搭配 RxSwift、Combine 可解耦 UI 和业务逻辑

🧪 四、内存管理与性能优化****

6. 怎么分析一个内存泄漏问题?****

参考答案:****

• Instruments -> Leaks 工具

• 使用 Xcode Debug Memory Graph

• 检查循环引用,如 ViewController 和其闭包

7. 如何优化一个卡顿的页面?****

参考答案:****

• Instruments -> Time Profiler 监测主线程调用耗时

• 异步加载图片/数据

• 使用离屏渲染优化、减少视图层级

• TableView 预估高度、Cell 复用

📱 五、多线程与异步****

8. GCD 中 dispatch_group、dispatch_barrier 用法?****

参考答案:****

• dispatch_group 可用于任务组执行完后统一回调。

• dispatch_barrier 在并发队列中实现写操作的串行化,防止数据竞争。

9. NSOperation 与 GCD 的对比?****

参考答案:****

• NSOperation 更面向对象,可设置依赖、取消、优先级。

• GCD 更轻量,适合简单的并发任务。

• 复杂调度用 NSOperationQueue 更灵活。

🧩 六、网络与数据处理****

10. AFNetworking 和 NSURLSession 有哪些区别?****

参考答案:****

• AFNetworking 是 NSURLSession 的封装,功能丰富(图片缓存、Reachability 等)。

• NSURLSession 更底层,系统 API,灵活定制。

• 建议新项目使用 NSURLSession 或基于 URLSession 的轻量框架如 Alamofire。

11. 如何实现一个可靠的断点续传功能?****

参考答案:****

• 使用 NSURLSessionDownloadTask 搭配 resumeData

• 本地持久化下载状态和 resumeData

• 处理边界条件:服务器是否支持 Range 请求

🎨 七、UI 与动画****

12. CALayer 与 UIView 有何关系?****

参考答案:****

• UIView 是 UI 控件,管理事件响应、布局等;

• CALayer 是绘制引擎,管理视图的可视内容;

• 每个 UIView 都有一个 CALayer,可用于动画、阴影、圆角等。

13. 如何实现一个复杂的页面切换动画?****

参考答案:****

• 使用 UIViewControllerAnimatedTransitioning 和 UIViewControllerTransitioningDelegate

• 或者使用 CATransition / UIView animateWithDuration

• 对于自定义交互,可结合手势使用 UIPercentDrivenInteractiveTransition

🧭 八、架构与模块化****

14. 说说你对组件化和模块化的理解?有哪些实践经验?****

参考答案:****

✅ 一、基本理解****

🧩 模块化(Modularization)****

定义:将项目按照功能或领域划分为独立可维护的模块(如首页、IM、支付、地图等),每个模块可独立开发、测试和复用。

目标

• 降低耦合度

• 提高协作效率

• 支持多人并行开发

• 缩短编译时间

🧱 组件化(Componentization)****

定义:在模块化基础上,进一步抽象出可以独立复用、插拔、组合的 UI 控件或业务功能(如登录组件、图片选择器、分享面板)。

特点

• 更小粒度

• 可用于多个模块中

• 类似 SDK 的设计

**模块化是为了解耦功能、组件化是为了复用能力。**两者配合能让团队开发效率更高、架构更清晰、项目更易维护。

📦 九、数据存储与本地缓存****

15. 本地持久化有哪些方式?各适用场景?****

参考答案:****

• NSUserDefaults:轻量级配置存储

• Keychain:安全信息,如密码、token

• SQLite/FMDB/CoreData:结构化数据

• NSKeyedArchiver/NSCoding:归档对象 🔐 十、安全与隐私****

16. 如何防止 iOS 应用被抓包?****

参考答案:****

• 使用 HTTPS + ATS

• 校验证书或使用 SSL Pinning

• 避免暴露明文数据、敏感接口加签名验证 🧪 十一、测试与上线****

17. 如何做单元测试和 UI 自动化测试?****

参考答案:****

• 单元测试:使用 XCTest,验证方法逻辑正确性

• UI 测试:使用 XCUITest 或第三方工具如 Appium

• Mock 数据、依赖注入、TDD 等可提升可测性 🧩 十二、项目经验与思考****

18. 你做过哪些技术选型?是怎么做权衡的?****

参考答案:****

• 比如选择 RxSwift vs Combine,是否团队能接受、社区支持度、兼容性

• 比如选择本地数据方案时是否支持增量更新、是否线程安全

category的原理,请具体说明,必要时可展示底层源码,及父类、子类分别有category,并且有相同方法,到底会执行哪个

✅ 一、什么是 Category?****

Category 是 Objective-C 提供的一种为已有类添加方法的机制(不能直接添加实例变量)。它的底层实现完全依赖于 运行时动态方法替换

✅ 二、Category 的底层原理(源码分析)****

我们来看下 Category 在运行时是如何被加载到类中的:

在 objc-runtime-new.mm 的 load_categories_nolock() 方法中:

void load_categories_nolock(Class cls, category_list *cats, ...) {
    // 把分类里的方法、属性、协议插入到类的 method list 头部
    attachCategories(cls, cats, ...)
}

具体看核心函数 attachMethodLists():

// 将分类中的方法,插入到原类的方法列表前面
static void attachMethodLists(Class cls, ...) {
    for (auto& methodList : newMethodLists) {
        // 插入顺序是分类优先
        addMethodListToClass(cls, methodList, BEFORE_EXISTING_LIST);
    }
}

📌 结论:分类的方法是在运行时加载,并且其方法列表是插入到类原有方法列表的前面,所以分类方法会覆盖类中原有的方法实现

✅ 三、多个 Category 同名方法的覆盖顺序?****

多个 Category 有相同方法时,谁后编译,谁先插入,谁覆盖前面的方法

✅ 四、父类、子类都有 Category 并且方法同名,执行哪个?****

这个考察的是 Objective-C 的消息查找机制

消息查找顺序:****

  1. 先查当前类的 method list(含分类插入的方法)

  2. 查父类

  3. 直到 NSObject

✅ 五、Category 能添加属性或成员变量吗?****

• 不能添加实例变量,因为编译期类结构已定。

• 可以通过 @property + objc_setAssociatedObject 添加关联对象实现伪“属性”。

✅ 六、实战建议和最佳实践

场景 建议****

扩展系统类 Category 非常合适,如 NSString+MD5

多人协作分类 避免多个分类定义同名方法

属性添加 用 AssociatedObject

有复杂状态逻辑 用子类或组合更安全 ✅ 七、总结一句话:****

Category 是运行时插入 method list 的机制,分类方法优先于原类方法,多个分类谁后编译谁覆盖,子类永远优先于父类。

详细阐述下runtime 🧠 一、什么是 Runtime?****

Objective-C 是动态语言,大量机制在运行时才确定,比如:

• 方法调用

• 属性访问

• 动态方法解析

• 消息转发

• 动态添加类、方法、属性等

这些特性是通过 Runtime 库(底层是 C 和汇编写的)实现的。

本质上是一个 C 语言编写的对象管理和消息传递的库,叫做 libobjc

🧱 二、核心结构和原理****

1. 对象结构(isa 指针)

NSObject *obj = [[NSObject alloc] init];

在内存中:

struct objc_object {
    Class isa;
}

• isa 指针指向对象的类。

• 每个类也是一个对象,叫 元类(MetaClass)

2. 类结构(Class)

struct objc_class {
    Class isa;
    Class super_class;
    const char *name;
    long version;
    struct method_list_t *methodLists;
    struct objc_property_list *propertyLists;
    ...
};

每个类中包含:

• 方法列表

• 属性列表

• 协议列表

• 成员变量列表

• 类名、父类指针等

🛠 三、常用 Runtime 函数(<objc/runtime.h>)

功能示例函数说明
获取类名/方法class_getName()获取类名
获取成员变量列表class_copyIvarList()遍历属性
获取方法列表class_copyMethodList()获取所有方法
方法交换method_exchangeImplementations()交换方法
添加方法class_addMethod()动态添加方法
替换方法class_replaceMethod()替换已有实现
获取/设置关联对象objc_setAssociatedObject()添加关联对象

🚀 四、常见应用场景****

1. Method Swizzling(方法交换)

Method originalMethod = class_getInstanceMethod(cls, @selector(viewDidLoad));
Method swizzledMethod = class_getInstanceMethod(cls, @selector(my_viewDidLoad));
method_exchangeImplementations(originalMethod, swizzledMethod);

用途:

• 统计埋点

• 监控崩溃

• 替换系统方法实现

2. 关联对象(Category 添加属性)

static const void *kNameKey = &kNameKey;

objc_setAssociatedObject(self, kNameKey, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
id name = objc_getAssociatedObject(self, kNameKey);

Category 不能新增实例变量,使用 Runtime 解决。

3. 自动归档 / 解档

unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i < count; i++) {
    NSString *key = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
    [coder encodeObject:[self valueForKey:key] forKey:key];
}

一行代码都不用手动写,自动归档所有成员变量。

4. 动态创建类

Class NewClass = objc_allocateClassPair([NSObject class], "MyDynamicClass", 0);
class_addMethod(NewClass, @selector(sayHello), (IMP)sayHelloFunc, "v@:");
objc_registerClassPair(NewClass);

可以在运行时生成类、添加方法等。 🔎 五、源码追踪举例****

[NSObject alloc] 是如何工作的?

+ (id)alloc {
    return _objc_rootAlloc(self);
}

进入 Runtime 源码后:

id
_objc_rootAlloc(Class cls) {
    return callAlloc(cls, false);
}

继续追踪 callAlloc:

• 会为对象分配内存

• 设置 isa

• 初始化默认值

消息发送机制

[obj sayHello];

编译后会变为:

objc_msgSend(obj, @selector(sayHello));

运行时流程如下:

  1. objc_msgSend 找 isa 指向的类

  2. 在方法缓存查找 SEL

  3. 如果找不到,走 resolveInstanceMethod:

  4. 再找不到走 forwardingTargetForSelector:

  5. 再找不到走 forwardInvocation:

  6. 最后触发崩溃 unrecognized selector sent to instance

📚 六、面试题及回答示例****

Q1:什么是 Method Swizzling?会有什么风险?****

Method Swizzling 是指交换两个方法的实现。常用于埋点、日志、替换系统方法。

风险包括:

• 多个 Swizzling 会互相影响(顺序问题)

• 可能破坏系统逻辑

• 方法签名不匹配会崩溃

Q2:Category 中为什么不能添加成员变量?你是怎么解决的?****

因为 Category 编译时不修改类结构,所以无法添加成员变量。

解决方法是使用 objc_setAssociatedObject 添加关联对象。

Q3:Category、Class Extension 和 Subclass 的区别?****

• Category:增加方法(不能加属性)

• Class Extension:私有属性或方法,编译期生效

• Subclass:完整继承,改写逻辑

Q4:父类和子类分别有相同 selector 的 Category,会调用哪个?****

Category 编译后方法在编译顺序中排在主类前面,会覆盖主类方法。但不同类的 Category 不会相互影响,调用时按类继承链查找。

🧩 七、总结一句话:****

Runtime 就是 Objective-C 的“灵魂”。它让语言在运行时可以动态修改行为,是实现很多高级特性(如 Category、KVC、KVO、消息转发等)的基础。

详细讲解下runLoop

💡 一、什么是 RunLoop?****

RunLoop(运行循环) 本质是一个事件处理机制,让线程在有任务时忙处理任务,没任务时休眠节省资源。

简单来说,RunLoop 就是一个循环,不断地接收事件、派发事件、处理回调,比如:

• 触摸事件

• 定时器

• performSelector

• 网络回调

• GCD 任务切换到主线程

主线程的 RunLoop 会一直运行,保证 App 保持响应。子线程默认不启动 RunLoop,但可以手动开启。

🔧 二、RunLoop 的底层结构****

RunLoop 在 iOS 中由 Core Foundation 提供:CFRunLoopRef。

结构层级如下:****

• RunLoop(CFRunLoopRef)

• 包含若干个 Mode(CFRunLoopModeRef)

• 每个 Mode 包含若干个 Source、Timer、Observer

每次运行 RunLoop 时只能运行在一个 Mode 中。

常见的 Mode:

名称描述*
kCFRunLoopDefaultMode默认模式,大部分任务都在这跑
UITrackingRunLoopMode滑动时的 Mode,不受普通 Timer 干扰
kCFRunLoopCommonModes虚拟 Mode,绑定多个真实 Mode(如上两个)

🌀 三、RunLoop 的工作流程****

当你启动一个 RunLoop 时,它会经历以下步骤:

  1. 通知 Observers,即将进入 RunLoop

  2. 进入 Loop,设置为休眠(等待事件)

  3. 某个事件(比如 Timer、输入源)唤醒它

  4. 分发事件并处理回调

  5. 通知 Observers,RunLoop 即将休眠/退出

  6. 重复以上过程,直到退出条件触发

📦 四、RunLoop 的五大组成部分

组成说明*
Input Source输入源,触发事件,如触摸、端口等
Timer Source定时器事件,如 NSTimer
Observer观察者,可以监听状态变化
Mode模式,控制事件处理策略
RunLoop整体容器(CFRunLoopRef)

🛠 五、常见应用场景****

1. 保持线程不退出(子线程)

NSThread *thread = [[NSThread alloc] initWithBlock:^{
    @autoreleasepool {
        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    }
}];
[thread start];

用于后台任务持续处理,比如:

• 音频播放线程

• socket 保活线程

2. NSTimer 要注意的点****

NSTimer 默认添加到 NSDefaultRunLoopMode:

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"tick");
}];

但滑动 UIScrollView 时是 UITrackingRunLoopMode,默认 Mode 的 Timer 会暂停。所以应放在 NSRunLoopCommonModes:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

3. GCD 的 dispatch_async 到主线程是怎么做的?

dispatch_async(dispatch_get_main_queue(), ^{
    // 实际是放入主线程 RunLoop 的事件队列中
});

主线程 RunLoop 会从队列中获取任务并处理。

4. performSelector:afterDelay: 为什么在子线程不执行?****

因为这个方法依赖于 RunLoop,如果子线程没启动 RunLoop,就无法调度回调。

🔎 六、源码分析入口****

可以看一下 RunLoop 底层源码框架:

CFRunLoopRun() {
    while (1) {
        // 通知 Observer:将要进入 RunLoop
        __CFRunLoopDoObservers(kCFRunLoopEntry);

        // 通知 Observer:准备处理 Timer、Source
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);

        // 执行 Source0(非端口类,比如 performSelector)
        __CFRunLoopDoSources0();

        // 执行 Source1(端口类,等 MachPort)
        mach_msg();

        // 执行 Timer
        __CFRunLoopDoTimers();

        // 通知 Observer:RunLoop 即将休眠
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);

        // 等待唤醒(内核层调用 syscall)
        __CFRunLoopSleep();

        // 被唤醒,处理唤醒原因
    }
}

❓ 七、面试高频问题****

Q1:RunLoop 与线程的关系?****

RunLoop 是线程的基础设施,每条线程都有唯一一个 RunLoop(通过 CFRunLoopGetCurrent() 获取),主线程的 RunLoop默认启动,子线程默认没有。

Q2:RunLoop 为什么不能卡主线程?****

因为主线程 RunLoop 控制 UI 渲染、触摸事件处理、动画刷新等,任何长时间阻塞都会导致卡顿甚至 watchdog 杀进程。

Q3:RunLoop 是如何实现定时器调度的?****

每次 RunLoop 进入都会查找 Timer 是否到期,到期就执行回调。底层使用时间戳 + mach port 机制唤醒。

Q4:RunLoop 为什么要有 Mode?****

防止不同场景下的事件互相干扰,比如滑动列表时暂停普通 Timer,提升响应。

Q5:RunLoop 的退出机制是?****

外部调用 CFRunLoopStop(),或设置最大循环次数、任务完成、线程退出等。

✅ 总结一句话:****

RunLoop 是一个事件驱动的循环机制,它支撑了 iOS 系统的响应能力、任务调度、Timer 定时、线程存活等一系列核心功能,是所有开发者必须掌握的底层基础。

详细解释下KVC KVO

🧩 一、KVC(Key-Value Coding)****

📖 1. 什么是 KVC?****

KVC(键值编码) 是一种通过字符串键来间接访问对象属性的机制,不用调用 getter/setter 方法。

举个例子:

[person setValue:@"Tom" forKey:@"name"];
NSString *name = [person valueForKey:@"name"];

就相当于:

person.name = @"Tom";
NSString *name = person.name;

⚙️ 2. KVC 的查找规则(setValue:forKey:)****

以 key = @"name" 为例,调用:

[obj setValue:@"value" forKey:@"name"];

查找顺序:

1.调用 setName: 方法(是否有 set<Key>: 方法)

2.是否存在名为 _name 或 name 的实例变量(即使是 @private 也可访问)

3.调用 setValue:forUndefinedKey: 抛异常(默认行为)

3. 取值规则(valueForKey:)

 NSString *value = [obj valueForKey:@"name"];

查找顺序:****

  1. 是否存在 getName、name、isName 等 getter 方法

  2. 是否有 _name、name 实例变量

  3. 调用 valueForUndefinedKey: 抛异常(默认行为)

🧨 4. 常见 KVC 特性****

✅ 支持集合操作(KeyPath)

[person setValue:@"Beijing" forKeyPath:@"address.city"];

✅ 支持批量赋值

NSDictionary *dict = @{@"name":@"Tom", @"age":@18};
[person setValuesForKeysWithDictionary:dict];

✅ 支持集合运算符(高级)

NSArray *students = ...;
NSNumber *avgAge = [students valueForKeyPath:@"@avg.age"];
NSNumber *sumAge = [students valueForKeyPath:@"@sum.age"];
NSNumber *maxAge = [students valueForKeyPath:@"@max.age"];

🔍 二、KVO(Key-Value Observing)****

📖 1. 什么是 KVO?****

KVO(键值观察) 是基于 KVC 实现的一种观察机制,可以监听某个对象属性的变化。

[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

当 person.name 改变时,回调如下方法:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    NSLog(@"%@ 发生了变化,新值是:%@", keyPath, change[NSKeyValueChangeNewKey]);
}

⚙️ 2. KVO 底层原理****

KVO 的实现是通过 动态生成子类 + isa 指针指向该子类,并重写 setter 方法实现的。

举个例子:****

观察 person.name 属性时:

  1. 系统会生成一个新的子类:NSKVONotifying_Person

  2. person 实例的 isa 指向这个子类

  3. 这个子类重写了 setName:,内部会:

• 调用 willChangeValueForKey:

• 调用原始的 setter

• 调用 didChangeValueForKey:

• 通知观察者属性变化

你可以用以下方法查看:

object_getClass(person); // 打印出 NSKVONotifying_Person

🧨 3. 注意事项****

• 必须使用 setter 修改属性,才能触发 KVO

• 如果直接修改 ivar,比如 _name = @"Tom",不会触发 KVO

• 如果你使用了 @dynamic、@synthesize,要特别留意 setter 是否存在

• KVO 会强引用观察对象,注意循环引用

• 一定要配对移除:

- (void)dealloc {
    [person removeObserver:self forKeyPath:@"name"];
}

🛠 4. 手动触发 KVO

[self willChangeValueForKey:@"name"];
_name = @"New Name";
[self didChangeValueForKey:@"name"];

适用于没有调用 setter 的情况。

🤔 三、KVC & KVO 的区别与联系

对比点KVCKVO
全称Key-ValueCoding Key-Value Observing
功能通过键访问属性监听属性变化
依赖机制反射 + setter / ivar基于 KVC + 动态 subclass
使用方式setValue:forKey:addObserver:forKeyPath:
应用场景快速赋值 / 遍历数据绑定 / 响应更新
🧪 四、面试题汇总(附答案提示)****

Q1:KVC 的 setValue:forKey: 是如何查找属性的?****

先找 setter 方法,然后找变量名 _key 或 key,最后调用 setValue:forUndefinedKey:。

Q2:KVO 是如何实现监听属性变化的?****

通过 runtime 动态创建子类,重写 setter 方法,在其中添加通知逻辑。

Q3:KVO 是怎么触发的?可以不使用 setter 吗?****

默认通过 setter 触发,也可以通过 willChangeValueForKey: / didChangeValueForKey: 手动触发。

Q4:KVC 支持访问 private 属性吗?为什么?****

支持,因为它是通过 runtime 遍历 ivar 实现的,即使是 @private 也可以直接访问。

Q5:KVO 的缺点有哪些?如何替代?****

缺点:

• 不易管理,容易 crash(忘记移除)

• 不支持多属性组合监听

• Debug 不友好

替代方案:

• ReactiveCocoa / RxSwift

• Combine(Swift)

• block-based KVO(iOS 11+)

请详细解释下block原理

🧩 一、Block 基础定义和用法****

Block 是可以捕获上下文变量的代码块,可以像函数一样调用。

int a = 10;
void (^myBlock)(void) = ^{
    NSLog(@"a = %d", a);
};
myBlock(); // 输出 a = 10

📦 二、Block 的结构与类型****

Block 本质是一个结构体,遵循某种函数调用语法。苹果定义了三种类型的 Block:

📁 1. 三种类型

类型名存储位置特点
NSGlobalBlock程序数据区(常量)不捕获外部变量
NSStackBlock栈上捕获了外部变量(默认)
NSMallocBlock堆上从栈拷贝而来,延长生命周期

🧪 示例:

void (^block1)(void) = ^{ NSLog(@"Hello"); };
NSLog(@"%@", [block1 class]); // __NSGlobalBlock__

int x = 10;
void (^block2)(void) = ^{ NSLog(@"x = %d", x); };
NSLog(@"%@", [block2 class]); // __NSStackBlock__

// 使用 copy 修饰符
void (^block3)(void) = [block2 copy];
NSLog(@"%@", [block3 class]); // __NSMallocBlock__

🔬 三、Block 底层结构(源码级别)****

Block 是一个结构体,定义在 Block_private.h:

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

struct __MyBlock_struct {
    struct __block_impl impl;
    struct __MyBlock_desc *desc;
    int capturedVar; // 捕获的变量
};

当你写下一个 Block,编译器就自动帮你生成了结构体和函数指针。

🧠 四、Block 的变量捕获机制****

✅ 1. 捕获局部变量(值捕获)

int a = 10;
void (^block)(void) = ^{ NSLog(@"a = %d", a); };
a = 20;
block(); // 输出 a = 10(值捕获)

Block 会拷贝变量的值,而不是引用。

♻️ 2. 捕获可变变量(__block 修饰)

__block int a = 10;
void (^block)(void) = ^{ a = 20; };
block();
NSLog(@"a = %d", a); // 输出 20

原理:****

• __block 会把变量包装成一个对象(__Block_byref 结构体)

• Block 持有这个对象的引用

• 所以 Block 内外访问的是同一个值

🧠 __block 的底层结构:

struct __Block_byref_a {
    void *__isa;
    struct __Block_byref_a *__forwarding;
    int __flags;
    int a;
};

🚦 五、Block 在 ARC 和 MRC 下的内存管理****

🌿 在 ARC 下:****

• 捕获的对象会被强引用

• 为避免循环引用,常使用 __weak 或 __unsafe_unretained

__weak typeof(self) weakSelf = self;
self.block = ^{
    [weakSelf doSomething];
};

⚙️ 在 MRC 下:****

• 栈上的 Block 必须使用 copy 转成堆上的 Block,否则在 Block 离开作用域后访问会崩溃。

// MRC 下必须这样:
self.block = [[^{
    [self doSomething];
} copy] retain];

🔁 六、循环引用问题****

如果 Block 持有 self,而 self 也持有 Block,就会造成 强引用循环

self.block = ^{
    NSLog(@"%@", self.title);
};

✅ 解决方案:使用 __weak

__weak typeof(self) weakSelf = self;
self.block = ^{
    [weakSelf doSomething];
};

🧪 七、面试题总结****

Q1:Block 有几种类型?分别有什么特点?****

NSGlobalBlock(无变量)、NSStackBlock(捕获变量)、NSMallocBlock(copy 之后)

Q2:Block 为什么默认是栈的?怎么变成堆的?****

为了性能,Block 默认分配在栈上,使用 copy 可转为堆。

Q3:为什么 __block 可以修改外部变量?****

因为 __block 把变量包装为对象,Block 持有的是对象的引用。

Q4:Block 中如何避免循环引用?****

使用 __weak typeof(self) 来打破循环引用链。

Q5:在属性中声明 Block 时用什么修饰符?

Block 应该用 copy 修饰,确保转为堆上存储,防止野指针问题。

🧱 八、拓展:Block 与 GCD 的关系****

GCD 使用 Block 实现并发任务提交:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 异步任务
});

本质上就是将 Block 封装为任务体。

✅ 总结一句话****

Block 是一个结构体,它封装了一段代码和执行上下文,能捕获变量、延迟执行,是 Objective-C 中实现回调、异步、函数式编程的核心。

weak原理,请详细说明***

__weak 是 Objective-C 中用于避免循环引用的关键机制,广泛应用于 block、delegate 等场景。下面我们深入讲解 __weak 的底层原理,帮助你从编译器、runtime、内存结构等层面理解它的工作方式。

🧠 一、为什么需要 __weak****

在 Objective-C 的引用计数系统(ARC)中,所有 __strong 指针都会对对象的引用计数加1。如果两个对象互相持有 strong 指针,会导致循环引用(Retain Cycle) ,对象得不到释放。 __weak 指针不会增加引用计数,且当目标对象销毁时,它会自动被置为 nil,避免了野指针。

🧬 二、__weak 的实现原理

1️⃣ 编译器行为****

当你写下:

__weak MyClass *weakObj = obj;

Clang 编译器会生成类似如下的代码:

objc_initWeak(&weakObj, obj);

这是 runtime 提供的 API,用于创建一个弱引用。

2️⃣ Runtime 数据结构:weak_table_t****

__weak 底层依赖一个全局的哈希表 weak_table_t,它维护着所有对象与其弱引用之间的映射关系:

struct weak_table_t {
    weak_entry_t *weak_entries; // 哈希桶数组
    size_t num_entries;         // 当前数量
};

每一个 weak_entry_t 是一个对象与其所有 __weak 引用地址的映射:

struct weak_entry_t {
    id referent;           // 被引用的对象
    weak_referrer_t *referrers; // 弱引用数组,记录所有 __weak 指针地址
    uintptr_t referrers_count;
};

3️⃣ 创建 weak 引用:objc_initWeak****

该函数完成以下操作:

• 检查目标对象是否已存在于 weak_table_t 中。

• 如果没有,就新增一个 weak_entry_t,并将 weak 指针地址加入 referrers。

• 不增加引用计数。

4️⃣ 对象销毁时:objc_release → object_dispose****

当对象引用计数为 0,销毁流程如下:

objc_release → dealloc → objc_destructInstance → clearDeallocating

clearDeallocating 会查找 weak_table_t 中是否有指向当前对象的弱引用:

• 若有,遍历 referrers,将所有 weak 指针置为 nil。

• 最后从 weak_table_t 中移除该 entry。

这就是 __weak 被自动置为 nil 的核心机制

⚠️ 四、注意事项

特性说明****
自动置 nilweak 引用目标对象销毁后自动变 nil
不增加引用计数weak 不会 retain 目标对象
线程安全weak 表操作加锁,性能略差于 strong
不能指向 stack__weak 只能引用 heap 上的对象,否则运行时 crash
不能用在 structweak 只能用于 Objective-C 对象,不支持纯 C 结构体

🔄 五、与 __unsafe_unretained 的对比

特性__weak__unsafe_unretained
自动置 nil✅ 是❌ 否,释放后是野指针
引用计数不增加不增加
安全性低(容易 crash)
用途block、delegate性能敏感场景下用得少

🧩 六、总结****

• __weak 是通过 runtime 全局表 weak_table_t 维护的弱引用映射。

• 创建时用 objc_initWeak 注册,销毁时用 clearDeallocating 清理。

• 自动置 nil 是其最大优势,能有效避免循环引用和野指针。

请详细解释响应链和事件传递

一、什么是响应链(Responder Chain)?****

响应链是一组响应者对象(如 UIView、UIViewController、UIApplication)按照一定规则串联成一条链,当一个事件发生时,如果当前对象无法处理,就会沿着链上传递,直到被处理或者丢弃

🧩 谁是响应者(Responder)?****

只要继承自 UIResponder 的类,都是响应者:

• UIView

• UIViewController

• UIApplication

• UIWindow

二、事件传递机制(Touch)****

事件传递分为三步:

1. UIApplication 接收系统事件****

系统产生事件(如触摸) → UIApplication 接收 → 将事件传递给主 window(keyWindow)

UIApplication -> UIWindow

2. UIWindow 传递给最合适的 UIView****

Window 会调用:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

用来寻找事件目标视图(target view)——最前面、最小尺寸、最精准命中的那个子视图

3. 事件沿着响应链传递****

找到目标 View 后,会调用它的 touchesBegan:withEvent: 方法。如果它不处理事件,就往上一个响应者传:

[view touchesBegan:event]
→ view.superview
→ viewController.view
→ viewController
→ UIWindowUIApplication
→ AppDelegate

🔁 响应链传递顺序:

UIViewUIView.superview → UIViewController.view → UIViewControllerUIWindowUIApplication → AppDelegate

三、事件传递 vs 响应链 的关系

概念传递路径作用****
事件传递UIApplication → UIWindow → UIView(通过 hitTest)定位事件应该交给哪个视图处理
响应链UIView → superview → VC → UIWindow → UIApplication如果当前视图不处理事件,就沿着链上传递

四、关键方法说明****

1. hitTest:withEvent: 和 pointInside:withEvent:****

hitTest:withEvent::****

决定哪个子视图最终接收事件。默认递归调用子视图的 hitTest:。

pointInside:withEvent::****

决定触摸点是否在视图范围内,控制是否参与事件响应。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.userInteractionEnabled || self.hidden || self.alpha <= 0.01) return nil;
    if (![self pointInside:point withEvent:event]) return nil;

    for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
        CGPoint convertedPoint = [subview convertPoint:point fromView:self];
        UIView *hitView = [subview hitTest:convertedPoint withEvent:event];
        if (hitView) return hitView;
    }
    return self;
}

五、如何自定义控制事件传递?****

  1. 重写 hitTest: 阻止某些视图响应****

  2. 使用 gestureRecognizer:shouldReceiveTouch: 决定哪个手势处理事件****

  3. 设置 userInteractionEnabled = NO 跳过该 view

六、实际问题举例****

❓按钮点击无效?****

检查顺序:

• 是否在 hitTest: 返回了 nil

• userInteractionEnabled 是否为 NO

• 父视图是否挡住它(即 hitTest 被父视图拦截)

❓多个手势冲突怎么办?****

使用:

gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:

或者设置:

requireGestureRecognizerToFail:

七、调试响应链的技巧

打印响应链:

UIResponder *responder = view;
while (responder) {
    NSLog(@"%@", responder);
    responder = [responder nextResponder];
}

八、小结

概念功能核心方法****
响应链谁响应事件nextResponder
事件传递谁“接收”事件hitTest:withEvent:
点命中检测是否在点击区域pointInside:withEvent:

block中添加一个通知,会不会产生循环引用,怎么避免

- (void)setup {
    [[NSNotificationCenter defaultCenter] addObserverForName:@"MyNotification"
                                                      object:nil
                                                       queue:[NSOperationQueue mainQueue]
                                                  usingBlock:^(NSNotification * _Nonnull note) {
        NSLog(@"%@", self.title); // 这里使用了 self
    }];
}