H5 worker 系列二 Laya中使用WorkerLoad解析png

本文主要介绍worker在Laya中如何使用,墙裂推荐先阅读H5 worker 系列一 基础知识。

一、Laya中应用

参考多线程worker 官方讲解
1.加载一个文件

//my_task.js
self.addEventListener('message', function (e) {
    var xmlreq = new XMLHttpRequest();
    xmlreq.responseType = "text";
    xmlreq.onload = function (e) {
        var data = e.currentTarget.response;
        self.postMessage(data);
    }
    xmlreq.open("get","../res/atlas/comp.atlas");
    xmlreq.send()
}, false);

//Main.ts
var worker: any = new Laya.Browser.window.Worker("libs/my_task.js");
worker.onmessage = function (event): void {
    console.log(event.data);
};
worker.postMessage("start");

2.使用官方提供的worker.js加载解码图片
官方提供的libs/worker.js对原生的worker进行了封装,后文介绍一下它的思路,现在先看一下如何运用。

package {
    import laya.net.Loader;
    import laya.utils.Handler;
    import view.TestView;
    import laya.net.WorkerLoader;
    import laya.webgl.WebGL;
    public class LayaUISample {
        public function LayaUISample() {
            //初始化引擎
            Laya.init(600, 400,WebGL);
            //设置Laya提供的worker.js的路径
            WorkerLoader.workerPath = "libs/worker.js";
            //开启worker线程
            WorkerLoader.enable = true;
            //加载引擎需要的资源
            Laya.loader.load([{url: "res/atlas/comp.json", type: Loader.ATLAS}], Handler.create(this, onLoaded));
        }
        private function onLoaded():void {
            //实例UI界面
            var testView:TestView = new TestView();
            Laya.stage.addChild(testView);
        }
    }
}

WorkerLoader.enable = true;一句话就开启worker了??有点神奇!看看神奇的WorkerLoader.enable = true;做了什么

//laya.core.js
/**
*是否启用。
*/
__getset(1, WorkerLoader, 'enable', function () {
    return WorkerLoader._enable;
}, function (v) {
    if (WorkerLoader.disableJSDecode &&
        (!Browser.window.createImageBitmap)) return;
    WorkerLoader._enable = v;
    if (WorkerLoader._enable &&
        WorkerLoader._preLoadFun == null)
        WorkerLoader._enable = WorkerLoader.__init__();
});

这里先是一个disableJSDecode开关,默认值为true。结合官方的worker.js源码可以知道,里面会优先使用createImageBitmap方法解码图片,但这个方法有兼容性问题,如果浏览器不支持createImageBitmap,那么worker.js会自己去解码。在这种不兼容的情况下,如果不想让worker.js自己用JS解码,那么就让disableJSDecode=true吧,WorkLoader会return掉,不会开启。

二、Browser.window.createImageBitmap这个方法是啥
safari上的支持度比较惨

参考mozilla.org createImageBitmap

createImageBitmap方法存在 windows 和 workers 中. 它接受各种不同的图像来源, 并返回一个Promise, resolve为ImageBitmap. 图像被剪裁成自(sx,sy)且宽度为sw,高度为sh的像素的矩形。

构造函数:createImageBitmap(image[, options]).then(function(response) { ... });createImageBitmap(image, sx, sy, sw, sh[, options]).then(function(response) { ... });

image 一个图像源, 可以是一个 , SVG ,

var canvas = document.getElementById('myCanvas'),
ctx = canvas.getContext('2d'),
image = new Image();

image.onload = function() {
  Promise.all([
    createImageBitmap(image, 0, 0, 32, 32),
    createImageBitmap(image, 32, 0, 32, 32)
  ]).then(function(sprites) {
    ctx.drawImage(sprites[0], 0, 0);
    ctx.drawImage(sprites[1], 32, 32);
  });
}

image.src = 'sprites.png';

关于Promise,可以参考JS异步处理系列一 ES6 Promise

