由于我们的客户端的元素和资源比较多,cocos框架的各种库质量参差不齐,导致了有些地方加载速度实在很慢。并且没有一个统一的内存管理机制导致了整个内存占用不太好控制。
同时手机的硬件环境实在是千差万别,在IOS上,由于CPU和IO比较好,很多东西重算代价倒不大。 但是在Android上,CPU本来就偏弱,然后很多国产性价比机器,零件都缩水在IO设备上,还附加了各种用于节能和降低发热的降频策略,锁CPU策略。导致了很多数据重建的延迟比较高。 然而我们很容易发现,大多数Android的机器的内存都非常高,动辄2-3GB。 所以就希望说我们的应用能够最大化的利用内存作为缓存,在IOS上内存不够时重算,在Android上就拼命地用内存坐缓存,加载loading速度。
于是乎有了个写个LRU算法作为资源管理的想法。并且既然要做,就做得尽量简单、可复用,最好还能到时候服务器上也用。
由于最主要还是由客户端的问题引起的,所以最先还是考虑客户端的需求。目标如下:
为解决第一个问题,我们把LRU对象池分为两部分,LRU管理器和对象池。其中LRU管理器负责管理所管辖的多种不同类型对象的对象池,然后负责判定LRU算法的缓存和失效淘汰规则。而对象池中则实际负责保存对象缓存。
对第二个问题,考虑到在IOS上有专门的事件通知内存报警,但是在Android没有,所以为了简便起见,统一设置告警走类似IOS的报警作为LRU的主动GC操作。这样的话Android需要自己判定低内村并触发内存回收。 关键在于回收的同时需要动态调整阈值,以适应当前的内存总量。目前的策略是单触发主动回收时,各项阈值减半,并大量回收资源。而push数量过多时触发一次资源回收,但是随之会将阈值的上限+1。总体上有点像TCP的拥塞算法。
最重要的就是LRU算法设计,为了减少LRU管理器的消耗,默认情况下对LRU资源池的操作都可以认为是O(1)的。
实际缓存池的实际使用过程中还是碰到了一些问题的。首先是cocos的很多组件本身有缓存机制,比如dragonbones和spine,还有sprite对贴图文件的缓存,对于这种对象实测缓存的影响不是特别大。
影响特别大得就是cocostudio创建的对象,我们里面实施了两层缓存。第一层是把csb文件缓存进来,这样可以减少IO操作,但是后来发现根据csb文件创建cocostudio节点反而花费了更多的时间(2/3的时间耗在这里)。 所以后来不得不对cocostudio创建的节点做缓存。然而如果是一开始就使用这个缓存的话就比较容易发现问题,我们中途开始切入这个缓存的话就发现。我们的很多UI模块代码并没有特别去重置CCNode,而是依赖析构作为资源回收和重置。 这导致了很多地方如果回收作为缓存的话,这次改动的地方,就变成下次读入以后的默认值。包括上一次添加到ListView里的节点都还在。
为了解决这个问题,我们不得不改动了cocos的源代码,对CCNode里一些公用的属性,比如children、position、anchor等属性做了快照。然后下次pull完以后恢复到快照。本来也想直接使用cocos的clone函数,无奈cocos的clone接口实现不全最后没有使用。 而cocostudio创建的一些子类的ui对象比如ListView、ImageView等等里那些不属于CCNode的属性,就要求使用前手动reset,因为如果每个子类都去加缓存的话对cocos的改动有点大,怕以后不好merge。
最后就是实际释放缓存的过程中,有些数据是在切换场景的时候释放的,这时候很多对资源的引用都会清空,不会再次使用。 比如sprite的贴图,清理的时候如果场景里还有引用到的地方,是不会清除的。 再比如dragonbones的骨骼和贴图,dragonbones自己有一层缓存和引用记录,但是它做得不好,在缓存清理的时候不通知被引用的Node,然后会导致被引用的Node在渲染时崩溃。所以清除dragonbones自身的缓存的同时还必须清理所有已有的对象。 而且特别是dragonbones和spine,即便目前没有使用在一场战斗中十有八九马上也会用到。战斗中卡一下的体验是非常不好的。所以对这些资源都会延后释放。
延后释放也会碰到一个问题,就是可能短时间内内存并没有被清理出来,然后会频繁调用主动GC。于是就有了上面第12条提到的限制。但是特别是IOS既然到了内存告警,最好先释放一部分出来。以备后用。 所以我们的缓存回收里加了一些分级,有些对象常规内存回收不做,紧急内存(IOS内存告警)回收会尽量释放一些应该不会再被引用到的资源。
暂时还未实施,但是这样的LRU算法设计也考虑了服务器上可能可以使用的场景。比如聊天服务器按活跃度来缓存玩家或者频道的聊天数据,淘汰冷数据。
另外在服务器端虽然不用太多地考虑内存回收问题,但是可以利用这个算法管理器的过期机制来提供定期保存的功能。甚至实现定期脏数据保存的功能。
**定期保存: ** 每个对象只push一次,在gc时保存并重新push进pool **定期脏数据保存: ** 每个对象在写脏时先pull再push一次,在gc时保存 并且这些功能都可以利用lru管理器的各类上限来实现过载保护
以上的代码位于: https://github.com/owent-utils/c-cpp/blob/master/include/MemPool/lru_object_pool.h 单元测试见: https://github.com/owent-utils/c-cpp/blob/master/test/case/LRUObjectPoolTest.cpp
Written with StackEdit.