Rhino


  • Home

  • Archives

协议

Posted on 2020-11-28

音频初见

Posted on 2020-07-29

音频初见

基本介绍

音频: 从形式上分为
短音频(音效播放):不需要进度|循环等控制 AudioToolbox.framework
(C语言框架

  • 本质:将音频注册到system sound service(简单底层的声音播放服务))
    特点:
  • 播放时间不超过30s
  • 数据格式PCM/IMA4
  • 音频文件必须打包成.caf,.aif,.wav,(.MP3也可以)

长音频(音乐播放)

  • 较大的音频文件,需要进行精确的控制的场景
  • AVFoundation.framework
  • 支持的音频格式有:AAC,ALAC,HE-AAC,iLBC,IMA4,MP3等.

音效播放

步骤:

  • 不要忘记导入头文件: #import \<AudioToolbox/AudioToolbox.h>
  • 1.调用AudioServicesCreateSystemSoundID(CFURLRef inFileURL, SystemSoundID* outSystemSoundID)函数获得系统声音ID。
  • 2.如果需要监听播放完成操作,则使用AudioServicesAddSystemSoundCompletion( SystemSoundID inSystemSoundID,CFRunLoopRef inRunLoop, CFStringRef inRunLoopMode, AudioServicesSystemSoundCompletionProc inCompletionRoutine, void* inClientData)方法注册回调函数。
  • 3.调用AudioServicesPlaySystemSound(SystemSoundID inSystemSoundID)
    AudioServicesPlayAlertSound(SystemSoundID inSystemSoundID) 方法播放音效(后者带有震动效果)
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
- (void)audioBox{

//获取资源路径
NSString *pathSource = [[NSBundle mainBundle]pathForResource:@"audiobox" ofType:@".caf"];
NSURL *sourceUrl = [NSURL fileURLWithPath:pathSource];

//系统声音id
SystemSoundID soundID = 0;
/**
注册音效服务

@param inFileURL#> 音频文件URL 需要桥接->C语言
@param outSystemSoundID#> 输出音效ID
@return 将音效文件加入到系统音频服务中并返回一个长整型音频ID
*/
AudioServicesCreateSystemSoundID((__bridge CFURLRef)(sourceUrl), &soundID);



/**
添加播放完毕回调函数

@param inSystemSoundID#> 音效ID
@param inRunLoop#> 添加到Runloop
@param inRunLoopMode#> RunloopMode
@param inCompletionRoutine#> 回调函数
@param inClientData#> 传递给回调函数的相关数据
@return ..
*/
AudioServicesAddSystemSoundCompletion(soundID, NULL, NULL, playCompletion, NULL);


//播放
AudioServicesPlayAlertSound(soundID); //播放音效并震动
// AudioServicesPlaySystemSound(soundID); //播放音效
}


/**
回调函数

@param ssID 音效id
@param clientData 回调时传递的数据
*/
void playCompletion(SystemSoundID ssID, void* __nullable clientData){
NSLog(@"播放完成");
//摧毁音效
AudioServicesDisposeSystemSoundID(ssID);
}

音乐播放 AVAudioPlayer

AVAudioPlayer支持多种音频格式,而且能够对进度,音量,播放速度等控制
头文件 #import \<AVFoundation/AVFoundation.h>
步骤:

  • 1.给定资源路径,或者二进制数据初始化播放器
  • 2.设置相关属性
  • 3.执行播放,更新相关UI(进度更新,歌词显示(CADislinkplay/NSTimer))

初始化方法

AVAudioPlayer可以接受本地资源路径(NSURL)/NSData类型数据(可以用来加载网络链接),但是实质上AVAudioPlayer不支持流式播放,只能播放完整的文件,使用后者加载网络链接,当播放的音频文件较大或者网络不好就需要过长的时间等待
每一个实例化方法返回一个AVAudioPlayer对象,每一个文件/Data对应一个AVAudioPlayer对象,传入一个NSError的指针,用于判断初始化是否成功.

1
2
- (nullable instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError **)outError;
- (nullable instancetype)initWithData:(NSData *)data error:(NSError **)outError;

播放控制相关API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//准备播放,分配播放所需的资源,并将其加入内部播放队列,加载音频文件到缓冲区,注意:即使在播放之前音频文件没有加载到缓冲区程序也会隐式调用此方法。
- (BOOL)prepareToPlay;
//播放,如果资源还没准备好,会隐式调用prepareToPlay方法,是异步的
- (BOOL)play;
//相对当前设备时间或指定的时间开始播放音频
- (BOOL)playAtTime:(NSTimeInterval)time

NSTimeInterval nowTime = self.audioPlayer.deviceCurrentTime;
[self.audioPlayer playAtTime:nowTime + 2.0];

//暂停播放 并且准备好继续播放
- (void)pause; /* pauses playback, but remains ready to play. */
//停止播放 不再准备好继续播放,再次调用会重新开始播放
- (void)stop; /* stops playback. no longer ready to play. */

属性相关

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
//获取是否正在播放
@property(readonly, getter=isPlaying) BOOL playing;
//获取当前音频声道数
@property(readonly) NSUInteger numberOfChannels;
//获取当前音频时长
@property(readonly) NSTimeInterval duration;


//获取创建时的音频路径 跟初始化方法相关联
@property(readonly, nullable) NSURL *url;
//获取创建时的音频数据 跟初始化方法相关联
@property(readonly, nullable) NSData *data;
//声道 -1.0~0.0左声道 0.0临界值 0.0~1.0为右声道
@property float pan
//音量 正常范围在0.0~1.0
@property float volume;
- (void)setVolume:(float)volume fadeDuration:(NSTimeInterval)duration NS_AVAILABLE(10_12, 10_0); /* fade to a new volume over a duration */

//是否可以改变播放速率 必须在prepareToPlay方法前调用
@property BOOL enableRate
//播放速率 1.0为正常速率 0.5一半 2.0双倍速播放
@property float rate

//当前播放的时间点,可以实现定点进行播放
@property NSTimeInterval currentTime;

//输出设备播放音频的时间,注意:如果播放中被暂停此时间也会继续累加
@property(readonly) NSTimeInterval deviceCurrentTime

//设置音频播放循环次数 如果为0则不循环,如果小于0则无限循环,大于0则表示循环次数
@property NSInteger numberOfLoops;

//获取音频设置字典
@property(readonly) NSDictionary<NSString *, id> *settings NS_AVAILABLE(10_7, 4_0); /* returns a settings dictionary with keys as described in AVAudioSettings.h */

/* returns the format of the audio data */
@property(readonly) AVAudioFormat *format NS_AVAILABLE(10_12, 10_0);


//是否开启仪表计数功能(启用音频测量) 默认为NO,一旦启用音频测量可以通过updateMeters方法更新测量值
@property(getter=isMeteringEnabled) BOOL meteringEnabled; /* turns level metering on or off. default is off. */

//更新仪表计数的值
- (void)updateMeters; /* call to refresh meter values */

//获得指定声道的分贝峰值,如果要获得分贝峰值必须在此之前调用updateMeters方法
- (float)peakPowerForChannel:(NSUInteger)channelNumber; /* returns peak power in decibels for a given channel */

//获得指定声道的分贝平均值,注意如果要获得分贝平均值必须在此之前调用updateMeters方法
- (float)averagePowerForChannel:(NSUInteger)channelNumber; /* returns average power in decibels for a given channel */