三、worker.js中对createImageBitmap方法的使用
function doCreateImageBitmap(t, e) {
    try {
        var a = getTimeNow();
        t = new self.Blob([t], {type: "image/png"}), 
        self.createImageBitmap(t).then(function (t) {
            var i = {};
            i.url = e, 
            i.imageBitmap = t, 
            i.dataType = "imageBitmap", 
            i.startTime = a, 
            i.decodeTime = getTimeNow() - a, 
            i.sendTime = getTimeNow(), 
            console.log("Decode By createImageBitmap," + i.decodeTime, e), 
            i.type = "Image", 
            postMessage(i, [i.imageBitmap])
        })["catch"](function (t) {
            showMsgToMain("cache:" + t), 
            pngFail(e, "parse fail" + t + ":ya")
        })
    } catch (i) {
        pngFail(e, "parse fail" + i.toString() + ":ya")
    }
}

关于Blob,可以参考H5直播系列一 Blob File FileReader URL

可以看到最终执行了postMessage(i, [i.imageBitmap]),postMessage第二个参数转移了数据的控制权。那么这个方法传入的t和e是啥,可以看看loadImage2方法:

function loadImage2(t) {
    var e, a = t.url;
    e = new XMLHttpRequest,
    e.open("GET", a, !0),
    e.responseType = "arraybuffer",
    e.onload = function () {
        var t, i, r = e.response || e.mozResponseArrayBuffer;
        if (t = new Uint8Array(r), self.createImageBitmap)
            return void doCreateImageBitmap(t, a);
        try {
            startTime = getTimeNow(), 
            i = new PNG(t), 
            i.url = a, 
            i.startTime = startTime, 
            i.decodeEndTime = getTimeNow(), 
            i.decodeTime = i.decodeEndTime - startTime, 
            pngLoaded(i)
        } catch (s) {
            pngFail(a, "parse fail" + s.toString() + ":ya")
        }
    }, e.onerror = function (t) {
        pngFail(a, "loadFail")
    }, e.send(null)
}

这里就能看出,如果createImageBitmap在浏览器中不支持,就会走到下面用PNG解析的路上去,这条路比较复杂,后面再说。而如果支持createImageBitmap的话,走到postMessage(i, [i.imageBitmap])就算OK了。本文第五部分会回到laya.core.js中,看看这个数据要怎么接着处理。

四、WorkerLoader和Loader

上面的WorkerLoader.enable = true;最终会走到__init__方法

//laya.core.js
WorkerLoader.__init__=function(){
    if (WorkerLoader._preLoadFun !=null)return false;
    if (!Browser.window.Worker)return false;
    WorkerLoader._preLoadFun=Loader["prototype"]["_loadImage"];
    Loader["prototype"]["_loadImage"]=WorkerLoader["prototype"]["_loadImage"];
    if (!WorkerLoader.I)WorkerLoader.I=new WorkerLoader();
    return true;
}

这个地方的写法比较神奇,直接把Loader里默认的_loadImage给替换了,当然换之前先备份成_preLoadFun。看一下WorkerLoader的_loadImage方法

/**
*@private
*加载图片资源。
*@param url 资源地址。
*/
__proto._loadImage=function(url){
    var _this=this;
    if (!WorkerLoader._enable||url.toLowerCase().indexOf(".png")< 0){
        WorkerLoader._preLoadFun.call(_this,url);
        return;
    }
    url=URL.formatURL(url);
    function clear (){
        laya.net.WorkerLoader.I.off(url,_this,onload);
    };
    var onload=function (image){
        clear();
        if (image){
            _this["onLoaded"](image);
            }else{
            WorkerLoader._preLoadFun.call(_this,url);
        }
    };
    laya.net.WorkerLoader.I.on(url,_this,onload);
    laya.net.WorkerLoader.I.loadImage(url);
}

url.toLowerCase().indexOf(".png")< 0说明,laya封装的work.js只解析png,别的还是会用_preLoadFun来解析。最后两行添加了一个对url的侦听,然后开始用loadImage去加载

/**
*加载图片
*@param url 图片地址
*/
__proto.loadImage=function(url){
    var data;
    data={};
    data.type="load";
    data.url=url;
    this.worker.postMessage(data);
}

看到this.worker.postMessage(data);,终于是通知worker线程可以加载了。worker线程里的回应如下

onmessage = function(t) {
    var e = t.data;
    switch (e.type) {
        case "load":
            loadImage2(e)
    }
};
五、回到主线程

