IT教程 ·

写给Unity开发者的iOS内存调试指南

Docker Compose搭建Redis一主二从三哨兵高可用集群

0x00 媒介

事变的过程当中,经常会发明有小伙伴对Unity的Profiler供应的内存数据与某些原生平台Profiler东西,比方iOS体系和Xcode,所供应的内存数据有差别而觉得猎奇。而且人人对怎样解读原生平台东西的数据越发感兴趣,一样比方iOS体系和Xcode。近来恰好看了一个来自Unite Copenhagen题为 Developing and optimizing a procedural game | The Elder Scrolls Blades - Unite Copenhagen 的演讲,个中就触及到了一些关于iOS内存的话题(虽然并非很细致)。恰好也连系事变中的一些履历,写一篇文章来讨论一下一个Unity开发者怎样处置惩罚和iOS内存有关的问题。重要内容包含剖析iOS体系的内存治理,运用Instrument检察Unity游戏的内存状态,运用敕令行东西深切发掘Unity游戏的内存问题以及文末小彩蛋。

0x01 iOS的内存治理 - Unity Profiler统计错了吗?

起首,我想强调的一点是,Profiler东西所供应的内存数据只是一个(组)数字,而且差别的东西存在有差别统计内存的战略。因而,一个重要的问题是我们看到的数据终究是怎样猎取的?
而依据所运用的东西差别,该东西用于查找数据的战略以及开发人员现实要查找的内容,末了的效果也有多是不一样的。因而,假如要寻觅一个数字来汇总某个运用或许游戏的一切内存信息,那末多是把问题想简朴了,或许说疏忽了体系的复杂性。比方,差别版本的iOS其对内存开支的统计都是有辨别的——在iOS12上运转的metal app的内存在 Xcode memory gauge的统计是高于iOS11的,这一样是由于苹果改变了对内存的统计战略,许多之前没有被统计的内存如今也被盘算到了内存开支中。而一样都是iOS,Xcode memory gauge的统计和Instrument中的统计也有大概不完全一致,而初期Instrument的Allocation则重要用来统计heap内存,只能说依据各自东西的统计划定规矩,人人都是准确的。因而,把时刻糟蹋在对照差别东西的数据上还不如以一个东西作为标尺来权衡内存开支或许是推断内存的优化是不是有用。

The accounting for purgeable, nonvolatile memory changed beginning in iOS 12 and tvOS 12. In iOS 11 and tvOS 11, allocations with this memory storage mode—commonly used by Metal apps to store buffers, textures, and state objects—weren’t counted toward an app’s memory limit and weren’t presented in tools like Xcode memory gauge.

所以,相识操作体系是怎样治理内存就变得非常重要,关于怎样解读Profiler东西供应的数据也很有协助。接下来我们先来讨论一下iOS体系对内存的治理机制,以后再来离别看看Xcode抓取的内存数据和Unity抓取的内存数据。
起首,每个历程都邑有一个地点空间。其局限由指针size支撑,比方32bit或64bit。而且地点空间起首会分为多个地区(regions),然后将这些地区细分为4KB(初期版本)或16KB(A7以后)为单元的page,这些page继承了该region的种种属性,比方是不是是只读、可读写等等。固然,有些page大概寄存的数据比这个page的尺寸要小,有的数据大概须要好几page才寄存,然则体系的内存单元是16kb的page,所以体系统计的内存开支约等于page的数目 x page的大小。
固然,体系另有实在的物理内存。

Virtual memory vs Resident memory

写给Unity开发者的iOS内存调试指南 IT教程 第1张
ref: WWDC 2013
经由过程虚拟内存使我们能够竖立从该地点空间到实在物理内存的映照,这点我想这些人人应当都晓得。而映照现实上是一个很风趣的事变。由于从每个app历程的角度来看,它具有一切的内存,即虚拟内存,但事实上只要一部份虚拟内存被映照到了实在的物理内存上,这部份被映照到物理内存的部份就是所谓的Resident memory。
就像上面这个图中形貌的一样,一个app分派了内存,能够看到在虚拟内存上分派了4个region,个中第3个region包含了13个page。 但此时,真正映照到物理内存上的只要6个page。而虚拟内存到实在物理内存的映照发作在对内存的第一次运用时,比方从内存中读取数据或是向内存中写数据。Resident memory一样也是Virtual memory,只不过这部份Virtual memory已映照到了实在的物理内存。 我想人人大概都经由过程XCode或许Instrument的统计看到过相似的数据,比方Instrument的VM Tracker中就离别列出了Resident和Virtual Size。

Dirty memory vs Clean memory

