米可世界

米可世界

1.Runloop 的流程,底层实现,项目里实际使用,监控卡顿监听的具体是哪个状态,Source0 和 Source1 区别

2.局部对象的释放时机;autoreleasepool的实现,weak 存在哪里

3.SDK 构建提效,有没有二进制化的经验,缓存

4.feed 流滑动卡顿可能有哪些原因;离屏渲染为什么会影响;那些异步的库,不也是在另一个屏幕缓冲区处理的吗?

5.首屏优化,有没有针对各个模块的,比如卡线程了,怎么检测,怎么优化

6.IM Socket 的流程

7.FMDB 的内部实现,是线程安全的吗?和其他如 WCDB 效率的比较;

8.用过哪些锁?效率比较;@synchronized 的作用是什么?

9.上传大图,怎么分片,怎么控制线程最大并发数,怎么告诉web上传进度?上传进度细颗粒度,每片都有哪些状态,现有上传单个图片的方法,怎么实时同步进度

10.SDWebImage 加载图片的流程,缓存机制,LRU怎么实现的?加载图片传入了 UIImageView 下载完图片后怎么拿到这个对象,因为是异步的

11.OOM 怎么避免?

12.MVCS 与 SectionProvider 怎么实现的

参考答案

1.Runloop 的流程,底层实现,项目里实际使用,监控卡顿监听的具体是哪个状态,Source0 和 Source1 区别

(1)1. RunLoop 的流程

RunLoop 是一个事件循环机制,内部主要流程可以简化为:

  1. 通知 Observers:进入 Loop 前,会发通知(如 kCFRunLoopEntry)。
  2. 处理 Timer:检查是否有已到期的 Timer 任务。
  3. 处理 Source0:非基于端口的事件(UI 事件、Block 回调等),需要主动唤醒。
  4. 处理 Source1:基于 Mach port 的事件(系统事件、IPC、触摸事件)。
  5. 进入休眠:调用 mach_msg_trap 等底层系统调用,线程进入休眠状态。
  6. 被唤醒
    • 有事件到来(Source、Timer、GCD dispatch)。
    • 外部手动唤醒(CFRunLoopWakeUp)。
  7. 通知 Observers:将要处理事件(kCFRunLoopBeforeSources / kCFRunLoopBeforeWaiting / kCFRunLoopAfterWaiting)。
  8. 循环往复:直到 CFRunLoopStop 被调用。

(2) 底层实现
• 核心结构:CFRunLoop 和 CFRunLoopMode,每个线程对应一个 RunLoop(存储在 pthread 的 TLS 中)。
• 事件驱动:通过 Mach port 和 内核态通信。比如触摸事件从 IOKit -> SpringBoard -> App -> RunLoop Mach port。
• 休眠/唤醒:利用系统调用 mach_msg_trap 进入内核,等待消息;有事件时唤醒。
• RunLoopObserver:内部维护一组回调,挂在不同状态点(Entry、BeforeTimers、BeforeSources、BeforeWaiting、AfterWaiting、Exit)。

(3)项目里的实际使用

  • 定时器:NSTimer / CADisplayLink / performSelector:afterDelay: 本质依赖 RunLoop。
  • 常驻线程:如 AFNetworking 的网络请求线程,使用 RunLoop 保持线程不退出。
  • 事件响应:触摸事件、UI 更新都跑在主线程 RunLoop 中。
  • GCD 与 RunLoop:dispatch_async 提交到主队列的 block,本质通过 CFRunLoopSource 执行。
  • 性能优化:比如在 NSRunLoopCommonModes 下添加任务,避免因 UI 滑动(UITrackingRunLoopMode)而阻塞。

(4)卡顿监控监听的状态

常见的 卡顿监控(UI 卡顿监控) 原理就是利用 RunLoop Observer,在关键状态点打点:
• 监听 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting。
• 在 BeforeSources 说明即将处理事件。
• 在 AfterWaiting 说明刚被唤醒,准备进入事件处理。
• 如果这两个状态之间持续很久没有切换,说明 RunLoop 卡在某个任务(主线程卡住)。

具体做法:

1.    在主线程 RunLoop 添加 Observer,监听这两个状态。
2.    同时开一个子线程用 dispatch_semaphore_wait + 超时机制判断。
3.    超过阈值(如 > 200ms)没有状态切换,判定为卡顿。
4.    结合堆栈采样(backtrace)就能定位具体卡住的位置。

