|
| 1 | +# 2018.01 |
| 2 | + |
| 3 | +为什么音频播放器突然没声音了呢? |
| 4 | +-------- |
| 5 | + |
| 6 | +**作者**: [Lefe_x](https://weibo.com/u/5953150140) |
| 7 | + |
| 8 | + |
| 9 | +做音频的同学可能都会遇到播放的音频突然没声音的情况,遇到这种情况后,一般控制台会抛出下面的错误: |
| 10 | + |
| 11 | +```objc |
| 12 | +[AVAudioSession setActive:withOptions:error:]: Deactivating an audio session that has running I/O. All I/O should be stopped or paused prior to deactivating the audio session.` |
| 13 | +``` |
| 14 | + |
| 15 | +遇到这个错误,会导致音频不能正常播放的情况。出现这种情况的主要原因当你设置 |
| 16 | + |
| 17 | +```objc |
| 18 | +[[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil]; |
| 19 | +``` |
| 20 | +
|
| 21 | +时还有某些操作占用了 `AVAudioSession` 权限,必须暂停或停止对 `AVAudioSession` 的使用。比如使用 `AVAudioPlayer` 播放某一音频时,此时音频正在播放,直接设置 `AVAudioSession` 的 `active` 为 `NO`,就会报上面提到的错误。而正确的做法是先暂停播放,再设置 `AVAudioSession` 的 `active` 为 `NO`。其正确的做法像下面代码所示,这样的好处是,当遇到设置失败后可以第一时间知道出错的时间点。 |
| 22 | +
|
| 23 | +```objc |
| 24 | +NSError *error; |
| 25 | +BOOL isSuccess = [[AVAudioSession sharedInstance] setActive:active withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error]; |
| 26 | +if (isSuccess) { |
| 27 | + NSLog(@"恭喜你成功设置"); |
| 28 | +} else { |
| 29 | + NSLog(@"设置失败"); |
| 30 | +} |
| 31 | +``` |
| 32 | + |
| 33 | +当然如果应用中有多个地方使用 `AVAudioSession`,建议项目中统一处理 `AVAudioSession` 的 `active`,这样避免出现错误,一旦出现错误,调试起来就非常费劲。 |
| 34 | + |
| 35 | +iOS 中音量控制解惑 |
| 36 | +-------- |
| 37 | + |
| 38 | +**作者**: [Lefe_x](https://weibo.com/u/5953150140) |
| 39 | + |
| 40 | + |
| 41 | +iOS 中音量中其实也有好多小窍门,这个小集帮你解惑。iOS 中主要有2个地方可以控制音量,一个是系统音量,用户主动按音量键,调整音量,这种方式会显示系统音量提示框;另一个是播放器的音量,比如通过 `AVAudioPlayer` 调整音量,这种不会显示系统提示音量框。 |
| 42 | + |
| 43 | +### 如何调节音量时不显示系统音量提示框 |
| 44 | + |
| 45 | +主要原理就是获取系统音量 `View`,并把它让用户不可见。但注意一点,你不能把 `MPVolumeView` 的 `hidden` 属性设置为 `YES`,这样导致的结果是用户调整音量时任然会显示系统音量提示框,如下代码所示。 |
| 46 | + |
| 47 | +``` |
| 48 | +_volumeView = [[MPVolumeView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)]; |
| 49 | +_volumeView.backgroundColor = [UIColor yellowColor]; |
| 50 | +
|
| 51 | +// 如果设置了 Hidden 为 YES,那么修改音量时会弹出系统音量框 |
| 52 | +_volumeView.hidden = NO; |
| 53 | +_volumeView.alpha = 0.01; |
| 54 | +for (UIView *view in [_volumeView subviews]){ |
| 55 | +if ([view.class.description isEqualToString:@"MPVolumeSlider"]){ |
| 56 | + self.volumeSlider = (UISlider*)view; |
| 57 | + break; |
| 58 | + } |
| 59 | +} |
| 60 | +[self.view addSubview:_volumeView]; |
| 61 | +``` |
| 62 | + |
| 63 | +### 获取系统音量 |
| 64 | + |
| 65 | +方法一:通过 `self.volumeSlider` 获取 |
| 66 | + |
| 67 | +如果想获取系统音量,可以通过第一种方式,`self.volumeSlider.value` 来获取,但是你发现第一次为 0,这很纠结,这样导致的结果就是获取的系统音量不准确。这是因为初始 `MPVolumeView` 时,`volumeSlider.value` 还没有赋值,如下图所示: |
| 68 | + |
| 69 | + |
| 70 | + |
| 71 | +可以发现,音量是后来通过 `[MPVolumeController updateVolumeValue]` 来更新的。所以我们可以通过监听 `self.volumeSlide` 值改变时的事件,达到获取系统音量的目的。 |
| 72 | + |
| 73 | +```objc |
| 74 | +[self.volumeSlider addTarget:self action:@selector(sliderValueDidChange:) forControlEvents:UIControlEventValueChanged]; |
| 75 | +``` |
| 76 | +
|
| 77 | +方法二:通过 `AVAudioSession` 获取 |
| 78 | +
|
| 79 | +```objc |
| 80 | +[[AVAudioSession sharedInstance] outputVolume]; |
| 81 | +``` |
| 82 | + |
| 83 | +这种方法直接了当。 |
| 84 | + |
| 85 | +### 自定义音量控件 |
| 86 | + |
| 87 | +如果想自定义音量控件,可以监听音量的变化,并且通过第一种方法隐藏系统音量提示框。通过监听通知,达到监听音量变化的效果。 |
| 88 | + |
| 89 | +### 监听音量变化 |
| 90 | + |
| 91 | +监听音量变化,通过监听通知 |
| 92 | + |
| 93 | +```objc |
| 94 | +AVSystemController_SystemVolumeDidChangeNotification |
| 95 | + |
| 96 | +[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(volumeChanged:) name:@"AVSystemController_SystemVolumeDidChangeNotification" object:nil]; |
| 97 | +``` |
| 98 | +
|
| 99 | +最终结果 `AVSystemController_AudioVolumeNotificationParameter` 表示音量的值,这里需要注意的是 `"AVSystemController_AudioVolumeChangeReasonNotificationParameter" = ExplicitVolumeChange;` 这个值,它会由于不同的场景,有不同的值。`ExplicitVolumeChange` 是用户点击音量按钮,`CategoryChange` 是用户按 `home` 键调起 `Siri`,`RouteChange` 这个时路线修改(不太清楚,什么情况下触发的)。 |
| 100 | +
|
| 101 | +```objc |
| 102 | +AVSystemController_SystemVolumeDidChangeNotification; object = <AVSystemController: 0x1c4001dc0>; userInfo = { |
| 103 | + "AVSystemController_AudioCategoryNotificationParameter" = "Audio/Video"; |
| 104 | + "AVSystemController_AudioVolumeChangeReasonNotificationParameter" = ExplicitVolumeChange; |
| 105 | + "AVSystemController_AudioVolumeNotificationParameter" = "0.5625"; |
| 106 | + "AVSystemController_UserVolumeAboveEUVolumeLimitNotificationParameter" = 0; |
| 107 | +}} |
| 108 | +``` |
| 109 | + |
| 110 | +### 注意点 |
| 111 | + |
| 112 | +如果通过代码修改了 `self.volumeSlide` 的 `value`,那么会显示出系统音量框,如果你发现某个页面突然蹦出一个系统音量框,原因大多数是你修改了这个值。 |
| 113 | + |
| 114 | + |
| 115 | + |
| 116 | +动态修改应用程序的icon |
| 117 | +-------- |
| 118 | + |
| 119 | +**作者**: [南峰子_老驴](https://weibo.com/touristdiary) |
| 120 | + |
| 121 | + |
| 122 | +偶然看到 `Price Tag` 有个替换应用图标的功能,如图,研究了一下。 |
| 123 | + |
| 124 | + |
| 125 | + |
| 126 | +这个功能是在 `iOS 10.3` 后新增的,主要的 `API` 如下所示: |
| 127 | + |
| 128 | +```objc |
| 129 | +@interface UIApplication (UIAlternateApplicationIcons) |
| 130 | +// If false, alternate icons are not supported for the current process. |
| 131 | +@property (readonly, nonatomic) BOOL supportsAlternateIcons NS_EXTENSION_UNAVAILABLE("Extensions may not have alternate icons") API_AVAILABLE(ios(10.3), tvos(10.2)); |
| 132 | + |
| 133 | +// Pass `nil` to use the primary application icon. The completion handler will be invoked asynchronously on an arbitrary background queue; be sure to dispatch back to the main queue before doing any further UI work. |
| 134 | +- (void)setAlternateIconName:(nullable NSString *)alternateIconName completionHandler:(nullable void (^)(NSError *_Nullable error))completionHandler NS_EXTENSION_UNAVAILABLE("Extensions may not have alternate icons") API_AVAILABLE(ios(10.3), tvos(10.2)); |
| 135 | + |
| 136 | +// If `nil`, the primary application icon is being used. |
| 137 | +@property (nullable, readonly, nonatomic) NSString *alternateIconName NS_EXTENSION_UNAVAILABLE("Extensions may not have alternate icons") API_AVAILABLE(ios(10.3), tvos(10.2)); |
| 138 | +@end |
| 139 | +``` |
| 140 | +
|
| 141 | +只读属性 `supportsAlternateIcons` 用于判断系统是否允许修改 `App` 图标,只有在允许的情况下才能修改。`-setAlternateIconName:completionHandler:` 用于执行修改操作,如果 `iconName` 设置为 `nil`,则恢复为主图标,使用方式如下代码所示: |
| 142 | +
|
| 143 | +```objc |
| 144 | +- (IBAction)changeIcon:(UIButton *)sender { |
| 145 | + if ([[UIApplication sharedApplication] supportsAlternateIcons]) { |
| 146 | + |
| 147 | + NSString *iconName = nil; |
| 148 | + if (sender.tag == 1) { |
| 149 | + iconName = @"rocket"; |
| 150 | + } else if (sender.tag == 2) { |
| 151 | + iconName = @"pin"; |
| 152 | + } |
| 153 | + |
| 154 | + [[UIApplication sharedApplication] setAlternateIconName:iconName completionHandler:^(NSError * _Nullable error) { |
| 155 | + |
| 156 | + }]; |
| 157 | + } |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +除了调用 `API` 外,最主要的还需要在 `info.plist` 中配置 `CFBundleIcons` 项,这是一个字典,可包含 `CFBundlePrimaryIcon`、`CFBundleAlternateIcons`、`UINewsstandIcon` 三个键。 |
| 162 | + |
| 163 | +`CFBundlePrimaryIcon` 为主图标,即 `Assets.xcassets` 中 `AppIcon` 的信息,一般置空。`CFBundleAlternateIcons` 即用于设置替换图标,具体的配置项描述可以参考[官方文档](https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html) ,通常的配置如图所示。 |
| 164 | + |
| 165 | + |
| 166 | + |
| 167 | +这里需要注意的是,替换图标应该放在工程的某个目录下,而不放在 `Assets.xcassets` 中,如图所示。 |
| 168 | + |
| 169 | + |
| 170 | + |
| 171 | + |
| 172 | +iOS 关于音频播放调研 |
| 173 | +-------- |
| 174 | + |
| 175 | +**作者**: [Lefe_x](https://weibo.com/u/5953150140) |
| 176 | + |
| 177 | + |
| 178 | +由于最近做音频方面的工作,就调研了一下关于音频播放的一些知识,中间也走过不少弯路,希望这篇小集能对关注我们的同学一点启示,少走一些弯路。最后提供一份我看过的资料。这里关于音频播放简单做一个总结。iOS 中音频播放有以下 5 种方式(如果你有更多的方式告诉我,非常感激),它们的使用场景各不同。 |
| 179 | + |
| 180 | +[1] 播放小于 30s 的音频: |
| 181 | +`AudioServicesPlaySystemSound` 可以播放小于等于 `30s` 的音频,主要用于播放一些提示音,你可以利用 `AudioServicesPlaySystemSoundWithCompletion` 的值播放完成的 `callback`。它有以下特点: |
| 182 | + |
| 183 | +- 使用系统音量,不能修改播放音量; |
| 184 | +- 立刻开始播放,不能暂停; |
| 185 | +- 不支持快进播放,也不可以循环播放; |
| 186 | +- 同一时刻只能播放一个音频; |
| 187 | +- 只能通过手机播放音频,不能通过其它设备输出,比如不能通过车载播放。 |
| 188 | + |
| 189 | +[查看更多的系统声音ID](http://iphonedevwiki.net/index.php/AudioServices) |
| 190 | + |
| 191 | +[2] `AVAudioPlayer` 播放本地的音频,或者已加载到内存中的音频流,主要用于播放本地的一些音频文件。注意它不能播放网络音频。它有以下特点: |
| 192 | + |
| 193 | +- 可以从任意位置播放,可快进,快退; |
| 194 | +- 可以循环播放; |
| 195 | +- 可以同时播放多个音频; |
| 196 | +- 可以控制播放速率; |
| 197 | + |
| 198 | +[3] `AVPlayer` 可以播放本地和网络音频,也可以播放视频,它支持流媒体播放,也就是说我们可以用它来做边下别播的使用场景。 |
| 199 | + |
| 200 | +[4] `AVQueuePlayer` 是 `AVPlayer` 的子类,它含有一个队列,主要用来播放一个音视频队列。 |
| 201 | + |
| 202 | +[5] `Audio Queue` 主要用来播放音频,录音,它比较底层,会有更多的控制权,如果 `APP` 主要功能是基于音频播放,推荐使用这个。 |
| 203 | + |
| 204 | +总的来说,如果普通的本地音频播放,可以选择 `AVAudioPlayer` ,这个不需要了解更多的音频知识,就可以达到一个基本的播放;如果想做流媒体播放,建议使用 `AVPlayer + Local Server` 的方式,类似于唱吧目前开源的方式。当然也可以选择 `Audio Queue`,不过这个难度比较高,需要对音频播放有一个整体的了解,推荐使用三方库 `FreeStream`,不过需要一些 `C++` 的知识,因为使用过程中有一些坑需要填,这样不得不阅读源码。最后推荐一些不错的文章。 |
| 205 | + |
| 206 | +[官方 Audio Queue](https://developer.apple.com/library/content/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html#//apple_ref/doc/uid/TP40005343-CH3-SW1) |
| 207 | + |
| 208 | +[官方 AudioSession](https://developer.apple.com/library/content/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40007875) |
| 209 | + |
| 210 | +[@pp锅的码农生活 博客](https://brownfeng.github.io/2016/07/25/iOS%E9%9F%B3%E9%A2%91%E7%B3%BB%E5%88%97(%E4%B8%80)/) |
| 211 | + |
| 212 | +[@cy_zju 博客](http://msching.github.io/blog/2014/07/08/audio-in-ios-2/) |
| 213 | + |
| 214 | + |
| 215 | +iOS中NSArray/NSSet的一些巧妙用法 |
| 216 | +-------- |
| 217 | + |
| 218 | +**作者**: [Vong_HUST](https://weibo.com/VongLo) |
| 219 | + |
| 220 | +最近用到很多操作集合类型的方法,这里总结分享一下,也欢迎大家一起补充。 |
| 221 | + |
| 222 | +* 假设我们已经有一个 `NSArray<Model *>` 类型的数组,但是我们想把这个数组中的 `Model` 的某个属性取出组成一个新的数组,一般情况下可能是直接去遍历,但是 `NSArray/NSSet` 有一个更便捷的方法 `valueForKey:`,可以快速取出对应属性组成的数组。但是有个问题就是这个方法的效率比 `for` 循环低,数据量不大的时候使用还是没有问题的。如下面两张图: |
| 223 | + |
| 224 | + |
| 225 | + |
| 226 | + |
| 227 | + |
| 228 | +* 要取两个数组的交集的时候,可以先将 `NSArray` 转换成 `NSMutableSet`,再通过取二者交集即可。但是需要注意一点的是数组中的元素最好复写一下 `isEqual:` 和 `hash` 方法,保证取交集后的结果是正确的。 |
| 229 | + |
| 230 | +* 要将数组内元素排序或者过滤等操作,可以结合 `NSSortDescriptor` 和 `NSPredicate` 使用,可以避免掉大量冗余的 `for` 循环之类的代码。关于 `NSPredicate` 的用法可以参考 [NSHipster](http://nshipster.com/nspredicate/) 和 `Realm` 的 [Cheetsheet](https://academy.realm.io/posts/nspredicate-cheatsheet/) |
| 231 | + |
| 232 | +* 关于图中 `valueForKey:` 的参数为什么不直接用 `@"name"` 而是用 `NSStringFromSelector(@selector(name))`,是因为后者会有代码提示可以避免硬编码带来的错误,同时后续该 `key` 换名字了之后,会有对应的警告。这个也是从 `AFNetworking` 中学到的。如图所示: |
| 233 | + |
| 234 | + |
| 235 | + |
| 236 | +对清除图片缓存的思考 |
| 237 | +-------- |
| 238 | + |
| 239 | +**作者**: [高老师很忙](https://weibo.com/517082456) |
| 240 | + |
| 241 | +众所周知,使用 `+[UIImage imageNamed:]` 方法加载图片是会进图片缓存的,清除缓存是系统触发,并没有为我们提供API;使用 `+[UIImage imageWithContentsOfFile:]` 方法加载图片是不会进入图片缓存的。如果想要有图片缓存机制,并且能手动清除图片缓存,我们可以这样做: |
| 242 | + |
| 243 | +从 `+[UIImage imageWithContentsOfFile:]` 方向下手: |
| 244 | +我们可以自己维护一套图片缓存,`Swizzle +[UIImage imageWithContentsOfFile:]` 方法加入缓存机制。加载图片后,加入到 `NSCache` 缓存,再次取该图片时,优先取 `NSCache` 内的缓存,如果缓存内没有再去真正加载。`NSCache`在 `Memory Warning` 的时候会自动清除缓存,我们也可以使用 `-[NSCache removeAllObjects]` 手动清除缓存。当然,你也可以不使用 `Swizzle` ,写一个 `Manager` 也是可以的,我只是提供一种思路。 |
| 245 | + |
| 246 | +从 `+[UIImage imageNamed:]` 方向下手: |
| 247 | +在 `Memory Warning` 或进入后台时,系统会自动帮我们清除使用 `+[UIImage imageNamed:]` 的图片缓存。我们也可以通过模拟发送 `UIApplicationDidReceiveMemoryWarningNotification` 或 `UIApplicationDidEnterBackgroundNotification` 来清除图片缓存,风险可以根据实际情况来评估。 |
| 248 | + |
| 249 | +还可以从私有API来下手,`+[UIImage imageNamed:]` 系统底层是通过 `UIAssetManager` 来管理图片缓存的,如下两图所示,我们可以模拟调用 `_clearCachedResources` 方法来实现清除缓存。 |
| 250 | + |
| 251 | +如果有其他思路的,欢迎提出! |
| 252 | + |
| 253 | + |
| 254 | + |
| 255 | + |
| 256 | + |
0 commit comments