易游网-易游模拟器

 找回密码
 立即注册
查看: 4395|回复: 1

[游戏开发] cocos2dx lua客户端性能优化【通用篇】

[复制链接]

3382

主题

3401

帖子

38

积分

超级版主

Rank: 8Rank: 8

积分
38

技术达人

发表于 2021-1-11 14:38:19 | 显示全部楼层 |阅读模式

1、性能指标
移动端最为直观的性能指标是CPU、内存、渲染等
游戏的lua工程里配置 CC_SHOW_FPS = true
我们的移动端会显示几个性能指标
GL verts:OpenGL每帧发送给显卡的顶点数量
GL calls:每帧OpenGL绘制次数
60.2/0.004 帧率(fps),画面每秒传输帧数。前一个数字代表实际帧数,
后一个数字代表这一帧的执行时常

2、开启cocos2dx的内存信息我们一般可以查看到如下内容:
availbytes: 当前可用的内存。除以1024可得,可用1474M内存(≈1.4G)。
totalbytes: 物理内存。除以1024,即设备总共可用2047M内存(≈2G)。
threshold:报警线,模拟器上默认是15%的物理内存。
lowMemory:内存剩余量是否过低;当availbytes小于threshold就是true。
luaMemory:lua对象的内存占用量。

3、卡顿定位---性能指标FPS
FPS(帧率):帧率代表每秒刷新次数。即一帧为多长时间。例如:fps 60 表示每秒60帧,即每帧1/60 ≈0.016s;fps 30 即
每帧 1/30 ≈ 0.03s
帧率越高,刷新越快,越流畅!即60帧比30帧流畅,但是会消耗更多的CPU。
每一帧刷新会执行定时器、渲染、脚本逻辑、动画等。Fps 设置为60的时候,代表,我们每一帧执行的内容时常
不能超过0.016s,一旦超过该时常,实际帧率就会下降,相应的刷新慢,就会有卡顿感。
那么,如果我们的FPS性能显示为  60.2/0.004,就代表,我们当前的实际帧率为60.2帧,当前帧的执行时常
为0.004s,非常流畅。假如该值为,15.1/0.158,就会有肉眼可见的卡顿了。
我们可以通过直观的查看这个值,来确认当前是否卡顿。实际帧率50左右,就已经算是比较流畅了,当实际
帧率低于30帧就会有一定的卡顿,当低于20就会有明显卡顿,低于10的话,画面就如幻灯片一般了。。。
有时候我们需要查看帧率的波动,来判断我们的游戏是否真的流畅,因为有些卡顿并非肉眼可见,但是帧率会带给我们启示。
越高的帧率代表每秒刷新的次数越多,CPU的执行就越频繁,实际对设备的性能要求也越高。

4、卡顿定位---性能指标GL calls
GL calls(每帧绘制调用次数):
OpenGL是我们移动端引擎的渲染库。使用它的接口,进行图像的渲染显示。
每一帧的时间长度可以近似计算为:
帧时长 = 渲染时长 + 其他业务处理时长
按照流畅的60帧设定,帧时常最大0.016s的话, GL calls越多,就代表我们的渲染调用越多,对性能产生影响。
有时候,我们的业务逻辑代码其实很少,但是仍然很卡,就可能是GL calls过高引起的。
我们应该尽量减少GL calls的大小,无论当前的游戏是否卡顿。理想值是100左右,超过200就要好好考虑优化一下。
由于每帧都要渲染界面,所以,calls越高,则对应性能压力越大。
理论上,我们越复杂的界面,calls越高;但是不恰当的界面排布,也会
使calls毫无意义的升高。
一般2d棋牌的游戏界面calls基本就是100~150左右,超过这个范围,我们就要留心了。


5、卡顿定位---性能指标 GL verts
GL verts(每帧发送给显卡的顶点数目):
OpenGl  会以顶点数组为参数,渲染图形。我们的图形越多,顶点数就会越多。
顶点数越大,对传输GPU的带宽压力越大。但是我们的游戏渲染体量,基本不用考虑优化,除非大型3d游戏。

