【问题标题】:How to implement Android callbacks in C# using async/await with Xamarin or Dot42?如何使用带有 Xamarin 或 Dot42 的 async/await 在 C# 中实现 Android 回调?
【发布时间】:2013-05-27 20:00:23
【问题描述】:

如何在 C# 中使用 async/await 和 Xamarin for Android 实现回调?这与 Android 的标准 Java 编程相比如何?

【问题讨论】:

    标签: c# android async-await xamarin dot42


    【解决方案1】:

    使用 Xamarin for Android 4.7 版,在撰写本文时仍处于公开可用的测试版中,我们可以使用 .NET 4.5 功能来实现“异步”方法和对它们的“等待”调用。一直困扰着我的是,如果在 Java 中需要任何回调,函数中的逻辑代码流被中断,当回调返回时,您必须在下一个函数中继续代码。考虑这种情况:

    我想收集 Android 设备上所有可用的 TextToSpeech 引擎的列表,然后询问每个引擎安装了哪些语言。我编写的小“TTS 设置”活动向用户展示了两个选择框(“微调器”),其中一个列出了该设备上所有 TTS 引擎支持的所有语言。下面的另一个框列出了第一个框中所选语言的所有可用语音,同样来自所有可用的 TTS 引擎。

    理想情况下,此活动的所有初始化都应在一个函数中进行,例如在 onCreate() 中。使用标准 Java 编程是不可能的,因为:

    这需要两个“破坏性”回调——首先是初始化 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#

    首先,为了简化事情,我为我的 Activity 创建了一个基类,它提供 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.
        ...
    }
    

    使用 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 许可证支付 $$,然后您在 Mono 运行时中为 Google Play 商店创建的每个 APK 的额外重量约为 5 MB,我们必须分发?我希望 Google 提供 Mono 虚拟机作为与 Java/Dalvik 同等权利的标准系统组件。

    附注回顾了这篇文章的投票,我发现它也得到了一些反对票。猜猜他们一定来自 Java 爱好者! :) 同样,也欢迎有关如何改进我的 Java 代码的建议。

    附注2 - 有一个有趣的exchange on this code with another developer on Google+,帮助我更好地理解 async/await 实际发生的情况。

    2013 年 8 月 29 日更新

    Dot42 还在他们的 C# 产品中为 Android 实现了“async/await”关键字,我尝试将这个测试项目移植到它。我的第一次尝试失败了,在 Dot42 库的某个地方发生了崩溃,等待(当然是异步的 :) )从他们那里得到修复,但是当谈到来自 Android 活动事件处理程序的“异步”调用时,他们观察到并实现了一个有趣的事实:

    默认情况下,如果有一些活动“配置更改”,而您正在等待活动事件处理程序中长时间异步操作的结果,例如方向改变,活动被系统销毁并重新创建。如果在进行此类更改后,您从“异步”操作返回到事件处理程序代码的中间,则活动的“this”对象不再有效,并且如果您在此活动中存储了一些指向控件的对象,它们也是无效(它们指向旧的、现已销毁的对象)。

    我在我的生产代码(Java)中遇到了这个问题,并通过配置要通知的活动来解决这个问题,而不是在此类事件上销毁和重新创建。 Dot42 带来了另一种选择,非常有趣:

    var data = await webClient
                 .DownloadDataTaskAsync(myImageUrl)
                 .ConfigureAwait(this);
    

    .configureAwait(this) 扩展(加上活动 OnCreate() 中的另一行代码来设置事物)确保您的“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 成员的要求更新该代码,目前看来这篇文章没有引起太多关注。

    【讨论】:

    • await Task.Run(delegate { Event1.WaitOne(); }); 我觉得这真的很难看。一个更好的解决方案是使用TaskCompletionSource。所有那些空的catches 也不是一个好习惯。
    • 太好了,谢谢@svick!将试验 TaskCompetionSource 并改进我的代码,而我的免费 Xamarin 试用期将持续。我还在学习 C#、Java 和 Android,所以所有 cmets 都对我有帮助。空捕获 - 在某些地方,我真的不在乎是否有错误的 TTS 引擎,毕竟是第 3 方代码,崩溃,并且仍然想继续我的循环。但你是对的,至少将异常输出到 Debug 日志以了解发生了什么会很好。
    • @svick - 希望有一个快速的代码示例展示我如何在我的应用程序上下文中使用 TaskCompletionSource,等待回调。请注意,我们不能在等待回调返回时阻塞当前线程。谢谢!
    • @GregK。只需查看 TCS 的文档即可。老实说,这是一个非常简单的课程;通常只看它的 API 就足以弄清楚如何使用它。
    • @GregK。您使用 TCS 类创建一个任务,该任务表示事件何时准备就绪,但不会阻塞线程。然后你可以await那个任务。然后,您只需设置 TCS 的结果,而不是设置自动重置事件。这就是 svick 的建议。这是使用Task 模型发出信号的适当方式。使用自动重置事件是为非异步阻塞而设计的。
    【解决方案2】:

    我使用以下模型将回调转换为异步:

    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();
        }
    }
    

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

    小心,使用错误的方式会造成死锁等。如果超时用完,锁定会阻止结果更改。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-11-12
      • 2017-11-20
      • 1970-01-01
      • 1970-01-01
      • 2016-02-18
      • 2022-11-18
      • 2021-05-08
      相关资源
      最近更新 更多