刚开始学习使用flex,这几天用air做了个压缩软件,要使用progressbar显示压缩和解压缩进度,自然而然想到要用线程控制progressbar刷新进度。但是查了api惊讶的发现flex是单线程的,那么手动模式的progressbar如何做到刷新进度和其他工作同步进行呢?查资料的时候看到了这篇好文章《Flex 4: A multi threading solution》,觉得值得翻译,国内相关的资料还是太少了,翻译不恰当之处请多多包涵。当然,现在AS3已经有开源项目可以模拟多线程了,以后我会写文章加以介绍。
原文地址:http://blogs.infosupport.com/blogs/alexb/archive/2010/03/01/flex-4-a-multi-threading-solution.aspx
源代码下载在最后。
--------------------------------------------------------------分割线-----------------------------------------------------------------------
Flex 4: 一种多线程解决方案
如果你经常开发Silverlight 应用,就会体会到Flex不支持多线程是多么令人烦恼的事情。尤其是当我们需要要运行一个长时间任务,同时又需要展示任务完成进度时,单线程的缺点更加明显的体现了出来:由于无法用一个后台进程处理这个任务,UI线程将会忙于处理任务而无法同时更新UI,这也意味着直到任务完成,UI才能得到更新(对于progressbar这类组件来说,我们只能看到它显示0%和100%两个状态,因为处理过程中progressbar都没有机会得到更新,当任务完成progressbar更新时,进度已经是100%了)。在这篇文章中,我将展示如何在flex缺乏多线程机制的条件下,仍旧能让UI可以显示任务处理进度。
Flex示例程序
这篇博文的Flex示例程序是一个能将图片灰化的应用。当然更好的处理方式是使用PixelBender和过滤器,但是这个例子很好的体现了我们无法在处理任务的同时汇报任务进度。看一下下面这张截图:
这是一张鹦鹉的图片,图片下面有两个按钮和一个进度条。下一步,看一下下面的代码:
<?xml version="1.0" encoding="utf-8"?> <s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" creationComplete="saveOriginalImageData(event)" > <fx:Script> <![CDATA[ private var _originalImageData : BitmapData; private function greyWithPauseHandler(event:MouseEvent):void { var clone : BitmapData = _originalImageData.clone(); _parrot.source = new Bitmap(clone); setProgress(0,0); convertToGreyScaleWithPauses(clone); } private function convertToGreyScale(toGreyScale: BitmapData) : void { var totalPixels: int = toGreyScale.width * toGreyScale.height; var amountForProgressUpdate : int = totalPixels / 16; setProgress(0, totalPixels); for(var i : int = 0; i < totalPixels; i++) { var y: int = i / toGreyScale.width; var x :int = i - y * toGreyScale.width; var pixel:uint = toGreyScale.getPixel(x,y); var red:uint = (pixel >> 16) & 255; var green:uint = (pixel >> 8) & 255; var blue:uint = pixel & 255; var grey:uint = (red * 0.3) + (green * 0.59) + (blue * 0.11); toGreyScale.setPixel(x,y,(grey<<16) | (grey<<8) | grey); if(i % amountForProgressUpdate == 0 && i != 0) { setProgress(i + 1, totalPixels); } } setProgress(totalPixels, totalPixels); } private function convertToGreyScaleWithPauses(toGreyScale: BitmapData) : void { var totalPixels: int = toGreyScale.width * toGreyScale.height; setProgress(0, totalPixels); UIUtilities.pausingFor(0,totalPixels, function(i:int) : void { var y: int = i / toGreyScale.width; var x :int = i - y * toGreyScale.width; var pixel:uint = toGreyScale.getPixel(x,y); var red:uint = (pixel >> 16) & 255; var green:uint = (pixel >> 8) & 255; var blue:uint = pixel & 255; var grey:uint = (red * 0.3) + (green * 0.59) + (blue * 0.11); toGreyScale.setPixel(x,y,(grey<<16) | (grey<<8) | grey); },setProgress ,this); _parrot.source = new Bitmap(toGreyScale); } private function setProgress(processed : int, amountThatNeedsToBeProcessed : int) : void { _progress.setProgress(processed, amountThatNeedsToBeProcessed); } private function greyHandler(event:MouseEvent):void { var clone : BitmapData = _originalImageData.clone(); _parrot.source = new Bitmap(clone); setProgress(0,0); // Need an artificial delay (Timer) to let the Image update itself and show the original image before // the greying begins... convertToGreyScale(clone); } private function saveOriginalImageData(event:Event):void { _originalImageData = Bitmap(_parrot.content).bitmapData; } ]]> </fx:Script> <mx:ProgressBar mode="manual" minimum="0" maximum="1000000000000" id="_progress" horizontalCenter="0" bottom="6" label="Greying: %3%%"/> <mx:Image id="_parrot" source="@Embed('./assets/parrot_heada.jpg')" left="10" right="10" top="10" bottom="77" scaleContent="true"/> <s:Button label="Grey it with pauses" horizontalCenter="-107" bottom="48" click="greyWithPauseHandler(event)" /> <s:Button label="Grey it" bottom="48" horizontalCenter="65" click="greyHandler(event)"/> </s:Application>
首先点击 "Grey it"按钮,你将看到ui卡住了一会,当应用程序完成任务时,这张图片已经灰化了,之后ui恢复正常,进度条跳到100%。
下一步,点击“Grey it with pauses” 按钮,你将会看到图片逐渐灰化并且进度条进度也同时发生改变,同时“Grey it with pauses”花了比"Grey it"更长的时间完成任务。让我来解释下发生了什么:
package { 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更新。在上面的例子里,我提供了一个匿名函数在下一帧调用。在这个匿名函数中,我做了两件事:
另一个有趣的部分在第61行。在callLater()被调用的那次循环之前的那次循环,我给用户机会来设定进度值,而UI需要这些值来调用updateProgressFunction,并且提供了已经迭代数和即将需要的迭代数。通过这种方法,我在调用callLater()前将调用UI的次数最小化。
结论
通过使用我的UIUtilities.pausingFor()方法,我展示了如何让UI仍旧相应并且更新UI,就像在一个后台进程中使用了一个普通的for循环。我的UIUtilities.pausingFor()方法可以很容易的扩展为一个pausingForEach()方法。由于UIUtilities的api十分简单,请注意使用它将会产生性能消耗。虽然让任务在没有更新UI的情况下结束可能更快,但是对于用户来说体验性就不会很好了。
当然,这并不是真正提供了一种多线程处理的方法,而且真正的多线程在多核处理器上将提供更好的性能。但是就目前的flex来看,我们必须使用某些和本文提供的相似的方法来应对多线程的缺失。尽管这篇文章的标题带着flex4的标志,文章提供的方法也同样可以在flex3中使用。你可以在源代码中找到解决方法。