6、卡顿定位---内存
内存(内存泄漏的危害。。。)
通过原生接口获取到memoryinfo.一般可以获取到
memoryInfo.availbytes是实际当前可用的内存【可以理解为剩余内存】
memoryInfo.totalbytes是设备的内存上限
memoryInfo.threshold是设备剩余内存过低的警报,默认是15%的设备总内存
memoryInfo.lowMemory返回当前是不是触发了低内存报警
通过 collectgarbage(‘count’)*1024 获取了当前lua虚拟机内,分配的内存
如果availbytes一直在降低,需要慎重考虑一下是否我们内存泄漏了。内存泄漏分两个方面:
1.我们的cocos资源的泄漏,retain和release要配对
2.lua的全局变量不断的增加,它们不会被GC清理掉。表的key和value如果是强引用,也是不会自动清理的
一般情况下,lua本身的内存泄漏量级是很小的,如果是cocos资源泄漏,会是一个很大的数字。慎重使用retain。
内存泄漏的概念:
1.Lua的cocos接口,实际上是调用了内部的C++代码,创建了节点,分配内存资源。正常情况下,节点有比较合理的生命周期,但是我们代码会手动进行retain,如果release不及时,会导致内存泄漏
2.除了cocos资源的内存,lua本身也可能会有内存泄漏。本身lua脚本自带GC,没有内存泄漏概念的。但是Lua的GC对全局表,并不会释放,那么就会导致一些内存一直都不进行释放。
介绍一下lua表的强引用和弱引用
说明cocos中内存的几个类型:
1.c++的这种内存,它导出给Lua调用,然后内部分配资源。所有的场景资源,几乎都是c++
2.Lua的内存,lua本身是由一个c实现的虚拟机拉起来的,它本身也会分配内存,比如一个Local变量,全局的表等,这一部分内存跟C++资源相比非常小


7、卡顿定位---性能检测工具
正所谓,工欲善其事必先利其器!
虽然,我们已经了解了各种性能指标。小范围的性能判断,我们可以通过加测试
代码,肉眼观察去发现。但是,如果对产品整个的性能做一个评估,那么还是需要使
用一些工具才行。
1.wetest 腾讯的质量开放平台,可以生成移动端性能报告
通过连接wetest,运行我们的产品,可以得到非常详细的性能报告。
2.profiler 引擎或脚本性能分析工具
一种侵入式的代码时间戳工具,我们可以在一个范围内,获取运行的时间分布和
调用的堆栈。

8、卡顿定位---wetest
Wetest:移动端测试平台。对产品进行一下整体的性能评测非常重要。
操作步骤:
1.官网登陆->产品->性能测试->客户端性能->下载wetest助手/免root助手
2.在PC上运行免root助手;在手机上运行wetest助手,根据官方的教程,启动我们的游戏,开始玩。
3.运行一段时间游戏,然后退出到wetest,停止检测。生成测试报告,并上传。
4.在wetest官网上,我们的账号那里,可以找到我们已经上传过的报告。

Wetest测试参数 Wetest的建议值
FPS均值         25
CPU小于60%比率  90%以上
FPS大于18的比率 90%
FPS内存峰值     700M以下
DrawCall峰值    250
网络流量        <5.12KB

Wetest的统计数据非常详细。甚至包括耗电量、CPU温度、电池温度、内存占用的图标等
对于我们来说,起作用:
1.大体来判断是否通过wetest的测试
2.通过卡顿点和截图,来分析卡顿的操作

报告中我们可以查看到,CPU、内存、FPS等信息
其中,性能评分,通过颜色有三档,基本上在第一档的话还是可以的
2.Fps均值,可以查看疑似卡顿点的截图
3.CPU和内存的图表走势【可以分析内存泄漏的问题】

9、卡顿定位---profiler代码
Profiler:对某些代码块进行性能评估。
        当我们使用wetest确定了卡顿的大体位置时候,我们恐怕仍然难以准确定位瓶颈的位置。我们需要对代码进行性能评估,这样,我们就必须在代码上动手脚。Profiler就是这样一个工具,它在代码调用的开始和结束进行设置,这样可以获取函数调用的堆栈和相应的时间消耗,得到性能细节。
一、引擎提供的profiler
        引擎提供的profiler工具,需要使用定制的引擎。
