Apache Pivot后台线程与UI线程
文章中用到的一些术语的说明:
UI操作:修改了UI组件的某些属性或则特性,比如修改按钮显示的文本或则图标等或读取UI组件的属性或者特性。
非UI操作:不能有修改或则读取任何与UI组件相关的属性或者特性。
background Task(后台任务): 用于执行非UI操作的线程。
UI thread:用于执行UI操作的线程,一般一个应用程序只有一个UI线程。
很多UI框架,用户界面都有一个独立的线程,称作 UI thread。用户在UI上的操作有些很快就完成,然而有些操作需要耗时比较长的时间(比如同文件加载数据或者从数据库查询数据或者从网络请求数据等),在这种情况下,如果把操作放在UI thread中执行,整个用户界面就会被"挂起"了,直到操作完成用户才可以进行其它的UI操作,当然这种低效率的操作是开发者所不允许的,很多UI框架都采用了另外一个独立于UI thread的线程来执行耗时比较长的由UI操作引入的非UI操作。
在Pivot中,用于执行耗时较长的操作的,一般使用后台任务(background task)来实现,后台任务在Pivot中由类
org.apache.pivot.util.concurrent.Task表示。
对于后台任务有一些限制:
1. 由于Pivot的对UI 组件的操作只能在UI thread中,因此所有后台任务是不能操作任何UI组件的。
2.后台任务的执行会有一个输出结果,但是UI thread并不知道后台任务何时执行结束以及执行结果?
为了解决上面2个后台任务的限制,Pivot提供了不同的方法来解决这些问题:
1.在后台任务(非UI线程)操作UI组件时,通过
ApplicationContext.queueCallback方法,把要执行的内容加入到UI可执行队列中,该队列的可执行体由UI thread负责轮询调用,类似与其他UI框架中的消息机制。
2. UI thread为了知道UI组件操作所创建的后台任务的执行结果,可以使用TaskAdapter和TaskListener监听后台任务的执行结果,并把任务执行后要执行的操作加入UI thread,由UI thread负责执行实际的代码。
下面我们开看一下Pivot tutorial的Meter代码,并分析后台线程和UIthread之间的互操作。
后台线程与UI Thread之间的互操作包括: UI thread 创建执行后台线程和后台线程修改UI 组件。
1. UI界面的WTKX代码
< Window title = "Meters" maximized = "true" |
02 |
xmlns:wtkx = "http://pivot.apache.org/wtkx" |
03 |
xmlns = "org.apache.pivot.wtk" > |
07 |
< TablePane.Column width = "1*" /> |
10 |
< TablePane.Row height = "1*" > |
11 |
< Border styles = "{padding:2}" > |
13 |
< BoxPane styles = "{horizontalAlignment:'center', verticalAlignment:'center'}" > |
14 |
< Label text = "Progress:" /> |
15 |
< Meter wtkx:id = "meter" preferredWidth = "200" preferredHeight = "16" /> |
20 |
< TablePane.Row height = "-1" > |
21 |
< BoxPane styles = "{horizontalAlignment:'center', padding:6}" > |
22 |
< PushButton wtkx:id = "progressButton" styles = "{minimumAspectRatio:3}" /> |
UI界面的Java 代码:
在下面的代码中,我们实现了一个Task的类,叫
SampleTask,用于模拟长时间允许的后台任务,并在任务的执行过程中修改UI组件Meter的相关操作。
package org.apache.pivot.tutorials.progress; |
002 |
import org.apache.pivot.collections.Map; |
003 |
import org.apache.pivot.util.concurrent.Task; |
004 |
import org.apache.pivot.util.concurrent.TaskExecutionException; |
005 |
import org.apache.pivot.util.concurrent.TaskListener; |
006 |
import org.apache.pivot.wtk.Application; |
007 |
import org.apache.pivot.wtk.ApplicationContext; |
008 |
import org.apache.pivot.wtk.Button; |
009 |
import org.apache.pivot.wtk.ButtonPressListener; |
010 |
import org.apache.pivot.wtk.DesktopApplicationContext; |
011 |
import org.apache.pivot.wtk.Display; |
012 |
import org.apache.pivot.wtk.Meter; |
013 |
import org.apache.pivot.wtk.PushButton; |
014 |
import org.apache.pivot.wtk.TaskAdapter; |
015 |
import org.apache.pivot.wtk.Window; |
016 |
import org.apache.pivot.wtkx.WTKXSerializer; |
017 |
public class Meters implements Application { |
018 |
public class SampleTask extends Task<Void> { |
019 |
private int percentage = 0 ; |
021 |
public Void execute() throws TaskExecutionException { |
024 |
while (percentage < 100 |
030 |
ApplicationContext.queueCallback( new Runnable() { |
033 |
meter.setPercentage(( double )percentage / 100 ); |
036 |
} catch (InterruptedException exception) { |
037 |
throw new TaskExecutionException(exception); |
043 |
private Window window = null ; |
044 |
private Meter meter = null ; |
045 |
private PushButton progressButton = null ; |
046 |
private SampleTask sampleTask = null ; |
048 |
public void startup(Display display, Map<String, String> properties) |
050 |
WTKXSerializer wtkxSerializer = new WTKXSerializer(); |
051 |
window = (Window)wtkxSerializer.readObject( this , "meters.wtkx" ); |
052 |
meter = (Meter)wtkxSerializer.get( "meter" ); |
053 |
progressButton = (PushButton)wtkxSerializer.get( "progressButton" ); |
054 |
progressButton.getButtonPressListeners().add( new ButtonPressListener() { |
056 |
public void buttonPressed(Button button) { |
057 |
if (sampleTask == null ) { |
061 |
sampleTask = new SampleTask(); |
062 |
sampleTask.execute( new TaskAdapter<Void>( new TaskListener<Void>() { |
063 |
@Override taskExecuted在task任务执行结束并且没有抛出异常时被调用 |
064 |
public void taskExecuted(Task<Void> task) { |
067 |
@Override executeFailed在task任务执行结束并且抛出异常时被调用 |
068 |
public void executeFailed(Task<Void> task) { |
071 |
private void reset() { |
074 |
meter.setPercentage( 0 ); |
075 |
updateProgressButton(); |
082 |
updateProgressButton(); |
085 |
updateProgressButton(); |
086 |
window.open(display); |
089 |
public boolean shutdown( boolean optional) { |
090 |
if (window != null ) { |
096 |
public void suspend() { |
099 |
public void resume() { |
101 |
private void updateProgressButton() { |
102 |
if (sampleTask == null ) { |
103 |
progressButton.setButtonData( "Start" ); |
105 |
progressButton.setButtonData( "Cancel" ); |
108 |
public static void main(String[] args) { |
109 |
DesktopApplicationContext.main(Meters. class , args); |
从上面的代码我们看到了UI 线程如何启动一个后台线程,并且要求后台线程执行结束后通知UI线程 和后台线程如何执行UI操作的方法。注意:这里所的后台线程执行UI的操作,并不是指UI操作的代码在后台线程中执行,而是后台线程把要执行的UI操作的代码加入UI thread的执行队列,由UI thread轮询执行(因为UI thread本质上就是一个轮询UI事件的线程)。
task对象在调用execute方法时,如果不传递任何参数,那么等同于在UI thread直接执行操作一样,没有创建任何后台线程。
如果task对象在调用execute时,传递的参数为TaskListener的一个实现的对象(非
TaskAdapter)时,在
taskExecuted和executeFailed回调方法中不能直接调用UI组件的任何操作代码,除非传递的参数对象为TaskAdapter才可以直接调用,或者在传递
TaskListener的实现(非TaskAdapter)的实例时,如果一定要操作UI组件,那么可以使用
ApplicationContext.queueCallback对要执行的相关UI操作进行封装前转UI操作到UI thread执行。
示例中使用的就是传递一个TaskAdapter的一个实例作为execute的参数。因此可以在TaskListener的实现中直接调用UI操作,因为TaskAdapter的taskExecuted和executeFailed已经为我们封装了相关的代码,使得操作可以被前转到UI thread执行。
如果熟悉了解SWT的,可能会更容易理解上面的代码。其实很多UI都是类似的做法只是表示方式不同而已,比如古老的MFC 使用的是消息