最近遇到了一个棘手的问题,不得不研究一下 SWFUpload 的 AS 代码。

SWFUpload 的 JS 代码早就已经被我改的面目全非了,改 AS 想来也是迟早的事…

Bug 是这样的,先看图:

怎样重现这个 bug 呢,也很容易,作者说不能重现…看来他充其量也只能算是个 NB 的 Flash 开发,哈哈。

首先说明这个 bug 是 SWFUpload 组件本身的,不是我改出来的,下面我们就用官方网站上的 demo 来试验。

按以下步骤操作,肯定可以重现,不过要保证 IE 装了 MS Develop Tools。

  1. 打开 IE,如果 Develop Tools 开着,关了它
  2. 浏览网页 http://demo.swfupload.org/v220/simpledemo/index.php 在这里你可以随意刷新页面
  3. 打开 Develop Tools,注意这时你不能再刷新页面
  4. 在 Develop Tools 的 Script 面板里输入代码,我们用官方的 destroy 方法:SWFUpload.instances["SWFUPload_0"].destroy();
  5. 点「Run Script」执行代码

你可以看到代码执行后 console 里打印的是 true,说明 destroy() 成功。可是稍微停顿之后 悲剧就发生了…

其实不用官方的 destroy() 方法,用以下方式也是一样:

var x = document.getElementsByTagName("object")[0];
x.parentNode.removeChild(x);

这说明不是 destroy() 这个 JS 方法的问题,是 Flash 内部的问题。

我在自己的代码里加个断点,debug 进去之后发现了两段可疑代码,所有的 JS 都找不到这样的代码,猜应该是 Flash 自动生成的。

第一段:

try {
    document.getElementById("swfupload-1303117382646-0").SetReturnValue(__flash__toXML(SWFUpload.instances["swfupload-1303117382646-0"].testExternalInterface()));
} catch (e) {
    document.getElementById("swfupload-1303117382646-0").SetReturnValue("<undefined/>");
}

第二段:

function __flash__arrayToXML(obj) {
    var s = "<array>";
    for (var i = 0; i < obj.length; i++) {
        s += "<property id="" + i + "">" + __flash__toXML(obj[i]) + "</property>";
    }
    return s + "</array>";
}
function __flash__argumentsToXML(obj,index) {
    var s = "<arguments>";
    for (var i = index; i < obj.length; i++) {
        s += __flash__toXML(obj[i]);
    }
    return s + "</arguments>";
}
function __flash__objectToXML(obj) {
    var s = "";
}
function __flash__escapeXML(s) {
    return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
}
function __flash__toXML(value) {
    var type = typeof(value);
    if (type == "string") {
        return "<string>" + __flash__escapeXML(value) + "</string>";
    } else if (type == "undefined") {
        return "<undefined/>";
    } else if (type == "number") {
        return "<number>" + value + "</number>";
    } else if (value == null) {
        return "<null/>";
    } else if (type == "boolean") {
        return value ? "<true/>" : "<false/>";
    } else if (value instanceof Date) {
        return "<date>" + value.getTime() + "</date>";
    } else if (value instanceof Array) {
        return __flash__arrayToXML(value);
    } else if (type == "object") {
        return __flash__objectToXML(value);
    } else {
        return "<null/>";
    }
}
function __flash__addCallback(instance, name) {
    instance[name] = function () {
        return eval(instance.CallFunction("<invoke name="" + name + "" returntype="javascript">" + __flash__argumentsToXML(arguments, 0) + "</invoke>"));
    }
}
function __flash__removeCallback(instance, name) {
    instance[name] = null;
}

因为它们很可能是 Flash 自己生成的,很可能是 Flash 加载后做的,所以我只能用最土但是最有用的办法,通过 setTimeout 把这些代码加在自己的代码后面(需要稍稍改一下上面的代码 NB 的 JS 开发都懂的),并给他们加上 console,那样我就可以理直气壮的 debug 到了。

经过 debug 之后就发现了以下的事情:

AS 中有代码:

this.testExternalInterfaceCallback = "SWFUpload.instances["" + this.movieName + ""].testExternalInterface";

完美切合了第一个代码片段,而 this.testExternalInterfaceCallback 在 SWFUpload 的构造方法里有 Timer 会每隔一秒钟调用。

// Start periodically checking the external interface
var oSelf:SWFUpload = this;
this.restoreExtIntTimer = new Timer(1000, 0);
this.restoreExtIntTimer.addEventListener(TimerEvent.TIMER, function():void {
    oSelf.CheckExternalInterface();
});
this.restoreExtIntTimer.start();
private function CheckExternalInterface():void {
    if (!ExternalCall.Bool(this.testExternalInterfaceCallback)) {
        this.SetupExternalInterface();
        this.debug("ExternalInterface reinitialized");
        if (!this.hasCalledFlashReady) {
            ExternalCall.Simple(this.flashReadyCallback);
            this.hasCalledFlashReady = true;
        }
    }
}

testExternalInterfaceCallback 在失败的情况下会认为是 false,故而触发 SetupExternalInterface 方法,SetupExternalInterface 中每一次的 addCallback 都会触发一次 __flash__addCallback 而产生一个「Object required」错误。

这样上面都走通了,而 Firefox/Safari/Opera/Chorme 下面不会有这样的问题,猜(我只能猜)是它们的回收做的比较好 - 或者 pluin 版本的 FlashPlayer 比 ActiveX 版本的做的好,在 Flash 从 DOM 上移出之后同样把它内部的东西都给销毁了。

接下来就是 Fix 了,很简单,我们只要在执行 JS 的 destroy 的时候告诉 Flash 先把 Timer 停掉就行了。

AS 代码,添加方法:

private function Destroy():void {
    if (!this.restoreExtIntTimer) {
        return;
    }
    this.restoreExtIntTimer.stop();
    this.restoreExtIntTimer = null;
}

并在 SetupExternalInterface 中把它加进去:

ExternalInterface.addCallback("Destroy", this.Destroy);

JS 中在 destroy() 方法里,适当的地方调用一下:

this.callFlash("Destroy");

这样就搞定这个 bug 了。