Optimizing Mobile Applications

 目标:了解&分析移动性能,减少内存消耗并缩短帧时间,以尽可能的在移动设备上让游戏顺利运行。

--------------------------------------------

1.用Instruments检测CPU time


通常我们希望自己的游戏运行的更快,占用资源更小。要做到这点首先需要知道运行时的状况,比如哪些步骤/数据 消耗过高。

检测这个流程的工具如下:



首先是Instruments:


因为IL2CPP?,Instruments可以针对Unity程序提供毫秒级的分析数据,还可以把一些在Unity Profiler看不到的数据,比如程序的startup time等显示。

引导博客:怎样运行Instruments


  • Instruments可以显示Unity程序运行的CPU Time tree。比如下图红色框标出来的就是startup。



在startup时,我们的行为在两处可能造成影响:

1.PlayerLoadGlobalManagers那一行,表示在加载singletons(单例)和registries(注册),如果在一开始有任何的Resource Load(以const),则在此处起作用。

2.第一个场景里gameobject上挂的脚本里,如果在其AWAKE方法里有运行昂贵的代码,也会在此有影响(半天加载不出来)。如果有这种情况,考虑在程序其他的生命周期里调用(applications lifecycle)。


Instruments也可以看running 时的汇总性能数据,多帧或单帧的都可以,比如下图playerloop function,是Unity 的主循环代码,每帧循环一次。



因为我们做的是video game,所以渲染首先进行,如果在脚本里有任何的渲染callback将在PlayerRender有影响。

还有BaseBehaviourManager::CommonUpdate处理FixUpdate(),Update()以及LateUpdate()的回调(callback)。

UI::CanvasManager(several methods)表示如果有任何使用Unity Canvas(包括Unity UI系统)渲染,则在此处有影响。

DelayedCallManager::Update 表示一些多帧运行的地方,比如Coroutines。

PhysicsManager::FixedUpdate 表示所有的物理运行,比如OnTriggerEnter等则在此处有影响。(如果是2D系统,则对应的是Physics2DManager)




  • 当然我们要做的最重要的是监控我们的scripts的执行信息,比如下面就是Unity Standard Assets在Ipad上的执行(transpiled)截图。



这里值得看的是EventSystem_Update,可以看到这是Scene最昂贵的执行(几乎是其他执行的百倍)。那它执行的是什么呢,点开之后我们会跟踪到CPU time主要耗在每帧通过raycasting定位鼠标的位置。但是一般我们在Ipad上没有鼠标,所以这些CPU time就是完全的无意义消耗。


  • 接着看Coroutine



ScriptingInvocation::Invoke下可以看到一个奇怪的方法:U3CDoBobCycle。这是该协程的名字,这是用来在每帧适应camera的位置。按说这个协程每一帧都应在DelayedCallManager下,但事实并非如此。

要解释这个就要说明Coroutine的运行模式:



可以看到Coroutine被分成两部分执行,所以追踪coroutine的执行情况要在不同部分查看。


Instruments还可以用来确定Unity在什么时候什么位置loading assets。

如果看到方法(method): VirtualRedirectTransfer,表示是Unity Serialization System把data转换为Object。

通常这发生在加载资源时,比如texture,a mono behavior等。在此例为Particle System。



所以如果你发现如果程序在VirtualRedirectTransfer时很卡,那就需要预加载资源。比如通过assets pool,或者异步加载资源(loading asynchronous)。

有时候也会在很内层的地方发现该method (calls to clone object),这时候表示把data从source object 转移(transfer)到target object上。


2.通过Memory Profiler检测内存情况

在5.3版本以上可以使用。

怎样使用Profiler




MemoryProfiler最重要的功能是追踪脚本里的变量的引用,返回该引用对应的占用的内存。所以如果有内存的问题,点击snapshot,它会给你该内存的引用链(reference chain),这是追踪它的好方法。

因为该工具建于公共API,所以你可以用任何你想要的方式修改(modify)。

该工具托管在(hosted on)bit bucket(5.3时候的版本,现在可以通过Unity内Window→Profile直接用)。地址



当前版本Profile界面如下



有时候打开这个工具会直接卡死,因为要采集所有内存数据,有时候需要长达30分钟。

一个常见的在mobile上使用的例子:如果使用Asset Bundle,那开始加载assetbundle里面的texture。然后assetbundle卸载错误,然后又重新加载,并加载该texture。

这种情况发生是因为Unity引擎没有办法得知该texture已经加载了,它并不存在于Persisten Manager。