/* The channels property lets you assign the output to play to specific channels as described by AVAudioSession's channels property */
/* This property is nil valued until set. */
/* The array must have the same number of channels as returned by the numberOfChannels property. */
//获得或设置播放声道
@property(nonatomic, copy, nullable) NSArray<AVAudioSessionChannelDescription *> *channelAssignments

代理方法

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
<!--音频播放结束后调用的函数 被中断停止播放并不会被调用--> 
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag;


<!--播放遇到错误时调用的函数-->
- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError * __nullable)error;


/**
音乐播放器被打断

@param player .
*/
-(void)audioPlayerBeginInterruption:(AVAudioPlayer *)player{
NSLog(@"播放被中断");
//如果音频被中断,比如有电话呼入,该方法就会被回调,该方法可以保存当前播放信息,以便恢复继续播放的进度
}
/**
* 音乐播放器停止打断
*
* @param player .
*/
-(void)audioPlayerEndInterruption:(AVAudioPlayer *)player
{
NSLog(@"audioPlayerEndInterruption-停止打断");
[player play];
//继续播放
}

后台播放

1.需要添加info.plist
1

1
2
3
4
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>

或者
2
2.添加代码支持

1
2
3
//设置锁屏仍能继续播放
[[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayback error:nil];
[[AVAudioSession sharedInstance] setActive: YES error: nil];

监听耳机事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//添加通知,拔出耳机后暂停播放
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(routeChange:) name:AVAudioSessionRouteChangeNotification object:nil];

/**
* 一旦输出改变则执行此方法
*
* @param notification 输出改变通知对象
*/
-(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"]) {
[self.audioPlayer pause];
}
}
}

后台播放信息显示

1.导入框架 #import \<MediaPlayer/MediaPlayer.h>
2.添加代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
//设置歌曲题目
[dict setObject:@"题目" forKey:MPMediaItemPropertyTitle];
//设置歌手名
[dict setObject:@"歌手" forKey:MPMediaItemPropertyArtist];
//设置专辑名
[dict setObject:@"专辑" forKey:MPMediaItemPropertyAlbumTitle];
//设置显示的图片
UIImage *newImage = [UIImage imageNamed:@"43.png"];
[dict setObject:[[MPMediaItemArtwork alloc] initWithImage:newImage]
forKey:MPMediaItemPropertyArtwork];
//设置歌曲时长
[dict setObject:[NSNumber numberWithDouble:300] forKey:MPMediaItemPropertyPlaybackDuration];
//设置已经播放时长
[dict setObject:[NSNumber numberWithDouble:150] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
//更新字典
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];

注:实现后台播放显示歌词的方法实现思路:将歌词信息,绘制到专辑图片上进行显示

后台播放远程控制

1.注册接受远程控制

1
2
3
4
5
6
7
8
9
10
11
- (void)viewDidAppear:(BOOL)animated {
// 接受远程控制
[self becomeFirstResponder];
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
}

- (void)viewDidDisappear:(BOOL)animated {
// 取消远程控制
[self resignFirstResponder];
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
}

2.实现监听方法

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
- (void)remoteControlReceivedWithEvent:(UIEvent *)event {
if (event.type == UIEventTypeRemoteControl) {
//判断是否为远程控制
switch (event.subtype) {
//播放
case UIEventSubtypeRemoteControlPlay:
if (![self.audioPlayer isPlaying]) {
[self.audioPlayer play];
}
break;
case UIEventSubtypeRemoteControlPause:
//暂停
if ([self.audioPlayer isPlaying]) {
[self.audioPlayer pause];
}
break;
case UIEventSubtypeRemoteControlNextTrack:
NSLog(@"下一首");
break;
case UIEventSubtypeRemoteControlPreviousTrack:
NSLog(@"上一首 ");
break;
default:
break;
}
}
}

注:
event是事件类型对象,type是事件类型枚举

1
2
3
4
5
6
typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches, //触摸事件
UIEventTypeMotion, //运动事件
UIEventTypeRemoteControl, // 远程控制事件
UIEventTypePresses NS_ENUM_AVAILABLE_IOS(9_0),//物理按钮事件类型
};

subtype中的枚举便是点击这些控制键后传递给我们的消息,我们可以根据这些消息在app内做逻辑处理

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
typedef NS_ENUM(NSInteger, UIEventSubtype) {
// available in iPhone OS 3.0
UIEventSubtypeNone = 0,
// for UIEventTypeMotion, available in iPhone OS 3.0
//摇晃
UIEventSubtypeMotionShake = 1,
//UIEventTypeRemoteControl相关的枚举信息
// for UIEventTypeRemoteControl, available in iOS 4.0
//点击播放按钮或者耳机线控中间那个按钮
UIEventSubtypeRemoteControlPlay = 100,
//点击暂停按钮
UIEventSubtypeRemoteControlPause = 101,
//点击停止按钮
UIEventSubtypeRemoteControlStop = 102,
//点击播放与暂停开关按钮(iphone抽屉中使用这个)
UIEventSubtypeRemoteControlTogglePlayPause = 103,
//点击下一曲按钮或者耳机中间按钮两下
UIEventSubtypeRemoteControlNextTrack = 104,
//点击上一曲按钮或者耳机中间按钮三下
UIEventSubtypeRemoteControlPreviousTrack = 105,
//快退开始 点击耳机中间按钮三下不放开
UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
//快退结束 耳机快退控制松开后
UIEventSubtypeRemoteControlEndSeekingBackward = 107,
//开始快进 耳机中间按钮两下不放开
UIEventSubtypeRemoteControlBeginSeekingForward = 108,
//快进结束 耳机快进操作松开后
UIEventSubtypeRemoteControlEndSeekingForward = 109,
};

拓展:后台播放远程控制的相关自定义

MPRemoteCommandCenter属于\<MediaPlayer/MediaPlayer.h>框架,是一个单例类,处理远程控制中心事件
可以自定义你的控制中心,锁屏时控制,siri语音控制.它管理一系列命令Playback Commands,Feedback Command,Previous/Next Track Commands),Skip Interval Commands,Seek Command等
MPRemoteCommand是这些所有命令的父类
下面简单介绍:

1
2
3
4
5
6
7
8
9
10
11
//是否开启命令
@property (nonatomic, assign, getter = isEnabled) BOOL enabled;

//Target-action 添加事件处理
- (void)addTarget:(id)target action:(SEL)action;
//当属性enable = NO时,移除
- (void)removeTarget:(id)target action:(nullable SEL)action;
- (void)removeTarget:(nullable id)target;

//返回一个事件对象 MPRemoteCommandEvent是一个命令事件的父类,对应不同的命令进行了继承扩展,主要添加一些属性,比如拖拽音乐进度的positionTime
- (id)addTargetWithHandler:(MPRemoteCommandHandlerStatus(^)(MPRemoteCommandEvent *event))handler;

远程控制三部曲

1:监听远程控制事件

1
- (void)beginReceivingRemoteControlEvents

2:捕获远程控制事件

1
- (void)remoteControlReceivedWithEvent:(UIEvent *)event

3.取消远程控制事件

1
- (void)endReceivingRemoteControlEvents

网易云的锁屏实现

注:添加的RemoteComand必须设置Enable为YES,并添加Target才会显示,并且添加相同类型RemoteComand,有优先级的选择显示,更改原有默认显示的上一曲,播放/暂停,下一曲会失效,需要重新添加

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
MPRemoteCommandCenter *rcc = [MPRemoteCommandCenter sharedCommandCenter];

