问题

前不久在QA发现一个问题,在iOS8 beta1上使用我们的app播放歌曲时进入某些内嵌的web页面(UIWebview实现)时歌曲会暂停播放,但是界面仍然显示为正在播放状态。把真机连上Xcode6调试后发现在进入部分网页时会再console上打印如下log:

AVAudioSession.mm:623: -[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.

bt后堆栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
frame #1: 0x299632fe libAVFAudio.dylib`-[AVAudioSession setActive:error:] + 26
frame #2: 0x3551b92e WebCore`WebCore::AudioSession::setActive(bool) + 62
frame #3: 0x35af2674 WebCore`WebCore::MediaSessionManager::updateSessionState() + 100
frame #4: 0x35af03b6 WebCore`WebCore::MediaSessionManager::addSession(WebCore::MediaSession&) + 74
frame #5: 0x35af0002 WebCore`WebCore::MediaSession::MediaSession(WebCore::MediaSessionClient&) + 38
frame #6: 0x35735a20 WebCore`WebCore::HTMLMediaSession::create(WebCore::MediaSessionClient&) + 20
frame #7: 0x35724c68 WebCore`WebCore::HTMLMediaElement::HTMLMediaElement(WebCore::QualifiedName const&, WebCore::Document&, bool) + 976
frame #8: 0x3570ad24 WebCore`WebCore::HTMLAudioElement::create(WebCore::QualifiedName const&, WebCore::Document&, bool) + 36
frame #9: 0x35718184 WebCore`WebCore::audioConstructor(WebCore::QualifiedName const&, WebCore::Document&, WebCore::HTMLFormElement*, bool) + 56
frame #10: 0x3571803a WebCore`WebCore::HTMLElementFactory::createElement(WebCore::QualifiedName const&, WebCore::Document&, WebCore::HTMLFormElement*, bool) + 230
frame #11: 0x3533a26c WebCore`WebCore::HTMLDocument::createElement(WTF::AtomicString const&, int&) + 88
frame #12: 0x3533a1ae WebCore`WebCore::jsDocumentPrototypeFunctionCreateElement(JSC::ExecState*) + 242
frame #13: 0x2c1cc4d4 JavaScriptCore`llint_entry + 21380

发现是WebCore调用了AVAudioSession的setActive方法,并且把active置为了NO。这个过程其实类似于音乐在播放时被其他事件打断(例如电话、siri)一样,audio会被打断,同时会发送kAudioSessionBeginInterruption事件通知app音频播放已经被打断,需要修正播放器和UI状态;打断结束后回发送kAudioSessionEndInterruption事件通知app恢复播放状态。区别在于WebCore的打断并没有任何通知,所以就导致界面上的播放状态为播放中而实际音乐却被打断。

适配

接下来就要对这个问题进行适配了:

  1. 首先,联系了前段组的同事对出现问题的页面进行检查,之后被告知是某个页面的js中调用了一些播放相关的代码导致了这个问题,这些js是之前版本中使用的,现在已经被废弃但没有及时的删除。在删除这些js后,问题自然就消失了。
  2. 客户端本身也应该做一些适配来防止下次再有页面出现类似问题,目前我能想到的办法是做一个AVAudioSession的category,method swizzle方法setActive:withOptions:error:在设置active值时发送通知来修改UI的状态。

在使用AudioUnit的过程中发现当app在后台时调用extern OSStatus AudioUnitInitialize(AudioUnit inUnit)方法返回561015905错误码,解析成string后是!pla,google错误码后毫无收获,于是只能workaround。面对这个问题我的workaround是当出现初始化失败的情况下会在程序进入前台时再尝试调用AudioUnitInitialize方法来初始化AudioUnit。至此问题已经在一定程度上得到了解决,只要用户进入前台就可以正确初始化AudioUnit并且播放音乐。

今天在应对某个用户反馈时发现该用户在使用remoteControl过程中无法启动播放的情况正是因为后台init AudioUnit会失败导致程序无法如预期工作。于是灵光一闪,觉得在初始化AudioUnit之前先调用AudioSessionInitialize并setActive是否就可以解决问题。尝试之后发现果然可以…(之前都在AudioUnitInitialize成功后才去init audiosession)。

iOS

在使用iOS-Universal-Framework制作framework的过程中经常会遇到编译出来的framework只能被真机使用或者只能被模拟器使用的情况。

造成这个问题的原因是由于在编译时选择的目标设备不同的情况下编译出来framework体系结构不同,选择真机进行编辑时会编译产生armv7armv7sarm64下的库文件,而选择模拟器会产生i386x86_64下的库文件。 具体查看的方法可以执行下列命令:

1
2
3
4
5
$ lipo -info /Debug-iphoneos/Someframework.framwork/Someframework
# Architectures in the fat file: Someframework are: armv7 armv7s arm64 

$ lipo -info /Debug-iphonesimulator/Someframework.framwork/Someframework
# Architectures in the fat file: Someframework are: i386 x86_64 

要同时对模拟器和真机进行支持,只要对两个编译出来的framework进行合并就可以了。

执行如下命令就可以进行合并

1
$ lipo –create /Debug-iphoneos/Someframework.framwork/Someframework Debug-iphonesimulator/Someframework.framwork/Someframework –output Someframework

完成后再查看framework的版本

1
2
$ lipo -info Someframework
# Architectures in the fat file: Someframework are: armv7 armv7s arm64 i386 x86_64

问题

前两天公司有一位同事在使用AVAudioPlayer的过程中遇到了这样一个问题:

他需要播放一段网络上的音频,实现策略是把音频下载到本地,然后使用AVAudioPlayer进行播放。代码大致是这样的:

1
2
3
4
NSString *path = .../xxx.mp3; //mp3 file path
NSData *data = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:path]];
NSError *error = nil;
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithData:data error:&error];