(5)source0 与 source1 区别

  1. Source0
    • 定义:非基于内核端口(port)的事件源。
    • 特点:
    • 纯用户态,不会主动唤醒 RunLoop,需要外部手动唤醒。
    • 常用于 App 内部事件分发,例如 performSelector:onThread:。
    • 触发方式:
    • 调用 CFRunLoopSourceSignal(source) 标记为待处理。
    • 再调用 CFRunLoopWakeUp(runloop) 唤醒 RunLoop。
    • 例子:
    • performSelector:onThread:
    • 手动创建的 CFRunLoopSource0

  2. Source1
    • 定义:基于 Mach port 的事件源。
    • 特点:
    • 内核态驱动,系统事件到达时可以直接唤醒 RunLoop。
    • 主要用来处理 系统消息(如触摸、系统回调、CFMachPort)。
    • 触发方式:
    • 当端口有消息时,内核会唤醒 RunLoop,执行对应的回调。
    • 例子:
    • 系统的触摸事件、屏幕旋转事件。
    • CFMachPort、CFSocket

2.局部对象的释放时机;autoreleasepool的实现,weak 存在哪里

(1)局部对象的释放时机

  • 在 ARC 下,局部变量对象(比如方法里的 NSObject *obj = [[NSObject alloc] init];)的释放时机主要取决于 作用域 + 引用计数

  • 编译器会在合适的位置插入 objc_release。

  • 通常规则:

    1. 如果是强引用(strong),在超出作用域时编译器插入 release。
    2. 如果放在了 @autoreleasepool 内,且对象通过 autorelease 创建(如 [NSString stringWithFormat:@”xx”]),则会在 pool 清空时统一 release。
    3. 如果没有显式放进 autoreleasepool,那么局部对象的 autorelease 会挂在当前线程的默认 autoreleasepool page 上(RunLoop 每次循环会清理一次)。
  • 所以:
    • alloc/init 的对象:离开作用域立即释放。
    • 工厂方法 (stringWith…) 返回的对象:通常是 autorelease,释放时机取决于最近的 autoreleasepool drain。

(2) autoreleasepool 的实现

  • @autoreleasepool {} 在编译后会被转化为:
    1
    2
    3
    4
    5
    void* context = objc_autoreleasePoolPush();
    { // pool 内部作用域
    ...
    }
    objc_autoreleasePoolPop(context);

​ • 底层实现依赖 AutoreleasePoolPage:
​ • 每个线程维护一个栈状的双向链表结构。
​ • objc_autorelease(obj) 会把对象的指针放入当前 page。
​ • pool 被 pop 时,会遍历这个 page,依次对对象发送 release。
​ • RunLoop 机制:
​ • 主线程的 RunLoop 每次事件循环开始会 push 一个 pool,结束时 pop。
​ • 这保证了常见的 UIKit API 里 autorelease 对象能在一次事件处理后被释放。

(3)weak 的存储位置
• weak 指针并不是单纯的内存地址,它需要 运行时的弱引用表(weak_table_t) 管理:
• weak_table_t 是一个 hash 表,key 是对象地址(被引用的对象),value 是指向该对象的所有 weak 指针地址集合。
• 当对象的引用计数归零时,dealloc 流程会调用 objc_destroyWeak,把所有指向该对象的 weak 指针置为 nil。
• 所以 weak 指针存储在:
• 指针本身依然在栈上 / 堆上(取决于变量声明位置),
• 但 runtime 维护了一份全局的弱引用表,用来统一管理 对象 ↔ weak 指针列表 的关系。
• 注意:
• weak 本身是一个普通指针,只不过 runtime 在对象销毁时会把它自动清空。
• 所以 weak 查找和赋值都会经过 runtime 的弱引用表操作,有一定性能开销。

3.SDK 构建提效,有没有二进制化的经验,缓存

模块化/组件化
SDK 内部拆分为稳定依赖(binary、SPM precompiled)+ 高频改动部分(源码),避免全量编译。

构建缓存
CI 里缓存 DerivedData、Pods、SPM 构建产物。

4.Feed 流滑动卡顿可能有哪些原因;离屏渲染为什么会影响;那些异步的库,不也是在另一个屏幕缓冲区处理的吗?