//feedBack
[rcc.likeCommand setEnabled:YES];
//使用block
[rcc.likeCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
NSLog(@"like!");
return MPRemoteCommandHandlerStatusSuccess;
}];
rcc.likeCommand.localizedTitle = @"喜欢";


[rcc.dislikeCommand setEnabled:YES];
//使用target-action 不喜欢
[rcc.dislikeCommand addTarget:self action:@selector(previousCommandAction)];
rcc.dislikeCommand.localizedTitle = @"上一首";
[rcc.bookmarkCommand setEnabled:YES];
//取消Action
// [rcc.dislikeCommand removeTarget:self action:@selector(previousCommandAction)];


//书签
[rcc.bookmarkCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
NSLog(@"bookmark");
return MPRemoteCommandHandlerStatusSuccess;
}];
rcc.bookmarkCommand.localizedTitle = @"收藏";

效果图:
3

锁屏控制播放进度

主要使用:MPChangePlaybackPositionCommand

1
2
3
4
5
6
7
8
//改变音乐播放进度
MPChangePlaybackPositionCommand *changePlayBackPosition = [rcc changePlaybackPositionCommand];
[changePlayBackPosition setEnabled:YES];
[changePlayBackPosition addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
MPChangePlaybackPositionCommandEvent *events = (MPChangePlaybackPositionCommandEvent *)event;
self.audioPlayer.currentTime = events.positionTime;
return MPRemoteCommandHandlerStatusSuccess;
}];

流式音频播放(播放网络音频)

使用AudioToolbox框架中的音频队列服务Audio Queue Services。
第三方框架:FreeStreamer

#import “FSAudioStream.h”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//初始化
FSAudioStream *player = [[FSAudioStream alloc]initWithUrl:[self getFileUrl]];
player.onFailure = ^(FSAudioStreamError error, NSString *errorDescription) {
NSLog(@"播放错误error:%@",errorDescription);
};
player.onCompletion = ^{
NSLog(@"播放完成");
};
//设置音量
[player setVolume:0.5];
//播放
[player play];

//添加引用
self.audioStream = player;

豆瓣:DOUAudioStreamer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#import "DOUAudioStreamer.h"


@interface AudioModel : NSObject<DOUAudioFile>

@property (nonatomic, strong) NSURL *audioFileURL;

@end

@implementation AudioModel

- (NSURL *)audioFileURL{
NSString *urlStr= @"http://sc1.111ttt.com/2017/1/03/07/296072048390.mp3";
NSURL *url=[NSURL URLWithString:urlStr];
self.audioFileURL = url;
return url;
}

@end

AudioModel *file = [[AudioModel alloc]init];
[file audioFileURL];
self.stream = [[DOUAudioStreamer alloc]initWithAudioFile:file];
[self.stream play];

参考:
https://developer.apple.com/library/content/samplecode/MPRemoteCommandSample/Introduction/Intro.html

音频初见

Posted on 2017-04-07

音频初见

基本介绍

音频: 从形式上分为
短音频(音效播放):不需要进度|循环等控制 AudioToolbox.framework
(C语言框架

  • 本质:将音频注册到system sound service(简单底层的声音播放服务))
    特点:
  • 播放时间不超过30s
  • 数据格式PCM/IMA4
  • 音频文件必须打包成.caf,.aif,.wav,(.MP3也可以)

长音频(音乐播放)

  • 较大的音频文件,需要进行精确的控制的场景
  • AVFoundation.framework
  • 支持的音频格式有:AAC,ALAC,HE-AAC,iLBC,IMA4,MP3等.

音效播放

步骤:

  • 不要忘记导入头文件: #import \<AudioToolbox/AudioToolbox.h>
  • 1.调用AudioServicesCreateSystemSoundID(CFURLRef inFileURL, SystemSoundID* outSystemSoundID)函数获得系统声音ID。
  • 2.如果需要监听播放完成操作,则使用AudioServicesAddSystemSoundCompletion( SystemSoundID inSystemSoundID,CFRunLoopRef inRunLoop, CFStringRef inRunLoopMode, AudioServicesSystemSoundCompletionProc inCompletionRoutine, void* inClientData)方法注册回调函数。
  • 3.调用AudioServicesPlaySystemSound(SystemSoundID inSystemSoundID)
    AudioServicesPlayAlertSound(SystemSoundID inSystemSoundID) 方法播放音效(后者带有震动效果)
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
- (void)audioBox{

//获取资源路径
NSString *pathSource = [[NSBundle mainBundle]pathForResource:@"audiobox" ofType:@".caf"];
NSURL *sourceUrl = [NSURL fileURLWithPath:pathSource];

//系统声音id
SystemSoundID soundID = 0;
/**
注册音效服务

@param inFileURL#> 音频文件URL 需要桥接->C语言
@param outSystemSoundID#> 输出音效ID
@return 将音效文件加入到系统音频服务中并返回一个长整型音频ID
*/
AudioServicesCreateSystemSoundID((__bridge CFURLRef)(sourceUrl), &soundID);



/**
添加播放完毕回调函数

@param inSystemSoundID#> 音效ID
@param inRunLoop#> 添加到Runloop
@param inRunLoopMode#> RunloopMode
@param inCompletionRoutine#> 回调函数
@param inClientData#> 传递给回调函数的相关数据
@return ..
*/
AudioServicesAddSystemSoundCompletion(soundID, NULL, NULL, playCompletion, NULL);


//播放
AudioServicesPlayAlertSound(soundID); //播放音效并震动
// AudioServicesPlaySystemSound(soundID); //播放音效
}


/**
回调函数

@param ssID 音效id
@param clientData 回调时传递的数据
*/
void playCompletion(SystemSoundID ssID, void* __nullable clientData){
NSLog(@"播放完成");
//摧毁音效
AudioServicesDisposeSystemSoundID(ssID);
}

音乐播放 AVAudioPlayer

AVAudioPlayer支持多种音频格式,而且能够对进度,音量,播放速度等控制
头文件 #import \<AVFoundation/AVFoundation.h>
步骤:

  • 1.给定资源路径,或者二进制数据初始化播放器
  • 2.设置相关属性
  • 3.执行播放,更新相关UI(进度更新,歌词显示(CADislinkplay/NSTimer))

初始化方法

AVAudioPlayer可以接受本地资源路径(NSURL)/NSData类型数据(可以用来加载网络链接),但是实质上AVAudioPlayer不支持流式播放,只能播放完整的文件,使用后者加载网络链接,当播放的音频文件较大或者网络不好就需要过长的时间等待
每一个实例化方法返回一个AVAudioPlayer对象,每一个文件/Data对应一个AVAudioPlayer对象,传入一个NSError的指针,用于判断初始化是否成功.

1
2
- (nullable instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError **)outError;
- (nullable instancetype)initWithData:(NSData *)data error:(NSError **)outError;

播放控制相关API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//准备播放,分配播放所需的资源,并将其加入内部播放队列,加载音频文件到缓冲区,注意:即使在播放之前音频文件没有加载到缓冲区程序也会隐式调用此方法。
- (BOOL)prepareToPlay;
//播放,如果资源还没准备好,会隐式调用prepareToPlay方法,是异步的
- (BOOL)play;
//相对当前设备时间或指定的时间开始播放音频
- (BOOL)playAtTime:(NSTimeInterval)time

NSTimeInterval nowTime = self.audioPlayer.deviceCurrentTime;
[self.audioPlayer playAtTime:nowTime + 2.0];

