高性能封装storage的使用

大家好,我是来自【欢乐互娱】的欢乐小学生,之前分享了一篇部分优化大致思路的文档,今天就选择其中一个进行具体解析,附带开箱即用源码

优化契机
前段时间一个同事发现游戏在出现明显卡顿,排查后发现是频繁调用本地数据读写造成的

封装setstorage函数的原理
1:每次get和set都在内存中缓存,优先读取内存中的数据
2:对可以检测玩家切后台,退出游戏操作的机型,则缓存本地数据在切后台,退游戏时候进行
3:对不可获取的平台,则在数据发生改变时写入本地缓存

原理图

开箱即用源码带注释

/**

 * 重写读取本地数据的接口

 * 将数据做一层缓存,优先从缓存中读取

 */

export default class LocalStorage

{

    //被重写的原方法

    private m_stOldSetStorage: Function;

    private m_stOldSetStorageSync: Function;

    private m_stOldGetStorage: Function;

    private m_stOldGetStorageSync: Function;

    private m_stOldClearStorage: Function;

    //所有缓存数据(取出来的缓存数据有可能是空的)

    private m_mapLocalData: {[key: string]: any} = {};

    //判断当前数据是否研已经被获取或者写入

    private m_mapGetTag: {[key: string]: boolean} = {};

    //数据是否已经被更新

    private m_bDirty: boolean = false;

    //是否支持监听游戏关闭或者结束(支持则所有写入放在游戏结束)

    private m_bSupListen: boolean = false;

    private readonly GAME_DATA: string = "game_data";

    //被优化的io次数

    private m_iCount: number = 0;

    //是否打开日志显示

    private m_bOpenLog: boolean = false;

    //同步写入本地缓存

    private SetStorage(key: string,value: string | any): void

    {

        if(this.SetData(key,value,true) && !this.m_bSupListen)

        {

            this.AddLog();

            this.m_stOldSetStorage(key,value);

        }

    }

    //异步写入本地缓存

    private SetStorageSync(key: string,value: string | any): void

    {

        if(this.SetData(key,value,true) && !this.m_bSupListen)

        {

            this.m_stOldSetStorageSync(key,value);

        }

    }

    //异步获取本地缓存

    private GetStorage(key: string,callback: Function): void

    {

        if(this.m_bSupListen || this.m_mapGetTag[key])

        {

            callback(this.GetData(key));

        }

        else

        {

            let callBack2 = (val) =>

            {

                this.SetData(key,val);

                callback(val);

            }

            this.m_stOldGetStorage(key,callBack2);

        }

    }

    //同步获取本地缓存

    private GetStorageSync(key: string): any

    {

        if(this.m_bSupListen || this.m_mapGetTag[key])

        {

            return this.GetData(key);

        }

        else

        {

            let data = this.m_stOldGetStorageSync(key);

            this.SetData(key,data);

            return data;

        }

    }

    //清理本地缓存

    private ClearStorage(): void

    {

        this.m_mapLocalData = {};

        this.m_bDirty = false;

        this.m_stOldClearStorage();

    }

    //写入数据到字典中

    private SetData(key: string,value: string | any,isSave: boolean = false): boolean

    {

        this.m_mapGetTag[key] = true;

        if(this.m_mapLocalData[key] != value)

        {

            this.m_mapLocalData[key] = value;

            this.m_bDirty = true;

            return true;

        }

        else

        {

            if(isSave)

            {

                this.m_iCount++;

            }

            return false;

        }

    }

    //从字典中获取数据

    private GetData(key: string): any

    {

        this.AddLog();

        return this.m_mapLocalData[key];

    }

    //统计优化的缓存次数

    private AddLog(): void

    {

        this.m_iCount++;

        if(this.m_bOpenLog)

        {

            console.log("减少缓存io次数:",this.m_iCount);

        }

    }

    //重写sdk的部分方法,加入缓存实现

    public ChangeStorageFunc(): void