page有多是dirty的也有多是clean的。要怎样辨别dirty和clean呢?简朴的说,dirty的页就是我们的app或许游戏对这个page的内容举行了修正即分派了内存同时也修正了内存的内容,罕见的就是malloc在heap上分派的内存。这部份内存是不能被接纳的,由于这些数据明显须要被保留在内存中以保证程序一般的运转。
而clean的页则是没有对其内容举行修正,能够被体系收回和从新建立的。比方内存映照文件(Memory-mapped file),假如操作体系须要更多的内存,那末就能够将其抛弃。由于体系老是能够从磁盘中从新加载它,建立内存空间和磁盘上文件的映照关联。clean的内存是能够被开释和从新建立的。然则能够看到,虽然Memory-mapped file并没有斲丧实在的物理内存,然则它斲丧了历程的虚拟内存。
除此以外另有可执行文件的__TEXT段以及一些framework的DATA CONST段,也会归为clean memory。
在WWDC2018上,iOS的开发人员举了一个很抽象的例子。即分派20,000个integers构成的array,此时会有page被建立,假如只对第一个元素和末了一个元素赋值,则第一个page和末了一个page——即首尾元素地点的page——会变成dirty,然则首尾之间的page仍然是clean,即只分派了内存而没有修正或写数据。

写给Unity开发者的iOS内存调试指南 IT教程 第2张
ref: WWDC 2018

Compressed memory

当内存吃紧时,会接纳clean page。而dirty page是不能被接纳的,那末假如dirty memory过多会怎样呢?在iOS7之前,假如历程的dirty memory太高则体系会直接停止历程。iOS7以后,引入了Compressed Memory的机制。由于iOS没有传统意义上的disk swap 机制(mac OS有),因而我们在苹果的Profiler东西中看到的Swapped Size指的实在就是Compressed Memory。
iOS7以后,操作体系能够经由过程内存紧缩器来对dirty内存举行紧缩。起首,针对那些有一段时刻没有被接见的dirty pages(多个page),内存紧缩器会对其举行紧缩。然则,在这块内存再次被接见时,内存紧缩器会对它解压以准确的接见。举个例子,某个Dictionary运用了3个page的内存,假如一段时刻没有被接见同时内存吃紧,则体系会尝试对它举行紧缩从3个page紧缩为1个page从而开释出2个page的内存。然则假如以后须要对它举行接见,则它占用的page又会变成3个。

Unity Profiler错了吗?

能够看到,从操作体系内存治理的角度来看,一个历程的内存现实上是非常复杂的。而Unity纪录的内存数据,以“Reserved Total - Unity”为例,则重要来自引擎内MemoryManager的纪录。MemoryManager会依据差别的状况挪用对应的Allocator来举行引擎的内存分派。

写给Unity开发者的iOS内存调试指南 IT教程 第3张
比方我们能够以Unity 3D Game Kit这个免费项目为例,运用Instrument来检察一下它的内存分派。

写给Unity开发者的iOS内存调试指南 IT教程 第4张
能够看到MemoryManager挪用了UnityDefaultAllocator。 而下图的这个分派则运用了IphoneNewLabelAllocator来分派内存。

写给Unity开发者的iOS内存调试指南 IT教程 第5张
也就是说Unity的代码分派的内存,Unity是会举行纪录的。然则我们能够看到除了Unity的代码自身分派的内存,另有许多framework或许第三方library也会分派内存。然则这部份内存,Unity的Profiler是不会纪录的。

0x02 运用Instrument调试Unity 游戏的内存

这部份我引荐Valentin Simonov的这篇文章Understanding iOS Memory (WiP),对运用Instrument调试内存引见的非常清楚。

0x03 运用敕令行东西深切发掘内存问题

除了运用Instrument来观察内存问题以外,我们还能够经由过程很棒的Xcode memory debugger东西来查找内存问题。尤其是将Memgraph导出后,还能够借助种种敕令行东西来辅佐观察以猎取更多信息。

写给Unity开发者的iOS内存调试指南 IT教程 第6张
而且偶然人人也会埋怨说在Xcode的Memory Report页面看到的内存数据偶然候不仅和Unity Profiler不一样,偶然以至和Instrument等苹果本身的机能东西数值也不一样。上文已说过了,差别的东西有差别的数据是一般的。然则我们一样能够经由过程Memgraph和敕令行东西来检察一下,Memory Report的数据着重什么内容。
照样以Unity 3D Kit这个工程作为演示,测试装备为iPhone X,不过在入手动手之前我们起首须要开启Scheme -> Run -> Diagnostics -> Malloc Stack选项。

