如何使用Xamarin或Dot42在C#中使用async/await实现Android回调?

12

如何在使用Xamarin开发安卓应用时,利用C#的async/await实现回调?与安卓标准Java编程相比,这有何不同?

2个回答

17
使用Xamarin for Android 4.7版本(目前仍处于公开beta阶段),我们可以使用.NET 4.5特性来实现“async”方法和对它们的“await”调用。如果在Java中需要任何回调,它总是让我感到困扰,因为函数中代码的逻辑流被打断了,当回调返回时,必须在下一个函数中继续代码。考虑以下情况:我想收集Android设备上所有可用的TextToSpeech引擎列表,然后询问每个引擎安装了哪些语言。我编写了一个名为“TTS Setup”的小活动,向用户呈现两个选择框(“旋转器”),一个列出此设备上所有TTS引擎支持的所有语言。下面的另一个框列出第一个框中选择的语言的所有可用声音,同样来自所有可用的TTS引擎。

TtsSetup screen capture, first spinner lists all TTS languages, second all voices After choosing English and clicking the voices spinner

理想情况下,这个活动的所有初始化工作应该在一个函数中完成,例如在onCreate()中。但是,在标准Java编程中不可能实现,因为:
这需要两个“干扰性”回调——首先初始化TTS引擎——只有在调用onInit()回调时它才能完全运行。然后,当我们有了一个初始化的TTS对象,我们需要向其发送一个“android.speech.tts.engine.CHECK_TTS_DATA”意图,并再次在我们的活动回调onActivityResult()中等待结果。这又打乱了逻辑流程。如果我们正在遍历可用的TTS引擎列表,即使这个迭代的循环计数器不能是单个函数中的局部变量,而必须成为一个私有类成员。我认为这相当混乱。
下面我将尝试概述实现此目的所需的Java代码。
混乱的Java代码收集所有TTS引擎及其支持的语音
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#与异步方法
首先,为了简化事情,我创建了一个基类来提供CreateTtsAsync()以避免Java代码中的DISRUPTION 1,并提供了StartActivityForResultAsync()以避免DISRUPTION 2方法。
// 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引擎,并查询可用的语言和声音,这样可以避免在三个不同的函数中运行循环。
// 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.
    ...
}

Java项目和C#项目,使用带有Xamarin for Android插件的Visual Studio 2012,现已发布在GitHub上。

https://github.com/gregko/TtsSetup_C_sharp
https://github.com/gregko/TtsSetup_Java

你觉得呢?

使用Xamarin为Android免费试用版学习这个很有趣,但是购买Xamarin许可证是否值得花费这笔钱,以及每次创建Google Play Store的APK文件时额外增加约5MB的Mono运行时包的负担,这是否值得呢?我希望Google能够提供Mono虚拟机作为标准系统组件,并与Java/Dalvik享有平等的权利。

P.S. 我看到这篇文章的投票结果,发现它也收到了一些反对票。猜想这些反对票可能来自Java爱好者! :) 同时,欢迎提出改进我的Java代码的建议。

P.S. 2 - 在Google+上与另一位开发人员交流了这段代码, 这帮助我更好地理解了async/await的实际情况。

更新8/29/2013

Dot42在他们的C#产品中也实现了'async/await'关键字用于Android,我尝试将此测试项目移植到该平台。我的第一次尝试由于Dot42库中的某个崩溃而失败,我正在等待(当然是异步地 :))他们的修复,但是当涉及到从Android活动事件处理程序中进行'async'调用时,他们观察到并实现了一个有趣的事实:
默认情况下,如果在活动事件处理程序内部等待长时间的异步操作的结果时发生某些活动"配置更改",例如方向更改,则系统会销毁并重新创建活动。如果在这样的更改之后从'async'操作返回到事件处理程序代码的中间,则活动的'this'对象不再有效,如果您存储了指向此活动中控件的对象,则它们也无效(它们指向旧的、现在已被销毁的对象)。
我在我的生产代码中(使用Java)遇到了这个问题,并通过配置活动以在此类事件上得到通知而不是被销毁和重新创建来解决了这个问题。Dot42提供了另一种有趣的选择:
var data = await webClient
             .DownloadDataTaskAsync(myImageUrl)
             .ConfigureAwait(this);

.configureAwait(this)扩展方法(再加上在activity OnCreate()方法中设置的一行代码),确保当您从await返回时,即使发生配置更改,'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#代码都应更新以考虑这样的周期,目前只有Dot42可以做到这一点,而Xamarin则不行。我将根据其他SO成员的要求更新该代码,但目前似乎这篇文章并没有得到太多关注。


2
等待任务运行(await Task.Run(delegate { Event1.WaitOne(); });)我认为这真的很丑陋。更好的解决方案是使用 TaskCompletionSource。而且所有那些空的 catch 也不是一个好习惯。 - svick
太好了,谢谢@svick!在我的免费Xamarin试用期内,我将尝试使用TaskCompletionSource来改进我的代码。我仍在学习C#,Java和Android,所以所有的评论都对我有帮助。在某些地方,空catches确实不重要,如果有错误的TTS引擎或第三方代码崩溃,我仍然希望继续循环。但是你说得对,至少将异常输出到Debug日志中,以便知道发生了什么。 - gregko
@GregK。只需查看TCS的文档即可。它是一个非常简单的类;通常仅查看其API就足以了解如何使用它。 - Servy
@Servy - 我找了又找,还是搞不明白。所有关于TCS的例子都是关于启动新任务、新线程等等的。这个问题不同,在创建TTS对象后,我们必须在同一个(UI)线程上等待直到OnInit()回调被调用。我认为在今天进行的更正之后,将异步调用隔离在基类中,代码已经干净简单了。 - gregko
3
你可以使用TCS类创建一个任务来表示事件准备就绪,但不会阻塞线程。然后你可以使用await等待该任务。与设置自动重置事件不同的是,你只需设置TCS的结果即可。这就是svick建议的方法。这是使用“Task”模型进行信号传递的适当方式。使用自动重置事件是为了非异步阻塞。 - Servy
显示剩余2条评论

0
我使用以下模型将回调转换为异步:
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();
    }
}

这非常灵活,可以在活动中使用,在回调对象内等等。

请注意,错误使用会创建死锁等问题。锁定可防止超时后结果更改。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接