实现 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推荐的算法明显比虾米好. 虾米可以听的曲子都 ...
随机推荐
- React-异步组件及withRouter路由方法的使用
所有组件的代码都打包在bundle.js里,加载首页的时候,把其它页面的代码也加载了,影响首页加载速度.我们希望访问首页的时候只加载首页,访问详情页的时候再去加载详情页的代码.异步组件可以帮我们实现, ...
- python语言程序设计8
1, 说实话,我挺伤心的,感觉 有点像烂剧里的主演...也许我早几天明白的话,会不会结果会不一样?但是之前还真没往这方面想过,但是确实是开了一个口子了,也不急吧.努力把现在的事给做好,变帅变高,那很 ...
- C#_根据银行卡卡号判断银行名称
/// <summary> /// 银行信息 /// </summary> public class BankInfo { #region 数组形式存储银行BIN号 /// & ...
- Linux系统入门教程:如何在 Linux 中修改默认的 Java 版本
提问:当我尝试在Linux中运行一个Java程序时,我遇到了一个错误.看上去像程序编译所使用的Java版本与我本地的不同.我该如何在Linux上切换默认的Java版本? 当Java程序编译时,编译环境 ...
- Redis常用操作-------List(列表)
1.BLPOP key [key ...] timeout BLPOP 是列表的阻塞式(blocking)弹出原语. 它是 LPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 ...
- cf946d 怎样逃最多的课dp
来源:codeforces D. Timetable Ivan is a student at Berland ...
- D. Bicolorings
传送门 [http://codeforces.com/contest/1051/problem/D] 题意 相当于有个2列n行得棋盘,棋盘上的格子只能是黑或者白,问你联通块为k得方案数有多少,结果对 ...
- VS2013安装和单元测试
1. VC2013安装过程及使用感受 刚上大一的时候老师推荐我们用VC++6.0.当时也就听了老师的话用VC++6.0编程了一段时间.后来上了大二买了电脑VC++6.0支持不了WIN8.1所以我就开始 ...
- 【ML】ICLR2016_Delving Deeper into Convolutional Networks
ICLR2016_DELVING DEEPER INTO CONVOLUTIONAL NETWORKS Note here: Ballas recently proposed a novel fram ...
- ☆C++学习心得
C++是我进大学的学的第一种编程语言,在高中的时候有电脑课,有教过部分的VB语言,所以其实对编程也并不是非常的陌生,刚开是上课也觉得感觉不难,都懂,没多少课后,恍了个神..居然听不懂了!老师经常让我们 ...