通过访问 UI 线程启动后台任务

Posted

技术标签:

【中文标题】通过访问 UI 线程启动后台任务【英文标题】:Starting background task with access to the UI thread 【发布时间】:2017-01-26 11:43:56 【问题描述】:

我一直在拨打我在 xamarin.android 应用程序中遇到的一个来回问题。我将尝试提供尽可能详细的信息以明确我的问题。

当用户单击按钮时,我想要执行一个长时间运行的任务。我试图运行的任务是下载文件,并将其转换为其他格式。用户在启动下载时知道任务,并希望它完成。我已经编写并测试了代码,我只需要正确放置它。 一些必须被排除的限制:

下载和转换任务是async,因为我使用的库之一强制执行异步方法

应用程序关闭时任务不得关闭,因为用户正在等待下载完成。

其中一种方法必须从 UI 线程运行(这就是文档所说的),但我相信该方法将足以接收 UI 上下文,例如一些活动背景。如果我以服务作为上下文运行命令,我会收到以下错误Java.Lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare() service,所以我假设我需要 MainActivity 上下文

我在使用 Service/Intentservice/AsyncTask 之间进行了相当大的争论,并且遇到了几个问题,只有在我实际在手机上进行测试时才会发生这种情况

Service/Intentservice,无法获取UI线程的上下文,我尝试使用以下方法:

VideoConverter vc = new VideoConverter();
Java.IO.File convertedFile = await vc.ConvertFile(Application.Context,
                                    outPutFile,
                                    logFunction,
                                    progressFunction);

但我仍然遇到同样的错误。 同样,每当我报告进度时,我都会用图标重建通知,然后出现这个奇怪的警告:

09-18 22:47:12.276 W/IconPackHelper(28042): Unable to cache icon /data/system/theme/icons/com.App_7f020002_0.png
09-18 22:47:12.362 W/IconPackHelper(28042): Unable to cache icon /data/system/theme/icons/com.App_7f020002_0.png
09-18 22:47:12.431 W/IconPackHelper(28042): Unable to cache icon /data/system/theme/icons/com.App_7f020002_0.png
09-18 22:47:12.503 W/IconPackHelper(28042): Unable to cache icon /data/system/theme/icons/com.App_7f020002_0.png

每当我构建通知时,这是否重要?

总而言之,我应该使用什么来满足我的需求? AsyncTask 似乎是最好的选择,但由于我使用的是 xamarin,我可以使用 await/async,我应该如何构建我的代码?运行活动中的所有代码或将其分解为更多的化合物? 为什么这个错误只出现在我的手机上而不是模拟器上?

非常感谢您的帮助!干杯!

编辑

根据我收到的反馈,我认为最好实际使用将在前台运行的服务,我已经实现了这种方法,但我遇到了一个错误,我提供了更多细节。

这里我从服务内部调用我的异步方法,因为我不能用异步覆盖,我必须使用 Task.Factory 来运行异步函数

 public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId)
 

     Task.Factory.StartNew(async () => 
         await downloadAndConvert(id);
     );
 

这是对一个类的调用,该类将提供特定的转换。注意上下文。

 async downloadAndConvert(string id)
      ...
      VideoConverter vc = new VideoConverter();
      Java.IO.File convertedFile = await vc.ConvertFile(Application.Context,
                                        outPutFile,
                                        logFunction,
                                        progressFunction);
      // Error is firing here

      ...
 

这里是VideoConverter类对库的调用,接下来注意上下文的to。

 public async Task<File> ConvertFile(
        Context contex,
        File inputFile,
        Action<string> logger = null,
        Action<int, int> onProgress = null)
 

 ...

 await FFMpeg.Xamarin.FFMpegLibrary.Run(
            contex,
            cmdParams
            , (s) =>  ... 
            );

   ...
 

由于ffmpeg xamarin library 是开源的,我已经能够检索导致错误的行。

// lets try to download
var dlg = new ProgressDialog(context);

所以错误是库尝试使用传递的上下文创建一个ProgressDialog,但由于只有一个活动上下文能够启动 ProgressDialog,它对我的​​服务/应用程序上下文的破坏。

FFmpeg 库中的调用是为第一次运行打开一个ProgressDialog,因为它会下载库并对其进行初始化。我不想更改库代码,因为它很容易通过 nuget 获得,而我不这样做'我真的不想自己建造图书馆。 如果这是最优雅的解决方案,我会这样做,我可以将ProgressDialogs 切换为通知,它会正常工作。

这是我遵循的完整堆栈跟踪:

Java.Lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in /Users/builder/data/lanes/3511/f4db8a57/source/mono/mcs/class/referencesource/mscorlib/system/runtime/exceptionservices/exceptionservicescommon.cs:143 
  at Java.Interop.JniEnvironment+InstanceMethods.CallNonvirtualVoidMethod (Java.Interop.JniObjectReference instance, Java.Interop.JniObjectReference type, Java.Interop.JniMethodInfo method, Java.Interop.JniArgumentValue* args) [0x000a7] in /Users/builder/data/lanes/3511/0e59c362/source/Java.Interop/src/Java.Interop/Java.Interop/JniEnvironment.g.cs:12083 
  at Java.Interop.JniPeerMembers+JniInstanceMethods.FinishCreateInstance (System.String constructorSignature, Java.Interop.IJavaPeerable self, Java.Interop.JniArgumentValue* parameters) [0x00060] in /Users/builder/data/lanes/3511/0e59c362/source/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:148 
  at Android.App.ProgressDialog..ctor (Android.Content.Context context) [0x00078] in /Users/builder/data/lanes/3511/0e59c362/source/monodroid/src/Mono.Android/platforms/android-24/src/generated/Android.App.ProgressDialog.cs:48 
  at FFMpeg.Xamarin.FFMpegLibrary+<Init>d__7.MoveNext () [0x00179] in D:\git