(1) Feed 流滑动卡顿的常见原因
卡顿一般是因为 主线程在 16.67ms(60fps 下)或者 8.3ms(120fps 下)没有完成一次绘制提交。常见原因:

  • 布局计算过重
    • 大量 autoLayout 约束计算
    • cell 高度动态计算不缓存
  • 主线程阻塞

    • 大量 JSON 解析、IO、图片解码放在主线程
    • 复杂业务逻辑或锁竞争
  • 绘制负担过重

    • 大量圆角、阴影、mask、group opacity 等触发离屏渲染
    • 大图缩放、解码延迟
  • 频繁创建/销毁对象

    • cell 重复 init,而不是重用
  • 图片处理问题

    • 未做预解码、下采样,导致 GPU/CPU 开销大

(2)为什么离屏渲染会影响滑动流畅度

  • 什么是离屏渲染 (Offscreen Rendering)

    • GPU 本来在 On-Screen Buffer(屏幕缓冲区)里画东西,顺序执行即可。

    • 一旦遇到圆角 + mask、阴影、layer.shouldRasterize 等情况,GPU 会:

      1. 先新建一个 Offscreen Buffer
      2. 把内容画到这个 buffer
      3. 再拷贝回 On-Screen Buffer
  • 开销在哪

    • 新建 buffer 本身消耗内存
    • GPU 切换上下文(Context Switch)有性能开销
    • 结果要 copy 回主屏幕 buffer,增加带宽占用
  • 为什么会卡顿

    • 这不是“异步”的,而是 GPU 的额外工作,依然要卡在 渲染管线(Display Link vsync 同步)。
    • 如果离屏渲染量大,GPU 跟不上 vsync 节奏,就掉帧 → feed 滑动卡顿。

(3)“异步绘制库” 和离屏渲染的区别

你说的 “异步的库”(比如 YYAsyncLayer, Texture (AsyncDisplayKit))其实跟 Core Animation 的 Offscreen Rendering 不一样:

  • 异步绘制库

    • 把文本排版、图片合成、UI 绘制逻辑放到 后台线程的 CGContext 里完成
    • 得到一张最终的位图(Bitmap)
    • 主线程只把这张图交给 layer.contents
    • → 避免了主线程阻塞 & 避免 GPU 临时建 buffer
  • GPU 离屏渲染

    • 渲染阶段 GPU 为了实现特效而“不得不”建一个临时 buffer
    • 发生在绘制提交之后、硬件管线中
    • → 无法用“放后台线程”解决,因为是 GPU 硬件的需求

可以这样理解:

  • 异步绘制库:CPU 先画好 → 减轻主线程 & GPU 压力
  • 离屏渲染:GPU 临时建 buffer → 加重 GPU 压力

总结:

  • Feed 卡顿大多是 主线程阻塞 + GPU 负担过重
  • 离屏渲染会卡,是因为 GPU 在渲染管线上多了额外 buffer 开销
  • 异步绘制库的“异步 buffer”不是 GPU 的 Offscreen Buffer,而是 CPU 侧先生成位图,两者概念完全不同。

5.首屏优化,有没有针对各个模块的,比如卡线程了,怎么检测,怎么优化
卡线程检测方法

1.    Runloop 卡顿监控
•    利用 CFRunLoopObserver 监听主线程 RunLoop 的状态(kCFRunLoopBeforeSources、kCFRunLoopAfterWaiting 等)。
•    当 RunLoop 某次循环超过阈值(如 >200ms)还未完成,就认为发生了卡顿。
•    工具类库:YYAsyncLayer、FBRetainCycleDetector + 自己封装卡顿监控。
2.    堆栈采样 (Stack Sampling)
•    使用子线程定时采样主线程调用栈(比如每 50ms 取一次)。
•    当检测到主线程长时间无响应时,打印采样栈,可快速定位阻塞点。
•    开源方案:KSCrash、PLCrashReporter。
3.    系统工具
•    Instruments → Time Profiler:定位函数耗时。
•    Instruments → Main Thread Checker:发现 UI API 是否在子线程调用。
•    Instruments → System Trace:更底层地看线程调度与锁等待。

常见模块的首屏优化点:
(1)启动阶段(冷启动)

  • 优化检测
  • 利用 DYLD_PRINT_STATISTICS 环境变量观察动态库加载耗时。

    • Xcode 的 App Launch 模板可分析启动阶段。
  • 优化手段

    • 减少动态库数量,合并 Pod。
    • 用 App Clips 或延迟加载来减少首次启动体积。
    • +load 和 +initialize 里避免做耗时操作。