写给Unity开发者的iOS内存调试指南 IT教程 第7张
运转游戏后从主菜单点击入手动手游戏加载第一个场景,我们能够在Memory Report中看到此时的内存已达到了1.48G。然则Memory Report中它的内存刻度仍然在绿色部份,所以量力而行的讲Memory Report的刻度并非一个好的优化发起,由于这个内存开支在iphone7上就直接会致使游戏被体系中断。

写给Unity开发者的iOS内存调试指南 IT教程 第8张

Animation Leak?

我们直接进入到Xcode memory debugger,假如想要在这里搜检是不是有内存leak的问题,能够点击Filter中的选项。这里有一个罕见的假“leak”状况。

写给Unity开发者的iOS内存调试指南 IT教程 第9张
假如我们看一下它的客栈信息的话,大多是和Animation有关的。这里我征询了一下这个功用的开发者,确认这是一个苹果的误报,Unity照样会一般开释这部份内存的。固然假如人人碰到其他新鲜的和引擎有关的leak,能够根据这篇文章的引见给Unity提交Bug Report。
以后我们能够将此时的数据导出为.memgraph文件。接下来就能够运用一些敕令行东西来处置惩罚这些数据了。

写给Unity开发者的iOS内存调试指南 IT教程 第10张

VMMAP Summary

第一个敕令行东西是vmmap,运用它我们能够检察当前的虚拟内存的数据。
起首拿到一个memgraph文件时,我们能够斟酌运用这个指令同时加上--summary标记来输出当前虚拟内存的一个总览。
vmmap --summary Unity3DKit_ipx.memgraph
终端的输出如下图所示:

写给Unity开发者的iOS内存调试指南 IT教程 第11张
我们能够发明一些风趣的处所。起首有前4列是我们之前讨论过的内容:VIRTUAL SIZE、RESIDENT SIZE、DIRTY SIZE、SWAPPED SIZE。离别示意虚拟内存的大小,映照到物理内存的大小,Dirty内存的大小以及Compressed内存的大小。 我们能够看到TOTAL的部份,这个游戏历程分派了2.7G的虚拟内存个中有1.6G映照到了物理内存上,而DIRTY SIZE的值是1.4G——这个值很靠近Memory Report中的数值,而SWAPPED SIZE的数值为52mb,依据苹果工程师在WWDC2018上的演讲,这个值是紧缩前的内存而不是紧缩后的内存。因而我们重要来关注DIRTY SIZE这一项。

IOKit

其次我们能够看到IOKit的开支最大,它的虚拟内存不仅达到了832.5mb,而且现实映照到物理内存上的空间也达到了750.4mb。这部份重如果一些和GPU相干的一些内存,比方render targets, textures, meshes, compiled shaders等等。而这个测试项目也的确是mesh、texture的内存占用很大。

MALLOC 和 Heap

再次,我们能够看到MALLOC_**分派了许多内存。这部份内存重如果挪用Malloc举行分派的,个中即包含Unity的原生也就是C++代码的分派也包含第三方库和体系运用Malloc分派的内存,这部份内存在所谓的Heap上,在这几行的背面能够看到“see MALLOC ZONE table below”,也就是能够在下面看到各个heap zone的一个归类。在这里我们能够应用第二个敕令行东西heap来搜检一下Heap内存的内容。
heap --sortBySize Unity3DKit_ipx.memgraph
运用heap指令,我们还能够增加--sortBySize标志来对数据举行排序(默许根据范例实例的数目举行排序)。

写给Unity开发者的iOS内存调试指南 IT教程 第12张
能够看到Heap的绝大部份内存都被non-object占用了,达到了近700mb,而现实的object的内存分派实在都是很小的,比方类GpuProgramMetal的实例有573个,然则内存实在只占用了223kb。此时人人肯定对non-object的内容很感兴趣,不过在这个页面里好像也看不到太多的内容。所以接下来我们能够增加--showSize标志,将兼并在一起的数据根据size举行分组。
heap --showSize --sortBySize Unity3DKit_ipx.memgraph
如许就清楚多了。

写给Unity开发者的iOS内存调试指南 IT教程 第13张
能够看到non-object这一类中,排名最高的几块内存分派的尺寸离别是1个31mb、3个10mb以及1个8.4mb,如许我们就肯定了这个时刻的观察方向。
固然,heap指令还供应了更多的功用,比方那些有Class Name的对象分派,我们能够经由过程ClassName婚配的体式格局猎取每个该范例实例的内存地点。此时须要-addresses标签即可。比方我们输出Unity的GpuProgramMetal类的一切实例的地点信息,能够看到实在这个类的实例自身并不大,然则它援用的真正的shader资本则多是内存开支的大户之一。
heap -addresses GpuProgramMetal Unity3DKit_ipx.memgraph

