SWFUpload 2.5 bug 修改 (1)

关于 SWFUpload

SWFUpload 是一个客户端文件上传工具,最初由 Vinterwebb.se 开发,它通过整合 Flash 与 JavaScript 技术为 WEB 开发者提供了一个具有丰富功能继而超越传统标签的文件上传模式。

主要特点:

  • 可以同时上传多个文件;
  • 类似 AJAX 的无刷新上传;
  • 可以显示上传进度;
  • 良好的浏览器兼容性;
  • 兼容其他 JavaScript 库 ( 例如: jQuery, Prototype 等 ) ;
  • 支持 Flash 8 和 Flash 9 ;

SWFUpload 不同于其他基于 Flash 构建的上传工具,它有着优雅的代码设计,开发者可以利用 XHTML 、 CSS 和 JavaScript 来随心所欲的定制它在浏览器下的外观;它还提供了一组简明的 JavaScript 事件,借助它们开发者可以方便的在文件上传过程中更新页面内容来营造各种动态效果。

这么多说的就是一个意思, SWFUpload 使用灵活方便,不少人在用。
但是,它在我们项目实际应用中发现问题。

目前 SWFUpload 2.5 存在问题

目前 swfupload 2.5 存在的 bug 及修改方法:

  • 在目前 v2.5 的代码里 bitmapData 不能用于 「 applyFilter 」 函数,那段代码需要去除(注释也可)或修改。对程序无明显影响。
  • 使用 jpegencoder.swc 的 init() 方法有内存 free up 问题,可以改成单例模式「 singleton 」 调用组件 jpegencoder.swc 。

如果有需要使用 SWFUploader ,可到我的 github 上下载 fix 后的代码。完全向前兼容。
在这篇文章中主要讲述 debug 过程。

线上问题反馈

从用户反馈过来,上传图片速度过慢。-- 用户界面 loading 的菊花图转比较久。

定位问题

单从描述来看太过笼统,需要先具体定位到哪个环节出了问题。

  • 我在本机上测试上传,感觉速度很快(图片大小平均大概 1M , width 最大为 800 像素),几乎秒传。
  • 后来发起群众后发现在另一同事机器上传图片也很慢。

我们先将「上传整体流程」分析,大致可分为以下几个部分:

  • 选择图片然后图片压缩
  • 通过网络上传
  • 修剪图象及入库
  • 主库与从库进行同步

从反馈问题来看,有可能出现的地方是在 压缩、网络 IO 上传、修剪图象及入库这 3 部分。
如何找到具体是哪一部分,首先看看我们执行图片 resize 上传的阈值是多少。

对应的 查看了一下 PublishUploader.js ,发现有这样的代码。

this.startResizedUpload(null,800,800,SWFUpload.RESIZE_ENCODING.JPEG,80,false);

从代码作用是将所有图片最大尺寸 resize 为 800*800 象素。用 jpeg 图像压缩算法,并且图片质量是 80 。

于是,我进行以下操作:

  • 上传一张小于 800px 的图与一张大于 800px 的图,这两张图的总像素近似进行比较
  • 分别在几台笔记本上试试

经过实际测试,在「公司级网络」传输速度下,小于 800px 的图从上传到服务器返回在 1s 左右。
而 大于 800px 的图约 6s+ 才能执行完整个上传流程。

diff 这两个过程中,结论比较明显:

  • 图片 resize 压缩占的时间花了上传时间的 5 倍 -- 5s 。
  • 后后经测试,发现在 3000px 的图上传压缩的时间占了 10s 左右。

修改 SWFUpload

扫过 SWFUpload 代码,了解它的上传原理流程:

  • FileReference 获取路径和图片内容
  • loader 载入图片
  • 判断宽和高进行 resize 和压缩编码
  • 进行 HTTP POST
  • 服务器返回进行 JS 回调

所以,在 Flash 里用 FileReference 是无法获取图片的宽高,如果如要得到图片的宽度和高度,需要创建一个 loader 把图片传入,通过 loader 的「 complete 」异步事件得到图片的高宽。