(2) 主线程耗时操作

  • 检测

    • Runloop 卡顿监控 + Instruments。
  • 优化

    • UI 布局计算移到子线程(AutoLayout 的约束尽量减少层级)。
    • 图片解码、JSON 解析放子线程(YYImage / YYModel 的异步方案)。
    • 渲染复杂 UI 时用 CALayer 或离屏渲染优化。

(3)网络模块

  • 检测

    • 在首屏接口加埋点(发起时间 - 响应时间)。
    • Charles / Wireshark 抓包分析耗时。
  • 优化

    • 核心数据接口提前并发请求,不要串行。

    • 使用缓存(磁盘缓存、内存缓存、预加载)。

    • 大图延迟加载,必要时用占位图。

(4) 图片加载

  • 检测

    • Instruments → Allocations/Time Profiler 观察解码与内存情况。
  • 优化

    • 预解码(SDWebImage、YYImage)。
    • 降低分辨率,避免原图直接展示。
    • 使用 WebP/HEIF 格式。

(5)渲染与绘制

  • 检测
  • Instruments → Core Animation 检测 FPS、离屏渲染。

    • GPU Driver template 观察 GPU 是否被打满。
  • 优化

    • 减少视图层级,避免过度使用透明度/圆角/阴影。
    • 尽量用 CAShapeLayer 替代复杂绘制。
    • 提前渲染静态页面的快照。

(6)数据处理 & 本地 IO

  • 检测

    • Time Profiler 定位磁盘/数据库耗时。
  • 优化

    • 首页不要做大量磁盘读写。
    • CoreData/SQLite 的查询下沉到子线程。
    • UserDefaults 批量写时要避免阻塞。

6.IM Socket 的流程

1.建立连接

  • 使用 CFStream(CoreFoundation)、CFSocket、或者三方库(如 CocoaAsyncSocket)来建立 TCP 连接。

  • 或者直接用 WebSocket(iOS 13+ 推荐 URLSessionWebSocketTask,早期可用 SocketRocket)。

  • 流程:

    1. 客户端发起连接请求(IP + 端口)。
    2. 服务器返回是否成功。
    3. 成功后,进入可读写状态。

2.登录/鉴权

  • 建立连接后,通常第一步是发送 登录包 / 鉴权信息(例如 userId、token、设备信息)。
  • 服务器验证通过后,会返回 登录成功 ACK
  • 只有在鉴权成功后,Socket 才允许继续收发消息。

3.维持长连接

  • 长连接需要保持心跳(心跳包 ping/pong)。
  • 客户端定时(如 30s/60s)发送 心跳包,服务器回应 心跳 ACK
  • 如果心跳超时(连续几次未响应),认为连接断开,需要自动重连。

4.消息收发

  • 发送消息:

    • 将消息序列化成约定的数据格式(JSON / Protobuf / 二进制协议)。
    • 封装消息头(包含消息类型、长度、消息 ID 等)。
    • 通过 Socket write/send 发出。
  • 接收消息:

    • Socket 输入流回调或代理触发。
    • 先读消息头,解析数据包长度。
    • 再按协议读取完整消息体。
    • 反序列化成业务层可用的模型对象。

5.消息确认(ACK机制)

  • 为了保证消息可靠性,通常采用 消息回执机制

    • 客户端发送消息 → 服务器收到后,返回 ACK(确认包,包含消息 ID)。
  • 如果客户端在一定时间内未收到 ACK,则重发消息。

6.断线重连

  • 网络变化(WiFi/4G切换)、App 后台 → 前台,都会导致连接中断。

  • 需要实现 自动重连机制

    • 发现连接断开 → 进入重连流程。
    • 指数退避重试(如 1s → 2s → 4s → 8s…,有上限)。
    • 连接成功后,重新鉴权、同步离线消息。

7.离线消息同步

  • 断线期间可能会有消息未收到。
  • 重新连接成功后,客户端需要调用接口获取 离线消息,再和本地消息队列合并。

8.退出/释放

  • 用户主动退出登录时,发送 退出包
  • 关闭 Socket,释放资源。

7.FMDB 的内部实现,是线程安全的吗?和其他如 WCDB 效率的比较;