二、Lua代码的profiler
注意:无论是lua或者引擎提供的工具,都会增加CPU消耗!也就是,如果你按照60帧,每帧0.016s来判断,函数调用时间,可能会不准确,毕竟还有profiler的消耗。
Profiler.lua文件代码,如下,需要自己拷贝。
local profiler = class('profiler')

profiler._ENABLE = 0

function profiler:start()
        if profiler._ENABLE ~= 1 then return end
        self._REPORTS                = {}
        self._REPORTS_BY_TITLE = {}
        self._STARTIME = os.clock()
        debug.sethook(profiler._profiling_handler, 'cr', 0)
end

function profiler:stop()
        if profiler._ENABLE ~= 1 then return end
        self._STOPTIME = os.clock()
        debug.sethook()
        local totaltime = self._STOPTIME - self._STARTIME
        table.sort(self._REPORTS, function(a, b)
                return a.totaltime > b.totaltime
        end)
       
        --报告输出
        print("*****************************************************************************************************************************************")
        for _, report in ipairs(self._REPORTS) do
                --输出消耗占比
                local percent =(report.totaltime / totaltime) * 100
                if percent < 1 then
                        break
                end               
                printf("%10.3f, %10.2f%%, %7d,     %s", report.totaltime, percent, report.callcount, report.title)
        end
        print("*****************************************************************************************************************************************")
        printf("[Profiler]during time = %f", totaltime)
end

function profiler:_profiling_call(funcinfo)
        local report = self:_func_report(funcinfo)
        assert(report)
        report.calltime        = os.clock()
        report.callcount = report.callcount + 1
end

function profiler:_profiling_return(funcinfo)
        local stoptime = os.clock()
        local report = self:_func_report(funcinfo)
        assert(report)
        if report.calltime and report.calltime > 0 then
                report.totaltime = report.totaltime +(stoptime - report.calltime)
                report.calltime = 0
        end
end

function profiler._profiling_handler(hooktype)
        local funcinfo = debug.getinfo(2, 'nS')
        if hooktype == "call" then
                profiler:_profiling_call(funcinfo)
        elseif hooktype == "return" then
                profiler:_profiling_return(funcinfo)
        end
end

function profiler:_func_title(funcinfo)
        assert(funcinfo)
        local name = funcinfo.name or 'anonymous'
        local line = string.format("%d", funcinfo.linedefined or 0)
        local source = funcinfo.short_src or 'C_FUNC'
        return string.format("%-30s: %s: %s", name, source, line)
end

function profiler:_func_report(funcinfo)
        local title = self:_func_title(funcinfo)
        local report = self._REPORTS_BY_TITLE[title]
        if not report then
                report ={
                        title = self:_func_title(funcinfo),
                        callcount = 0,
                        totaltime = 0
                }
                self._REPORTS_BY_TITLE[title] = report
                table.insert(self._REPORTS, report)
        end
        return report
end

return profiler

写个测试demo来试下profiler.lua
local profiler = require "Profiler.lua"
profiler._ENABLE = 1
profiler:start()
local test_func = function(n)
        for i = 1, n do
                print("test_func", i)
        end
end

local test_func1 = function(n)
        for i = 1, n do
                print("test_func1", 1)
        end
end

for i = 1, 100 do
        test_func(i)
        if i % 2 == 0 then
                test_func1(i)
        end
end

profiler:stop()

运行上面demo就能看到各个函数的代码位置,执行次数,执行耗时。

10、卡顿分析---卡顿原理的分析
看一段引擎代码:
timeBeginPeriod(1);
while(!glview->windowShouldClose())
{
        QueryPerFormanceCounter(&nNow);
        if(nNow.QuadPart - nLast.QuadPart > _animationInterval.QuadPart)
        {
                nLast.QuadPart = nNow.QuadPart - (nNow.QuadPart % _animationInterval.QuadPart);
                director->mainLoop();
                glview->pollEvents();
        }
        else
        {
                Sleep(1);
        }
}
timeEndPeriod(1);

void DisplayLinkDirector::mainLoop()
{
        if(_purgeDirectorInNextLoop)
        {
                _purgeDirectorInNextLoop = false;
                purgeDirector();
        }
        else if(_restartDirectorInNextLoop)
        {
                _restartDirectorInNextLoop = false;
                restartDirector();
        }
        else if(!_invalid)
        {
                drawScene();
               
                // release the objects
                PoolManager::getInstance()->getCurrentPool()->clear();
        }
}

