如何使用Xamarin for Android使用async / await在C#中实现回调?这与Android的标准Java编程相比如何?
答案 0 :(得分:17)
使用Xamarin for Android版本4.7,在撰写本文时仍处于公开测试阶段,我们可能会使用.NET 4.5功能来实现“async”#39;方法和'等待'打电话给他们。它一直困扰着我,如果Java中需要任何回调,函数中的代码逻辑流程就会中断,你必须在回调返回时继续下一个函数中的代码。请考虑以下情况:
我想在Android设备上收集所有可用TextToSpeech引擎的列表,然后询问每个引擎安装了哪种语言。我写的小“TTS设置”活动向用户呈现了两个选择框(“旋转器”),一个列出了该设备上所有TTS引擎支持的所有语言。下面的另一个框列出了第一个框中所选语言的所有可用语音,同样来自所有可用的TTS引擎。
理想情况下,此活动的所有初始化都应在一个函数中进行,例如在onCreate()中。标准Java编程不可能,因为:
这需要两个“破坏性”回调 - 首先初始化TTS引擎 - 只有在回调onInit()时才能完全运行。然后,当我们有一个初始化的TTS对象时,我们需要向它发送一个“android.speech.tts.engine.CHECK_TTS_DATA”意图,并在我们的活动回调onActivityResult()中再次等待它。逻辑流程的另一个中断。如果我们迭代可用的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代码欢迎。
首先,为了简化操作,我为我的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.
...
}
Java项目和C#项目,使用Visual Studio 2012和Xamarin for Android附加组件,现在发布在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平等权利的标准系统组件。
P.S。审查了对这篇文章的投票,我发现它也得到了一些下选票。猜猜他们一定是来自Java爱好者! :)同样,欢迎提出如何改进我的Java代码的建议。
P.S。 2 - 有一个有趣的exchange on this code with another developer on Google+,帮助我更好地理解async / await实际发生了什么。
Dot42还实施了#as; / await&#39; Android版C#产品中的关键字,我尝试移植到这个测试项目。我的第一次尝试失败了,在Dot42库的某个地方发生了崩溃,等待(异步,当然:))来修复它们,但是有一个有趣的事实,他们观察并实现了“async&#39;来自Android活动事件处理程序的调用:
默认情况下,如果有某些活动&#34;配置更改&#34;当你在活动事件处理程序中等待长异步操作的结果时,例如,方向改变,活动被系统破坏并重新创建。如果在此类更改后您从“异步”返回操作到事件处理程序代码的中间,&#39;这个&#39;活动的对象不再有效,如果您在此活动中存储了一些指向控件的对象,它们也是无效的(它们指向旧的,现在已销毁的对象)。
我在生产代码(在Java中)遇到了这个问题,并通过配置要通知的活动来解决此问题,而不是在此类事件上销毁和重新创建。 Dot42带来了另一种选择,非常有趣:
var data = await webClient
.DownloadDataTaskAsync(myImageUrl)
.ConfigureAwait(this);
.configureAwait(this)扩展(加上活动OnCreate()中的另外一个代码行来设置东西)确保你的“这个”&#39;对象仍然有效,指向当前的活动实例,当您从等待返回时,即使发生配置更改。我认为至少要注意这个难点,当你开始在Android UI代码中使用async / await时,请参阅Dot42博客上的更多文章:http://blog.dot42.com/2013/08/how-we-implemented-asyncawait.html?showComment=1377758029972#c6022797613553604525
我遇到的异步/等待崩溃现在已在Dot42中修复,并且效果很好。实际上,由于智能处理这个&#39;而且比Xamarin代码更好。 Dot42中的对象在活动破坏/重新创建周期之间。我应该更新上面的所有C#代码以考虑这样的周期,目前在Xamarin中不可能只在Dot42中。我会根据其他SO成员的要求更新该代码,因为现在看来这篇文章并没有引起太多关注。
答案 1 :(得分: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();
}
}
这非常灵活,可以在回调对象ect中的Activities中使用。
小心,使用错误的方法会造成死锁等。如果超时用完,锁定会阻止结果更改。