(1)FMDB 的线程安全性

  1. FMDatabase
  • FMDatabase 本身 不是线程安全的
    • 官方明确建议:一个 FMDatabase 实例只能在单一线程中使用,不能跨线程共享。
  1. FMDatabaseQueue
  • 为了保证线程安全,FMDB 提供了 FMDatabaseQueue。

  • 内部实现方式:

    • 维护一个 dispatch_queue(串行队列)。
      • 通过 inDatabase: 或 inTransaction: 方法,将所有对数据库的操作 block 串行提交到这个队列里执行。
    • 这样可以保证同一时间只有一个线程在访问数据库,从而避免 SQLite 的并发写入问题(SQLite 本身写操作也是串行化的)。
  1. FMDatabasePool
    • 为了优化多读场景,FMDB 还提供 FMDatabasePool。
    • 内部维护多个数据库连接(通常用于读操作)。
    • 读操作可以并发分配给不同的连接,写操作仍然会被串行化。

结论:

  • 单独使用 FMDatabase → 线程不安全
  • 使用 FMDatabaseQueue/Pool → 线程安全

8.用过哪些锁?效率比较;@synchronized 的作用是什么?

  1. OSSpinLock(已废弃)
    • 自旋锁,忙等,会不断轮询等待锁释放。
    • 性能很高,但存在优先级反转问题(高优先级线程可能被低优先级线程“饿死”),Apple 已经不推荐使用。
    • iOS 10+ 建议用 os_unfair_lock 代替。
  2. os_unfair_lock
    • 自旋锁的替代品,会在等待时挂起线程,避免优先级反转。
    • 适合短时间、高频加锁的场景。
    • 性能比 pthread_mutex 更好。
  3. pthread_mutex(互斥锁)
    • POSIX 标准互斥锁。
    • 比较通用,支持递归锁(PTHREAD_MUTEX_RECURSIVE)。
    • 开销比 os_unfair_lock 大一些,因为需要进入内核等待。
  4. NSLock
    • Objective-C 封装,内部基于 pthread_mutex。
    • 提供了面向对象的 API,简单易用。
    • 性能略低于 os_unfair_lock。
  5. NSRecursiveLock
    • 递归锁,允许同一线程多次获得同一把锁,避免死锁。
    • 底层也是 pthread_mutex(recursive)。
  6. NSCondition / NSConditionLock
    • 条件锁,用于线程间的条件同步(等待某个条件满足才继续)。
    • 常用于“生产者—消费者”模型。
  7. dispatch_semaphore
    • GCD 提供的信号量机制。
    • 可用于资源计数、并发数控制,也可以当成锁来用。
    • 性能和可控性较好。
  8. @synchronized
    • Objective-C 关键字,基于 objc_sync_enter/objc_sync_exit 实现。
    • 内部使用哈希表管理对象锁。
    • 使用简单,但性能比 NSLock 差一些,因为额外做了对象管理和异常处理。
    • 适合快速实现线程安全,但不推荐在性能敏感场景中使用。

9.上传大图,怎么分片,怎么控制线程最大并发数,怎么告诉web上传进度?上传进度细颗粒度,每片都有哪些状态,现有上传单个图片的方法,怎么实时同步进度

(1)分片策略

  • 分片大小:一般 2MB5MB 一片比较合理(20M → 104 片)。

    • 分片太小:请求数太多,开销大;
    • 分片太大:失败重传成本高。
  • 分片切割:用 NSData 的 subdataWithRange: 或者 InputStream,把整张图片按顺序拆成多个 NSData。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
NSData *imageData = UIImageJPEGRepresentation(image, 0.9);
NSUInteger chunkSize = 2 * 1024 * 1024; // 2MB
NSUInteger length = [imageData length];
NSUInteger offset = 0;
NSMutableArray *chunks = [NSMutableArray array];

while (offset < length) {
NSUInteger thisChunkSize = MIN(chunkSize, length - offset);
NSData* chunk = [imageData subdataWithRange:NSMakeRange(offset, thisChunkSize)];
[chunks addObject:chunk];
offset += thisChunkSize;
}

(2)控制并发数

  • 不要一次性全部并发上传(可能耗尽带宽/内存/线程)。
  • 推荐用 NSOperationQueueGCD 信号量来限制最大并发数。

例子:最多并发 3 个分片上传

1
2
3
4
5
6
7
8
9
10
11
12
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3); // 最大并发 3
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

for (NSData *chunk in chunks) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

[self uploadChunk:chunk completion:^{
dispatch_semaphore_signal(semaphore);
}];
});
}