在本文第三部分说到,如果浏览器支持createImageBitmap的话,worker线程走到postMessage(i, [i.imageBitmap])会交给主线程继续处理。

1.构造函数

function WorkerLoader(){
    /**
    *使用的Worker对象。
    */
    this.worker=null;
    WorkerLoader.__super.call(this);
    var _$this=this;
    this.worker=new Browser.window.Worker(WorkerLoader.workerPath);
    this.worker.onmessage=function (evt){
        _$this.workerMessage(evt.data);
    }
}
__proto.workerMessage=function(data){
    if (data){
        switch(data.type){
            case "Image":
                this.imageLoaded(data);
                break ;
            case "Msg":
                this.event("image_msg",data.msg);
                break ;
            }
    }
}

/**
*@private
*/
__proto.imageLoaded=function(data){
    if (data && data.buffer && data.buffer.length < 10){
        WorkerLoader._enable=false;
        this._myTrace("buffer lost when postmessage ,disable workerloader");
        this.event(data.url,null);
        this.event("image_err",data.url+"\n"+data.msg);
        return;
    }
    if (!data.dataType){
        this.event(data.url,null);
        this.event("image_err",data.url+"\n"+data.msg);
        return;
    };
    var canvas,ctx;
    var imageData;
    switch(data.dataType){
        case "buffer":
            canvas=new HTMLCanvas("2D");
            ctx=canvas.source.getContext("2d");
            imageData=ctx.createImageData(data.width,data.height);
            imageData.data.set(data.buffer);
            canvas.size(imageData.width,imageData.height);
            ctx.putImageData(imageData,0,0);
            canvas.memorySize=0;
            break ;
        case "imagedata":
            canvas=new HTMLCanvas("2D");
            ctx=canvas.source.getContext("2d");
            imageData=data.imagedata;
            canvas.size(imageData.width,imageData.height);
            ctx.putImageData(imageData,0,0);
            imageData=data.imagedata;
            canvas.memorySize=0;
            break ;
        case "imageBitmap":
            imageData=data.imageBitmap;
            if (!Render.isWebGL){
                canvas=new HTMLCanvas("2D");
                ctx=canvas.source.getContext("2d");
                canvas.size(imageData.width,imageData.height);
                ctx.drawImage(imageData,0,0);
                canvas.src=data.url;
            }else
            canvas=imageData;
            break ;
        }
    if (Render.isWebGL)
        /*__JS__ */canvas=new laya.webgl.resource.WebGLImage(canvas,data.url);;
    this.event(data.url,canvas);
}

我们还是先看浏览器支持createImageBitmap的情况,此时i.imageBitmap = t, i.dataType = "imageBitmap",也就是上面switch分支的最后一种情况。此时imageData正是由createImageBitmap返回的ImageBitmap,然后就可以通过event抛出去了,类型是url,和上面laya.net.WorkerLoader.I.on(url,_this,onload);对应上。

六、浏览器不支持createImageBitmap,要自己去解析

上面曾经介绍过,不支持createImageBitmap时会怎么样

function loadImage2(t) {
    var e, a = t.url;
    e = new XMLHttpRequest,
    e.open("GET", a, !0),
    e.responseType = "arraybuffer",
    e.onload = function () {
        var t, i, r = e.response || e.mozResponseArrayBuffer;
        if (t = new Uint8Array(r), self.createImageBitmap)
            return void doCreateImageBitmap(t, a);
        try {
            startTime = getTimeNow(), 
            i = new PNG(t), 
            i.url = a, 
            i.startTime = startTime, 
            i.decodeEndTime = getTimeNow(), 
            i.decodeTime = i.decodeEndTime - startTime, 
            pngLoaded(i)
        } catch (s) {
            pngFail(a, "parse fail" + s.toString() + ":ya")
        }
    }, e.onerror = function (t) {
        pngFail(a, "loadFail")
    }, e.send(null)
}

1.先看一下pngLoaded