//暂停播放 并且准备好继续播放
- (void)pause; /* pauses playback, but remains ready to play. */
//停止播放 不再准备好继续播放,再次调用会重新开始播放
- (void)stop; /* stops playback. no longer ready to play. */

属性相关

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
//获取是否正在播放
@property(readonly, getter=isPlaying) BOOL playing;
//获取当前音频声道数
@property(readonly) NSUInteger numberOfChannels;
//获取当前音频时长
@property(readonly) NSTimeInterval duration;


//获取创建时的音频路径 跟初始化方法相关联
@property(readonly, nullable) NSURL *url;
//获取创建时的音频数据 跟初始化方法相关联
@property(readonly, nullable) NSData *data;
//声道 -1.0~0.0左声道 0.0临界值 0.0~1.0为右声道
@property float pan
//音量 正常范围在0.0~1.0
@property float volume;
- (void)setVolume:(float)volume fadeDuration:(NSTimeInterval)duration NS_AVAILABLE(10_12, 10_0); /* fade to a new volume over a duration */

//是否可以改变播放速率 必须在prepareToPlay方法前调用
@property BOOL enableRate
//播放速率 1.0为正常速率 0.5一半 2.0双倍速播放
@property float rate

//当前播放的时间点,可以实现定点进行播放
@property NSTimeInterval currentTime;

//输出设备播放音频的时间,注意:如果播放中被暂停此时间也会继续累加
@property(readonly) NSTimeInterval deviceCurrentTime

//设置音频播放循环次数 如果为0则不循环,如果小于0则无限循环,大于0则表示循环次数
@property NSInteger numberOfLoops;

//获取音频设置字典
@property(readonly) NSDictionary<NSString *, id> *settings NS_AVAILABLE(10_7, 4_0); /* returns a settings dictionary with keys as described in AVAudioSettings.h */

/* returns the format of the audio data */
@property(readonly) AVAudioFormat *format NS_AVAILABLE(10_12, 10_0);


//是否开启仪表计数功能(启用音频测量) 默认为NO,一旦启用音频测量可以通过updateMeters方法更新测量值
@property(getter=isMeteringEnabled) BOOL meteringEnabled; /* turns level metering on or off. default is off. */

//更新仪表计数的值
- (void)updateMeters; /* call to refresh meter values */

//获得指定声道的分贝峰值,如果要获得分贝峰值必须在此之前调用updateMeters方法
- (float)peakPowerForChannel:(NSUInteger)channelNumber; /* returns peak power in decibels for a given channel */

//获得指定声道的分贝平均值,注意如果要获得分贝平均值必须在此之前调用updateMeters方法
- (float)averagePowerForChannel:(NSUInteger)channelNumber; /* returns average power in decibels for a given channel */


/* The channels property lets you assign the output to play to specific channels as described by AVAudioSession's channels property */
/* This property is nil valued until set. */
/* The array must have the same number of channels as returned by the numberOfChannels property. */
//获得或设置播放声道
@property(nonatomic, copy, nullable) NSArray<AVAudioSessionChannelDescription *> *channelAssignments

代理方法

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
<!--音频播放结束后调用的函数 被中断停止播放并不会被调用--> 
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag;


<!--播放遇到错误时调用的函数-->
- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError * __nullable)error;


/**
音乐播放器被打断

@param player .
*/
-(void)audioPlayerBeginInterruption:(AVAudioPlayer *)player{
NSLog(@"播放被中断");
//如果音频被中断,比如有电话呼入,该方法就会被回调,该方法可以保存当前播放信息,以便恢复继续播放的进度
}
/**
* 音乐播放器停止打断
*
* @param player .
*/
-(void)audioPlayerEndInterruption:(AVAudioPlayer *)player
{
NSLog(@"audioPlayerEndInterruption-停止打断");
[player play];
//继续播放
}

后台播放

1.需要添加info.plist
1

1
2
3
4
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>

或者
2
2.添加代码支持

1
2
3
//设置锁屏仍能继续播放
[[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayback error:nil];
[[AVAudioSession sharedInstance] setActive: YES error: nil];

监听耳机事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//添加通知,拔出耳机后暂停播放
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(routeChange:) name:AVAudioSessionRouteChangeNotification object:nil];

/**
* 一旦输出改变则执行此方法
*
* @param notification 输出改变通知对象
*/
-(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"]) {
[self.audioPlayer pause];
}
}
}

后台播放信息显示

1.导入框架 #import \<MediaPlayer/MediaPlayer.h>
2.添加代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
//设置歌曲题目
[dict setObject:@"题目" forKey:MPMediaItemPropertyTitle];
//设置歌手名
[dict setObject:@"歌手" forKey:MPMediaItemPropertyArtist];
//设置专辑名
[dict setObject:@"专辑" forKey:MPMediaItemPropertyAlbumTitle];
//设置显示的图片
UIImage *newImage = [UIImage imageNamed:@"43.png"];
[dict setObject:[[MPMediaItemArtwork alloc] initWithImage:newImage]
forKey:MPMediaItemPropertyArtwork];
//设置歌曲时长
[dict setObject:[NSNumber numberWithDouble:300] forKey:MPMediaItemPropertyPlaybackDuration];
//设置已经播放时长
[dict setObject:[NSNumber numberWithDouble:150] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
//更新字典
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];

注:实现后台播放显示歌词的方法实现思路:将歌词信息,绘制到专辑图片上进行显示

后台播放远程控制

1.注册接受远程控制

1
2
3
4
5
6
7
8
9
10
11
- (void)viewDidAppear:(BOOL)animated {
// 接受远程控制
[self becomeFirstResponder];
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
}

- (void)viewDidDisappear:(BOOL)animated {
// 取消远程控制
[self resignFirstResponder];
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
}

2.实现监听方法

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
- (void)remoteControlReceivedWithEvent:(UIEvent *)event {
if (event.type == UIEventTypeRemoteControl) {
//判断是否为远程控制
switch (event.subtype) {
//播放
case UIEventSubtypeRemoteControlPlay:
if (![self.audioPlayer isPlaying]) {
[self.audioPlayer play];
}
break;
case UIEventSubtypeRemoteControlPause:
//暂停
if ([self.audioPlayer isPlaying]) {
[self.audioPlayer pause];
}
break;
case UIEventSubtypeRemoteControlNextTrack:
NSLog(@"下一首");
break;
case UIEventSubtypeRemoteControlPreviousTrack:
NSLog(@"上一首 ");
break;
default:
break;
}
}
}

注:
event是事件类型对象,type是事件类型枚举

1
2
3
4
5
6
typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches, //触摸事件
UIEventTypeMotion, //运动事件
UIEventTypeRemoteControl, // 远程控制事件
UIEventTypePresses NS_ENUM_AVAILABLE_IOS(9_0),//物理按钮事件类型
};