如果要更优雅,可以用 NSOperationQueue,设置 maxConcurrentOperationCount = 3。

(3)上传进度
方式 A: NSURLSessionUploadTask + progress

  • 每个分片创建一个 NSURLSessionUploadTask,监听其 NSProgress 对象。
  • NSProgress 能返回当前上传的字节数,结合所有分片,汇总出整体进度。

方式B: NSURLSession delegate (更灵活)

汇总整体进度 所有分片进度的加权平均

  • 每个分片有状态(Pending/Uploading/Success/Failed)。
  • 上传时监听 NSURLSession 的进度回调,实时拿到字节数。
  • 汇总所有分片进度得到整体进度。
  • 再把整体进度同步给 UI 或 Web。

10.SDWebImage 加载图片的流程,缓存机制,LRU怎么实现的?加载图片传入了 UIImageView 下载完图片后怎么拿到这个对象,因为是异步的

(1)SDWebImage 加载图片的流程

  1. 入口:
    1
    [imageView sd_setImageWithURL:url placeholderImage:nil options:0 completed:nil];

传入 UIImageView 和 URL 后,会走到 SDWebImageManager 的 loadImageWithURL。

  1. 检查缓存:
    • 先查 内存缓存(SDImageCache.memoryCache,底层是 NSCache + LRU)
    • 内存没有,再查 磁盘缓存(通过 key -> 文件路径 / SQLite 索引)
  2. 下载图片(如果缓存都没有):
    • 调用 SDWebImageDownloader,内部基于 NSURLSession 创建下载任务,异步下载数据。
    • 支持下载队列、并发控制、请求去重、进度回调。
  3. 解码 & 缓存:
    • 图片数据下载完成后,会进行 解码(decode),避免主线程卡顿。
    • 然后写入 内存缓存 + 磁盘缓存。
  4. 回调 UI:
    • 下载完成后,主线程回调,把 UIImage 设置到传入的 UIImageView.image 上。

(2)缓存机制

SDWebImage 的缓存分两级:

  1. 内存缓存
  • 使用 NSCache 封装(带自动清理机制,内存紧张时会自动回收)。

  • 内部实现了 LRU (Least Recently Used) 算法:

- 每次访问一个图片,会把它放到链表头。
- 淘汰时从链表尾部移除最久未使用的对象。
- 在 SD 里,具体用的是 SDMemoryCache,基于 YYMemoryCache 的思想,维护了一个 **双向链表 + 哈希表** 结构。
  1. 磁盘缓存
  • 旧版本用 NSKeyedArchiver 直接存二进制文件。

  • 新版本用 sqlite + 文件混合存储:

- 小文件直接存 sqlite。
- 大文件写到磁盘,sqlite 里保存路径索引。
  • 有过期策略、容量控制(例如超过 1 周 / 超过 500MB 自动清理)。
    (3)LRU 实现思路(简化版)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // LRU 的核心:哈希表 + 双向链表
    class LRUCache {
    NSMutableDictionary *dict; // key -> node
    Node *head, *tail; // 双向链表

    - (id)get:(NSString *)key {
    Node *node = dict[key];
    if (node) {
    [self moveToHead:node]; // 最近使用过,移动到头部
    return node.value;
    }
    return nil;
    }

    - (void)put:(NSString *)key value:(id)value {
    Node *node = dict[key];
    if (node) {
    node.value = value;
    [self moveToHead:node];
    } else {
    node = [[Node alloc] initWithKey:key value:value];
    dict[key] = node;
    [self addToHead:node];
    if (dict.count > capacity) {
    Node *removed = [self removeTail]; // 淘汰最久没用的
    [dict removeObjectForKey:removed.key];
    }
    }
    }
    }

(4)UIImageView 与异步回调
UIImageView 是通过 AssociatedObject 绑定 URL 的,所以异步回调时 SDWebImage 能知道要给哪个 imageView 设置图。

11.OOM 怎么避免?

避免 OOM 的核心就是 减少单次内存峰值 + 控制整体内存占用 + 及时释放资源。实际项目中通常要针对业务做专项优化,比如:
• 大图 -> 缩放解码
• 视频 -> 硬解码,流式处理
• 数据 -> 分页加载,NSCache 管理
• 缓存 -> 收到内存警告立即清理

12.MVCS 与 SectionProvider 怎么实现的

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×