写给Unity开发者的iOS内存调试指南 IT教程 第14张
同时,有了各个对象地点的内存地点,我们就能够经由过程下面要提到的malloc_history敕令来查找它们是怎样来的。然则如今我们照样把目光转向内存分派比较大的目的吧。
此时返回终端,继承输出虚拟内存的信息,不过此次我们只关注MALLOC_LARGE的分派,所以我们能够借助grep来过滤出我们的目的。
vmmap -verbose Unity3DKit_ipx.memgraph | grep "MALLOC_LARGE"
此次输出了MALLOC_LARGE范例下的内存信息,包含它的地点、尺寸以及地点Heap Zone等等信息。我们能够在这里找到我们的目的,一个30mb、3个10mb以及一个8mb的内存分派。

写给Unity开发者的iOS内存调试指南 IT教程 第15张
接下来我们就来看一下分派它们的客栈挪用吧。这里我们会运用malloc_history敕令,同时加上--fullStacks标志来输出客栈信息。
malloc_history Unity3DKit_ipx.memgraph --fullStacks 0x0000000127c60000
能够看到这30mb的分派是为了给FMOD分派内存池。

写给Unity开发者的iOS内存调试指南 IT教程 第16张
别的3个10mb的分派,一样也是做相似的事变。可见这个项目运用的声响资本许多。末了我们来看一下这个8mb的分派是从哪里来的。
malloc_history Unity3DKit_ipx.memgraph --fullStacks 0x0000000113400000

写给Unity开发者的iOS内存调试指南 IT教程 第17张
能够看到是开启多线程衬着时,Unity建立CommandQueue时分派的内存。

VM_ALLOC == Mono Size?

接下来,我们能够看到vmmap –summary输出的效果中,有一项叫做VM_ALLOC。依据Valentin Simonov的说法,VM_ALLOC对应的是Mono内存也就是托管内存的大小。终究是不是云云呢?我们一样能够经由过程上面的体式格局,来检察一下VM_ALLOC部份的内存分派客栈。 起首我们照样经由过程vmmap和grep来过滤出VM_ALLOC部份的内存信息。
vmmap -verbose Unity3DKit_ipx.memgraph | grep "VM_ALLOC"
能够看到这部的内存分派并不多,我们一样挑选2块分派最大的内存动手。

写给Unity开发者的iOS内存调试指南 IT教程 第18张
我们起首运用malloc_history来检察一下3m部份的挪用客栈。
malloc_history Unity3DKit_ipx.memgraph --fullStacks 0x0000000152bd4000

写给Unity开发者的iOS内存调试指南 IT教程 第19张
我们能够看到这3m的内存是C#剧本中挪用了SimplFXSynth的RenderAudio要领而触发了GC分派,托管堆举行了扩容。针对剧本中的要领定位,我们能够经由过程RuntimeInvoker这个标记来定位它在客栈中的位置。

写给Unity开发者的iOS内存调试指南 IT教程 第20张
风趣,接下来我们再来看看1mb的这块内存是怎样分派的。
malloc_history Unity3DKit_ipx.memgraph --fullStacks 0x0000000150084000
此次是由于Unity的ScriptingGCHandle::Acquire要领在托管堆上举行了内存分派。

写给Unity开发者的iOS内存调试指南 IT教程 第21张
可见,VM_ALLOC这部份内存重要对应了Unity的Mono托管堆的内存而且这个项目的Mono内存并不大。而详细是哪一个函数触发了GC分派,则能够经由过程malloc_history来检察。

Command Summary

至此,运用敕令行调试和查找iOS平台上内存问题就引见完了。简朴来个小结,拿到一个Unity游戏的内存.Memgraph文件以后,能够先经由过程vmmap --summary来检察一下内存的全景图。关于heap也就是malloc分派的内存,能够进一步经由过程heap指令来进一步剖析。 而一旦猎取了目的对象的内存地点以后,就能够运用malloc_history指令来猎取分派这块内存的客栈信息了。固然条件是要开启Malloc Stack的选项。以后,能够做一个自动化的剖析东西,对数据举行处置惩罚和输出来定位内存问题。

0x04 小彩蛋

 

  • Unity 3D Game Kit是一个很棒的Unity的进修工程。它的教授教养页面能够检察这里:

写给Unity开发者的iOS内存调试指南 IT教程 第22张

  • iOS13以后供应了一个新的API-os_proc_available_memory,应用这个API我们能够猎取当前这个历程还能猎取若干内存的预估值。嗯,怪不得我的iphone7跑不动这个项目。

写给Unity开发者的iOS内存调试指南 IT教程 第23张

win10配置CUDA+Tensorflow2.0的一些经验

参与评论