Egret引擎内存优化最佳实践

1,703 阅读5分钟

Egret引擎内存优化最佳实践

最近我们在开发一款服饰类的中度游戏项目,里面用到了大量的服饰图片。游戏测试过程中发现内存一直在不断增长,在部分低端机型下出现闪退现象,下面来说说解决过程。

毫无疑问,这是一个对内存进行有效管理的问题。明确了方向,那我们就从下几步进行排查: 1、排查是否存在内存泄露,看js对象的创建后是否有删除引用,是否有移除侦听; 2、使用对象池,减少重复对象的频繁创建 3、优化图片素材尺寸,减少不必要图片的加载 这样改完之后,测试发现效果并不明显,内存依旧会不断增加。

  • 难道是图片内存没有释放? 看着这么大的内存,基本可以明确是图片的原因

  • 那图片内存要怎样才能释放? 检查代码已经没有地方对图片进行引用了,按理说应该会被垃圾回收器回收啊!

  • 怎么办? 查引擎文档,发现有个RES.destroyRes的API,作用是:销毁单个资源文件或一组资源的缓存数据。 再阅读源码,发现Egret引擎会对资源进行引用,如果不调用RES.destroyRes进行资源释放,图片内存不会进行回收! 结论:通过引擎加载的所有图片,如果不手动销毁对象,那么图片会一直缓存在内存里面。加载的图片越多,内存占用越大,直到闪退。Egret引擎本身没有任何内存管理策略。

  • 再接下来怎么办? 知道了原因,就好办了,针对每个功能模块,在合适的位置,释放图片资源。 游戏素材一般包括以下这些: 1、 UI图片,比如弹窗、界面、按钮,一般打包进代码包 2、 远程加载的图片,比如大背景,根据用户数据特定加载显示的图片 3、 Mp3声音文件

我们制定了以下资源内存管理的策略:

  1. 针对大的UI素材 png、jpg等,针对每个功能模块,在界面关闭的时候 调用 destroyImage释放图片资源。
  2. 针对 远程http的的图片, A、非列表item使用的图片用 reSetImage方法代替设置img.source, 同时关闭界面时 调用destroyImage释放图片资源。 B、滚动列表里面用到的图片 用HttpImageCacheManage 管理,在关闭界面时释放图片资源。
  3. mp3声音文件,针对每个功能模块,在关闭界面时释放

下面为几个核心方法的代码:

/**
  * 释放资源,一般是释放 eui.image的资源
  * 使用场景: 在界面关闭的时候调用;
  * 使用示例:destroyImage(this.bg);
  * 使用建议:尺寸大于1024的图片对象调用本方法释放掉
  * 参数:支持 资源名,url,或image对象
  */ 
 public destroyImage(name:string | eui.Image){
     if(name instanceof eui.Image){
         if(name.source){
             let source = name.source;
             if(DEBUG) console.warn("释放:" + source)
             name.source = "";
             if(RES.getRes(<any>source))
                 return RES.destroyRes(<any>source);
         }
         return false;
     }
     if(DEBUG) console.warn("释放:" + name)
     if(RES.getRes(<any>name))
         return RES.destroyRes(<any>name);
     return false;
 }
 /*
  * 释放Image资源,并设置image新的source 
  * 使用场景: 用本方法 替代 img.source = xxxx; 
  * 使用示例:reSetImage(this.bg, xxxx);
  * 使用建议:尺寸大于512的,或使用过1次后不再使用的图片对象调用本方法释放掉
  */
 public reSetImage(name:eui.Image, newSource:string, nowDestroy:boolean=false){
     if(!name) return null;
     if(name.source){
         let source = <string>name.source;
         if(source != newSource){
             if(DEBUG) console.warn("释放:" + source)
             if(nowDestroy){
                 if(RES.getRes(source))
                     RES.destroyRes(source);
             }
             else{
                 name.once(egret.Event.COMPLETE, ()=>{
                     if(RES.getRes(source))
                         RES.destroyRes(source);
                 }, this)
                 name.source = newSource;
             }
         }
         return;
     }
     name.source = newSource;
 }
     
enum HttpImageTYPE { type1, type2, type3}; //图片类型分类
/**
 * 图片缓存管理:针对部分不适合在更换source时释放的Http图片,采用标记的形式管理,在界面关闭的时候释放
 * 使用方法:
 * item里面:  HttpImageCacheManage.add(HttpImageTYPE.type1, url);
 * ui.hide里面: HttpImageCacheManage.destroy(HttpImageTYPE.type1);
 *  */
class HttpImageCacheManage {

    private static pools = {};

	public constructor() {
	}

    //把素材按类型添加到列表中,方便后面释放
    public static add(type:number, url:string){
        if(!this.pools[type]) this.pools[type] = {};

        if(!this.pools[type][url])
            this.pools[type][url] = 1;
    }

    //根据type类型释放对应的素材
    public static destroy(type:number){
        let list = this.pools[type];
        for(var key in list){
            let texture = RES.getRes(key);
            if(texture && texture.$bitmapData){
                let hashCode = texture.$bitmapData.hashCode;
                var tempList = egret.BitmapData["_displayList"][hashCode];
                if(tempList && tempList.length > 0) continue; //表示正在使用
            }
            if(DEBUG) console.warn("释放:" + key)
            RES.destroyRes(key);
            delete list[key]
        }
        if(DEBUG){
            if(HttpImageCacheManage["dtime"]) egret.clearTimeout(HttpImageCacheManage["dtime"]);
            HttpImageCacheManage["dtime"] = egret.setTimeout(()=>{
                console.warn("释放资源后内存占用:");
                RES.profile();
            }, this, 20);
        }
    }

}

请特别注意的是: 在释放图片资源时需要判断图片是否正在使用,比如A界面用到了图片1,B界面也用到了图片1,B界面关闭的时候就不能把图片1释放掉,图片资源(egret.Texture)在内存里面是共用的,如果释放了,在A界面图片就显示不出来了。

Egret引擎没有提供egret.Texture 是否正在使用的方法,需要我们自己读取私有变量来判断。

public static checkIsUsing(resName:string){
	let texture = RES.getRes(resName);
    if(texture && texture.$bitmapData){
         let hashCode = texture.$bitmapData.hashCode;
         var tempList = egret.BitmapData["_displayList"][hashCode];
         if(tempList && tempList.length > 0)
				return true; //表示正在使用
     }
	return false;
}

通过以上策略的使用,图片内存得到了有效管理,再重度的游戏,都可以在低端机上正常运行。而且,代码修改成本很低!

最后,以上方法是基于 Egret引擎RES组件管理素材。有些开发者没有使用RES来管理素材,而是直接使用 egret. URLLoader 或 egret. ImageLoader 来加载的图片素材,这种情况需要怎样释放图片内存呢? 答:只需要针对不用的图片资源(egret.Texture)调用dispose方法、并删除引用就ok了!

本文未经允许,禁止转载~~