实现 60fps 的网易云音乐首页
网易云音乐是一款很优秀的音乐软件,我也是它的忠实用户。最近在研究如何更好的开发TableView,接着我写了一个Model驱动的小框架 - MDTable。为了去验证框架的可用性,我选择了网易云音乐的首页来作为Demo,语言是Swift 3。
本文的内容包括:
实现网易云音乐首页的思路
如何建立一个轻量级的UITableViewController(不到100行)
性能瓶颈原因以分析及如何优化到接近60fps
Note:本文并没有用Reveal去分析网易云音乐iOS客户端的原始UI布局,所以实现方式肯定和原始App有出入。另外,本文仅代表个人观点,与雇主没有任何关系。
最后效果如下
容器
整体上分析来看,网易云音乐的首页是一个异构的滚动视图。由上至下依次是:
Banner - 轮播
Menu - 三个入口
6个分类,推荐歌单,独家放送,推荐MV,精选专栏,主播电台,最新音乐。每一个分类的UI布局都不一样。
并且这些布局都不是动态的,所谓动态的就是向淘宝京东首页那种,做不同的活动,首页可以按照不同的方式去显示内容,而不需要从App Store下载新的版本。
基于这些,有两种实现方式:
用单纯的UIScrollView作为容器,其他的内容作为SubView添加到ScrollView中,但要手动控制每一个视图进入屏幕和消失的事件,来进行图片的懒加载。采用这种方式可以选择天猫开源的LazyScrollView:https://github.com/alibaba/LazyScrollView
用UITableView作为容器,其他的每一行内容都是一个Cell。
本文选择了后者,原因也很简单:我是为了评估MDTable,而MDTable是一个基于TableView的框架。
Banner
网易云音乐Banner的最上面是一个轮播图,效果如下
可以看到视图大致分为两部分:
ScrollView - 容器
ItemView - 轮播的具体内容
ImageView - 背景图
Label - 标签,就是图中的广告部分。
轮播图有很多种实现方式,这里我选择了之前写的ParallexBanner。
这是一个支持视差效果的Banner,所谓视差效果,就是类似这种:
ParallexBanner原理我在这篇博客里有详细介绍,这里就不浪费篇幅了。
另外,那个标签Label也很容易实现,只要用一个左边是圆角的UILabel即可,这里写了个方便的扩展
extension UIView {
func roundCorners(_ corners: UIRectCorner, radius: CGFloat) {
let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
}
}
Menu
Menu的目标效果如下:
中间的“每日歌曲推荐”这个选项有点意思,因为中间的文字是会随着日期变的。实现起来也很简单,图片留白,中间放一个Lable即可。
这是最后我选择的布局方式:
侧面看起来:
也就是说,SubView是这样子的:
UIImageView - 红色背景圆圈
UILabel - 标题(每日歌曲推荐)
UIImageView - 图标(日历图标)
UILabel - 日期时间(25)
Note: 这里先不管按下态,按下态在下文统一讲解。
Cells
我们先从UI效果入手,一共有六种异构的Cell。
第一个冒出来的想法是Cell中放置CollectionView,CollectiionViewLayout也很简单,采用系统提供的FlowLayout即可。
图个省事,每一个CollectionViewCell我都采用Xib的方式,用AutoLayout布局的。
然后,每一个TableViewCell的子类如下:
//初始化
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
let flowLayout = UICollectionViewFlowLayout(www.zenmebanw.com)
flowLayout.itemSize = CGSize(width: xxx, height: xxx)
collectionView = UICollectionView(frame: contentView.bounds, collectionViewLayout: flowLayout)
contentView.addSubview(collectionView)
let nib = UINib(nibName: "xxx", bundle: Bundle.main)
collectionView.register(nib, forCellWithReuseIdentifier: "cell")
}
用CollectionView写的第一个版本在这里。 感兴趣的同学可以下载下来看看,进入界面后滚动,能够明显的感到掉帧,具体的优化过程在后文。
蒙版
像这样的一个视图,需要在图像上展示白色的图标和文字,这就引入了一个问题:
如果展示文字的区域的背景图也是白色的怎么办?
答案是在图片上面盖一层半透明的渐变蒙版:
按下态
所谓按下态就是当你的手指放到一个视图上,UI会有一些变化告诉用户。比如网易云音乐的按下态是图片上加上一个半透明的遮罩:
实践的过程中发现,视图有如下特点:
支持点击手势
支持长按手势
手指接触后一小段时间(0.1秒)左右才会显示按下态,直接点击并不会出现一瞬间的半透明遮罩
按下态触发后,上下移动并不会造成TableView滚动
看来想要实现这种效果,不是简单的重写touchBegan之类的方法就能实现了。
最后,我选择了三种手势,分别用来处理点击,长按和按下态,源代码AvatarItemView。点击和长按没什么好说的,主要讲解下按态:
按下态采用一个Lazy的CoverView:
lazy var highLightCoverView: UIView = {
let view = UIView().added(to: self)
view.backgroundColor = UIColor.black.withAlphaComponent(0.5)
return view
}()
按下由一个长按手势触发:
highLightGesture = UILongPressGestureRecognizer(...)
highLightGesture.delegate = self
highLightGesture.minimumPressDuration = 0.1
//手势
func handleHight(_ sender: UILongPressGestureRecognizer){
switch sender.state {
case .began,.changed:
let location = sender.location(in: self)
let touchInside = self.bounds.contains(location)
avatarImageView.highLightCoverView.isHidden = !touchInside
default:
avatarImageView.highLightCoverView.isHidden = true
}
}
同时,为了防止两个长按手势冲突,实现手势代理方法,和保证按下态的时候TableView不滚动:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer.isKind(of: www.lieqibiji.com/ UIPanGestureRecognizer.self){
return false
}
if gestureRecognizer == longPresssGesture {
return false
}
return true
}
Controller
开发MDTable的目的就是获得一个更轻量级的TableView。事实上,在Controller里,我只用15行代码就实现了这个样一个复杂的TableView。
DispatchQueue.global(qos: .userInteractive).async {//准备MDTable的数据
let menuSection = MenuSection.mockSection
let recommendSection = RecommendSection.mockSection
let exclusiveSection = ExclusiveSection.mockSection
let mvSection = NMMVSection.mockSection
let columnistSection = NeteaseColumnlistSection.mockSection
let channelSection = ChannelSection.mockSection
let latestMusicSection = LatestMusicSection. www.wmyl15.com/ mockSection
self.sections = [menuSection,recommendSection,exclusiveSection,mvSection,columnistSection,channelSection,latestMusicSection]
DispatchQueue.main.async {//绑定数据
self.tableView.manager = TableManager(sections: self.sections)
self.tableView.tableFooterView = footer
}
}
以主播电台为例,对应Controller中的这一行
let channelSection = ChannelSection.mockSection
ChannelSection是MDTable提供的基础类型Section的子类:表示主播电台这个TableView Section,对应MVVM设计模式中的ViewModel角色
class ChannelSection: Section, SortableSection{
static var mockSection:ChannelSection{
get{
let channelTitleRow = NMColumnTitleRow(title: "主播电台")
let channelRow = NMChannelRow(channels: channels)
let channelSection = ChannelSection(rows: www.wmyl11.com [channelTitleRow,channelRow])
return channelSection
}
}
}
其中,NMChannelRow是ReactiveRow的子类,对应MVVM设计模式中的ViewModel角色
class NMChannelRow:ReactiveRow {
var channels:[NMChannel] //Models
var isDirty = true
init(channels:[NMChannel]){
self.channels = channels
super.init()
//行高相关信息
self.rowHeight = NMChannelConst.itemHeight * 2.0
self.reuseIdentifier = "www.wmyl11.com NMChannelRow"
self.shouldHighlight = false
self.initalType = .code(className: NMChannelCell.self)
}
}
接着,我们再来看看NMChannelCell,也就是View的角色
class NMChannelCell: MDTableViewCell {
weak var row:NMChannelRow?
override func render(with row: RowConvertable) {
//重写render方法,把ViewModel绑定到View
guard let _row = row as? NMChannelRow else {
return;
}
self.row = _row
if _row.isDirty{
_row.isDirty = false
reloadData()
}
}
}
排序
因为是模型驱动的TableView,只要修改模型的顺序即可。这里定义了一个协议,表示一个Section支持可排序:
protocol SortableSection {
var sortTitle: String {get set} /www.caibayule88.com/排序的标题
var sequence: Int {get set} /www.qinLinyuLe.cn/ 顺序
var defaultSequeue:Int {get} /www.quyingyulecs.com
/默认顺序
var identifier: String {get} /www.sLthyLvip.cn/唯一id
}
接着,我们只需要在点击排序的时候,对Section进行过滤即可
let sortableSections = sections.filter { $0 is SortableSection }.map{$0 as! SortableSection}
let sortController = NeteaseCloudMusicSortController(sections: sortableSections)
let navController = BaseNavigationController(rootViewController: sortController)
present(navController, animated: true, completion: nil)
性能优化
到这里,我用MDTable很容易的就实现了网易云音乐的首页。但是卡顿的首页不是我想要的(事实上网易云音乐在5s上上下滚动能够感受到明显的卡顿),于是就开始了漫长的性能优化之路。如果你对卡顿分析好无头绪,建议先读读ibireme的这篇文章:《iOS 保持界面流畅的技巧》。
分析卡顿
分析卡顿一般会从CPU和GPU两个方面入手,相信我除非你的UI层次特别复杂,比如大量的阴影遮罩之类的,一般来说GPU都不是卡顿的瓶颈。
卡顿的原因一般有三个:
UI对象的创建,属性修改
布局
渲染
iOS设备是每秒60帧,也就是说一帧从”CPU计算->GPU渲染->显示”只有16.7ms。
一般来说,当你在滚动的时候,发现CPU持续占用超过50ms,肉眼就能明显的感觉到掉帧,肉眼很难分别出60fps和59fps。
首先分析CPU,使用工具Time Profiler:
图片解码
图片解码是一个常见的优化点,原因是
当你创建一个UIImage的时候,默认发生实际的解码,只有当图片要被显示到屏幕上的时候,才会发生实际的解码,解码是在CPU上进行了。
由于Demo是采用UIImage(named:""),并不会后台解码,于是写了个异步设置的方法
func asyncSetImage(_ image:UIImage){
DispatchQueue.global(qos: .userInteractive).async {
let decodeImage = image.decodedImage(www.haoyyuLe699.cn)
DispatchQueue.main.async {
self.image = decodeImage
}
}
}
解码的原理也很简单,提前把图片绘制到一个CGContext中,再从Context获取图片,这样能够强制图片解码。通常三方库(KingFisher,SDWebImage)都自带后台解码。
XIB
这是我用CollectionView实现第一个版本时候的截图:
可以清楚的看到,初始化xib占用了11ms。原理也很简单,直接从代码创建和读文件创建,肯定读文件要慢很多,
于是,第一个优化点就是
删除xib文件,用代码手动写。
AutoLayout
AutoLayout是一种很方便的技术,通过添加约束我们可以实现各种复杂的布局。但是同样,它也是很昂贵的,CPU在布局的时候需要进行不少计算。眼神阅读:Auto Layout Performance on iOS。
所以,这个优化点很容易想到:
用手动Layout代替AutoLayout。其实优化AutoLayout对本文的场景带来的性能提升并不大,因为我们的视图较少,并且层级简单。但是写Demo的时候,我不想再引入一套DSL进行AutoLayout,手动Layout代码还清楚一些。
实现 60fps 的网易云音乐首页的更多相关文章
- 3.Android高仿网易云音乐-首页复杂发现界面布局和功能/RecyclerView复杂布局
0.效果图 效果图依次为发现界面顶部,包含首页轮播图,水平滚动的按钮,推荐歌单:然后是发现界面推荐单曲,点击单曲就是直接进入播放界面:最后是全局播放控制条上点击播放列表按钮显示的播放列表弹窗. 1.整 ...
- 网易云音乐APP分析
网易云音乐-感受音乐的力量 你选择的产品是? 网易云音乐 为什么选择该产品作为分析? 之前用的一直是QQ音乐,但是有一天一个朋友分享了一首网易云上的音乐(顺便分享一下歌名:Drop By Drop) ...
- Java爬取网易云音乐民谣并导入Excel分析
前言 考虑到这里有很多人没有接触过Java网络爬虫,所以我会从很基础的Jsoup分析HttpClient获取的网页讲起.了解这些东西可以直接看后面的"正式进入案例",跳过前面这些基 ...
- Android项目实战之高仿网易云音乐项目介绍
这一节我们来讲解这个项目所用到的一些技术,以及一些实现的效果图,让大家对该项目有一个整体的认识,推荐大家收藏该文章,因为我们发布文章后会在该文章里面加入链接,这样大家找着就很方便. 目录 第1章 前期 ...
- 高仿Android网易云音乐OkHttp+Retrofit+RxJava+Glide+MVC+MVVM
简介 这是一个使用Java(以后还会推出Kotlin版本)语言,从0开发一个Android平台,接近企业级的项目(我的云音乐),包含了基础内容,高级内容,项目封装,项目重构等知识:主要是使用系统功能, ...
- Swift高仿iOS网易云音乐Moya+RxSwift+Kingfisher+MVC+MVVM
效果 列文章目录 因为目录比较多,每次更新这里比较麻烦,所以推荐点击到主页,然后查看iOS Swift云音乐专栏. 目简介 这是一个使用Swift(还有OC版本)语言,从0开发一个iOS平台,接近企业 ...
- OC高仿iOS网易云音乐AFNetworking+SDWebImage+MJRefresh+MVC+MVVM
效果 因为OC版本大部分截图和Swift版本一样,所以就不再另外截图了. 列文章目录 因为目录比较多,每次更新这里比较麻烦,所以推荐点击到主页,然后查看iOS云音乐专栏. 目简介 这是一个使用OC语言 ...
- Jsonp调用网易云音乐API搜索播放歌曲
效果如下图: 基本就是正常的文件播放,暂停,停止,设置循环,随机播放,加速,减速,上一曲,下一曲,再多个选择本地文件加入到播放列表的功能.然后想着给加个能搜索网络歌曲并且播放的功能,今天研究了一下,成 ...
- 网易云音乐PC端刷曲快捷键
文章首发于szhshp的第三边境研究所(szhshp.org), 转载请注明 网易云音乐PC端刷曲快捷键 好吧我承认我特别懒 云音乐其实做的还不错,FM推荐的算法明显比虾米好. 虾米可以听的曲子都 ...
随机推荐
- Java获取指定包名下的所有类的全类名的解决方案
最近有个需求需要获取一个指定包下的所有类的全类名,因此特意写了个获取指定包下所有类的全类名的工具类.在此记录一下,方便后续查阅 一.思路 通过ClassLoader来查找指定包 ...
- [Partition][Index]对于Partition表而言,是否Global Index 和 Local Index 可以针对同一个字段建立?
对于Partition表而言,是否Global Index 和 Local Index 可以针对同一个字段建立? 实验证明,对单独的列而言,要么建立 Global Index, 要么建立 Local ...
- SC1243sensor噪点问题调试
接手一块SC1243sensor的板子调试,仔细核对了原理图和PCB发现,PCB不是很好,电源处理不够好,但是出图了,问题是有噪点,麻点,根据经验要求软件修改了PCLK的极性噪点消失,问题解决. 1: ...
- Linux下Redis主从复制以及SSDB主主复制环境部署记录
前面的文章已经介绍了redis作为缓存数据库的说明,本文主要说下redis主从复制及集群管理配置的操作记录: Redis主从复制(目前redis仅支持主从复制模式,可以支持在线备份.读写分离等功能.) ...
- Gerrit日常维护记录
Gerrit代码审核工具是个好东西,尤其是在和Gitlab和Jenkins对接后,在代码控制方面有着无与伦比的优势. 在公司线上部署了一套Gerrit系统,在日常运维中,使用了很多gerrit命令,在 ...
- 作业20171123 beta-review 成绩
申诉 对成绩有疑问或不同意见的同学,请在群里[@杨贵福]. 申诉时间截止2017年12月13日 17:00. 成绩 review NABCD-评论 SPEC-评论 bug found 答复 bugfi ...
- linux 第七周 总结及实验
姬梦馨 原创作品 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 第七周 Linux内核如何装载和启动一 ...
- Linux内核设计与实现 第三章
1. 进程和线程 进程和线程是程序运行时状态,是动态变化的,进程和线程的管理操作都是由内核来实现的. Linux中的进程于Windows相比是很轻量级的,而且不严格区分进程和线程,线程不过是一种特殊的 ...
- 广商博客沖刺第一天(new ver):
項目名稱:廣商博客 沖刺二天傳送門 此次Sprint的目标:全部sprint任務完成 时间:1星期左右 每日立会 Daily Standup Meeting: 1#A3008 晚上8点开始,大概1小时 ...
- 广商博客冲刺第二天new
队名:雷锋队 队员:叶子鹏 王佳宁 张奇聪 张振演 曾柏树 项目:广商博客(嵌入APP) 执笔人:王佳宁 第一天沖刺傳送門 第三天沖刺傳送門 今天主要是写需求分析,在经过组员的热烈地讨论,需求分析如下 ...