重构先从退出和切房开始
在我维护的语聊项目里,房间控制器承接了不少工作:加入频道、处理消息、更新麦位、显示礼物和跳转其他页面。项目按业务目录组织,很多视图通过设置模型刷新内容。这套方式能跟上需求,但功能逐渐增加以后,一个状态变化可能影响播放器、等待队列和几个展示层。再加一个入口不难,确认旧房间已经结束却越来越需要耐心。
所以我更愿意先整理生命周期,再决定要不要改架构。拿切房来说,旧频道的音频、旧消息触发的展示、尚未完成的动画,并不应该跟到新房间。现有代码在切房流程里集中调用动画停止入口,分别停止礼物、入场效果、跑马灯等对象,再清掉引用。这样查问题时能找到一个明确的收尾位置,不必到每个消息分支里寻找遗漏。
这里还要区分页面暂时不可见和真正退出。语聊产品可能把房间缩成悬浮入口,用户进入其他页面时,语音业务仍然存在。把所有清理都塞进页面消失回调,会把正常的房间会话一起停掉;只依赖析构,又可能等不到合适的停止时机。我的取舍是先说清业务什么时候结束,再让各个组件负责自己的资源。
先确认留下的是什么
遇到内存持续增长,我会先固定复现条件:同一台设备、相同素材和播放次数,依次观察进入房间、连续播放、停止以及退出后的变化。首次解码和缓存建立,本来就可能抬高内存。需要继续确认的是,重复进入以后,对象数量是否持续增加,已经不用的播放器和视图是否仍然留着。单看一次峰值,很容易把缓存、临时分配和泄漏混在一起。
具体定位时,我会用 Allocations 看分配增长,再用内存图追踪相关对象的持有关系。业务已经不需要的对象,如果还被队列、回调或其他管理对象引用,应该沿着引用找回去。Apple 的 Block 文档解释了对象捕获和强引用环。ARC 能管理引用计数,但不能替我决定业务何时结束,也不能自动拆掉强引用环。看到一个 Block 里出现 self,还需要继续找是否存在回到该对象的持有路径,不能直接把它判成泄漏。
定时器尤其需要把“释放”和“停止工作”分开考虑。项目里的滚动文字用调度源定时更新,停止方法会取消定时器并清空引用;跑马灯等待逻辑也有独立的失效入口。弱引用能减少对象被回调留住的机会,但不能代替取消任务。我检查这类代码时,会把整个回调看完,确认没有一部分使用弱引用、另一部分又直接捕获原对象的情况。
移除视图以后,工作也要结束
房间动画不一定都挂在同一个容器里。当前代码里,礼物气泡和跑马灯就有独立的展示层。只清空最初那个动画容器,会漏掉挂在房间根视图上的内容。停止入口因此需要找到实际的展示对象,停止播放器和滚动文字,移除内容视图,再清掉当前内容、点击区域以及等待队列中的对应状态。界面看不见了,不等于这些工作已经结束。
另一个细节是懒加载。正常展示时,访问属性可以顺便创建组件;清理时沿用同样的写法,却可能把原本不存在的动画管理对象创建出来。项目现在的切房清理直接操作已经存在的实例,退出页面时同步状态也避开不必要的 getter。我把这种改动看作重构的一部分:停止方法只处理已有资源,减少隐藏的创建行为,也让代码更容易推断。
异步完成回调还可能比停止动作晚一步。跑马灯当前使用播放代次和内容视图身份一起校验:开始新一轮播放或主动停止时更新代次,旧动画完成后,只有仍属于当前内容的回调才能推进队列。这样解决的是旧回调干扰新状态的问题。它和弱引用解决的持有问题不同,两种检查都需要保留,不能用其中一种替代另一种。
清理要覆盖异常路径
正常播放结束只是其中一条路径。资源下载失败、解析失败、切换房间和应用进入后台,都可能提前结束展示。礼物播放器当前在失败时会释放播放中的状态、移除当前展示并尝试下一条;统一停止时清空等待内容并停止播放器。只在成功完成回调里清理,既可能留下资源,也可能让后续队列一直以为“还在播放”。
验证这类改动,我会重新走相同的操作,分别确认对象是否按预期释放、旧回调是否还会触发新展示,以及房间交互有没有被挡住。如果内存已经回落但界面仍然卡顿,就继续用 Time Profiler 看主线程上的文件读取、解码和布局耗时。引用关系正确,并不代表每帧工作都足够轻。
这类重构带给我的直接判断标准,是切房后旧内容能否停干净,失败后下一条能否继续,暂时离开页面又能否保留该保留的业务。这些操作可以重复验证,也比单看一次内存峰值更容易发现遗漏。
延伸阅读
对应项目Talka