    {

        //缓存原有的数据写入方法

        let plat = Core.PlatformSDK;

        this.m_stOldSetStorage = plat.SetStorage;

        this.m_stOldSetStorageSync = plat.SetStorageSync;

        this.m_stOldGetStorage = plat.GetStorage;

        this.m_stOldGetStorageSync = plat.GetStorageSync;

        this.m_stOldClearStorage = plat.ClearStorage;

        //重新注入新的实现

        plat.SetStorage = this.SetStorage.bind(this);

        plat.SetStorageSync = this.SetStorageSync.bind(this);

        plat.GetStorage = this.GetStorage.bind(this);

        plat.GetStorageSync = this.GetStorageSync.bind(this);

        plat.ClearStorage = this.ClearStorage.bind(this);

        if(this.m_bSupListen)

        {

            this.m_mapLocalData = JSON.parse(this.m_stOldGetStorageSync(this.GAME_DATA)) || {};

        }

    }

    //保存所有数据当游戏关闭或者退出时,或延迟保存

    public SaveAll(): void

    {

        if(this.m_bSupListen && this.m_bDirty && this.m_stOldGetStorageSync)

        {

            this.m_bDirty = false;

            this.m_stOldGetStorageSync(this.GAME_DATA,JSON.stringify(this.m_mapLocalData));

        }

    }

}

有兴趣的可以关注【数字媒体与游戏开发】公众号,后续我会补充很多实现的细节, 我们可以深入交流

5赞

先点赞再看

太棒了,欢迎大佬帮忙指正

看完了~

  1. 思路非常赞,所有key在一个对象上,整个游戏的所有缓存都在这个对象上,读写也只是一个key,极大减少了IO
  2. 我看到代码中有一个保存的时机是在应用隐藏或者退出时,这里要求很严格,比如如果应用异常崩溃了,那么可能不会持久化到本地,下次进入可能会丢失缓存数据
  3. 另外一个个人感觉,「能不重写就不重写」,可以弄个 LFLocalStorage 之类,使用时使用这个类。出发点是,项目可能也会 npm i 第三方库,不清楚他们对于 LocalStorage 的使用程度,冒然替换不知道会引发什么问题
2赞

实际上localstorage在Android端是用sqllite,读写频繁或者数据量大是会造成卡顿,推荐用jsb.fileUtils读写+节流函数

1:第二个主要是对支持监听的切换后台和退出游戏的渠道使用的,有一个变量判断游戏是否支持,突然崩溃可能会导致游戏缓存数据丢失。如果不支持则在数据发生变动时立刻读写
2:第三个其实重写是对我们项目原有的storage进行重写,因为已经大量调用,不好再重写一个新类,其实我们项目也是对storage进行了封装,只是比较简单,使用重写的办法可以减少很多代码修改工作也不会产生任何影响
3:感谢大佬指导 :kissing_heart:

我这个是上层封装,是针对所有渠道的。不过还是谢谢大佬指导 :smiling_face_with_three_hearts:

给大佬打 call :100:

个人觉得这个方案不太好,遇到游戏卡死。闪退的时候没办法,我是通过有修改操作的时候缓存,然后定时后进行写入,中途如果还有修改操作就延迟定时器,同样如果数据过大就分批次写入

1:应该这样说SaveGameDateOnHide这个函数在隐藏或者关闭时候一定会触发。其他时候都是延时触发,可以有效减少缓存失败的情况
2:我觉得localstorage的数据量一般不大,没必要分批次写入,分批次可能还会照成更多的io操作,我是存成一个数据一起写入
3:谢谢大佬指点,我这就优化一波实现

另外,你这个存储器没有解决类型提示和默认值的问题,我之前写过一个存储方案,可以解决存储数据类型提示和默认值的问题,就是导出一个对象当做存储数据,然后存储器初始化时重载对象里面所有键的 get set 并读取本地存储数据覆盖默认值,然后在 get set 回调里面去更新数据

这个视项目而定,你可以自己搜搜,论坛里面还是有人反馈卡顿是因为大量数据同时写入引起的

是,我这个主要是优化之前项目代码。所以setstorage,getstorage都是私有的,没有做类型提示和默认值这块的设计

感谢大佬提醒,这个我要思考一下怎么优化,因为如果拆封成多个数据的话整个流程会更加麻烦

1赞