subtype中的枚举便是点击这些控制键后传递给我们的消息,我们可以根据这些消息在app内做逻辑处理

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
typedef NS_ENUM(NSInteger, UIEventSubtype) {
// available in iPhone OS 3.0
UIEventSubtypeNone = 0,
// for UIEventTypeMotion, available in iPhone OS 3.0
//摇晃
UIEventSubtypeMotionShake = 1,
//UIEventTypeRemoteControl相关的枚举信息
// for UIEventTypeRemoteControl, available in iOS 4.0
//点击播放按钮或者耳机线控中间那个按钮
UIEventSubtypeRemoteControlPlay = 100,
//点击暂停按钮
UIEventSubtypeRemoteControlPause = 101,
//点击停止按钮
UIEventSubtypeRemoteControlStop = 102,
//点击播放与暂停开关按钮(iphone抽屉中使用这个)
UIEventSubtypeRemoteControlTogglePlayPause = 103,
//点击下一曲按钮或者耳机中间按钮两下
UIEventSubtypeRemoteControlNextTrack = 104,
//点击上一曲按钮或者耳机中间按钮三下
UIEventSubtypeRemoteControlPreviousTrack = 105,
//快退开始 点击耳机中间按钮三下不放开
UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
//快退结束 耳机快退控制松开后
UIEventSubtypeRemoteControlEndSeekingBackward = 107,
//开始快进 耳机中间按钮两下不放开
UIEventSubtypeRemoteControlBeginSeekingForward = 108,
//快进结束 耳机快进操作松开后
UIEventSubtypeRemoteControlEndSeekingForward = 109,
};

拓展:后台播放远程控制的相关自定义

MPRemoteCommandCenter属于\<MediaPlayer/MediaPlayer.h>框架,是一个单例类,处理远程控制中心事件
可以自定义你的控制中心,锁屏时控制,siri语音控制.它管理一系列命令Playback Commands,Feedback Command,Previous/Next Track Commands),Skip Interval Commands,Seek Command等
MPRemoteCommand是这些所有命令的父类
下面简单介绍:

1
2
3
4
5
6
7
8
9
10
11
//是否开启命令
@property (nonatomic, assign, getter = isEnabled) BOOL enabled;

//Target-action 添加事件处理
- (void)addTarget:(id)target action:(SEL)action;
//当属性enable = NO时,移除
- (void)removeTarget:(id)target action:(nullable SEL)action;
- (void)removeTarget:(nullable id)target;

//返回一个事件对象 MPRemoteCommandEvent是一个命令事件的父类,对应不同的命令进行了继承扩展,主要添加一些属性,比如拖拽音乐进度的positionTime
- (id)addTargetWithHandler:(MPRemoteCommandHandlerStatus(^)(MPRemoteCommandEvent *event))handler;

远程控制三部曲

1:监听远程控制事件

1
- (void)beginReceivingRemoteControlEvents

2:捕获远程控制事件

1
- (void)remoteControlReceivedWithEvent:(UIEvent *)event

3.取消远程控制事件

1
- (void)endReceivingRemoteControlEvents

网易云的锁屏实现

注:添加的RemoteComand必须设置Enable为YES,并添加Target才会显示,并且添加相同类型RemoteComand,有优先级的选择显示,更改原有默认显示的上一曲,播放/暂停,下一曲会失效,需要重新添加

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
MPRemoteCommandCenter *rcc = [MPRemoteCommandCenter sharedCommandCenter];

//feedBack
[rcc.likeCommand setEnabled:YES];
//使用block
[rcc.likeCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
NSLog(@"like!");
return MPRemoteCommandHandlerStatusSuccess;
}];
rcc.likeCommand.localizedTitle = @"喜欢";


[rcc.dislikeCommand setEnabled:YES];
//使用target-action 不喜欢
[rcc.dislikeCommand addTarget:self action:@selector(previousCommandAction)];
rcc.dislikeCommand.localizedTitle = @"上一首";
[rcc.bookmarkCommand setEnabled:YES];
//取消Action
// [rcc.dislikeCommand removeTarget:self action:@selector(previousCommandAction)];


//书签
[rcc.bookmarkCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
NSLog(@"bookmark");
return MPRemoteCommandHandlerStatusSuccess;
}];
rcc.bookmarkCommand.localizedTitle = @"收藏";

效果图:
3

锁屏控制播放进度

主要使用:MPChangePlaybackPositionCommand

1
2
3
4
5
6
7
8
//改变音乐播放进度
MPChangePlaybackPositionCommand *changePlayBackPosition = [rcc changePlaybackPositionCommand];
[changePlayBackPosition setEnabled:YES];
[changePlayBackPosition addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
MPChangePlaybackPositionCommandEvent *events = (MPChangePlaybackPositionCommandEvent *)event;
self.audioPlayer.currentTime = events.positionTime;
return MPRemoteCommandHandlerStatusSuccess;
}];

流式音频播放(播放网络音频)

使用AudioToolbox框架中的音频队列服务Audio Queue Services。
第三方框架:FreeStreamer

#import “FSAudioStream.h”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//初始化
FSAudioStream *player = [[FSAudioStream alloc]initWithUrl:[self getFileUrl]];
player.onFailure = ^(FSAudioStreamError error, NSString *errorDescription) {
NSLog(@"播放错误error:%@",errorDescription);
};
player.onCompletion = ^{
NSLog(@"播放完成");
};
//设置音量
[player setVolume:0.5];
//播放
[player play];

//添加引用
self.audioStream = player;

豆瓣:DOUAudioStreamer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#import "DOUAudioStreamer.h"


@interface AudioModel : NSObject<DOUAudioFile>

@property (nonatomic, strong) NSURL *audioFileURL;

@end

@implementation AudioModel

- (NSURL *)audioFileURL{
NSString *urlStr= @"http://sc1.111ttt.com/2017/1/03/07/296072048390.mp3";
NSURL *url=[NSURL URLWithString:urlStr];
self.audioFileURL = url;
return url;
}

@end

AudioModel *file = [[AudioModel alloc]init];
[file audioFileURL];
self.stream = [[DOUAudioStreamer alloc]initWithAudioFile:file];
[self.stream play];

参考:
https://developer.apple.com/library/content/samplecode/MPRemoteCommandSample/Introduction/Intro.html

菜鸟Xcode起航

Posted on 2017-04-04

#Xcode的快捷键

##Xcode的说明
Xcode截图

Window 对象

Posted on 2017-03-24

Window

核心对象:它是客户端JavaScript程序的全局对象

定时器

延迟调用setTimeout() 间歇调用setInterval()

单词interval:间隔,间歇,空隙;差距,差别,隔阂

at invervals 每隔一定的时间或距离,不时;到处

in the intervals 不一会儿,不久

  1. setTimeout()指定时间(毫秒)之后单次调用传入的函数,setInterval()指定毫秒数的间隔里重复调用传入的函数
  2. 同为全局函数,window对象的方法
  3. 返回值可以传递给clearTimeout(),clearInterval()用于取消函数的执行
  4. 参数,第一个参数为调用的函数(也可以传入一个字符串,但是实质为求值,执行eval());第二个参数为毫秒数,表示时间(传入的方法执行需要的时间间隔)
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
//延迟调用
var funcTimeout = setTimeout(function(){
console.log(new Date);
},1000);
//不建议使用字符串的方式调用
setTimeout("console.log('Hello world')",1000);

//取消延迟函数的执行
clearTimeout(funcTimeout);

//间隔调用 定义一个间隔执行一定次数的方法
var maxCount = 10;
var count = 0;
var func = null;
function inter(){
count ++;
////取消间隔调用
if (count > maxCount) {
console.log('Done');
clearInterval(func);
}else{
console.log('Hello Rhino' + count);
}
}
//间隔执行
func = setInterval(inter,1000);

Xcode

JavaScript 对象

Posted on 2017-03-22

对象

对象是 JavaScript 的数据类型。它将很多值(原始值或者其他对象)聚合在一起,可通过名字访问这些值,因此我们可以把它看成是从字符串到值的映射。对象是动态的,可以随时新增和删除自有属性。对象除了可以保持自有的属性,还可以从一个称为原型的对象继承属性,这种「原型式继承(prototypal inheritance)」是 JavaScript 的核心特征。