所以如果我们使用Profile就可以发现两个texture,欸,它们俩size相同,点击进去查看发现它们有相同的name,但instanceID不同,所以我们知道肯定哪儿出错了。通过追踪引用链里的参数所在就可以发现是什么导致。



另一个例子:图像效果(image effects),ipad air 2有完整的渲染图像方案,全分辨率后台缓冲为36兆字节(back buffers are 36 megabytes for a full resolution),即的4K分辨率(4k resolution),一般pc很少渲染4K(除非是非常牛逼的显卡)。所以我们需要考虑downsizing render buffers,比如1080P,60兆字节。渲染效果差不多时一半,这可以释放出很大内存。


我们可以创建一个自动系统来控制你的应用的导入设置(a automated system for controlling the import settings on your project)。这样就可以自动的进行各种导入而减少人为导致错误。

比如GUI系统使用的是4K 未压缩的纹理,ios使用的是60兆字节,在iphone4上是1/3,通过压缩可以到8兆字节,这样可以节省52兆字节的内存(真的,他到底在说什么,完全听不懂)。


导入资源通常会出的错误

(asset auditing: common errors)



Asset Auditing可以通过使用以下类和方法来解决最后一项。




这个类内最重要的是包括若干方法,比如OnPreprocessModel,OnPreprocessAudio,OnPreprocessAnimation等等。



上面的问题常见的解决方法

1.Textures



