C++
Objective-C
Objective-C++
Delegate
Blocks

シンプルな方法で一回きりdelegateをcallbackに

More than 3 years have passed since last update.

巷は結構Swift化したようですが、C++と連携したい非UI部分はどうしてもObjective-C++のままもうしばらく残ると予想しています。Objective-Cのdelegateパターンが苦手だったのでC++の単なるcallback (block)形式に持ち込む方法を考えました。ポイントは2重callbackにすることと、意図的に循環参照に持ち込んでオブジェクトを保持し、callbackで循環を断ち切ること。割とシンプルだと思います。

AVAudioPlayerを使う例で紹介します。もちろん任意のdelegateを使ったクラスに使える仕組みだと思います。


やりたいこと

単純に以下の関数形式でオーディオファイルを再生できるようになりたいです。これならC++クラスからでもすんなり呼べるくらいに内情を完全に隠蔽できています。本来のdelegate方式だとAVAudioPlayerDelegateの継承が必要なのでC++クラスからは事実上呼べません。

#include <functional>

using AudioPlayCallback = std::function<void(bool successfully)>;
void playAudio(NSString* absPath, AudioPlayCallback callback);


blockを使ってdelegateを隠蔽するクラスを作る

以下のようにdelegateをコールバックに変換する受け皿を用意します。


  • delegateであるaudioPlayerDidFinishPlayingの中身で普通のcallbackを呼び直します。

  • この手のコードを書くとき、循環参照になってオブジェクトが死ななくなるミスを起こしがちなので、deallocメソッドをoverrideしてbreakを貼れるようにしておくとデバッグが容易になります。

  • regainメソッドについては下で説明します。

#import <Foundation/Foundation.h>

#import <AVFoundation/AVFoundation.h>
#import "audioplay.h" // 上のプロトタイプ宣言が書いてある

@interface AVAudioPlayerBlocks : NSObject<AVAudioPlayerDelegate>
@end

@implementation AVAudioPlayerBlocks{
AVAudioPlayer *_player;
AudioPlayCallback _callback;
}

-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag{
_callback((bool)flag);
_player = nil;
}

-(bool)playAudio: (NSString*) absPath callback:(AudioPlayCallback) callback{
_callback = callback;
NSError *error = nil;
NSURL *url = [[NSURL alloc] initFileURLWithPath:absPath];

_player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];

if ( error != nil ){
NSLog(@"Error %@", [error localizedDescription]);
return nil;
} else {
[_player setDelegate: self];
[_player play];
return _player;
}
}

- (AudioPlayCallback) regain{
auto callback = _callback;
_callback = nil;
return callback;
}

- (void)dealloc{
//ここにbreakを貼ればdeleteタイミングが分かる
}
@end


オブジェクトの寿命をコントロールしながらcallbackする

上で作ったAVAudioPlayerBlocksをうまく使ってplayAudio関数を実装します。


  • 安直にAVAudioPlayerBlocksを関数ローカルに作ると、関数終了後にオブジェクトがすぐ死んでしまい、callbackまで待てません。そこでAVAudioPlayerBlocksに渡すcallbackをC++のlambdaで作る際にオブジェクトをキャプチャしておきます。

  • lambdaで一時オブジェクトblockをキャプチャして延命しましたが、この後lambda自体がblockの_callbackメンバ変数にセットするので見事に循環参照が出来上がってます。そこで、lambda関数の最後で自身をblockから取り返すregainメソッドを呼んでおきます。これで無事にメモリ解放されるようになりました。

void playAudio(NSString* absPath, AudioPlayCallback callback){

AVAudioPlayerBlocks *block = [[AVAudioPlayerBlocks alloc] init];

// blockをキャプチャして生き残らせる(ただしこのstd::functionをblockに持たせるので循環参照になる)
auto callback_local = [block, callback](bool flag) -> void{
callback(flag);
auto _ = [block regain]; // 渡したstd::functionを回収して循環参照を断ち切る(捨てるだけだとこの関数実行中に自身が消滅するので、受け取るのは必須)
};

[block playAudio:absPath callback:callback_local];
}