一、基础知识类****
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 的消息查找机制:
消息查找顺序:****
-
先查当前类的 method list(含分类插入的方法)
-
查父类
-
直到 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));
运行时流程如下:
-
objc_msgSend 找 isa 指向的类
-
在方法缓存查找 SEL
-
如果找不到,走 resolveInstanceMethod:
-
再找不到走 forwardingTargetForSelector:
-
再找不到走 forwardInvocation:
-
最后触发崩溃 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 时,它会经历以下步骤:
-
通知 Observers,即将进入 RunLoop
-
进入 Loop,设置为休眠(等待事件)
-
某个事件(比如 Timer、输入源)唤醒它
-
分发事件并处理回调
-
通知 Observers,RunLoop 即将休眠/退出
-
重复以上过程,直到退出条件触发
📦 四、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"];
查找顺序:****
-
是否存在 getName、name、isName 等 getter 方法
-
是否有 _name、name 实例变量
-
调用 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 属性时:
-
系统会生成一个新的子类:NSKVONotifying_Person
-
person 实例的 isa 指向这个子类
-
这个子类重写了 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 的区别与联系
| 对比点 | KVC | KVO |
|---|---|---|
| 全称 | Key-Value | Coding 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 的核心机制。
⚠️ 四、注意事项
| 特性 | 说明**** |
|---|---|
| 自动置 nil | weak 引用目标对象销毁后自动变 nil |
| 不增加引用计数 | weak 不会 retain 目标对象 |
| 线程安全 | weak 表操作加锁,性能略差于 strong |
| 不能指向 stack | __weak 只能引用 heap 上的对象,否则运行时 crash |
| 不能用在 struct | weak 只能用于 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
→ UIWindow
→ UIApplication
→ AppDelegate
🔁 响应链传递顺序:
UIView → UIView.superview → UIViewController.view → UIViewController → UIWindow → UIApplication → 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;
}
五、如何自定义控制事件传递?****
-
重写 hitTest: 阻止某些视图响应****
-
使用 gestureRecognizer:shouldReceiveTouch: 决定哪个手势处理事件****
-
设置 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
}];
}