播放页是QQ音乐内曝光量最大的二级页,是端内展示歌曲信息、提供播控操作、进行推荐宣发的重要入口。随着QQ音乐的快速发展,播放页也从一个简单播控页面逐渐演变到了现在业务众多、UI多变的复杂页面。在该转变的过程中,播放页Android端的代码也根据不同时期的需要,进行了持续演进。本文将简要回顾Android端播放页代码在过去不同时期的结构特点,并重点介绍在最近一次代码结构调整中,我们探索出的一种适合多人开发和代码复用的复杂页面管理模式。
图1:QQ音乐播放页背景MVC在QQ音乐发展之初,播放页承载的功能较少,业务逻辑也比较简单,主要负责用户浏览歌曲信息、进行播控操作等功能。在代码架构上采用了如下图所示的MVC结构:View负责UI展示,Controller负责业务逻辑,Model负责数据交互。
图2:播放页初期MVC结构在具体实现时:
View层由XML布局文件、PlayerActivity实现:
xml负责布局
PlayerActivity负责为对应的View控件设置点击事件、进行数据绑定
Controller层同样由PlayerActivity实现,主要负责:
接收model层的数据更新广播,获取最新数据更新UI;
接受View层点击事件,触发model层数据更新
页面路由
Model层则由端内各种Manager实现。
SubControllers上述MVC的架构在QQ音乐发展初期基本满足我们的需要,实现了需求的快速迭代。但是随着QQ音乐的快速发展,作为QQ音乐曝光量最大的页面之一,播放页成为端内重要的推荐宣发入口,承载的功能逐渐增多,PlayerActivity代码行数快速膨胀。为了解决上诉问题,在后续的需求迭代中开始有意将复杂业务从PlayerActivity中独立出来,拆分出新的SubController与PlayerActivity共同承担Controller层的职责,在具体职责划分时:
PlayerActivity负责接收公共数据更新的广播(如播放歌曲或播放状态的变化),并根据各Controller的需要调用相关API驱动业务逻辑和UI更新。
各SubControllers则封装了对应业务的UI、页面路由及对应Model的数据更新逻辑。
同时为了方便各SubController获取自己业务相关的ViewReference并避免重复调用findViewById,PlayerActiivty在XML被inflate之后将所有子View的引用存储在了ViewHolder中,各SubController通过ViewHolder获取自己所需的View引用。此时衍生出的架构如下图所示:
图3:播放页后期SubController结构上述策略在一定程度上缓解了PlayerActivity日益膨胀的代码,且由于当时各业务模块之间相互独立,UI控件与Controller之间保持了N:1的关系;此时各Controller的职责相对单一,各模块之间依赖关系简单,数据流也是自上而下的单向数据流,代码复杂度相对可控,因此该架构在一段时间内也很好的支撑了业务的发展。
图4:SubController之间简单的依赖关系和数据流动但是随着q音内容的多元化,播放页的业务模块越来越多,不同的业务模块之间也开始相互影响,上述SubControllers的模式开始显示出其不足,为后续的维护和扩展带来了困难:
由于不同的业务模块之间开始相互影响,以及没有及时对这种影响进行代码层面的管控,原来简洁的依赖关系和数据流动逐渐恶化成了下图混乱的结构,这导致代码理解困难,根据代码很难还原一个UI控件的更新逻辑,bug频出:
图5:SubController结构的恶化数据流混乱:
不同Controller之间开始出现相互调用,数据流不再是简单的自上而下,开始横向在SubControllers之间流动
Controller和UI控件开始出现多对多关系,一个View可能在多个Controller里被更新,进一步增加了代码的理解成本:
由于ViewHolder持有了所有的View,在快速开发过程中,各Controller很容易扩大自己的职责,绕过对应业务的Controller直接从ViewHolder中更新其他View;
这导致为了梳理一个UI控件的更新逻辑,除了需要