刚开始学习使用Flex,这几天用air做了个压缩软件,要使用progressbar显示压缩和解压缩进度,自然而然想到要用线程控制progressbar刷新进度。但是查了api惊讶的发现flex是单线程的,那么手动模式的progressbar如何做到刷新进度和其他工作同步进行呢?查资料的时候看到了这篇好文章《Flex 4: A multi threading solution》,觉得值得翻译,国内相关的资料还是太少了,翻译不恰当之处请多多包涵。当然,现在AS3已经有开源项目可以模拟多线程了,以后我会写文章加以介绍。
源代码下载:[MultiThreadingApp.rar]
Flex 4: 一种多线程解决方案
如果你经常开发Silverlight 应用,就会体会到Flex不支持多线程是多么令人烦恼的事情。尤其是当我们需要要运行一个长时间任务,同时又需要展示任务完成进度时,单线程的缺点更加明显的体现了出来:由于无法用一个后台进程处理这个任务,UI线程将会忙于处理任务而无法同时更新UI,这也意味着直到任务完成,UI才能得到更新(对于progressbar这类组件来说,我们只能看到它显示0%和100%两个状态,因为处理过程中progressbar都没有机会得到更新,当任务完成progressbar更新时,进度已经是100%了)。在这篇文章中,我将展示如何在flex缺乏多线程机制的条件下,仍旧能让UI可以显示任务处理进度。
Flex示例程序
这篇博文的Flex示例程序是一个能将图片灰化的应用。当然更好的处理方式是使用PixelBender和过滤器,但是这个例子很好的体现了我们无法在处理任务的同时汇报任务进度。看一下下面这张截图:

这是一张鹦鹉的图片,图片下面有两个按钮和一个进度条。下一步,看一下下面的代码:
1 | <?xml version="1.0" encoding="utf-8"?> |
首先点击 “Grey it”按钮,你将看到ui卡住了一会,当应用程序完成任务时,这张图片已经灰化了,之后ui恢复正常,进度条跳到100%。
下一步,点击“Grey it with pauses” 按钮,你将会看到图片逐渐灰化并且进度条进度也同时发生改变,同时“Grey it with pauses”花了比”Grey it”更长的时间完成任务。让我来解释下发生了什么:
- 请看第67行,当点击 “Grey it”按钮时,这个方法将被调用。它克隆了原始的Bitmap数据(存放在creationComplete event中),并且用第62行的setProgress()方法设置进度条进度,然后调用convertToGreyScale()方法完成所有任务。问题是当将原始图片数据提供给“_parrot”图片组件时,用户图形接口无法在convertToGreyScale()方法完成之前更新。唯一的方法就是人为的制造一个延迟,例如用一个Timer计时器,来找到图片更新花费的时间。这就是我为什么先让你点击”Grey it”按钮的原因。另一方面,这么做还有个缺点就是无法看到图片灰化的过程。
- 在第23行的convertToGreyScale()方法中,Bitmap数据用一种普通的算法完灰化,可能这个算法不是最理想的,但是它足够完成任务了。大部分程序员使用嵌套循环,而我使用了一个简单循环来说明下面的部分。这个方法一共更新进度条16次,这个次数由第25行决定。如果我在每一次循环时都更新进度条,我将发现发生了脚本超时(这里我没有理解什么意思),即使将这个超时设定到最大的60秒也一样。就像你看到的一样,用户只能看到进度条在方法完成时前进到100%,因为只有在这个时候Flex才能更新UI。事实上所有对setProgress()方法的调用都可以移除了,因为它们实际上没有任何作用。
- 下一步,看一下第44行的 convertToGreyScaleUsingPauses()方法。这个方法在点击“Grey it with pauses” 按钮时调用。它的核心部分在第48行:UIUtilities.pausingFor() 在这里被调用了。循环体在这里转换为一个匿名函数,并且必须接受一个int参数,而且被调用次数取决于前两个参数。在这个匿名函数之后,我提供了另外一个函数来设定进度。而pausingFor函数确保了暂停发生,在这个暂停中UI可以得到更新。看一下下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71package
{
import mx.core.UIComponent;
public final class UIUtilities
{
/**
* Executes a for loop that pauses once in a while to let the UI update itself. The parameters of this method
* are derived from a normal
* for(var i :int =0; i <10;i++) {
* }
* loop.
* @param fromInclusive The 0 in the loop above.
* @param toExclusive The 10 in the loop above.
* @param loopBodyFunction The loop body, everything between {} in the loop above. This function must accept an int,
* which represents the current iteration.
* @param updateProgressFunction The method that needs to be called to update the UI, for example a progressbar.
* this method must accept two ints, The first is the number of iterations processed, the other is the total number of
* of iterations that need to be processed.
* @param componentInDisplayList Any component that is connected to the displaylist. This method makes use
* of the callLater() method which is available on any UIComponent. The root Application is an easy choice.
* @param numberOfPauses The number of times this method pauses to let the UI update itself.
* The correct amount is hardware dependent, 8 pauses doesn't mean you'll see 8 UI updates. Experiment
* to find the number that suits you best. A higher number means less performance, but more ui updates and
* visual feedback.
**/
public static function pausingFor(fromInclusive:int, toExclusive :int,loopBodyFunction : Function,updateProgressFunction : Function,componentInDisplayList:UIComponent,
numberOfPauses : int = 8) : void {
executeLoop(fromInclusive,toExclusive, toExclusive / numberOfPauses, loopBodyFunction,updateProgressFunction, componentInDisplayList)
}
private static function executeLoop(fromInclusive:int, toExclusive :int,numberOfIterationsBeforePause : int, loopBodyFunction : Function,
updateProgressFunction : Function,componentInDisplayList : UIComponent) : void {
var i : int = fromInclusive;
for(i; i < toExclusive;i++) {
//determine the rest of the number of iterations processed and the numberOfIterationsBeforePause
//This is needed to determine whether a pause should occur.
var rest : Number = i % numberOfIterationsBeforePause;
//If the rest is 0 and i not is 0, a pause must occur to let the ui update itself
if(rest == 0 && i != 0) {
//use callLater to pause and let the UI update.....
componentInDisplayList.callLater(
//Supply anonymous function to the callLater method, which can be called after the pause...
function(index:int) : void {
//after pauzing, resume work...
loopBodyFunction(index);
//We need to continue with the callFunction method. The current index has already
//been processed so continue this method with the next index
executeLoop(index + 1,toExclusive,numberOfIterationsBeforePause,loopBodyFunction,updateProgressFunction,componentInDisplayList);
},[i]);
//When using callLater to let the UI update, my own code must be finished. So break out of the loop
break;
} else {
//No time for a pause
loopBodyFunction(i);
//Just before a pause occurs, report progress so that a user can set progress values
if(rest == numberOfIterationsBeforePause - 1) {
updateProgressFunction(i + 1, toExclusive);
}
}
}
//Final progress update
updateProgressFunction(i + 1, toExclusive);
}
}
}
这是UIUtilities类的源代码。唯一的静态公共方法是pausingFor()。请花点时间仔细阅读一下注释,注释解释了这个函数具体做了什么,以及每一个参数的作用。这个方法被另外一个私有的静态方法executeLoop()调用,而executeLoop()几乎和pausingFor()拥有相同的参数,除了numberOfIterationsBeforePause。这个参数是pausingFor()方法里toExlusive参数除以numberOfPauses参数的结果。
让我们看看第34行的executeLoop()方法。我写了注释来解释这个方法。最重要的部分在第46行:callLater()。callLater()是所有UIComponent都拥有的,并且是flex中最被轻视的方法之一。它接受一个函数作为第一个参数。一个参数array作为第二个参数 。当你调用这个方法,flex将会在下一帧调用作为参数的函数,因此UI就能够在当前帧的剩下时间内得到更新。这意味着当前帧必须有足够的剩余时间,而且接下来不能有任何你自己的代码需要在callLater()调用后执行,否则你仍旧看不到任何的UI更新。在上面的例子里,我提供了一个匿名函数在下一帧调用。在这个匿名函数中,我做了两件事:
- 让loopBodyFunction参数执行。
- 然后再调用executeLoop()。这样我确保了在UI有足够时间更新以后,executeLoop()在被callLater()暂停后,在当前帧恢复执行
另一个有趣的部分在第61行。在callLater()被调用的那次循环之前的那次循环,我给用户机会来设定进度值,而UI需要这些值来调用updateProgressFunction,并且提供了已经迭代数和即将需要的迭代数。通过这种方法,我在调用callLater()前将调用UI的次数最小化。
结论
通过使用我的UIUtilities.pausingFor()方法,我展示了如何让UI仍旧相应并且更新UI,就像在一个后台进程中使用了一个普通的for循环。我的UIUtilities.pausingFor()方法可以很容易的扩展为一个pausingForEach()方法。由于UIUtilities的api十分简单,请注意使用它将会产生性能消耗。虽然让任务在没有更新UI的情况下结束可能更快,但是对于用户来说体验性就不会很好了。
当然,这并不是真正提供了一种多线程处理的方法,而且真正的多线程在多核处理器上将提供更好的性能。但是就目前的flex来看,我们必须使用某些和本文提供的相似的方法来应对多线程的缺失。尽管这篇文章的标题带着flex4的标志,文章提供的方法也同样可以在flex3中使用。你可以在源代码中找到解决方法。
v1.5.2