美文网首页
H5 worker 系列二 Laya中使用WorkerLoad解

H5 worker 系列二 Laya中使用WorkerLoad解

作者: 合肥黑 | 来源:发表于2018-10-17 18:11 被阅读203次

    本文主要介绍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 一个图像源, 可以是一个 <img>, SVG <image>, <video>, <canvas>, HTMLImageElement, SVGImageElement, HTMLVideoElement, HTMLCanvasElement, Blob, ImageData, ImageBitmap, 或 OffscreenCanvas 对象.

    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解

          本文链接:https://www.haomeiwen.com/subject/bitrzftx.html