对象最常见的用法是创建(create)、设置(set)、查找(query)、删除(delete)、检测(test)和枚举(enumerate)它的属性。

属性包括名字和值。属性名可以是包含空字符串在内的任意字符串,但对象中不能存在两个同名的属性。值可以是任意 JavaScript 值,或者在 ECMAScript 5 中可以是 getter 或 setter 函数。

除了名字和值之外,每个属性还有一些与之相关的值,称为「属性特性(property attribute)」:

  • 可写(writable attribute),表明是否可以设置该属性的值。
  • 可枚举(enumerable attribute),表明是否可以通过 for-in 循环返回该属性。
  • 可配置(configurable attribute),表明是否可以删除或修改该属性。

在 ECMAScript 5 之前,通过代码给对象创建的所有属性都是可写的、可枚举的和可配置的。在 ECMAScript 5 中则可以对这些特性加以配置。

除了包含属性特性之外,每个对象还拥有三个相关的「对象特性(object attribute)」:

  • 对象的类(class),是一个标识对象类型的字符串。
  • 对象的原型(prototype),指向另外一个对象,本对象的属性继承自它的原型对象。
  • 对象的扩展标记(extensible flag),指明了在 ECMAScript 5 中是否可以向该对象添加新属性。

最后,用下面术语来对 JavaScript 的「三类对象」和「两类属性」进行区分:

  • 内置对象(native object),是由 JavaScript 规范定义的对象或类。例如,数组、函数、日期和正则表达式都是内置对象。
  • 宿主对象(host object),是由 JavaScript 解释器所嵌入的宿主环境(比如 Web 浏览器)定义的。客户端 JavaScript 中表示网页结构的 HTMLElement 对象均是宿主对象。
  • 自定义对象(user-defined object),是由运行中的 JavaScript 代码创建的对象。
  • 自有属性(own property),是直接在对象中定义的属性。
  • 继承属性(inherited property),是在对象的原型对象中定义的属性。

创建对象

可以使用对象字面量、new 关键字和 ECMAScript 5 中的 Object.create() 函数来创建对象。

使用对象字面量创建对象(推荐)

创建对象最简单的方式就是在 JavaScript 代码中使用对象字面量。对象字面量是由若干名值对组成的映射表,名值对中间用冒号分隔,名值对之间用逗号分隔,整个映射表用花括号括起来。属性名可以是 JavaScript 标识符也可以是字符串直接量(包括空字符串)。属性的值可以是任意类型的 JavaScript 表达式,表达式的值(可以是原始值也可以是对象值)就是这个属性的值。例如:

1
2
3
4
5
6
7
8
9
10
// 推荐写法
var person = {
name : "xxx",
age : 21
};

// 也可以写成
var person = {};
person.name = "xxx";
person.age = 21;

使用 new 关键字创建对象

new 关键字创建并初始化一个新对象。关键字 new 后跟随一个函数调用。这里的函数称做构造函数(constructor),构造函数用以初始化一个新创建的对象。JavaScript 语言核心中的原始类型都包含内置构造函数。例如:

1
2
3
var person = new Object();
person.name = "xxx";
person.age = 21;

其中 var person = new Object(); 等价于 var person = {}; 。

使用 Object.create() 函数创建对象

ECMAScript 5 定义了一个名为 Object.create() 的方法,它创建一个新对象,其中第一个参数是这个对象的原型。Object.create() 提供第二个可选参数,用以对对象的属性进行进一步描述。Object.create() 是一个静态函数,而不是提供给某个对象调用的方法。使用它的方法很简单,只须传入所需的原型对象即可。例如:

1
2
3
var person = Object.create(Object.prototype);
person.name = "xxx";
person.age = 21;

其中 var person = Object.create(Object.prototype); 也等价于 var person = {}; 。

原型(prototype)

所有通过对象字面量创建的对象都具有同一个原型对象,并可以通过 JavaScript 代码 Object.prototype 获得对原型对象的引用。通过关键字 new 和构造函数调用创建的对象的原型就是构造函数的 prototype 属性的值。因此,同使用 {} 创建对象一样,通过 new Object() 创建的对象也继承自 Object.prototype。同样,通过 new Array() 创建的对象的原型就是 Array.prototype,通过 new Date() 创建的对象的原型就是 Date.prototype。

没有原型的对象为数不多,Object.prototype 就是其中之一。它不继承任何属性。其他原型对象都是普通对象,普通对象都具有原型。所有的内置构造函数(以及大部分自定义的构造函数)都具有一个继承自 Object.prototype 的原型。例如,Date.prototype 的属性继承自 Object.prototype,因此由 new Date() 创建的 Date 对象的属性同时继承自 Date.prototype 和 Object.prototype。

这一系列链接的原型对象就是所谓的「原型链(prototype chain)」。

属性的查询和设置

前面有提到过,可以通过点 . 或方括号 [] 运算符来获取属性的值。对于点 . 来说,左侧应当是一个对象,右侧必须是一个以属性名称命名的简单标识符。对于方括号来说 [] ,方括号内必须是一个计算结果为字符串的表达式,这个字符串就是属性的名称。例如:

1
2
3
4
5
6
7
// 推荐写法
console.log(person.name); // "xxx"
console.log(person.age); // "21"

// 也可以写成
console.log(person["name"]); // xxx
console.log(person["age"]); // 21

和获取属性的值写法一样,通过点和方括号也可以创建属性或给属性赋值,但需要将它们放在赋值表达式的左侧。例如:

1
2
3
4
5
6
7
8
9
// 推荐写法
person.name = "sophie"; // 赋值
person.age = 30; // 赋值
person.weight = 38; // 创建

// 也可以写成
person["name"] = "sophie"; // 赋值
person["age"] = 30; // 赋值
person["weight"] = 38; // 创建

当使用方括号时,方括号内的表达式必须返回字符串。更严格地讲,表达式必须返回字符串或返回一个可以转换为字符串的值。

属性的访问错误

查询一个不存在的属性并不会报错,如果在对象 o 自身的属性或继承的属性中均未找到属性 x,属性访问表达式 o.x 返回 undefined。例如:

1
2
var person = {};
person.wife; // undefined

但是,如果对象不存在,那么试图查询这个不存在的对象的属性就会报错。null 和 undefined 值都没有属性,因此查询这些值的属性会报错。例如:

1
2
var person = {};
person.wife.name; // Uncaught TypeError: Cannot read property 'name' of undefined.

除非确定 person 和 person.wife 都是对象,否则不能这样写表达式 person.wife.name,因为会报「未捕获的错误类型」,下面提供了两种避免出错的方法:

1
2
3
4
5
6
7
8
9
// 冗余但易懂的写法
var name;
if (person) {
if (person.wife)
name = person.wife.name;
}

// 简练又常用的写法(推荐写法)
var name = person && person.wife && person.wife.name;

删除属性

delete 运算符用来删除对象属性,事实上 delete 只是断开属性和宿主对象的联系,并没有真正的删除它。delete 运算符只能删除自有属性,不能删除继承属性(要删除继承属性必须从定义这个属性的原型对象上删除它,而且这会影响到所有继承自这个原型的对象)。

检测属性

JavaScript 对象可以看做属性的集合,我们经常会检测集合中成员的所属关系(判断某个属性是否存在于某个对象中)。可以通过 in 运算符、hasOwnPreperty() 和 propertyIsEnumerable() 来完成这个工作,甚至仅通过属性查询也可以做到这一点。