function pngLoaded(t) {
    var e = {};
    e.url = t.url, 

    canUseImageData ? 
    (e.imagedata = t.getImageData(), e.dataType = "imagedata") : 
    (e.buffer = t.getImageDataBuffer(), e.dataType = "buffer"),
    
     e.width = t.width, 
     e.height = t.height, 
     e.decodeTime = getTimeNow() - t.startTime, 
     console.log("Decode By PNG.js," + (getTimeNow() - t.startTime), t.url), 
     e.type = "Image", 

     canUseImageData ? 
     postMessage(e, [e.imagedata.data.buffer]) : 
     postMessage(e, [e.buffer.buffer])
}

这里canUseImageData属性在work.js刚开始就会执行到,就是检测一下ImageData能不能兼容

var canUseImageData = !1;
testCanImageData();
function testCanImageData() {
    try {
        cc = new ImageData(20, 20),
        canUseImageData = !0
    } catch (t) { }
}

关于ImageData,可以参考h5 canvas ImageData

2.PNG
上面调用的时候,是这样传入的

t = new Uint8Array(r)
i = new PNG(t)
pngLoaded(i)

看一下大致结构

image.png

我们在外面调用的就是getImageData和getImageDataBuffer这两个方法。具体如何解析png格式,这里就不做分析了。看源码,用的是github png.js

七、WorkerLoad的释放

在H5 worker 系列一 基础知识中有提到:

Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。

那么在laya中使用了WorkerLoader.enable = true;后,加载完一张png后显然是没有关闭的,因为还要加载别的png,还要在运行时加载png。简单来说,Loader的_loadImage已经被替换成WorkerLoader的_loadImage。后面所有加载png的操作,都会交给WorkerLoader处理。我们可以简单测试一下:

1.在worker.js里,增加一个消息响应类型test

//worker.js
onmessage = function(t) {
    var e = t.data;
    switch (e.type) {
        case "load":
            loadImage2(e);
        case "test":
            console.log("cuixu test");
            break;
    }
};

2.发送消息
laya.net.WorkerLoader.I.worker.postMessage({type:'test'});,console消息出来了

3.怎么才能结束这个worker进程
截止laya 1.7.20版本,没有在laya.core.js中提供terminate方法,或者在worker.js中提供close方法。其实我们在enable方法中针对false处理一下即可。

if (WorkerLoader._enable && WorkerLoader._preLoadFun==null){
    WorkerLoader._enable=WorkerLoader.__init__();
}else{
    if(WorkerLoader.I){
        WorkerLoader.I.worker.terminate();
    }
    if(WorkerLoader._preLoadFun){
        Loader["prototype"]["_loadImage"]=WorkerLoader._preLoadFun;
    }
}

4.因为运行时加载新png,这个worker进程最好不要关,但是闲置也是浪费。是不是可以扩展一下,利用它做点别的任务,这就要修改官方的worker.js了。不过想想,还是专项任务,用专项worker吧,用完自己关掉比较好。

八、doCreateImageBitmap失败

在实际运行中,发现console里会有几次这样的parse failInvalidStateError: The source image could not be decoded.:ya,这是怎么回事?
1.在doCreateImageBitmap方法中,有这么一段异常处理

        })["catch"](function (t) {
            showMsgToMain("cache:" + t), 
            pngFail(e, "parse fail" + t + ":ya")
        })

因为createImageBitmap方法返回的是个Promise,所以这里写的不那么标准。其实应该把["catch"]后面的那个function放在then的第二个参数里的,当然最终效果一致。不过console出来的t有点区别,用then第二个参数是DOMException: The source image could not be decoded.默认的打印是这样的parse failInvalidStateError: The source image could not be decoded.:ya

2.加载失败的后果

var onload = function (image) {
    clear();
    if (image) {
        _this["onLoaded"](image);
    } else {
        //失败之后使用原版的加载函数加载重试
        //_this.event(Event.ERROR, "Load image failed");
        WorkerLoader._preLoadFun.call(_this, url);
    }
};

3.随便看一个图片,http://192.168.198.102:8080/libs/res/atlas/lobby.png,原来是路径不对。那只能这样修复了:laya.net.WorkerLoader.workerPath = "worker.js";把workerPath默认的"libs/worker.js"前面的libs去掉。

你可能感兴趣的:(H5 worker 系列二 Laya中使用WorkerLoad解析png)