分析我们看起来界面会卡顿的原因:
主循环中包含的内容:渲染、定时器、事件、动画等。
卡顿的原因是,刷新频率下降,导致画面看起来定格,刷新间隔太大,导致连贯性受到影响。

究竟是什么导致了卡顿
1.FPS帧率
2.游戏主循环
3.渲染界面
4.业务逻辑:定时器、脚本执行、网络回调等
卡顿实际上就是我们的实际帧率太低。影响实际帧率的地方,
就是渲染消耗时长和逻辑消耗时长。


Cocos2dx 事件循环分析:
_animationInterval.QuadPart是设置的fps的帧时间当两次循环的时间间隔超过了fps设置的帧间隔,就会启动刷新mainLoop,在mainLoop中,会执行Scene->drawScene在drawScene中执行_scheduler->update(_deltaTime);无论是定时器、动画等,都由这里驱动。
在mainLoop中还会进行非常重要的渲染工作。可以看到,cocos2dx把用户代码和引擎核心的渲染、事件代码都放在一起进行执行,无论是界面渲染消耗,还是用户代码消耗,都会影响实际的帧率。渲染和用户代码,两者还会互相影响,一旦刷新降低,渲染频率下降,界面自然就会卡顿,同理,定时器也不再准确。
        所以,根据cocos引擎的特性,我们可以把卡顿归类一下。不同的类型,由不同的方式去解决。
        1.由于界面资源加载导致的卡顿。它牵涉到io操作,资源解析,渲染等,非常耗时。
        2.由于资源量大,渲染时长导致的卡顿。 Draw calls值过高。
        3.由于游戏脚本逻辑计算耗时过长,导致卡顿。包含,变量的分配申请、大量的定时器计算、脚本计算等。
        有些卡顿,是由于我们代码不当,导致的,它需要我们进行优化,比如之后要说的合图,减少draw calls。
但是,还存在一些卡顿,是无法避免的。因为按照60fps设置,每一帧仅仅只有0.016s,刨去渲染,我们能够计算的量是有限的,这样的计算要么需要异步执行,要么需要预加载。


11、卡顿分析---资源和缓存
移动端有以下几种资源:
1.图片2.音频3.配置4.csb 等
Cocos引擎同样也提供了对资源的缓存功能
1.纹理缓存 textureCache (支持异步加载)2.精灵帧缓存 spriteFrameCache(支持异步加载) 3.动画(action)缓存 animationCache(不支持异步加载)
4.音频缓存(引擎默认缓存 Music[preloadMusic] Sound[preloadEffect]) 5.timeline序列帧动画缓存
6、csb节点资源 不缓存;但是依赖的资源会自动缓存比如纹理,不支持异步加载
7、csb序列帧动画 缓存;首次读取csb解析,之后的创建均由缓存实例进行clone 不支持异步加载。
每一张真实的图片(png,jpg等)就是一个纹理(texture);plist可以把多个图片合成一张图,即一个纹理,通过该纹理可以获取合并的不同图片资源,即精灵帧(spriteframe)。
csb内部描述的是不同类型的ui资源,和其依赖的图片、音频、帧事件等。
我们可以手动对某些资源进行缓存;其实, cocos会在加载csb资源的时候,自动将依赖的资源缓存。

12、卡顿分析---资源加载
cocos2dx对csb文件加载时,有两个接口。createNode,createTimeline
1.createNode 在调用的时候,会把文件读取[IO读取],然后解析,把所有依赖的资源加载起来,返回node。cocos引擎会自动缓存加载起来的资源,这样,下一次createNode的时候,依赖相同的资源,就不需要再加载了。
2.createTimeline 在调用的时候,会把文件读取[IO读取],然后解析成action,解析的过程不需要加载任何资源。然后会把action直接缓存起来。下次获取相同的action,会通过已经创建的action直接clone一份新的。
众所周知,io操作非常耗时,所以createNode其实性能不佳的,它每一次调用都需要读取本地文件并解析。
而createTimeline会比createNode好一点,它在创建之后,会通过clone的方式创建。对内存的操作,比IO要快很多。
不幸的是,引擎并未给我们提供异步加载csb的接口。
所以,如果某一帧需要大量创建节点,导致影响帧率【比如棋牌游戏的结算界面】,我们可以有两种优化方式:
1.提前把节点依赖的资源预加载,这样可以提高createNode的性能
2.提前createNode,把无法优化的卡顿挪到loading界面去
3.如果哪些资源带有异步接口,可以使用异步的方式,不阻塞在主循环中