in 运算符的左侧是属性名(字符串),右侧是对象。如果对象的自有属性或继承属性中包含这个属性则返回 true。例如:

1
2
3
4
var o = { x: 1 }
console.log("x" in o); // true,x是o的属性
console.log("y" in o); // false,y不是o的属性
console.log("toString" in o); // true,toString是继承属性

对象的 hasOwnProperty() 方法用来检测给定的名字是否是对象的自有属性。对于继承属性它将返回 false。例如:

1
2
3
4
var o = { x: 1 }
console.log(o.hasOwnProperty("x")); // true,x是o的自有属性
console.log(o.hasOwnProperty("y")); // false,y不是o的属性
console.log(o.hasOwnProperty("toString")); // false,toString是继承属性

propertyIsEnumerable() 是 hasOwnProperty() 的增强版,只有检测到是自有属性且这个属性的可枚举性(enumerable attribute)为 true 时它才返回 true。某些内置属性是不可枚举的。通常由 JavaScript 代码创建的属性都是可枚举的,除非在 ECMAScript 5 中使用一个特殊的方法来改变属性的可枚举性。例如:

1
2
3
4
5
var o = inherit({ y: 2 });
o.x = 1;
o.propertyIsEnumerable("x"); // true:,x是o的自有属性,可枚举
o.propertyIsEnumerable("y"); // false,y是继承属性
Object.prototype.propertyIsEnumerable("toString"); // false,不可枚举

除了使用 in 运算符之外,另一种更简便的方法是使用 !== 判断一个属性是否是 undefined。例如:

1
2
3
4
var o = { x: 1 }
console.log(o.x !== undefined); // true,x是o的属性
console.log(o.y !== undefined); // false,y不是o的属性
console.log(o.toString !== undefined); // true,toString是继承属性

然而有一种场景只能使用 in 运算符而不能使用上述属性访问的方式。in 可以区分不存在的属性和存在但值为 undefined 的属性。例如:

1
2
3
4
5
6
7
var o = { x: undefined }        // 属性被显式赋值为undefined
console.log(o.x !== undefined); // false,属性存在,但值为undefined
console.log(o.y !== undefined); // false,属性不存在
console.log("x" in o); // true,属性存在
console.log("y" in o); // false,属性不存在
console.log(delete o.x); // true,删除了属性x
console.log("x" in o); // false,属性不再存在

枚举属性

除了检测对象的属性是否存在,我们还会经常遍历对象的属性。通常使用 for-in 循环遍历,ECMAScript 5 提供了两个更好用的替代方案。

for-in 循环可以在循环体中遍历对象中所有可枚举的属性(包括自有属性和继承的属性),把属性名称赋值给循环变量。对象继承的内置方法不可枚举的,但在代码中给对象添加的属性都是可枚举的。例如:

1
2
3
4
5
var o = {x:1, y:2, z:3};            // 三个可枚举的自有属性
o.propertyIsEnumerable("toString"); // false,不可枚举
for (p in o) { // 遍历属性
console.log(p); // 输出x、y和z,不会输出toString
}

有许多实用工具库给 Object.prototype 添加了新的方法或属性,这些方法和属性可以被所有对象继承并使用。然而在 ECMAScript 5 标准之前,这些新添加的方法是不能定义为不可枚举的,因此它们都可以在 for-in 循环中枚举出来。为了避免这种情况,需要过滤 for-in 循环返回的属性,下面两种方式是最常见的:

1
2
3
4
for(p in o) {
if (!o.hasOwnProperty(p)) continue; // 跳过继承的属性
if (typeof o[p] === "function") continue; // 跳过方法
}

除了 for-in 循环之外,ECMAScript 5 定义了两个用以枚举属性名称的函数。第一个是 Object.keys(),它返回一个数组,这个数组由对象中可枚举的自有属性的名称组成。第二个是 Object.getOwnPropertyNames(),它和 Ojbect.keys() 类似,只是它返回对象的所有自有属性的名称,而不仅仅是可枚举的属性。在 ECMAScript 3 中是无法实现的类似的函数的,因为 ECMAScript 3 中没有提供任何方法来获取对象不可枚举的属性。

属性的 getter 和 setter

我们知道,对象属性是由名字、值和一组特性(attribute)构成的。在 ECMAScript 5 中,属性值可以用一个或两个方法替代,这两个方法就是 getter 和 setter。由 getter 和 setter 定义的属性称做「存取器属性(accessor property)」,它不同于「数据属性(data property)」,数据属性只有一个简单的值。

当程序查询存取器属性的值时,JavaScript 调用 getter 方法。这个方法的返回值就是属性存取表达式的值。当程序设置一个存取器属性的值时,JavaScript 调用 setter 方法,将赋值表达式右侧的值当做参数传入 setter。从某种意义上讲,这个方法负责「设置」属性值。可以忽略 setter 方法的返回值。

和数据属性不同,存取器属性不具有可写性(writable attribute)。如果属性同时具有 getter 和 setter 方法,那么它是一个读/写属性。如果它只有 getter 方法,那么它是一个只读属性。如果它只有 setter 方法,那么它是一个只写属性,读取只写属性总是返回 undefined。定义存取器属性最简单的方法是使用对象直接量语法的一种扩展写法。例如:

1
2
3
4
5
6
7
8
var o = {
// 普通的数据属性
data_prop: value,

// 存取器属性都是成对定义的函数
get accessor_prop() { /*这里是函数体 */ },
set accessor_prop(value) { /* 这里是函数体*/ }
};

存取器属性定义为一个或两个和属性同名的函数,这个函数定义没有使用 function 关键字,而是使用 get 或 set。注意,这里没有使用冒号将属性名和函数体分隔开,但在函数体的结束和下一个方法或数据属性之间有逗号分隔。

序列化对象(JSON)

对象序列化(serialization)是指将对象的状态转换为字符串,也可将字符串还原为对象。ECMAScript 5 提供了内置函数 JSON.stringify() 和 JSON.parse() 用来序列化和还原 JavaScript 对象。这些方法都使用 JSON 作为数据交换格式,JSON 的全称是「JavaScript 对象表示法(JavaScript Object Notation)」,它的语法和 JavaScript 对象与数组直接量的语法非常相近。例如:

1
2
3
o = {x:1, y:{z:[false,null,""]}};       // 定义一个对象
s = JSON.stringify(o); // s是 '{"x":1,"y":{"z":[false,null,""]}}'
p = JSON.parse(s); // p是o的深拷贝

JSON 的语法是 JavaScript 语法的子集,它并不能表示 JavaScript 里的所有值。它支持对象、数组、字符串、无穷大数字、true、false 和 null,可以序列化和还原它们。NaN、Infinity 和 -Infinity 序列化的结果是 null,日期对象序列化的结果是 ISO 格式的日期字符串(参照 Date.toJSON() 函数),但 JSON.parse() 依然保留它们的字符串形态,而不会将它们还原为原始日期对象。函数、RegExp、Error 对象和 undefined 值不能序列化和还原。JSON.stringify() 只能序列化对象可枚举的自有属性。对于一个不能序列化的属性来说,在序列化后的输出字符串中会将这个属性省略掉。JSON.stringify() 和 JSON.parse() 都可以接收第二个可选参数,通过传入需要序列化或还原的属性列表来定制自定义的序列化或还原操作。

Swift 2.2 新特性NOTE

Posted on 2016-12-07

Swift 2.2 新特性NOTE

慕课网:http://www.imooc.com/learn/750

1. 弃用 ++,–运算符

var a = 3
//a++
a += 1
//a--
a -= 1                                        