如果Read/Write is enabled,那么会有两份texture的copy在内存中,所以最好关掉(unless you're actually doing pixel operations on your textures).

disable mipmaps(贴图)是因为啥实在看不懂(21:00)。


2.Models



但是如果有Mesh Collider的话就不应该关闭(21:30)。

然后检查Mensh Renderer的选项是否都是需要的



3.Audio



注意,并不是要求导入MP3,只是在压缩的时候选择MP3,Unity会自己完成。因为玩家通常注意力在游戏操作上,对于Audio并没有太高的要求,所以比特率越小越好。


管理内存原理



堆(白色)包含了资源和脚本内的对象。当脚本需要时更多的内存将被分配



GC定期运行,清除未在使用的对象(过一遍所有对象确保没有引用,清除了两个空闲的int[] Array)。并释放对应内存。



但释放出的内存不会被填上,即内存碎片



如果需要分配新的内存,已有的heap内不够的话,那么该heap会expanding。Unity的内存只会扩展,不会收缩。



这对于移动设备来说是有很大问题的(内存很小)



即时内存分配所引起的问题是,比如需要临时分配一个很大的内存,然后被GC清掉,因为堆不会收缩,所以会有很大的一片空的,浪费的块。



这是解决方案(是啥完全不懂)


内存保护(26:30)











3.改善CPU性能的一些方法


如果你的游戏占用了低于180兆内存,那很好(why?),但是如果它每秒只有3帧,没人会玩儿这样的游戏。


1.Loading Time

下面将从loading time开始讲起,毕竟没有人喜欢盯着闪屏(splash screens)。


1.解析数据

很多开发者喜欢把数据存成json或者xml并在游戏启动时解析(parse this at startup time)。很不幸的是,解析text文件比如json或xml非常慢。有些开发者则基于反应(reflection  ?)加载json或xml,但在c#里refleciton也是非常慢的。



如上图,最好是使用Unity内嵌的JsonUtility class,如果你实在需要用一些网上的其他方式(比如LitJson),等则有如下3个方法加速解析。



  • 1.

如上图,最好就不要解析text,直接把text data 拷贝(bake)进binary format(二进制?),这样Unity可以像scriptable object或者一个c# class一样本地序列化(serialize natively)。

比如当你在AssetBundle里加载时,这种方式就是把数据直接从storage复制到了memory,而不是先读text里写了啥。

适用于比较static的数据,比如游戏设置等。


  • 2.

有时候上面的做法也行不通,因为数据是动态生成的(dynamically generated),所以没法提前转换成scriptable objects.

解决办法就是把加载分块以降低一次性需要加载的工作量,比如隔几帧加载一小块。或者更好的办法时,只解析你目前所需要的那一小点数据,留着剩下的以后解析(缓存其他部分,but how?)



  • 3.

有时候你的数据是独立的一大块(giant monolithic trunk of Json),它来源于网络并动态生成(dynamically generated and comes from a web service)。

那么可以创建threads来deserialize(这是啥)。33:00

要非常小心,而且只能使用在纯C#类上。




2.从Resources文件夹加载




我们习惯于把所有的assets都放在resources文件夹里,但随着该文件夹的增大,Unity要计算这些resources的索引也在增大。

如果你在Resource里放入成千上万个文件夹,一个比较慢的设备在startup时为了计算这些索引将会花费数百毫秒(hundreds of milliseconds)。没人能忍受盯着闪屏超过2秒就为了加载index,仅仅是因为你太懒了不愿意把临时资源(temporary assets)从resources文件夹移走。

如果有些文件不是临时的,不是调试性的(debug),那么考虑放入Assets Bundles来降低这一startup加载时间。


2.Runtime

之前在Profile工具的使用中讲过一些,在这里有一些小的其他需要注意的。


1.Animator & Shader property IDs



改变Material, Shader, Animator的属性时,定位属性不要用string而用int形式的hash(Animator的trigger!)。


为什么要这样做

比如上图第一行,设置"_Color"属性的颜色。

每使用一次该方法,Unity都会把这个"_Color"属性先散列(hash)后获取整数ID。这样做一次花费并不昂贵,但在有大量图像效果(image effects)的游戏中,每帧执行上百次这个方法很常见,这会无意义的耗费大量CPU。所以我们在startup时就应该把这些属性对应一个hash值以避免不停的计算新的hash。


怎样做



在加载时使用比如Shader.PropertyToID(属性名)把string转成hash,之后直接使用hash就好。(可以放在一个static class里的static method或者Awake method里)。


然后是一段关于Boxing的blabla(并不懂是什么36:00)


2.Strings

-----------吐槽---------------

刚发现CSDN博客版面又改了,目录换了方法显示,难受。

港真,连个私密博文功能都做成狗屎样完全不私密的你们不停的改版面有意思?产品死于大改有无听过?

----------吐槽完毕-----------

演讲人说他不知道为啥大家这么爱用字符串,他总是倾向于使用numbers。不过在这里解释一下string的解析过程以提供更好的方案。


  • 正则表达式

通常处理复杂字符串操作(complex string operation)时使用正则表达式(regular expressions)



你不是在JavaScript或Pearl上运行RegExp,它会在你运行计算时在内部产生大量的内存临时分配(temporary collections)。



比如上面一个简单的字符串相等比较RegExp就要耗费5KB内存,有些人说预先声明会好些(pre compiled?),这是结果



是好了一些,320B,但还是太多了。所以干脆就避免正则表达式。


  • 本地化时需要更精确的字符串操作时

那使用C#内置的字符串操作方法还是有坑。



在美国英语里上面的两个字符串是相等的(因为C#的处理),但在一些西语里是不同的。所以为了得到更好的效果用下面的语句替换。(一字节一字节的对比,不会经过处理)


  • 避免使用一些很慢的API

经常有人被String.StartsWith 或 String. EndsWith给绊倒(trip everybody up)。其实自己编写一个同样功能的function根本不难,但要是用这两个API大概会比你自己写出来的功能慢100倍。那要是用StringComparison.Ordinal呢?慢大概10倍。



Why? 因为C#是高级语言,每当使用时会被C#编译器翻译成中级语言IL,每当一个C# method被调用时,将会在IL中输入一个call或 call vert指令。这意味着在其他位置调用方法,或者干脆调用一个包含大量查找的virtual method。

对比下面两个同样功能的模块,左边的比右边的要多3次call。





演讲人把这个循环进行了100,000次迭代, 右边花费128毫秒,左边花费324毫秒。差不多是3倍的消耗,就因为使用的属性(myList.Count)。

如果把上面的List换成Array还能更快,即把唯一的一步列表访问(List access)也去掉, 大概会降到60毫秒(42:30)。



所以,最好不要在循环里调用属性,尽量使用Array而不是List如果没必要的话。


那如果我们使用一些Unity所设的,非常方便的属性比如Vector3.zero呢?一样的道理,还是call method并使用大量的内存,最好不要这样做。

像这样的简单类型使用const,复杂类型(complex type)使用static readonly。



区别简单复杂:直接可以声明时赋值(enter your value)而不用初始化的就是简单类型。使用const或static readonly会直接在代码需要时直接把值copy进去而不是调用属性。

对于字符串的操作来说则是不一样的,比如一个1MB的string,你const后copy,二进制文件则有10M?100M?(44:10)


对于Vector3.Scale, Quaternion.Set等用Math后分配给const变量, 对于Transform.Translate, Transform.Rotate自己写关于位置变量的循环来替代。



最后



在准备优化前记得先使用Profile进行追踪,上面的这些都是一些小优化,并不会拯救一个非常慢的游戏。但是如果想从18微秒和30FPS降到16微秒60FPS,这就派上大用场了。


完毕。












阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/WOCAISHIAIGE/article/details/79973300
个人分类: •Mobile optimization
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