Xamarin for Android で async/await を使用して C# でコールバックを実装するにはどうすればよいですか? また、これは Android 向けの標準的な Java プログラミングと比べてどうですか?
2 に答える
Xamarin for Android バージョン 4.7 では、この記事の執筆時点ではまだ公開されているベータ版であり、.NET 4.5 機能を使用して「非同期」メソッドを実装し、それらへの「待機」呼び出しを行うことができます。Javaでコールバックが必要な場合、関数内のコードの論理フローが中断され、コールバックが返されたときに次の関数でコードを続行する必要があることに常に悩まされていました。次のシナリオを検討してください。
Android デバイスで使用可能なすべての TextToSpeech エンジンのリストを収集し、それぞれにインストールされている言語を確認したいと考えています。私が書いた小さな「TTS セットアップ」アクティビティは、ユーザーに 2 つの選択ボックス (「スピナー」) を提示します。1 つには、このデバイスのすべての TTS エンジンがサポートするすべての言語がリストされています。下のもう 1 つのボックスには、最初のボックスで選択した言語で利用可能なすべての音声が一覧表示されます。これも、利用可能なすべての TTS エンジンからのものです。
このアクティビティのすべての初期化は、onCreate() などの 1 つの関数で行うのが理想的です。以下の理由により、標準の Java プログラミングでは不可能です。
これには、2 つの「破壊的な」コールバックが必要です。最初に TTS エンジンを初期化します。完全に機能するのは、onInit() がコールバックされた場合のみです。次に、TTS オブジェクトを初期化したら、「android.speech.tts.engine.CHECK_TTS_DATA」インテントを送信し、アクティビティ コールバック onActivityResult() で再び結果が返されるのを待つ必要があります。ロジック フローの別の中断。利用可能な TTS エンジンのリストを繰り返し処理している場合、この繰り返しのループ カウンターでさえ、単一の関数のローカル変数にすることはできず、代わりにプライベート クラス メンバーにする必要があります。私の意見ではかなり面倒です。
以下では、これを実現するために必要な Java コードの概要を説明します。
すべての TTS エンジンとそのサポートの声を収集するための厄介な Java コード
public class VoiceSelector extends Activity {
private TextToSpeech myTts;
private int myEngineIndex; // loop counter when initializing TTS engines
// Called from onCreate to colled all languages and voices from all TTS engines, initialize the spinners
private void getEnginesAndLangs() {
myTts = new TextToSpeech(AndyUtil.getAppContext(), null);
List<EngineInfo> engines;
engines = myTts.getEngines(); // at least we can get the list of engines without initializing myTts object…
try { myTts.shutdown(); } catch (Exception e) {};
myTts = null;
myEngineIndex = 0; // Initialize the loop iterating through all TTS engines
if (engines.size() > 0) {
for (EngineInfo ei : engines)
allEngines.add(new EngLang(ei));
myTts = new TextToSpeech(AndyUtil.getAppContext(), ttsInit, allEngines.get(myEngineIndex).name());
// DISRUPTION 1: we can’t continue here, must wait until ttsInit callback returns, see below
}
}
private TextToSpeech.OnInitListener ttsInit = new TextToSpeech.OnInitListener() {
@Override
public void onInit(int status) {
if (myEngineIndex < allEngines.size()) {
if (status == TextToSpeech.SUCCESS) {
// Ask a TTS engine which voices it currently has installed
EngLang el = allEngines.get(myEngineIndex);
Intent in = new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
in = in.setPackage(el.ei.name); // set engine package name
try {
startActivityForResult(in, LANG_REQUEST); // goes to onActivityResult()
// DISRUPTION 2: we can’t continue here, must wait for onActivityResult()…
} catch (Exception e) { // ActivityNotFoundException, also got SecurityException from com.turboled
if (myTts != null) try {
myTts.shutdown();
} catch (Exception ee) {}
if (++myEngineIndex < allEngines.size()) {
// If our loop was not finished and exception happened with one engine,
// we need this call here to continue looping…
myTts = new TextToSpeech(AndyUtil.getAppContext(), ttsInit, allEngines.get(myEngineIndex).name());
} else {
completeSetup();
}
}
}
} else
completeSetup();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == LANG_REQUEST) {
// We return here after sending ACTION_CHECK_TTS_DATA intent to a TTS engine
// Get a list of voices supported by the given TTS engine
if (data != null) {
ArrayList<String> voices = data.getStringArrayListExtra(TextToSpeech.Engine.EXTRA_AVAILABLE_VOICES);
// … do something with this list to save it for later use
}
if (myTts != null) try {
myTts.shutdown();
} catch (Exception e) {}
if (++myEngineIndex < allEngines.size()) {
// and now, continue looping through engines list…
myTts = new TextToSpeech(AndyUtil.getAppContext(), ttsInit, allEngines.get(myEngineIndex).name());
} else {
completeSetup();
}
}
}
ttsInit コールバックで新しい TTS オブジェクトを作成する行は、例外やその他のエラーが発生した場合に利用可能なすべてのエンジンをループし続けるために 3 回繰り返す必要があることに注意してください。上記はもう少しうまく書けるかもしれません。たとえば、内部クラスを作成して、ループ コードをローカライズし、ループ カウンターを少なくともメイン クラスのメンバーにならないようにできると考えましたが、それでも面倒です。この Java コードを改善するための提案を歓迎します。
はるかにクリーンなソリューション: 非同期メソッドを使用した Xamarin C#
まず、簡単にするために、上記の Java コードで DISRUPTION 1 を回避するために CreateTtsAsync() を提供するアクティビティの基本クラスを作成し、DISRUPTION 2 メソッドを回避するために StartActivityForResultAsync() を作成しました。
// Base class for an activity to create an initialized TextToSpeech
// object asynchronously, and starting intents for result asynchronously,
// awaiting their result. Could be used for other purposes too, remove TTS
// stuff if you only need StartActivityForResultAsync(), or add other
// async operations in a similar manner.
public class TtsAsyncActivity : Activity, TextToSpeech.IOnInitListener
{
protected const String TAG = "TtsSetup";
private int _requestWanted = 0;
private TaskCompletionSource<Java.Lang.Object> _tcs;
// Creates TTS object and waits until it's initialized. Returns initialized object,
// or null if error.
protected async Task<TextToSpeech> CreateTtsAsync(Context context, String engName)
{
_tcs = new TaskCompletionSource<Java.Lang.Object>();
var tts = new TextToSpeech(context, this, engName);
if ((int)await _tcs.Task != (int)OperationResult.Success)
{
Log.Debug(TAG, "Engine: " + engName + " failed to initialize.");
tts = null;
}
_tcs = null;
return tts;
}
// Starts activity for results and waits for this result. Calling function may
// inspect _lastData private member to get this result, or null if any error.
// For sure, it could be written better to avoid class-wide _lastData member...
protected async Task<Intent> StartActivityForResultAsync(Intent intent, int requestCode)
{
Intent data = null;
try
{
_tcs = new TaskCompletionSource<Java.Lang.Object>();
_requestWanted = requestCode;
StartActivityForResult(intent, requestCode);
// possible exceptions: ActivityNotFoundException, also got SecurityException from com.turboled
data = (Intent) await _tcs.Task;
}
catch (Exception e)
{
Log.Debug(TAG, "StartActivityForResult() exception: " + e);
}
_tcs = null;
return data;
}
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
if (requestCode == _requestWanted)
{
_tcs.SetResult(data);
}
}
void TextToSpeech.IOnInitListener.OnInit(OperationResult status)
{
Log.Debug(TAG, "OnInit() status = " + status);
_tcs.SetResult(new Java.Lang.Integer((int)status));
}
}
これで、TTS エンジンをループして 1 つの関数内で使用可能な言語と音声をクエリするコード全体を記述できるようになり、3 つの異なる関数全体でループが実行されるのを回避できます。
// Method of public class TestVoiceAsync : TtsAsyncActivity
private async void GetEnginesAndLangsAsync()
{
_tts = new TextToSpeech(this, null);
IList<TextToSpeech.EngineInfo> engines = _tts.Engines;
try
{
_tts.Shutdown();
}
catch { /* don't care */ }
foreach (TextToSpeech.EngineInfo ei in engines)
{
Log.Debug(TAG, "Trying to create TTS Engine: " + ei.Name);
_tts = await CreateTtsAsync(this, ei.Name);
// DISRUPTION 1 from Java code eliminated, we simply await TTS engine initialization here.
if (_tts != null)
{
var el = new EngLang(ei);
_allEngines.Add(el);
Log.Debug(TAG, "Engine: " + ei.Name + " initialized correctly.");
var intent = new Intent(TextToSpeech.Engine.ActionCheckTtsData);
intent = intent.SetPackage(el.Ei.Name);
Intent data = await StartActivityForResultAsync(intent, LANG_REQUEST);
// DISTRUPTION 2 from Java code eliminated, we simply await until the result returns.
try
{
// don't care if lastData or voices comes out null, just catch exception and continue
IList<String> voices = data.GetStringArrayListExtra(TextToSpeech.Engine.ExtraAvailableVoices);
Log.Debug(TAG, "Listing voices for " + el.Name() + " (" + el.Label() + "):");
foreach (String s in voices)
{
el.AddVoice(s);
Log.Debug(TAG, "- " + s);
}
}
catch (Exception e)
{
Log.Debug(TAG, "Engine " + el.Name() + " listing voices exception: " + e);
}
try
{
_tts.Shutdown();
}
catch { /* don't care */ }
_tts = null;
}
}
// At this point we have all the data needed to initialize our language
// and voice selector spinners, can complete the activity setup.
...
}
Visual Studio 2012 と Xamarin for Android アドオンを使用する Java プロジェクトと C# プロジェクトが GitHub に投稿されました。
https://github.com/gregko/TtsSetup_C_sharp
https://github.com/gregko/TtsSetup_Java
どう思いますか?
Xamarin for Android の無料試用版でこれを行う方法を学ぶのは楽しかったですが、Xamarin のライセンスに $$ を支払う価値があるのでしょうか。次に、Google Play ストア用に作成する各 APK の追加の重みで、Mono ランタイムで約 5 MB を配布する必要があります。 ? Java/Dalvik と同等の権利で、Google が Mono 仮想マシンを標準システム コンポーネントとして提供してくれることを望みます。
PS この記事の投票を確認しましたが、反対票もいくつかあることがわかりました。彼らはJava愛好家から来ているに違いないと思います! :) 繰り返しになりますが、私の Java コードを改善する方法についての提案も歓迎します。
PS 2 - Google+ で別の開発者とこのコードについて興味深いやり取りをしたことで、async/await で実際に何が起こるかをよりよく理解することができました。
2013 年 8 月 29 日更新
Dot42 も Android 向けの C# 製品に async/await キーワードを実装していたので、このテスト プロジェクトに移植してみました。私の最初の試みは、Dot42 ライブラリのどこかでクラッシュして失敗し、(もちろん非同期で :)) それらからの修正を待っていましたが、Android アクティビティ イベント ハンドラからの「非同期」呼び出しに関して、彼らが観察して実装した興味深い事実があります。 :
デフォルトでは、向きの変更など、アクティビティ イベント ハンドラー内で長時間の非同期操作の結果を待っている間にアクティビティの「構成変更」がある場合、アクティビティはシステムによって破棄され、再作成されます。このような変更の後、'async' 操作からイベント ハンドラー コードの途中に戻った場合、アクティビティの 'this' オブジェクトは無効になり、このアクティビティ内のコントロールを指すオブジェクトを格納した場合、それらも有効になります。無効です (それらは古い、現在は破棄されたオブジェクトを指しています)。
私は本番コード(Java)でこの問題に遭遇し、そのようなイベントで破棄および再作成されないようにアクティビティを通知するように構成することで回避しました。Dot42 には、非常に興味深い別の代替手段が付属していました。
var data = await webClient
.DownloadDataTaskAsync(myImageUrl)
.ConfigureAwait(this);
.configureAwait(this) 拡張機能 (アクティビティ OnCreate() 内のセットアップ用のコード行をもう 1 つ追加) により、「this」オブジェクトがまだ有効であり、待機から戻ったときにアクティビティの現在のインスタンスを指していることが保証されます。変化が起こります。Android UI コードで async/await を使い始めるときは、少なくともこの難しさを認識しておくとよいと思います。Dot42 ブログ ( http://blog.dot42.com/2013/08/how- ) でこれに関する詳細な記事を参照してください。 we-implemented-asyncawait.html?showComment=1377758029972#c6022797613553604525
Dot42 クラッシュの更新
私が経験した async/await クラッシュは Dot42 で修正され、うまく機能します。実際には、アクティビティの破棄/再作成サイクル間の Dot42 での「this」オブジェクトのスマートな処理により、Xamarin コードよりも優れています。上記の C# コードはすべて、このようなサイクルを考慮して更新する必要があります。現在、Xamarin では不可能であり、Dot42 でのみ可能です。他の SO メンバーからの要求に応じてそのコードを更新しますが、今のところ、この記事はあまり注目されていないようです。
次のモデルを使用して、コールバックを非同期に変換します。
SemaphoreSlim ss = new SemaphoreSlim(0);
int result = -1;
public async Task Method() {
MethodWhichResultsInCallBack()
await ss.WaitAsync(10000); // Timeout prevents deadlock on failed cb
lock(ss) {
// do something with result
}
}
public void CallBack(int _result) {
lock(ss) {
result = _result;
ss.Release();
}
}
これは非常に柔軟で、コールバック オブジェクトなどの内部のアクティビティで使用できます。
これを間違った方法で使用すると、デッドロックなどが発生することに注意してください。ロックは、タイムアウトが切れた後に結果が変更されるのを防ぎます。