为什么取消

  1. –a ; a– 容易让人迷惑
  2. += 更符合语意

2. Deprecate-C-Style-For-Loop

// 使用区间运算符
for i in 1...10{
    print(i)
}

for i in 1..<10{
    print(i)
}

// 反向遍历可以使用reversed
for i in (1...10).reversed(){
    print(i)
}

for i in (1..<10).reversed(){
    print(i)
}
// 使用stride to表示不包括类似于区间 ..<
for i in stride(from:0, to:10, by:2){
    print(i)
}
//through表示包含 类似于区间 ...
for i in stride(from:0, through:10, by:2){
    print(i)
}
//递减 步长为-2
for i in stride(from:10, to:0, by:-2){
    print(i)
}
//使用浮点数
for i in stride(from:0.5, to:1.5, by:0.1){
    print(i)
}

3.Tuple的比较, Deprecate-Tuple-Splat(没用过)

// tuple 的比较
let score1 = (chinese:90, math:95)
let score2 = (chinese:90, math:100)
let score3 = (chinese:100, math:90)

score1 < score2  //ture
score3 < score2 //false

4. Selectors-Can-Not-Be-a-String

//以前使用字符串,容易出现拼写错误
   //button.addTarget(self, action: "click", for: .touchUpInside)
    button.addTarget(self, action: #selector(click), for: .touchUpInside)

Block

Posted on 2016-02-10

Category

objc2.0,作用:为已经存在的类添加方法

  • 声明私有方法
  • 模拟多继承
  • 不同的功能组织到不同的category里,减少单个文件的体积,由多个开发者共同完成一个类
  • 按需加载想要的category

###Category与Extesion
Extesion是一个匿名类别,在编译期间决定,作为类的组成部分,在编译期间与头文件里的@interface和实现文件里面的@implement组成一个完成的类,与类共存亡.
Extesion一般用来隐藏类的私有信息,只有拥有一个类的源码才能添加,不能为系统类添加Extesion.
Category在运行期决议的,所以无法添加实例变量(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内存布局,这对编译型语言是灾难性的)

###方法覆盖
1)、category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA
2)、category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休^_^,殊不知后面可能还有一样名字的方法。

###Category的Load
1)、在类的+load方法调用的时候,我们可以调用category中声明的方法么?
2)、这么些个+load方法,调用顺序是咋样的呢?

1)、可以调用,因为附加category到类的工作会先于+load方法的执行
2)、+load的执行顺序是先类,后category,而category的+load执行顺序是根据编译顺序决定的。

###关联对象
category里面是无法为category添加实例变量的。但是我们很多时候需要在category中添加和对象关联的值,这个时候可以求助关联对象来实现。

Block

Posted on 2016-02-09

Block的理解

####什么是Block
带有自动变量的匿名函数

  • 自动变量
  • 匿名函数

####Block的使用

  1. 作为变量
1
2
3
4
5
6
//定义
returnType(^blockName)(Parameters) = ^(Parameters){
.....
};
//调用
blockName(Parameters);
  1. 作为属性
1
@property (nonatomic,copy)returnType (^blockName)(Parameters);
  1. 作为函数参数
1
2
3
- (void)testMethodWithHandler:(returnType (^blockName)(Parameters))block;

[self testMethodWithHandler:^returnType (Parameters){....}];
  1. 使用typedef
1
2
typedef returntype (^BlockName)(Parameters);
@property (nonatomic,copy)BlockName block;

####Block的本质

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};

struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 *descriptor;
// imported variables
};

objc中对象的定义:首地址是isa的结构体指针,所以block是对象类型

block的class类型:

  • _NSConcreteStackBlock 栈上创建的block
  • _NSConcreteMallocBlock 堆上创建的block
  • _NSConcreteGlobalBlock 作为全局变量的block
  • _NSConcreteWeakBlockVariable
  • _NSConcreteAutoBlock
  • _NSConcreteFinalizingBlock

#####_NSConcreteGlobalBlock
创建block时,就是声明struct,初始化struct的成员;执行block,就是调用struct的函数指针,编译完成之后,block中的代码会被提取出来,成为单独的C函数,调用这个C函数,并把struct指针传递过去.block的实际作用就相当于C语言的匿名函数.

全局block:block内部没有捕获任何外部变量就是一个全局block类型,调用block(),和C语言的函数无异,不需要考虑其生命周期

#####_NSConcreteStackBlock

当block内部引用了外部变量,当struct被创建时,它是存在于函数的栈帧上的,其class是固定的_NSConcreteStackBlock类型.其捕获的变量是会赋值到结构体的成员上,所以当block初始化完成之后,捕获的变量不能更改.
当函数返回时,函数的栈帧被销毁,这个block的内存也会被清除.所以当函数结束后仍然需要这个block时,就必须用Block_copy()方法将它拷贝到堆上,核心动作即(申请内存,将栈数据复制过去,将class改成_NSConcreteMallocBlock,向捕获的对象发送retain,增加block的引用计数)

block就是个匿名函数,只不过我们给了一个变量来引用这个匿名函数,在需要的时候调用。但是,栈block会随着函数栈帧的销毁而销毁,这样一来,我们用之前做引用的变量再去调用这么一块被销毁的内存,就会出现内存崩溃。

KVC与KVO

Posted on 2016-02-07

##KVC与KVO

###KVC(key-value-coding)键值编码
用来给一个对象的属性进行赋值或者访问.
通过类别的方式添加
@interface NSObject(NSKeyValueCoding)

使用:

  • (void)setValue:(nullable id)value forKey:(NSString *)key;
    • (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
      第二个方法通过路径的方式的访问,即可以访问成员变量是对象的属性 “Dog.age”

应用:

  1. 系统Storyboard控件拖线
  2. 访问私有成员变量
  3. 字典转模型

字典转模型(属性名相同,不能是关键字,嵌套对象模型时还需另做处理,一般使用第三方)

  • (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
    遇到不能识别的key执行的方法
  • (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

原理:

###KVO(key-value-observing)键值监听
利用一个key来找到某个属性并监听其值的改变。典型的观察者模式
类别:@interface NSObject(NSKeyValueObserving)

使用:

  1. 添加监听
  2. 实现监听方法,observeValueForKeyPath: ofObject: change: context:
  3. 移除监听

相关API

  • (void)addObserver:(NSObject )observer forKeyPath:(NSString )keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;//添加监听
  • (void)removeObserver:(NSObject )observer forKeyPath:(NSString )keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));//删除监听
  • (void)removeObserver:(NSObject )observer forKeyPath:(NSString )keyPath;//删除监听
  • (void)observeValueForKeyPath:(nullable NSString )keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> )change context:(nullable void *)context;//监听实现方法

####手动实现KVO
重写该方法,对应的key但会NO

  • (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
    调用方法完成当值改变时发出通知
  • (void)willChangeValueForKey:(NSString *)key;
  • (void)didChangeValueForKey:(NSString *)key;

###KVO的实现原理
当一个类的属性被观察的时候,系统会通过runtime动态的创建一个该类的派生类,并且会在这个类中重写基类被观察的属性的setter方法,而且系统将这个类的isa指针指向了派生类,从而实现了给监听的属性赋值时调用的是派生类的setter方法。重写的setter方法会在调用原setter方法前后,通知观察对象值得改变。

12

立志当大神

I have a dream! change the world~

11 posts
4 tags
© 2020 立志当大神
Powered by Hexo
|
Theme — NexT.Pisces v5.1.4