所以,查看 ImageResizer.as ,看到 loader_Complete 的事件处理函数:

 if (this.newWidth < bmp.width || this.newHeight < bmp.height) {
   // Apply the blur filter that helps clean up the resized image result
   var blurMultiplier:Number = 1.15; // 1.25;
   var blurXValue:Number = Math.max(1, bmp.width / this.newWidth) * blurMultiplier;
   var blurYValue:Number = Math.max(1, bmp.height / this.newHeight) * blurMultiplier;
   var blurFilter:BlurFilter = new BlurFilter(blurXValue, blurYValue, int(BitmapFilterQuality.LOW));
   bmp.applyFilter(bmp, new Rectangle(0, 0, bmp.width, bmp.height), new Point(0, 0), blurFilter);
 }

这一段大概意思是,如果有把大图片 resize 成小图片,就会给图片加模糊滤镜,以防止出现马赛克。
先将以上代码注释掉,化简我们的逻辑。

然后再看还有另一段代码:

 var _bytes:ByteArray = resizedBmp.getPixels(resizedBmp.rect);
 _bytes.compress();
 var jpegEncoder:AsyncJPEGEncoder = new AsyncJPEGEncoder(this.quality, 0, 100);
 jpegEncoder.addEventListener(EncodeCompleteEvent.COMPLETE, this.EncodeCompleteHandler);
 jpegEncoder.addEventListener(ErrorEvent.ERROR, this.EncodeErrorHandler);
 jpegEncoder.encode(resizedBmp);
 
 /*
 this.ba = resizedBmp.getPixels(resizedBmp.rect);
 this.ba.position = 0;
 this.baOut = new ByteArray();
 
 var cLibEncoder:Object = (new CLibInit).init();
 this.debug(resizedBmp.width.toString());
 this.debug(resizedBmp.height.toString());
 cLibEncoder.encodeAsync(compressFinished, this.ba, this.bagOut, resizedBmp.width, resizedBmp.height, this.quality);
 */

未注释的代码是用一个开源异步 JPEG 编码类( AsyncJPEGEncoder )用于编码图片。

为什么它没有用内置的 JPEG 图片 lib 「 JEPGEncoder 」进行编码?是因为开源的 AsyncJPEGEncoder 是异步的,且它在效率上对比 Adobe 内置的要稍高一些。

注释的代码是用 Adobe alchemy 技术实现的一个 clib 组件,这个组件在代码目录下可以看到是 jpegencoder.swc 。

这个技术简单的说就是,它是把 c 的 libjpeg 编进 flash 里,并且可以高效执行 swc ,看网上的一些测试数据,确实比较高效。

看到这里或许我们该高兴了,如果将现在异步的 AsyncJPEGEncoder 类改成 jpegencoder.swc ,那问题不就解决了吗?

神奇的 BitmapData

我带着上面的想法试了一下,果然有效!
但不久就发现一个问题,有的图片上传成功,有的图片在 resize 的时候抛出异常「#2015 」。

google 了一下这个错误号,发现是 flash Player 10 的一个 限制

In AIR 1.5 and Flash Player 10, the maximum size for a BitmapData object is 8,191 pixels in width or height, and the total number of pixels cannot exceed 16,777,215 pixels. (So, if a BitmapData object is 8,191 pixels wide, it can only be 2,048 pixels high.) In Flash Player 9 and earlier and AIR 1.1 and earlier, the limitation is 2,880 pixels in height and 2,880 in width.

Starting with AIR 3 and Flash player 11, the size limits for a BitmapData object have been removed. The maximum size of a bitmap is now dependent on the operating system.

现在市面上比较好的全画幅相机,就会超过这个像素限制。
例如测试的一张样图 5616 × 3744=21026304 像素。
而我们目前的 SWFUploader 2.5 的编译是基于 flash player 10 。
所以,异常自然就抛出了。

但是

  • 为什么线上正在运行的 SWFUploader 没有这个异常问题?
  • 不是超过 1600w 像素的限制了吗,为什么还能正常上传还没有报错呢?

原来,是 bitmapdata 文档有一个 loader 无 1600w 像素限制的细节没有说。
先比较以下两段使用 bitmapdata 代码:

代码 A :

var bmp:BitmapData = (li.content as Bitmap).bitmapData; 
//载入图片并放入到 bitmapData 里

代码 B :

	 
bmp.applyFilter(bmp, new Rectangle(0, 0, bmp.width, bmp.height), new Point(0, 0), blurFilter); 
//应用 bitmapData 的 filter

两者在使用上有何不同?

  • 「代码 A 」是将图片 load 到 bitmtpData 里,它不会报错。
  • 「代码 B 」调用方法 applyFilter 创建出来的 Bitmap ,这样就会报错。

applyFilter 从文档里的一段描述可以看出此方法是创建了一个新的 bitmapData 对象:

applyFilter () method

public function applyFilter(sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point, filter:BitmapFilter):void

Takes a source image and a filter object and generates the filtered image.

以上代码差别 AS 3 文档没有说明用 loader 载入图片没有像素限制的问题。
线上代码因之前是其他同学修改过部分代码,无意用成了「代码 A 」 loader 方式载入。
因此并没有报错。

所以,此问题的解决方法为将使用 filter 的条件由

	 
if (this.newWidth < bmp.width || this.newHeight < bmp.height) { 
//bmp.applyFilter 
} 

改为

	 	
//( 0x01000000 等于 10 进制数 16777216 )
if (bmp.width * bmp.height < 0x01000000) { 
//bmp.applyFilter 
} 

像素问题 Bitmap 问题终于解决。

神奇的炼金术 alchemy

解决完上面的问题,再测试,又发现了一个问题。

  • 用 JS 创建 SWFUploader 上传图片第一次成功;
  • 再用这个 flash 实例传一张图就可耻的失败了,而且还不抛异常。

好吧,不抛异常,就只能加入调试埋 log 了,将几个代码关键函数加入对应的 log :

  • 加入压缩进度 log 。因为不管用何种 JPEG 组件,都是异步成生成 JPEG 图片。
  • 在重点逻辑的入口点与出口点加入相应 log ,
    • 加入到 load 图片成功;
    • 开始进入压缩函数块;
    • 压缩图片的宽高;
    • 调用 alchemy 结束;
    • 异步压缩结束。

再加入一个 log 函数:

	  
private function debug(msg:String):void {    
  ExternalCall.Debug('console.log',msg);  
}

这样,就可以在控制台里看到 log 了。 发现规律

  • 第 1 次 resize 图正常
  • 第 2 次除了压缩进度的有 log ,其他进展一直都是 0%。

再次 review JPEG 编码的代码块:

	 
var cLibEncoder:Object = (new CLibInit).init();
this.debug(resizedBmp.width.toString());
this.debug(resizedBmp.height.toString());
cLibEncoder.encodeAsync(compressFinished, this.ba, this.bagOut, resizedBmp.width, resizedBmp.height, this.quality);

分析个组件的使用,第一感觉就是组件使用过后居然没有销毁,这代码太囧了 =.=

alchemy 如果为了高效,应该是

  • 将 lib 驻入内存再开辟一块 buffer 给它
  • Resize 大图时,如果 buffer 处理不好,没有很好的 free up ,第二次 init 会不会有问题?
  • 如果开辟的内存小的话是不是更安全?

按照上面的这个思路,我将 resize 的参数改小,使 buffer 使用尽可能的小:

  • 将 resize 参数改为所有上传图都 resize 到 100*100, quiality 为 80 。

测试了一下发现都能正常上传多张图片。

那再按照第一次可上传后续不能传和 buffer 小可用的结论,我将使用 alchemy 组件初始化的使用改用为 static (即 singlton 类型)保证只实例化一次,看是否正常。

最后发现可正常 resize 多张图片,到此 alchemy 的问题也得已解决。

后记

这篇文章来源于我把 SWFUpload 的问题解决思路用邮件分享给大家。
如何改的,可以看我附件里的代码。
重要的不是代码怎么 fix 的,而是思路。

因为我觉得前端同学看上去都是只关注兼容性而不关注逻辑思维。这篇文章是算是抛砖引玉了。
对于我们使用的第三方代码,有空可以多熟悉,关键时刻也许不用手忙脚乱。

其他的 flash 第三方上传解决方案:

  • plupload - 特点是支持平台全( flash, silverlight, html5, html4) 都支持。 flash 支持客户端压缩图片, flash 编译出来的大小只有 18K 。压缩效率比 swfupload 低不少。
    • 5616 × 3744(5.1M) 图片, resize 成( 320*240 ,质量 90 )
    • plupload 用时在 10s+,而 Swfupload 用时在 2s- ;
    • 环境在 mac ML 系统 flash player 11 , safari 6 & chrome 22dev 。
  • uploadify
    • 平台支持全面, 与 plupload 类似
    • 但是不支持 resize 图片,
    • 在大图上传的时,网速慢时,上传整体时间就会比较久,全依赖于带宽。

虽然两年过去了, swfupload 的压缩性能还算是不错的(主要是依赖于 flash 平台的性能)。

相关链接汇总

Comments