这是一篇教学Blog. 重点不完全在播放器上, 目的是通过这个过程掌握以下知识点:
- 单例
- block传值
- 多线程
- 代理传值
- 通知
- 观察者
- 网络请求
- 数据解析
- 多控件布局
- 开发模式和框架设计
今天敲一个音乐播放器, 音乐源我就不共享了, 涉及到版权保护, 别问我的源是哪儿来的. 不告诉你们
这篇博客是一篇教学Blog, Demo不能直接用作生产, 但其中的逻辑是经得起推敲, UI部分美化美化一下即可. 要做到举一反三.
开始敲之前, 我们先看看当前可供使用的多媒体播放框架有哪些
简单介绍一下:
- AudioToolbox.framework的音频播放时间不能超过30s,数据必须是PCM或者IMA4格式,音频文件必须打包成.caf、.aif、.wav中的一种(注意这是官方文档的说法,实际测试发现一些.mp3也可以播放. 它的主要用途可以用作app的音效(不是背景音).
- MediaPlayer.framework框架下有两个常用的系统封装好的播放器:
MPMoviePlayController 和 MPMoviePlayViewController, 二者的区别在于, 后者的视频图像需要一张View视图作为载体, 你可以自己创建这个View, 那么也可以自由的控制它. 最明显的例子就是你可以用它做个浮窗播放器.- AVFoundation.framework 目前被AVKit框架替代了, 但是我没有跟进, 我就用它:
AVAudioRecorder播放器, 提供录音, 录音的的代码加起来没你jj长.
AVPlayer播放器, 一个能播放网络和本地视频/音频的播放器, 和MediaPlayer.framework框架下的两个播放器不同, 系统并未提供它的UI界面, 我们需要自己实现, 往好听了说: 这是一个可以高度自定义的播放器.
AVAudioPlayer与 AVPlayer播放器的区别在于, 这货只能播放本地音乐.
另附一张表格, 里面登记了大多数播放器的优缺点, 图片来源网络:
下面开始我们的音乐播放器之旅.
一. 产品原型图
当我们在实际生产过程中, 作为App前端开发工程师, 我们会拿到产品模型(原型图), 这个模型可能使用墨刀
为你精准绘制, 也可能某个页面使用草纸
为你勾勒, 不管怎样, 你肯定能拿到下面的东西, 这些图片,描绘了你要做的app大概长成什么样子.
-
歌曲列表
-
播放界面
-
播放界面滑动CD还有歌词呢
我们要做的就是上面样式的播放器, 如果您觉得太low, 请左右上角.
二. 功能模块划分:
括号后面的字母作为标记, 后面实现方法中会有这个字母, 您可以根据标记本节来查看当前代码属于哪一模块.
-
View层:两个界面
1.歌曲列表: 第一个界面是一个TableView界面.(A)
2.播放界面: 一个自定义界面, 需要我们布局.(B) -
Controller层:
上述两个View的控制器:
1.歌曲列表TableView的控制器.(C)
2.播放界面的控制器.(D) -
Model层:两个模型
1.歌曲信息模型, 存放每首歌曲的名称, 时长, url, 缩略图, 封面, 歌词等信息.(E)
2.歌词模型, 歌词的基本格式, 这里是[00:01]我大声说我爱的就是我
字符串格式.(F)
很多时候, 我们将一些功能模块单独独立起来, 做一次封装, 封装的好处, 找个机会开篇blog.
- Tools 工具封装:
1.一个从网络中获取歌曲信息的方法.(G)
2.能将MP3文件播放出声音的类(对AVPlayer的的封装).(H)
我们先按照上面的思路进行, 这个模块划分的原则因人而异, 当前项目比较简单, 无论怎么划分都不会产生太大差异性, 不过如果项目足够大, 一个有经验的开发者和新手之间的差距就体现出来了.
按照上述模块划分, 我们需要8个类, 每个类完成自己独特的功能, 所谓”工欲善其事, 必先利其器”, 我们打算从工具类Tools开始, 深入浅出, 然后深入, 深入, 再深入.
三.工具类Tools
3.1 数据请求(G)
在这个工具类里面, 我们将这个类设置成为一个单例类
将数据请求封装成单例的好处是显而易见,
- 首先做到了Model和Controllerc层的完全剥离, 从C层中调用数据请求的方法, 将请求回来的数据存放在单例类中, 也可以回传给C层做进一步的处理使用.
- 其次, 如果歌曲清单没有改变, 那么一次请求的数据应该贯穿应用程序的整个声明周期. 这样, APP运行的任何时刻, 我们都能获取到这个歌曲信息.
- 最后, 所有的页面(我们的APP只有两个页面)都可能使用某首歌曲的信息, 将数据存放到单例的另一个好处就是, 数据伴随单例, 扩大了作用域范围, 与上一条连用, 我们做到了任何页面任何时刻, 都可以随意的访问数据内容.
新建一个类, 继承NSObject, 名称为: GetDataTools
GetDataTools.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#import <Foundation/Foundation.h> // 定义block typedef void (^PassValue)(NSArray * array); @interface GetDataTools : NSObject // 作为单例的属性,这个数组可以在任何位置,任何时间被访问. @property(nonatomic,strong)NSMutableArray * dataArray; // 单例方法 +(instancetype)shareGetData; // 根据传入的URL,通过Block返回一个数组. -(void)getDataWithURL:(NSString *)URL PassValue:(PassValue)passValue; // 根据传入的Index,返回一个"歌曲信息的模型",这个模型来自上面的属性数组. -(MusicInfoModel *)getModelWithIndex:(NSInteger)index; @end |
GetDataTools.m
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
#import "GetDataTools.h" static GetDataTools * gd = nil; @implementation GetDataTools // 单例方法, 这个单例方法是不完全的, 如果C层开发者使用了[alloc init]的方式创建对象, 仍不为单例, 正确的封闭其他所有init方法, 或者重写调用我们当前的方法返回对象. +(instancetype)shareGetData { if (gd == nil) { static dispatch_once_t once_token; dispatch_once(&once_token, ^{ gd = [[GetDataTools alloc] init]; }); } return gd; } // 传入URL, 通过Block返回歌曲信息列表队列. -(void)getDataWithURL:(NSString *)URL PassValue:(PassValue)passValue { // 这里为什么要用子线程? // 因为,这里请求数据时:arrayWithContentsOfURL方法是同步请求(请求不结束,主线程什么也干不了) // 所以,为了规避这种现象,我们将请求的动作放到子线程中. // 创建线程队列(全局), 改天写个多线程的blog. dispatch_queue_t globl_t = dispatch_get_global_queue(0, 0); // 定义子线程的内容. dispatch_async(globl_t, ^{ // 在这对花括号内的所有操作都不会阻塞主线程了哦 // 请求数据 NSArray * array =[NSArray arrayWithContentsOfURL:[NSURL URLWithString:URL]]; // 解析,将解析好的"歌曲信息模型", 加入我们的属性数组, 以便外界能随时访问. for (NSDictionary * dict in array) { MusicInfoModel * model = [[MusicInfoModel alloc] init]; [model setValuesForKeysWithDictionary:dict]; [self.dataArray addObject:model]; } // !!!Block回传值 passValue(self.dataArray); }); } // 属性数组的懒加载(并不是必须用懒加载, 懒加载有懒加载的好处) -(NSMutableArray *)dataArray { if (_dataArray == nil) { _dataArray = [NSMutableArray array]; } return _dataArray; } // 根据传入的index返回一个"歌曲信息模型" -(MusicInfoModel *)getModelWithIndex:(NSInteger)index { return self.dataArray[index]; } @end |
在这个类中, 我们定义了三个方法:
- shareGetData; 单例方法, 单例的好处不在此处赘述. 单例很重要, 一定要熟练掌握.
- getDataWithURL: PassValue:; 这个方法是本类的核心功能了, 从网络中请求数据(异步), 通过block将数组返回. 注意: 这个类本身有一个成员变量_dataArray, 它里面存放了所有的歌曲信息, 其他页面可以通过单例.dataArray方法获取到, 但是我们这里仍然封装其返回一个新的数组的方法. 不为别的, 就是因为这种方式太重要了 – 为了使用而使用.
- getModelWithIndex; 您可能不明为为什么要写一个这么个方法,这个方法的产生是有后面的逻辑背景的, 这里因为blog书写不便, 就直接写在这里了.
以上是我们的数据请求类(GetDataTools).
3.2 播放器工具类(H)
在封装AVPlayer之前, 我们先了解一下AVPlayer有什么特点.
AVPlayer存在于AVFoundation中, 它更加接近于底层, 所以灵活性也更强,AVPlayer本身并不能显示视频,而且它也不像MPMoviePlayerController有一个view属性。如果AVPlayer要显示必须创建一个播放器层AVPlayerLayer用于展示,播放器层继承于CALayer,有了AVPlayerLayer之添加到控制器视图的layer中即可。要使用AVPlayer首先了解一下几个常用的类和它的属性/方法(本段源自网络):
- AVAsset: 属性, 主要用于获取多媒体信息,是一个抽象类,不能直接使用。
- AVURLAsset: 属性, AVAsset的子类,可以根据一个URL路径创建一个包含媒体信息的AVURLAsset对象。
- AVPlayerItem: 属性,一个媒体资源管理对象,管理者视音频的一些基本信息和状态,一个AVPlayerItem对应着一个视音频资源.
- replaceCurrentItemWithPlayerItem: 替换AVPlayer的当前Item.
- play: 方法, 播放媒体.
- pause: 方法, 暂停.
- seekToTime:completionHandler: 方法, 播放跳转, 调整播放进度.
下面我们解释一下AVPlayerItem:
-
AVPlayerItem
我们不妨做一次角色扮演游戏, 我是老板, 你是一位员工小张, 主要负责向客户推销一款产品.
今天早上我对你下达了这样命令:
小张, 隔壁的桌子上有一款新开发的产品, 你现在拿着它去给展厅的客户们介绍一下.
上面的命令透露出两个信息:
1.这个款产品是新产品, 你从来没有听说过, 你不知道它的任何参数.
2.你需要立刻完成这件事.
你不傻掉才怪. 这样的命令式不合逻辑, 不合设计思路的.
对比下面的命令:
小张, 这里有份资料, 里面记录着隔壁桌子上新产品的详细信息, 你拿去研究一下, 等到你完全掌握并且准备好时, 你告诉我, 我给你安排一个展厅向客户介绍它.
这条命令透露出的信息:
1.这个产品有说明书
2.你别着急, 慢慢研究, 研究好了你告诉我, 这个时间我(老板)先干点别的.
很明显下面的方式要好于上面.
同样, 你对AVPlayer下达命令也不能采用第一种方式, 你要告诉它, 你要播放的歌曲是什么名字, 有多长时间, 文件在什么位置, 歌曲的图片是什么, 这些东西你要给它写一份详细的说明书.
这个说明书就是AVPlayerItem.
每一个AVPlayer对象, 都有一个自己的AVPlayerItem属性, 名字叫做:currentItem, 我们可以通过
replaceCurrentItemWithPlayerItem:
方法来替换当前的Item, 将准备好的Item, 交给Player.
这个过程我们使用观察者模式模式来监视AVPlayerItem的准备情况. 一旦准备完毕, 会修改自身的status属性为AVPlayerItemStatusReadyToPlay
枚举值, 一旦观察到这种状态, 我们就开始真正的播放. -
方法: play 和 pause
这个两个是AVPlayer的播放控制方法, 我们在控制界面有个按钮, 点一下就播放, 再点一下就暂停, 反复重复. 貌似没有什么, 但是这里有个棘手的问题, AVPlayer的对象成员变量中, 居然没有来标识当前播放状态的! 也就是说, 你永远也不可能直接的获得当前AVPlayer正在播放中或者暂停了.
通常情况下, 我们通过AVPlayer的一个rate(播放速率)来间接得到播放状态, rate==0则暂停, 不为0则正在播放中. -
切换歌曲
AVPlayer并没有直接提供下一曲和上一曲的的功能, 但是我们可以通过上面的replaceCurrentItemWithPlayerItem:
方法, 将AVPlayer对象的Item替换掉, 之后让它播放, 就可以达到这个效果.
新建一个类, 继承NSObject, 名称为: MusicPlayTools
MusicPlayTools.h
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 31 32 33 34 35 36 |
#import <Foundation/Foundation.h> #import <AVFoundation/AVFoundation.h> // !!! 与block回传值作比较. // 定义协议. 通过代理方法返回当前歌曲的播放进度. // 如果外界想使用本播放器,必须遵循和实现协议中的两个方法. @protocol MusicPlayToolsDelegate <NSObject> // 外界实现这个方法的同时, 也将参数的值拿走了, 这样我们起到了"通过代理方法向外界传递值"的功能. -(void)getCurTiem:(NSString *)curTime Totle:(NSString *)totleTime Progress:(CGFloat)progress; // 播放结束之后, 如何操作由外部决定. -(void)endOfPlayAction; @end @interface MusicPlayTools : NSObject // 本类中的播放器指针. @property(nonatomic,strong)AVPlayer * player; // 本类中的,播放中的"歌曲信息模型" @property(nonatomic,strong)MusicInfoModel * model; // 代理 @property(nonatomic,weak)id<MusicPlayToolsDelegate> delegate; // 单例方法 +(instancetype)shareMusicPlay; // 播放音乐 -(void)musicPlay; // 暂停音乐 -(void)musicPause; // 准备播放 -(void)musicPrePlay; // 跳转 -(void)seekToTimeWithValue:(CGFloat)value; // 返回一个歌词数组 -(NSMutableArray *)getMusicLyricArray; // 根据当前播放时间,返回 对应歌词 在 数组 中的位置. -(NSInteger)getIndexWithCurTime; @end |
MusicPlayTools.m
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |