2012年10月14日

[Q&A]別画面で再生したAVAudioPalyerを停める方法



メールで質問を受けたので、それの回答をこちらで説明したいと思います。

質問は以下のようなものです。

親Viewで、AVAudioPlayerが載ってる子Viewを表示させ、再生等を行い、
一度親Viewに戻って、再度子Viewを表示させた場合、先程再生させた
音楽を停止する事が出来ないのです。
色々私なりにやったのですが、全然無理でした。


添付されていたサンプルコードの主要部分が以下です。親Viewで子Viewを呼び出す部分と子Viewのヘッダと、そこで音楽を再生している部分です。必要部分のみ抜き出して修正しています。

親ビューコントローラー(インプリメンテーションファイル)
-(IBAction)goToNextView:(id)sender
{
DANextViewController *nextView = [[DANextViewController alloc] initWithNibName:@"DANextViewController" bundle:[NSBundle mainBundle]];
[self.navigationController pushViewController:nextView animated:YES];
}


子ビューコントローラー(ヘッダ)
@interface DANextViewController : UIViewController <AVAudioPlayerDelegate>
{
NSURL *soundFile;
AVAudioPlayer *sound;
}
@end


子ビューコントローラー(インプリメンテーションファイル)
-(IBAction)playSound:(id)sender {
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
if(sound) [sound release];

soundFile = [NSURL fileURLWithPath:[[NSBundle mainBundle]pathForResource:@"1" ofType:@"mp3"]];
sound = [[AVAudioPlayer alloc] initWithContentsOfURL:soundFile error:nil];
sound.delegate = self;

sound.numberOfLoops = -1;
[sound prepareToPlay];
[sound play];
}

-(IBAction)restSound:(id)sender{
[sound stop];
}

- (void)viewDidUnload
{
[super viewDidUnload];
}

- (void)dealloc {
[soundFile release];
[sound release];
[super dealloc];
}


うまく動かないのは、主としてObjective-Cでのメモリ管理が理解できていないことが原因です。なお、このプロジェクトはARC対応していません。

改善結果だけ書く方が楽ですが、書かれていないコードとの整合性もありますし、他にも多くの問題が含まれているので問題点を順に指摘します。

(1)AVAudioPlayerは毎回違うインスタンスが作成されている。



子ViewControllerで使われている、AVAudioPlayer *soundは毎回違うインスタンスになっているので、二回目以降に開いた子画面で[sound stop]を呼んでも、最初に再生を始めたAVAudioPlayerにはその停止指令は送られない。よって停止されない。

これが主たる原因です。

デバッガでオブジェクトのアドレスを記録して比較すれば違うインスタンスにメッセージを送っていることはわかると思います。

対策としては以下のように親ビューから子ビューを呼び出す時に前のビューコントローラーを取っておいて使い回すのが手っ取り早いやり方です(必ずしもよい方法ではありません)。

@interface DAViewController ()
@property (nonatomic,retain) DANextViewController *nextView;
@end


- (IBAction)nextAction:(id)sender {
if ( !self.nextView ) {
self.nextView = [[DANextViewController alloc] initWithNibName:@"DANextViewController"
bundle:[NSBundle mainBundle]];
}
[self.navigationController pushViewController:self.nextView animated:YES];
}


これで毎回同じAVAudioPlayerのインスタンスを制御できます。

(2)開放忘れのオブジェクトがある



ちょっと横道にそれますが、さっきの書き方には間違いがあります。子ビューコントローラーのインスタンスのリテインカウントと保持している変数の数があっていません。


- (IBAction)nextAction:(id)sender {
if ( !self.nextView ) {
DANextViewController *viewController;
viewController = [[DANextViewController alloc] initWithNibName:@"DANextViewController"
bundle:[NSBundle mainBundle]];
self.nextView = viewController;
[viewController release];
}
[self.navigationController pushViewController:self.nextView animated:YES];
}


オリジナルのコードも同じような間違いがあるのでそれを指摘する目的で先ほどはあえて間違えて書きました。

質問者のコードにはインスタンス変数へ生成したオブジェクトを割り当てる書き方も、自動変数へ割り当てる書き方も同じ書き方をしているため、間違いを見つけにくい問題点があります。

インスタンス変数の変数名は自動変数の変数名と命名規則を変えるとか、後で示すようにインスタンス変数へはプロパティ経由でのみアクセスするなど工夫をしたほうがいいいでしょう。

(3)AVAudioPlayerのインスタンスを親で持つ



別画面に遷移しても音楽をならし続けたい場合、子ビューコントローラーでAVAudioPlayerを生成してその管理も任せるやり方は適切とは言えない。

子ビューコントローラーが管理するオブジェクトは、その子ビューコントローラーの生存期間内(つまりその子ビューが表示している間だけ)に限るのが適切だ。もしその生存期間を超えて使いたいのであれば、親のビューコントローラーあるいはアプリケーションデリゲートなどに持たせるほうがよい。

質問者の場合、引用していない箇所で複数の子ビューそれぞれで別々に音楽を切り替えて再生させようとしているようだ。全体像がわからにのだが、もし各画面でBGMを設定してメインビューで何かやっている間に再生させようとしているのであれば、メインビュー側でAVAudioPlayerを保持すべきだろう(そして子ビューコントローラーは毎回開放してインスタンスは保持しないほうがいい)。

インスタンスの生成や設定は子ビューでやるとして、それを親子で受け渡すべきだ。例えば以下のようにする。

親ビューコントローラー(ヘッダ)
@interface DAViewController : UIViewController
@property (nonatomic,retain) AVAudioPlayer *sound;

親ビューコントローラー(インプリメンテーションファイル)
- (IBAction)nextAction:(id)sender {
if ( !self.nextView ) {
DANextViewController *viewController;
viewController = [[DANextViewController alloc] initWithNibName:@"DANextViewController"
bundle:[NSBundle mainBundle]];
viewController.sound = self.sound;
viewController.delegate = self;
self.nextView = viewController;
[viewController release];
}
[self.navigationController pushViewController:self.nextView animated:YES];
}

- (void)nextViewConroller:(DANextViewController*)vc
didChangSound:(AVAudioPlayer*)aSound
{
self.sound = aSound;
}


親ビューコントローラー(ヘッダ)
@interface DANextViewController : UIViewController
@property (nonatomic,retain) AVAudioPlayer *sound;
@property (nonatomic.weak) id delegate;

親ビューコントローラー(インプリメンテーションファイル)

AVAudioPlayer *aSound = [[AVAudioAplayer alloc] init...];
self.sound = aSound;
[delegate nextViewController:self didChangeSound:aSound];


(4)AVAudioSessionの初期化位置



本質的ではないがいくつか問題点があるので指摘しておく。
子ビューコントローラーでAVAudioSessionのカテゴリー設定をしているが、これはアプリケーション全体の初期化時にすべきだ。親ビューコントローラーの初期化時かアプリケーションデリゲートの applicationDidFinishLaunching 系のメソッド内で1回だけやるのがよいと思う。

(5)deallocでのメモリ開放



UIViewControllerのdeallocが呼ばれるタイミングは難しい。もちろんUIViewControllerが管理するビューが閉じられた時に呼ばれたいのだが、別のオブジェクトに参照されている場合には閉じても開放されないかも知れない。ビューを表示している時に必要なオブジェクトの開放場所としては適当とは言えない。

(6)viewDidUnloadでのメモリ開放



viewDidUnloadはViewが開放されるときに呼ばれるので、メモリ不足があった場合に呼ばれることが想定されていた。だがiOS6ではこのメソッドはdeprecatedとなり呼ばれなくなっている。

ビューを表示している時だけ必要なオブジェクトの開放はviewDidDisappear:などで行うのがよいだろう。

(7)ARCを使おう



質問者のプロジェクトではメモリ管理が間違っている部分が何カ所かあるようだが一々指摘しない。コード全部を見ることができないので他でなんとかしているかもしれないから。だが、どこで確保開放しているのかはっきりしないコードはよくないコードなので、適切でないと言ってしまっていいと思う。

Objective-Cのメモリ管理は難しい。ARCが使える環境ではARCを使い、インスタンス変数へのアクセスはできる限りプロパティ経由にしたほうがいいだろう。もちろんARCを使っていると特有のメモリ管理問題に直面することがあるが、初心者が通常使う範囲での問題はARCを使わないよりARCを使った方が軽減されるのは間違いない。

最新のXcodeを使っていればsynthesizeなどを書く必要もないし、無名カテゴリローカルアクセスのためだけのプロパティも作ることができる。メモリ開放も=nilするだけなのでわかりやすくなる。

初心者こそARCを使ったコードを書くべきだ。

posted by 永遠製作所 at 21:00| 東京 ☁| Comment(0) | TrackBack(0) | iPhone/iPod touch | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント: [必須入力]

※ブログオーナーが承認したコメントのみ表示されます。
この記事へのトラックバックURL
http://blog.seesaa.jp/tb/297443117
※ブログオーナーが承認したトラックバックのみ表示されます。
※言及リンクのないトラックバックは受信されません。

この記事へのトラックバック