文章目录
一、AVAudioPlayer
1、简介
2、优缺点
3、如何使用
4、扩展功能
(1) 如何做后台播放
(2) 如何做输出改变监听(拔出耳机音乐暂停播放)
(3) 歌词轮播实现思路
(4) 关于NSTimer(循环引用、NSRunLoopMode)
一、AVAudioPlayer
1、简介
播放较大的音频或者要对音频有精确的,这种情况会选择使用AVFoundation.framework中的AVAudioPlayer来实现。
2、优缺点
缺点:AVAudioPlayer不支持加载网络媒体流,只能播放本地文件
优点:能够精确控制播放进度、音量、播放速度等属性
3、如何使用
(1) 初始化AVAudioPlayer对象,此时通常指定本地文件路径
(2) 设置播放器属性,例如重复次数、音量大小等
(3) 调用play方法播放
4、扩展功能
//
// MainViewController.m
// 2.音乐播放
//
// Created by cherish on 2018/4/8.
// Copyright © 2018年 Cherish. All rights reserved.
//
#import "MainViewController.h"
#import
#import "SDAutoLayout.h"
#import "Configure.h"
#import "CustomSlider.h"
#import "TimerWeakTarget.h"
@interface MainViewController ()<AVAudioPlayerDelegate>
//播放器
@property (nonatomic,strong) AVAudioPlayer *audioPlayer;
//歌手名字
@property (nonatomic,strong) UILabel *singerName;
//下载
@property (nonatomic,strong) UIButton *downLoad;
//收藏
@property (nonatomic,strong) UIButton *collection;
//进度条
@property (nonatomic,strong) CustomSlider *progressView;
//上一首
@property (nonatomic,strong) UIButton *lastSong;
//播放或暂停
@property (nonatomic,strong) UIButton *playOrPause;
//下一首
@property (nonatomic,strong) UIButton *nextSong;
//定时器
@property (nonatomic,strong) NSTimer *timer;
//音乐总时长
@property (nonatomic,strong) UILabel *duration;
//播放时间
@property (nonatomic,strong) UILabel *playTime;
@end
@implementation MainViewController
#pragma mark - ViewController Life
- (void)viewDidLoad
{
[super viewDidLoad];
self.title = kMusicTitle;
self.navigationController.navigationBar.barTintColor = [UIColor colorWithRed:103/255.0 green:103/255.0 blue:103/255.0 alpha:1];
[self setUI];
self.duration.text = [self getMinuteSecondWithTime:self.audioPlayer.duration];
}
#pragma mark - Private Methods
- (void)setUI
{
//背景图片
UIImageView *bgImageView = [[UIImageView alloc]initWithFrame:self.view.bounds];
bgImageView.contentMode = UIViewContentModeScaleAspectFill;
bgImageView.layer.masksToBounds = YES;
bgImageView.userInteractionEnabled = YES;
bgImageView.image = [UIImage imageNamed:@"bgImage"];
[self.view addSubview:bgImageView];
// 底部工具栏背景
UILabel *panLable = [[UILabel alloc]initWithFrame:CGRectMake(0, KSCreenHeight-230, KSCreenWidth, 230)];
panLable.backgroundColor = [UIColor colorWithRed:103/255.0 green:103/255.0 blue:103/255.0 alpha:1];
panLable.userInteractionEnabled = YES;
[bgImageView addSubview:panLable];
//歌手名字
self.singerName = [[UILabel alloc]init];
self.singerName.font = [UIFont systemFontOfSize:16.0f];
self.singerName.textAlignment = NSTextAlignmentCenter;
self.singerName.textColor = [UIColor whiteColor];
self.singerName.text = kMusicSinger;
//下载按钮
self.downLoad = [UIButton buttonWithType:UIButtonTypeCustom];
[self.downLoad setImage:[UIImage imageNamed:@"download"] forState:UIControlStateNormal];
[self.downLoad addTarget:self action:@selector(downloadAction) forControlEvents:UIControlEventTouchUpInside];
//收藏
self.collection = [UIButton buttonWithType:UIButtonTypeCustom];
[self.collection setImage:[UIImage imageNamed:@"collection"] forState:UIControlStateNormal];
[self.collection addTarget:self action:@selector(collectionAction) forControlEvents:UIControlEventTouchUpInside];
//进度条
self.progressView = [[CustomSlider alloc]init];
self.progressView.value = 0.0;
self.progressView.maximumTrackTintColor = [UIColor whiteColor];
self.progressView.alpha = 0.5;
//监听value变化
[self.progressView addTarget:self action:@selector(progressValueChanged) forControlEvents:UIControlEventValueChanged];
//播放时长
self.playTime = [[UILabel alloc]init];
self.playTime.textColor = [UIColor whiteColor];
self.playTime.font = [UIFont systemFontOfSize:12.0f];
self.playTime.textAlignment = NSTextAlignmentLeft;
self.playTime.text = @"00:00";
//总时长
self.duration = [[UILabel alloc]init];
self.duration.textColor = [UIColor whiteColor];
self.duration.font = [UIFont systemFontOfSize:12.0f];
self.duration.textAlignment = NSTextAlignmentRight;
//播放按钮
self.playOrPause = [UIButton buttonWithType:UIButtonTypeCustom];
[self.playOrPause setImage:[UIImage imageNamed:@"play"] forState:UIControlStateNormal];
[self.playOrPause setImage:[UIImage imageNamed:@"pause"] forState:UIControlStateSelected];
[self.playOrPause addTarget:self action:@selector(playOrPauseAction:) forControlEvents:UIControlEventTouchUpInside];
//上一首
self.lastSong = [UIButton buttonWithType:UIButtonTypeCustom];
[self.lastSong setImage:[UIImage imageNamed:@"last"] forState:UIControlStateNormal];
[self.lastSong addTarget:self action:@selector(lastSongAction:) forControlEvents:UIControlEventTouchUpInside];
//下一首
self.nextSong = [UIButton buttonWithType:UIButtonTypeCustom];
[self.nextSong setImage:[UIImage imageNamed:@"next"] forState:UIControlStateNormal];
[self.nextSong addTarget:self action:@selector(nestSongAction:) forControlEvents:UIControlEventTouchUpInside];
[panLable sd_addSubviews:@[self.singerName,self.downLoad,self.collection, self.progressView,self.playOrPause,self.lastSong,self.nextSong,self.duration,self.playTime]];
self.singerName.sd_layout
.leftSpaceToView(panLable, 10)
.topSpaceToView(panLable, 20)
.autoHeightRatio(0);
[self.singerName setSingleLineAutoResizeWithMaxWidth:140];
self.collection.sd_layout
.widthIs(40)
.heightEqualToWidth()
.topSpaceToView(panLable, 10)
.rightSpaceToView(panLable, 10);
self.downLoad.sd_layout
.widthIs(40)
.heightEqualToWidth()
.topSpaceToView(panLable, 10)
.rightSpaceToView(self.collection, 10);
self.progressView.sd_layout
.leftSpaceToView(panLable, 0)
.topSpaceToView(self.downLoad, 20)
.rightSpaceToView(panLable, 0)
.heightIs(2);
self.playTime.sd_layout
.leftSpaceToView(panLable, 10)
.topSpaceToView(self.progressView, 30)
.widthIs(100)
.autoHeightRatio(0);
self.duration.sd_layout
.rightSpaceToView(panLable, 10)
.topSpaceToView(self.progressView, 30)
.widthIs(KSCreenWidth/2.0)
.autoHeightRatio(0);
self.playOrPause.sd_layout
.centerXEqualToView(panLable)
.widthIs(65)
.heightEqualToWidth()
.topSpaceToView(self.progressView, 40);
self.lastSong.sd_layout
.widthIs(40)
.heightEqualToWidth()
.topSpaceToView(self.progressView, 40+12.5)
.rightSpaceToView(self.playOrPause, 30);
self.nextSong.sd_layout
.leftSpaceToView(self.playOrPause, 30)
.widthIs(40)
.heightIs(40)
.topEqualToView(self.lastSong);
}
-(NSString *)getMinuteSecondWithTime:(NSTimeInterval)time
{
int minute = (int)time / 60;
int second = (int)time % 60;
if (second > 9)
{
return [NSString stringWithFormat:@"%d:%d",minute,second];
}
return [NSString stringWithFormat:@"%d:0%d",minute,second];
}//通过获取时间展示"分钟:秒钟"
#pragma mark - OverRide Methods
-(NSTimer*)timer
{
if (!_timer)
{
_timer = [TimerWeakTarget scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(updateProgress) userInfo:nil repeats:YES];
}
return _timer;
}
- (AVAudioPlayer*)audioPlayer
{
if (!_audioPlayer) {
//获取本地播放文件路径
NSString *path = [[NSBundle mainBundle] pathForResource:kMusicFile ofType:nil];
NSError *error = nil;
//初始化播放器
_audioPlayer = [[AVAudioPlayer alloc]initWithContentsOfURL:[NSURL URLWithString:[path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]] error:&error];
//是否循环播放
_audioPlayer.numberOfLoops = 0;
//把播放文件加载到缓存中(注意:即使在播放之前音频文件没有加载到缓冲区程序也会隐式调用此方法。)
[_audioPlayer prepareToPlay];
//设置代理,监听播放状态(例如:播放完成)
_audioPlayer.delegate = self;
// 设置音频会话模式,后台播放
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
[audioSession setActive:YES error:nil];
// 添加通知(输出改变通知) ios 6.0 后
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(routeChange:) name:AVAudioSessionRouteChangeNotification object:nil];
if (error) {
NSAssert(YES,"音乐初始化过程报错");
}
}
return _audioPlayer;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}//移除通知
#pragma mark - Action Methods
- (void)downloadAction
{
NSLog(@"下载");
}//下载
- (void)collectionAction
{
NSLog(@"收藏");
}//收藏
- (void)lastSongAction:(UIButton*)sender
{
}//上一首
- (void)playOrPauseAction:(UIButton*)sender
{
sender.selected = !sender.selected;
if (sender.selected) {
[self playMusic];
}else{
[self pauseMusic];
}
}//播放或暂停
- (void)nestSongAction:(UIButton*)sender
{
}//下一首
- (void)updateProgress
{
//更新进度条值
self.progressView.value = self.audioPlayer.currentTime/self.audioPlayer.duration;
//当前播放时长
self.playTime.text = [self getMinuteSecondWithTime:self.audioPlayer.currentTime];
}//更新进度条
- (void)playMusic
{
if (!self.audioPlayer.isPlaying)
{
[self.audioPlayer play];
//开始计时
self.timer.fireDate = [NSDate distantPast];
}
}//播放
- (void)pauseMusic
{
if (self.audioPlayer.isPlaying)
{
[self.audioPlayer pause];
//暂停定时器
self.timer.fireDate = [NSDate distantFuture];
}
}//暂停
#pragma mark - Notification Method
- (void)progressValueChanged
{
self.audioPlayer.currentTime = self.progressView.value *self.audioPlayer.duration;
self.playOrPause.selected = NO;
[self playOrPauseAction:self.playOrPause];
}//监听progress值变化(拖动进度条到对应时间后,开始播放)
- (void)routeChange:(NSNotification*)notification
{
NSDictionary *dic = notification.userInfo;
int changeReason = [dic[AVAudioSessionRouteChangeReasonKey] intValue];
//等于AVAudioSessionRouteChangeReasonOldDeviceUnavailable表示旧输出不可用
if (changeReason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable)
{
AVAudioSessionRouteDescription *routeDescription=dic[AVAudioSessionRouteChangePreviousRouteKey];
AVAudioSessionPortDescription *portDescription= [routeDescription.outputs firstObject];
//原设备为耳机则暂停
if ([portDescription.portType isEqualToString:@"Headphones"])
{
//这边必须回调到主线程
dispatch_async(dispatch_get_main_queue(), ^{
self.playOrPause.selected = NO;
});
[self pauseMusic];
}
}
}//输出改变通知
#pragma mark - AVAudioPlayer Delegate
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
self.playOrPause.selected = NO;
//关闭会话
[[AVAudioSession sharedInstance]setActive:NO error:nil];
}//播放完成
@end
UI 绘制这边就做说明了,主要讲几个小知识点
(1) 如何做后台播放
1> 在plist文件下添加 key : Required background modes,并设置item0 = App plays audio or streams audio/video using AirPlay
2> 设置AVAudioSession的类型为AVAudioSessionCategoryPlayback并且调用setActive::方法启动会话。
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
[audioSession setActive:YES error:nil];
正常来说,当app退到后台,程序处于悬挂状态,即暂停播放。
在iOS中每个应用都有一个音频会话,这个会话就通过AVAudioSession来表示。
AVAudioSession同样存在于AVFoundation框架中,它是单例模式设计,通过sharedInstance进行访问。在使用Apple设备时大家会发现有些应用只要打开其他音频播放就会终止,而有些应用却可以和其他应用同时播放,在多种音频环境中如何去控制播放的方式就是通过音频会话来完成的。
下面是音频会话的几种会话模式:
注意 : 前面的代码中也提到设置完音频会话类型之后需要调用setActive::方法将会话激活才能起作用。
(2) 如何做输出改变监听(拔出耳机音乐暂停播放)
ios6.0后还可以监听输出改变通知。通俗来说,就是拔出耳机,音乐播放暂停。
代码如下:
//添加观察者
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(routeChange:) name:AVAudioSessionRouteChangeNotification object:nil];
// 通知方法
- (void)routeChange:(NSNotification*)notification
{
NSDictionary *dic = notification.userInfo;
int changeReason = [dic[AVAudioSessionRouteChangeReasonKey] intValue];
//等于AVAudioSessionRouteChangeReasonOldDeviceUnavailable表示旧输出不可用
if (changeReason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable)
{
AVAudioSessionRouteDescription *routeDescription=dic[AVAudioSessionRouteChangePreviousRouteKey];
AVAudioSessionPortDescription *portDescription= [routeDescription.outputs firstObject];
//原设备为耳机则暂停
if ([portDescription.portType isEqualToString:@"Headphones"])
{
//这边必须回调到主线程
dispatch_async(dispatch_get_main_queue(), ^{
self.playOrPause.selected = NO;
});
[self pauseMusic];
}
}
}//输出改变通知
(3) 歌词轮播实现思路
歌词应该是 时间 和 对应歌词 的字典类型数据结构。用UITableView实现。获取播放器当前播放时间,查找字典找到对应的key,进而找到对应的NSIndexPath,滚动到当前cell在屏幕中央即可。
(4) 关于定时器的小细节
创建定时器的2种常用类方法
1、timerWithTimeInterval开头的构造方法,我们可以创建一个定时器,但是默认没有添加到runloop中,我们需要在创建定时器后,需要手动将其添加到NSRunLoop中,否则将不会循环执行。
2、scheduledTimerWithTimeInterval开头的构造方法,从此构造方法创建的定时器,它会默认将其指定到一个默认的runloop中,并且timerInterval时候后,定时器会自启动。
注意 :NSTimer的创建和释放必须放在同一个线程中。
主要讲下 scheduledTimerWithTimeInterval 开头的构造方法创建定时器。
问题简述:
我们使用scheduledTimerWithTimeInterval创建一个NSTimer实例后,timer会自动添加到runloop中,此时会被runloop强引用,而timer又会对 target 强引用,这样就形成强引用循环了。如果不手动失效 timer,那么self这个VC将不能被释放,尤其是当我们这个VC是push进来的,pop将不会被释放。
补充说明,按钮添加添加点击事件的方法,其中的target会不会被强引用呢?答案肯定是不会的咯。
如何解决循环引用 ?
方案一 : 在vc的dealloc中释放timer?
由于已经存在循环引用了,vc的dealloc方法将不被调用。所以此方案pass掉。
方案二 :在viewdidDisappear 释放timer ?
- (void)viewWillDisappear:(Bool)animated
{
[super viewWillDisappear:animated];
//将定时器从当前rooloop移除。不可恢复。
[self.timer invalidate];
}
一定程度上能够解决问题,但是如果当前vc有继续push到新的页面,当返回的时候,timer已经挂了。显然也不是好的解决方案。pass掉。
方案三 :弱引用 ?__weak typeof(self) weakSelf = self; 替换target
__weak typeof(self) weakSelf = self; 不能解决
_timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:weakSelf selector:@selector(updateProgress) userInfo:nil repeats:YES];
虽然我们将weakSelf传入timer构造方法中,虽然我们看似弱引用的self对象,但target的说明中明确提到是强引用了这个target,也就是说timer强引用了一个弱引用的变量,结果还是强引用,这和你直接传self进来效果是一样的,并不能解除强引用循环。这样的做唯一作用是如果在timer运行期间self被释放了,timer的target也就置为nil,仅此而已。
此方案pass掉。
方案四 :包装一个target,让target是另一个对象,而不是ViewController即可。
//接口文件
@interface TimerWeakTarget : NSObject
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer *timer;
@property (nonatomic, weak) id target;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats;
//实现文件
@implementation TimerWeakTarget
+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats
{
TimerWeakTarget * timer = [TimerWeakTarget new];
timer.target = aTarget;
timer.selector = aSelector;
//此处的target已经被换掉了不是原来的VIewController而是TimerWeakTarget类的对象timer
timer.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:timer selector:@selector(fire:) userInfo:userInfo repeats:repeats];
return timer.timer;
}
-(void)fire:(NSTimer *)timer{
if (self.target) {
[self.target performSelector:self.selector withObject:timer.userInfo];
} else {
[self.timer invalidate];
}
}
@end
此时你会发现,self对timer强引用了,但是timer并没有对self,因为此时timer的target对象被替换成TimerWeakTarget实例。这样就解决了循环引用问题。
关于NSLoopMode的问题
由于+ (NSTimer *)scheduledTimerWithTimeInterval:..;
此时的timer会被加入到当前线程的runloop中,默认为NSDefaultRunLoopMode。如果当前线程是主线程,某些事件,如UIScrollView的拖动时,会将runloop切换到NSEventTrackingRunLoopMode模式,在拖动的过程中,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。从而此时的timer也就不会触发。
解决方案:把创建好的timer手动添加到指定模式中,此处为NSRunLoopCommonModes,这个模式其实就是NSDefaultRunLoopMode与NSEventTrackingRunLoopMode的结合。
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
本文代码下载