s-github\xamarin-ffmpeg-android\FFMpeg.Xamarin\FFMpegLibrary.cs:94 
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in /Users/builder/data/lanes/3511/f4db8a57/source/mono/mcs/class/referencesource/mscorlib/system/runtime/exceptionservices/exceptionservicescommon.cs:143 
  at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) [0x00047] in /Users/builder/data/lanes/3511/f4db8a57/source/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:187 
  at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) [0x0002e] in /Users/builder/data/lanes/3511/f4db8a57/source/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:156 
  at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) [0x0000b] in /Users/builder/data/lanes/3511/f4db8a57/source/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:128 
  at System.Runtime.CompilerServices.TaskAwaiter.GetResult () [0x00000] in /Users/builder/data/lanes/3511/f4db8a57/source/mono/mcs/class/referencesource/mscorlib/system/runtime/compilerservices/TaskAwaiter.cs:113 
  at FFMpeg.Xamarin.FFMpegLibrary+<Run>d__8.MoveNext () [0x000d0] in D:\git
s-github\xamarin-ffmpeg-android\FFMpeg.Xamarin\FFMpegLibrary.cs:191 
  --- End of managed Java.Lang.RuntimeException stack trace ---
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
    at android.os.Handler.<init>(Handler.java:200)
    at android.os.Handler.<init>(Handler.java:114)
    at android.app.Dialog.<init>(Dialog.java:119)
    at android.app.AlertDialog.<init>(AlertDialog.java:200)
    at android.app.AlertDialog.<init>(AlertDialog.java:196)
    at android.app.AlertDialog.<init>(AlertDialog.java:141)
    at android.app.ProgressDialog.<init>(ProgressDialog.java:77)

那么有什么方法可以传递活动上下文吗?另外,如果我在应用关闭时尝试创建ProgressDialog,应用会崩溃吗?

【问题讨论】:

【参考方案1】:

使用 Android 框架而不是反对它的一个关键原则是后台任务永远不能超过它们的托管组件。因此,什么样的组件拥有AsyncTask 取决于AsyncTask 应该保持运行多长时间。如果您希望任务在应用程序“关闭”时继续运行,那么您不能将其托管在 Activity 中。这正是Service 的用途。由于 Service 没有自己的 UI,因此您可以通过 Notification 和/或在任务完成时启动 Activity 向用户提供反馈。

【讨论】:

这就是我选择的,并在 中编写了我的示例,您对上下文问题有何建议? @DavidBarishev 你能贴出实际的错误和它发生的代码吗?【参考方案2】:

据我了解,您想要执行一个长时间运行的任务并且仍然可以访问 UI 线程,即使应用程序已关闭?好吧,AsyncTask 将一直运行直到它完成(即使您关闭应用程序),并且您可以覆盖 AsyncTaskonProgressUpdate 方法以处理 UI 线程上来自 doInBackground 的更新,但是,如果你的AsyncTask 在 UI 线程上做了一些事情并且应用程序被关闭,你可能会遇到异常,因为你的 UI 线程将被破坏。不过,如果要查看问题的完整上下文,更多的代码会很棒。希望这可以帮助。

顺便说一句,如果您从异步任务中获得 Java.Lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare(),请使用 runOnUiThread(Runnable r) 方法来处理导致该异常的任何 UI 代码。

【讨论】:

我无法从服务中调用 runOnUiThread。明天我会用 asynctask 看看 我的错误,以为你在异步任务中得到它。但是@Kevin Krumwiede 的服务/通知方法更符合这种类型任务(下载)的 Android 框架,如果用户在下载您的内容时没有在您的应用中执行任何其他操作。【参考方案3】:

我已找到引发错误的原因。 当你第一次运行这个库时,它必须下载本地库并安装它。它会在应用程序前面显示一个进度条(因此它需要一个活动上下文)。

这就是模拟器工作的原因,因为我在所有测试中都使用它,它已经安装了以前运行的库,当时代码仍在活动类上。

我通过首先检查是否安装了库来解决我的问题,如果没有提示用户安装它,因为这都是在 ui 线程上,所以我传递上下文没有问题。后来当用户下载了一些东西时,它不需要重新下载库,所以我可以传递任何上下文,因为它不会使用它(检查是没有上下文的,只有当库丢失时它才会调用他需要上下文的 init 方法提供进度条。)

我可能会分叉 xamarin ffmpeg 库并添加更多文档/将来使代码更清晰。

【讨论】:

以上是关于通过访问 UI 线程启动后台任务的主要内容,如果未能解决你的问题,请参考以下文章

Android中UI线程与后台线程交互设计的6种方法

转载Android中UI线程与后台线程交互设计的5种方法

在 iOS 中定期在后台线程中运行任务

Android 多线程 AsyncTask 完成后台任务并反馈给UI

使用后台线程BackgroundWorker处理任务的总结

如何在javaFX中将光标更改为WAIT时线程化后台任务?