如果我们发现性能的瓶颈在于资源的加载我们该如何优化呢?
1.异步加载,如果资源可以通过异步加载的方式完成,那么它就不会占用主循环的时间
2.选择预加载,在Loading的时候,提前加载好texture、spriteFrames等。它们会提高csb的创建速度
3.某些比较大的csb,它们通过预加载依赖资源无法达到流畅的话,只能预加载整个csb,并且retain保存。在使用的时候添加到场景
4.如果资源实例可以重复使用,请不要重复创建。哪怕它是timeline,clone也是要走c++new操作的,内存分配要走系统调用,一旦量大,也是很大的负担。

资源加载这一类的卡顿,顺便说下,可以通过查看引擎代码,分析接口的特性
建议:不要在一帧之内大量的创建csb,而且该csb还是首次创建,那么会非常消耗cpu

13、卡顿分析---渲染优化
总结一些比较常见的点:
1.Cocos studio中,尽量把相同渲染方式的节点相邻排布【中间可以有空节点】
2.由于渲染合并的基础是纹理资源要一致,那么可以把控件中依赖的资源打包成一个plist,减少纹理个数
3.多个文本渲染会增加draw calls,但是它们的切换代价很小。
4. ClippingNode、Layout、ScrollView等控件,一定会打断渲染合并
5.空节点不参与渲染,不会打断渲染合并

如下的场景节点结构,Cocos2dx 引擎是按什么顺序渲染的呢?
scene
        child1
        child2
        child3
                child6
                        child7
        Child4
                child5
答案:child1->child2->child3->child6->child7->child4->child5

渲染合并批次:如果child1 -> child5,也就是该场景都是sprite的话,他们是可以合并批次渲染,也就是gl calls = 1
如果中间child6是一个label,那么gl calls 将为 3
平常来说,相同类型的cocos类型,都是可以合并的。但事情总有例外:
1.每一个Label都会占用一次call,它们并不会合并
2.ClippingNode、layout、ScrollView等都会直接打断批次
3.相同的控件,如果纹理资源、shader参数命令、顶点不相同,也会打断合并
所以,我们排列了渲染队列之后,尽量使用相同的控件和纹理,是减少draw calls最简单直接的方式

说明label这样的文字渲染,多个虽然会提高calls,但是他们的切换其实性能影响不大,但是不要在label中间增加其他渲染成分,否则切换性能影响就大了
cocos studio中资源的排放布局影响很大

14、卡顿分析---合并图集
纹理资源切换,是渲染最大的性能损耗。渲染队列相邻的两个渲染命令,如果依赖的渲染纹理不相同就会产生纹理切换。
例如渲染队列如下:
sprite1->sprite2->sprite3->sprite4->sprite5->sprite6
Ep1:sprite1到sprite5所使用的spriteFrame都是不同的texture,很悲剧! draw call 将达到6
Ep2:sprite1到sprite5所使用的是同一个texutre的话,draw calls就是1。
由于sprite所需要的资源是spriteFrame,所以,我们尽量让相邻的sprite依赖的spriteFrame使用同一个texture。
Plist合图,会将多个图片纹理,合并成一张。引擎在使用spriteFrame的时候,会从同一个texture中获取。
所以,建议,我们同一个csb节点资源的控件资源,都合并成一个plist!






0

主题

6

帖子

0

积分

普通会员LV2

Rank: 1

积分
0
发表于 2021-4-10 15:24:27 | 显示全部楼层
第二点内存信息是用什么打开的
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|易游网-易游模拟器 Copyright @2015-2021 ( 浙ICP备15028007号-1 )

GMT+8, 2024-5-8 19:31 , Processed in 0.040040 second(s), 8 queries , Gzip On, MemCache On.

Powered by Discuz! X3.4 Licensed

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表