但他在init AVAudioPlayer时遇到了下面的错误。

1
Error Domain=NSOSStatusErrorDomain Code=1937337955 "The operation couldn’t be completed. (OSStatus error 1937337955.)"
Read on →

这是2013年下半年解决iOS7下ASIHTTPRequest内存泄露时所做的记录,现在搬运过来了。

现在这个修复方法已经被merge到ASIHTTPRequest的主分支上,经过测试可以通过apple的审核,大家可以直接从主分支fork并使用了。

发现问题

iOS7发布后,我们对产品进行了iOS7的适配。适配完成之后的某天,我使用Leaks对产品的新版本进行内存泄漏检测时发现ASIHTTPRequest存在内存泄漏问题,当时使用的设备是iTouch5,系统为iOS7.0.2。

Leaks检测结果

Leaks

(ps:使用的是ASIHTTPRequest iPhoneSample的检测图,结果是一样的)

发现之初,我以为是某处ASIHTTPRequest使用不当导致的泄漏,于是把leaks中的堆栈全部都检查了一边,但没有发现任何产品工程中的代码(其中一处泄漏的堆栈如图)。

Leaks中StackTrace结果

StackTrace

由于在iOS7发布之前的所有版本中并未看到类似的内存泄漏,所以我就开始怀疑是ASIHTTPRequest在iOS7才产生的。于是我在iOS5和iOS6的设备上进行了Leak Profile,结果没有发现任何泄漏。

对于这样的结果我仍然不是很确信,因为项目的需要我们对ASIHTTPRequest进行了一定的定制,修改了其中一部分代码。为了确定问题确实是出在ASIHTTPRequest上,我去github上翻出了ASIHTTPRequest的repo,pull了最新的代码,用Leaks在iOS7系统上进行了profile。在profile过程中我对iPhone Sample中的每个Tab以此进行了测试,结果在SynchronousQueue上并没有发现内存泄漏,在Upload上发现了和之前一样泄漏。随后在iOS5和iOS6上也进行了一样的测试,结果依然是没有任何泄漏。

自此确定了这是ASIHTTPRequest在iOS7下特有的内存泄漏,并且只会出现在有POST body的情况下。

Read on →

前言

从小我就相信“好记性不如烂笔头”这句谚语,所以搭Blog想法在我的脑海中已经酝酿了很久。刚工作那会就想利用Blog来记录工作中所积累的知识,对此我也进行了一些尝试,但最终因为国内的一些博客站点糟糕的排版、设计、代码高亮等等各种原因而放弃了。之后的一段时间由于工作忙碌、其他各种事情以及犯懒的缘故一直没有把这件事情落实下来。最近在项目进行的过程中发现自己对之前碰到过的一些技术问题的记忆逐渐变得模糊起来,于是才把搭建Blog这件事情重新提上了日程。经过一番Google之后我发现现在的程序猿们都偏向于Octopress这个开源的框架加上Github Pages服务来搭建Blog,看上非常的高大上,排版、代码高亮都做得非常棒,以Markdown写Blog的方式也非常符合我的日常工作习惯,于是决定立马付诸行动。

Read on →