腾讯游戏学院专家:手游开发,该如何做好Android内存优化?
编者按 手游占用手机内存过大,会影响玩家的体验。腾讯游戏学院专家Qling将在本文分享自己做Android内存优化的思路,希望能帮助到大家。
文 | Qling
腾讯互动娱乐 游戏客户端开发
在之前Android客户端内存优化工作中发现,Android的内存组成部分较多,而每一部分的含义以及测量工具在官方文档以及Google中都没有找到详细资料,最终通过分析相关Android源码以及测试对每部分含义有了一定了解,所以分享出来为同样做内存优化工作的同学提供一定思路,少走弯路。
个人觉得在做内存的优化前,先树立一个正确的认知是非常必要的,这样可以避免钻牛角尖,少做很多无用功。目前总结出两点认知如下:
测不准
在做优化工作时,大家必定要做的事就是先看看当前数值是多少,优化过之后再和优化前数值做对比,所以优化前要做的第一件事就是测量。而对内存而言,却很难精确测量某一时刻或者某个情景下当前的内存是多少,同样条件下每次测量的结果可能都会有一定浮动,所以不要太纠结上来就先去测一个准确值出来。其实从文章后面的内容也能知道,衡量Android内存的一些指标本身的定义就不是一个精确值。
三个误区
转场景内存有增量
一段时间内一直增长
进出场景Unity Profiler回落正常,但Android内存没有完全回落
优化工作中可能会经常碰到上述三种情况,很多时候可能会觉得发生了内存泄露,但其实也并不一定。转场景内存有增量不一定是内存泄露,只要保证在Unity Profiler里看到的Texture、Mesh以及SerializedFile(AssetBundle)等常见易泄露的资源卸载干净即可。一般情况下,Android或iOS并不会及时将所有App卸载数据进行清理,为了保证下次使用时的流畅性,OS会将部分数据放入到缓存,待自身内存不足时,OS Kernel会启动类似LowMemoryKiller的机制来查询缓存甚至杀死一些进程来释放内存。Unity Profiler回落但Android内存没有回落是因为Profiler里记录的是引擎真实使用的内存,而Android中的内存大小是包含了部分缓存。因此,并不能通过一两次的内存没有完全回落来说明内存泄露问题。
简单总结了一下在内存优化时的思维顺序。
文章接下来也会按照这样的顺序进行展开。
要优化,首先必须要量化,要量化就需要选取指标,个人觉得在指标的选取上需要满足以下几个条件:
易测量
符合逻辑
每次测量结果稳定,方差小
而对于Android的内存,其实已经有一些大家常用的衡量指标了,分别是USS、PSS、RSS和VSS,这几个指标的具体含义相信做内存优化的同学都很清楚,就不赘述了。在实际项目中一般会选择PSS作为衡量指标。
确定指标后,要做的就是通过一定的工具来测量相应指标。Android本身提供了非常多用于测量内存的工具,如free、showmap、procrank等,可以按照不同的需求采用不同的工具。对于PSS,个人觉得最方便易用的工具是dumpsys meminfo,用法如下:
adb shell dumpsys meminfo --package com.tencent.xxx
运行结果如下:
其中PrivateDirty列和Private Clean列是进程独占的总内存,不和其它进程共享。进程销毁时,它们占用的内存会重新释放回系统。Dirty内存是已经被修改的内存页,Clean内存则是没有被修改的内存页(例如正在执行的代码)。右侧的Heap Alloc列指应用中Dalvik堆和本地堆已经分配使用的大小。它的值比Pss Total,因为Android中所有进程都是从Zygote中fork出来的,包含了进程共享的部分。
其余每行的含义会在后续详细讲解,每行指标也都会有相应的工具查看。
通过工具得到具体数值后,接下来要做的就是优化了。而优化时需要采取的策略就是分清主要矛盾和次要矛盾,即找到占用内存的大头。在Android内存中,瓶颈主要来自上图中的Native、Gfx、Unknown三项。文章接下来会对这三项做详细解释。
Native
一个Android进程的内存从high-level层面讲,可以粗略分为Java堆和Native堆,其中Natvie堆顾名思义就是由C/C++等分配的内存,对Unity项目而言,一般即为Unity引擎申请的内存。通过DDMS工具抓到的Native内存如下图所示:
可以看到Native内存主要由libunity.so、libmono.so、libglsl.so以及公司的libapollo.so和libGCloudVoice.so等动态库组成。而Native内存中的内容一般包含以下几个:
Mesh
Font
Fmod
Texture(R/W)
Material/Shader
Animation Clip等
对于Native内存的查看,网上的资料都是介绍利用DDMS工具,但坦白讲,觉得这个工具的设计初衷就只是为Java服务的,所以要想用它来查看Native内存,需要非常非常复杂的配置流程,当时为了配好工具差不多花了三四天时间,走了很多弯路,而且网上的资料都不可行,最终还是通过看Android相关源码才搞定。只是DDMS的配置差不多都可以单独写一篇文章来介绍了,所以不再次详述。
其实在配置好DDMS后,会发现它显示的内容实在是太多了,目前看到的结果是除了Texture和Font等大对象外,其余分配都是由n个很小的分配组成的条目,并不是我们想象中有一个10MB的AnimationClip在Native里就会对应于一个10MB的分配。在XCode里查看iOS版,结果也是一样。因此除了像Texture以及Font这类对象的内存外,想借助DDMS排查其他对象的内存问题几乎是不可能的。
所以自己实现了一个小工具,通过使用Unity提供的Android底层对象封装,利用反射调用Android底层接口,得到实时Native、Gfx以及Unknown值。可以在不同模块点插入一些Sample得到两个Sample之间的内存变化量,进而在一个high-level层面查出内存增长不正常的地方,虽然工具很简单,但已帮助解决了很多Native内存问题。工具结果和相关代码片段如下图所示。
这里也有一个经验之谈,如果把从工具定位问题再到代码里优化看做是一个自底向上的过程的话,当自底向上行不通时(如DDMS数据太多)就可以考虑自顶向下的方式,从业务逻辑模块出发,慢慢定位到问题所在。
Native内存一个常见的问题是纹理开了Read/Write Enable,当纹理没有开启可读写时只会在Gfx中存在一份,但开启可读写后就会在Native内存中也存在一份。如下图所示:
可以发现开启可读写后,DDMS看到的Native内存里有一项Texture2D::Transfer的分配。
显存
Android上的显存分为Gfx和GL两部分,其中Gfx指用户态显存,内容包含贴图和Mesh,使用Unity Memory Profiler即可查看。
没找到移动端底层相关资料,可以借助上图理解用户态显存,猜测原理近似(如有理解错误还请大神不吝赐教)。上图是Windows Display Driver Mode(WDDM)的结构,以D3D为例,显卡驱动和D3D运行时都分为用户态和内核态。应用程序调用D3D API,运行在用户态的D3D运行时经过UMD(Userspace Mode Driver)生成Command Buffer,然后再由运行在内核态的D3D运行时和内核态的驱动处理相应buffer,交给GPU绘制。所以猜测移动端的Gfx即为用户态显卡驱动使用的内存。
与Gfx相对应,GL则是指内核态的显存,包含Texture、Vertex Buffer等。但GL指标在很多设备上并不会显示,这是因为这块显存一般是由GPU使用的,其大小需要由芯片厂商自己计算。Android提供了一个memtrack模块,如果芯片厂商实现了该模块,且Android系统版本在7.0以上,则可以通过dumpsys meminfo得到该指标。下图是高通的一个实现:
也可以通过命令
cat /d/kgsl/proc/pid/mem
来查看GL内存中的内容,如下图所示。
查看高通完整的实现代码可以知道,高通在计算GL大小时已经剔除了Gfx中的内存,所以在高通的架构下,Android中显存整体大小应该为Gfx+GL。
关于GL显存需要额外说明的是Unity从5.x版本开始,就包含一个由于申请VBO导致的GL显存过大的bug,目前该bug在5.6.3p2和2017.1.0p5中修复。对于Unity空项目,GL显存会从修复前的50MB降为20MB。
在优化贴图内存方面,本人也做了另外一份压缩贴图合并的工作。有的时候我们需要在运行时把一些压缩过的小贴图合并到一张大的Atlas中,Unity的Texture2D有一个PackTextures()的接口,但这个接口只有在小贴图是DXT1格式时,合成的Atlas也是DXT1。对于常用的ETC、PVRTC等贴图,合并出来的Atlas是RGBA32格式的,这样明显会增大内存。所以自己实现了一个合并压缩贴图的插件,插件支持几乎所有压缩格式的贴图合并,支持Mipmap,也支持Android、iOS和x64等各种平台。如果有同学需要,可以联系我。压缩格式的相关资料可以参看[2][3][4]。
Unknown
Unknown内存一般是Mono堆内存和Lua内存(如果项目中使用了Lua),Mono堆内存分配也可以通过Unity Memory Profiler查看,但需要将脚本引擎设置为IL2CPP。查看Lua内存也有相关Profiler可以使用。
创建一个空脚本,加入以下代码申请16MB内存,
byte[] b = newbyte[1<<24];
此时的Unknown内存如下图所示(申请前约为2MB)。
对整体PSS贡献不多的次要内容主要有Dalvik和EGL两项。
Dalvik
Dalvik是Java虚拟机使用的堆内存,一般是由Java申请的,可以使用DDMS中的工具进行详细查看(如下图所示),也可以通过其他工具如MAT等进行分析。
这一项一般较小且很稳定,所以可以忽略。对于公司项目而言,该项内存主要是由Apollo、MSDK等插件占用,目前大小为17MB。
EGL
EGL具体指EGLSurface,是由Android的SurfaceFlinger子系统(类似于Windows中的DWM(Desktop Window Manager))使用,用于将GPU中渲染的结果最终显示在屏幕上。下图很好的解释了SurfaceFlinger的作用,可以把它理解为一个合成器,它的输入可以来自不同进程,比如Launcher、NavigationBar和StatusBar分别属于不同的系统服务。
Android从4.4开始使用三缓冲区(Tripple-buffering),使用三缓冲区的原因可以在网上找到相关资料,不再赘述。Android采用EGLSurface作为一个Back Buffer,所以当App正在前台运行时,EGL内存大小为3个屏幕大小的Back Buffer,当App运行在后台不显示时EGL为一个屏幕大小的Back Buffer。具体Back Buffer大小与屏幕分辨率相关,如1080p的屏幕(1080x1920 RGBA32)的大小约为8MB。EGL同样也包含非常多的内容,感兴趣的同学可以查看[7]中的电子书。
可以通过
cat /d/kgsl/proc/pid/mem
查看某一个App使用的EGLSurface个数,同查看GL内存的指令相同,如下图(左)所示。也可以通过
dumpsys surfaceflinger
查看系统整体的EGLSurface情况,如下图所示。
关于EGL想额外提到的一个很玄学的点是,经过多次试验发现如果Plugins/Android/AndroidManifest.xml中设置了<uses-sdk标签,那么EGL内存就会翻倍。这个问题也跟Unity工作人员讨论过,但无果。由于用AndroidStudio建立的空项目不存在该问题,所以仍然怀疑是Unity的bug。
介绍完EGL,至此就可以对Android的显示有一个全局的认识,App调用相应3D API让GPU把内容绘制到EGLSurface中,这时SurfaceFlinger将EGLSurface以及Navigation Bar和Status Bar等做合成,然后显示在屏幕上。可能有的同学会问游戏一般都是全屏的,是不需要Navigation Bar或Status Bar的,但其实在游戏中从屏幕边缘向下或者向左右滑动时,仍然是会在游戏界面上显示Navigation Bar或Status Bar的。
其实在SurfaceFlinger到Screen之间,还有一个可选的模块HWC(Hardware Composer),用于最终把内容显示到屏幕上。如果存在HWC,那么SurfaceFlinger就只需要告诉HWC显示哪些内容即可,无需关心如何显示。HWC模块一般是由不同硬件厂商自己实现的,拿最简单的例子讲,不同机型的Navigation Bar实现都是不同的,有的厂商采用系统默认(软件Navigation Bar),有的则是实体按键。
参考资料
[1] https://source.android.com/devices/graphics/
[2] https://www.imgtec.com/blog/pvrtc-the-most-efficient-texture-compression-standard-for-the-mobile-graphics-world/
[3]https://www.khronos.org/registry/OpenGL/extensions/OES/OES_compressed_ETC1_RGB8_texture.txt
[4]https://www.khronos.org/assets/uploads/developers/library/2012-siggraph-opengl-es-bof/Ericsson-ETC2-SIGGRAPH_Aug12.pdf
[5] https://blog.uwa4d.com/archives/optimzation_memory_2.html
[6] https://developer.android.com/topic/performance/memory-overview.html
[7] https://mathias-garbe.de/files/introduction-android-graphics.pdf
[8] https://www.jianshu.com/p/59ad90bff2a7
[9] http://djt.qq.com/article/view/987
[10] https://source.android.com/devices/graphics/implement-vsync?hl=zh-cn
关于腾讯游戏学院专家团
如果你的游戏也富有想法充满创意,如果你的团队现在也遇到了一些开发瓶颈,那么欢迎你来联系我们。腾讯游戏学院聚集了腾讯及行业内策划、美术、程序等领域的游戏专家,我们将为全世界的创意游戏团队提供专业的技术指导和游戏调优建议,解决团队在开发过程中遇到的一系列问题。
申请专家资源请前往:
https://gwb.tencent.com/cn/tutor