2020年度大赏 | UWA问答精选

UWA每周推送的知识型栏目《厚积薄发 | 技术分享》已经伴随大家走过了252个工作周。精选了2020年十大精彩问答分享给大家,期待2021年UWA问答继续有您的陪伴。

UWA 问答社区:answer.uwa4d.com
UWA QQ群2:793972859(原群已满员)


Q1:IL2CPP的内存问题

最近看问答上面有个关于IL2CPP和Mono的对比,看到IL2CPP内存冲高会下降。关于这个,我问了Unity的官方技术,回答是:你好,Unity有自己的GC机制,为了避免频繁向操作系统申请/释放内存,Reserved Mono值会保持在一定区间内,达到某些条件或在某些特殊情况才会触发GC。 有人说是:“内存池管理逻辑都是一样的,属于上层管理一样。它们只是中间语言不一样而已,也是只涨不降。”也有其他大佬说是IL2CPP冲高会下降。现在很困惑,求解答。

A:看下来题主说的内存冲高不降,涉及两个指标,一个是Profiler里的Reserved Mono,一个是设备内存(PSS)。目前确实没有权威的文档说明这一点,所以下面通过真实数据来说明一下。

先说第一个(Reserved Mono)。

  1. 在Script Backend是Mono的情况下,如果选择的是旧版本里的Mono 2.x,或者新版本里的 .Net 3.5(Runtime Version),那么这个值是只升不降的。比如这个数据,Unused已经很高了,但也不会下降:

    2020年度大赏 | UWA问答精选

     

  2. 同样在Script Backend是Mono的情况下,如果选择的是.Net 4.x (Runtime Version),那么这个值是可以下降的(但不确定具体是从哪个版本开始的)。比如这个数据,可以看出虽然会下降,也并不是频繁执行下降操作的:

    2020年度大赏 | UWA问答精选

     

  3. 最后Script Backend是IL2CPP的情况下,那么这个值也是可以下降的。比如这个数据,看上去和上面的情况相差不是太大:

    2020年度大赏 | UWA问答精选

     

而对于第二个,设备内存。这个就和安卓系统的内存管理机制有关了,即使Unity把Reserved Mono降低了,减少了自身的内存占用,系统也不一定会立即会把这块内存释放,所以这里的行为就很难说清楚了。

该回答由UWA提供


Q2:加载配置内存过大问题

配置表太多占用内存过大时,除了采用Sqlite,还有什么好的解决办法没有,有没有大佬能否指点下。FlatBuffer不用全部进内存吗?如果不全部进内存,访问速度如何呢?

A1:第一问题参考如下:

  1. 可以针对重复数据进行剔除,尤其是一些字符串的配置。在配置导出时把这样的数据提取一份,其他用到的地方只是引用,会节省不少。
  2. 数据类型要合理。
  3. 可以使用类似FlatBuffer/ZeroFormatter的延迟加载的思路,在真正使用时再去反序列化。一次游戏过程中实际用到的配置量比较有限,使用这种策略可以尽可能的减少不必要数据的加载。

第二个问题参考如下:
我们上个项目也是到后期优化时遇到类似问题,只是参考了这种思路,并没有进行完全替换。我们当时在打包时,会对配置以行为单位,进行Offset和Length的计算,在Runtime阶段,初始加载只会加载每行的ID,对应的这一行的Offset和Length,然后后续逻辑调用配置表接口拿数据的时候,如果发现没有反序列化过,就根据Offset和Length再去构建一下相应的数据提供给上层。访问速度的话肯定不如开始直接全部加载好,但我们测下来影响不大。

感谢范君@UWA问答社区提供了回答

A2:字符串吃内存不说了,尽量少用或者复用。表格中比较多的会是那种:攻击-1000;防御-2000;血量-3000,每个int都是4个字节,数量多了会顶不住。这种可以考虑用一个int32/int64/uint32/uint64 去存多个数值。

感谢萧小俊@UWA问答社区提供了回答


Q3:Instruments如何看Mono内存分配

例如在分配了一个10MB数组,对应在Unity Profiler中会看到开辟了至少10MB大小的Mono内存。

那么在Instruments中,如何查看分配的内存信息呢?Allocations中的信息是此进程中分配的所有内存信息吗,尝试分配过100MB内存,Allocations中的统计没有任何增长。

2020年度大赏 | UWA问答精选

 

2020年度大赏 | UWA问答精选

 

A:我这边也做了测试:

2020年度大赏 | UWA问答精选

 

创建了100MB大小的int数组,Size实际应该是400MB。

然后到Profiler观察:

2020年度大赏 | UWA问答精选

2020年度大赏 | UWA问答精选

 

可以看到ManagedHeap正确分配了这400MB的空间。

然后打包iOS后到xCode运行,运行前首先吧Run这个Scheme的Malloc Stack勾上:

2020年度大赏 | UWA问答精选

 

Run以后点选Memory并导出Memory Graph来观察:

2020年度大赏 | UWA问答精选

 

由于应用程序的内存都是在VirtualMemory空间分配的,因此查看VM Regions的VM_ALLOCATE部分。

2020年度大赏 | UWA问答精选

 

于是就可已发现128X3+16刚好400MB的分配。
调用堆栈也很好确定:

2020年度大赏 | UWA问答精选

 

正式我们的测试代码。

然后我们来看Instruments。
首先是Allocations部分,有一点要注意,该栏的下部有一些选项:

2020年度大赏 | UWA问答精选

 

注意最后一个选项,如果选择第一个:
All Heap & Anonymous VM,All Heap对应App实际分配的物理空间,不包含VM,

Anonymous VM的官方解释是:
interesting VM regions such as graphics- and Core Data-related. Hides mapped files, dylibs, and some large reserved VM regions。

因此一些比较大的预留分配空间是不会显示的。
将这个选项切换为All VM Regions,就能看到分配的400MB了:

2020年度大赏 | UWA问答精选

 

并且右边详情页面也正确显示了调用堆栈:

2020年度大赏 | UWA问答精选

 

另外我们还可以从VM Tracker来观察,打开VMTracker的Snapshots:

2020年度大赏 | UWA问答精选

 

于是就能看到这400MB的详细分配信息:

2020年度大赏 | UWA问答精选

 

可以发现,Virutal Size略大于400MB,因为程序其他部分也要申请一些内存。而这400MB又分别保存在Resident和Swapped内,其中Resident部分又基本等于Dirty Size,说明这部分大小的空间被标记了Dirty是不能被交换出去的,剩下240MB左右空间是Clean空间,可以暂时被交换出去以保证有足够的物理空间能使用。这也是因为我们只是申请了这部分空间,并没有进行具体的赋值初始化和使用。

那如果赋值使用了呢?修改代码测试:

2020年度大赏 | UWA问答精选

 

运行Instruments后再观察:

2020年度大赏 | UWA问答精选

 

可以清楚的发现这400MB都在Dirty Size内。这种情况真正会给该App和iOS以内存压力。

推荐阅读:
《写给Unity开发者的iOS内存调试指南》
《Understanding iOS Memory (WiP)》

感谢黄程@UWA问答社区提供了回答


Q4:URP关于多个摄相机的性能优化

URP7.4.3,除开主相机外,还有一个子相机,用于将照到的模型渲到游戏主界面UI上,在Profiler中看到以下情况:

2020年度大赏 | UWA问答精选

 

可以看到,在子相机中也进行了包括对LOD的计算,但子相机的Cullingmask只开了一个名为RTModel的Layer,在这一层里只有一个3D对象。按说子相机CullScriptable这块开销不应该有才对。

目前怀疑可能的原因是URP会对每个Base Camera都进行这部分的计算,但如果用Overlay相机,又无法用原来的方式将相机的targetTexture渲到一张RawImage上了,有人遇到过么?

A:题主的疑惑是:子相机的CullScriptable这块的开销不应该有那么大对吧(毕竟只有一个物件)?

这里有两个问题:

  1. Culling到底做了什么,只有一个物件为什么要Culling那么久(难道只有一个物件也要做很多的准备工作)?
  2. 在Profiler里面看到的数据真的是真实数据吗?也就是说,子相机的Culling真的做了1.68ms吗?

抛开这两个问题,也可以有更好的做法:
我们一共两个相机,主相机和UI相机,那么UI上显示的3D物件怎么办呢?
我们有个虚拟相机,所谓相机,其实就是做一个VP矩阵,做一个RT,绘制可见的物件就可以了。使用Unity的SRP,随机选一个地方,设置VP矩阵,设置RT,接着绘制指定的物件(UI中所有的3D物件都会挂在这个物件下面),然后这个RT就可以随意使用了。

假如一个UI上有两个3D物件,尽量都放在一个RT上;如果不行,就放在两个或更多的RT上,只是会多几个绘制命令。几个RT(还不需要是全屏的),而且会多几个Swap RT的操作。由于我们项目没需求需要若干RT,所以假设一下,在这种需要若干RT的情况下,也可以用一个RT加多个Viewport来解决的。这个代码都是现成的,参考一下Cascade Shadow Map的做法,这样Swap RT也就省了。

综上所述,既然你都知道自己要绘制什么,就不要给Unity Culling的机会了。

在Development Build中连真机看到的性能数据,是真实数据吗?目前在使用类似于HLOD的方式来减少掉这个LOD的巨大开销。楼上说的“设置一下VP矩阵,设置RT”,还不太清楚这个VP矩阵的操作具体是个什么,可否详解下或者推荐些相关资料?

A:你提的HLOD和LOD和上面的Culling没关系。VP矩阵就是view矩阵和projection矩阵。相机的作用就是提供这俩矩阵的。

如果你在管线里面设置了相应的矩阵,然后绘制指定的物件,就可以完全不用多一个相机,毕竟多一个相机就多一个Culling。

如果你对VP矩阵不熟悉,不清楚怎么实现,也简单。依然用一个额外相机,关上这个相机的Culling,然后在渲染pass中,不要绘制cullingresult.visibleobject,而直接用Graphics.DrawMesh或者CommandBuffer.DrawMesh绘制你要显示的那个3D Object的物件就好了。

感谢王烁@UWA问答社区提供了回答


Q5:关于_CameraDepthTexture的疑惑

如果开启_CameraDepthTexture,Camera就需要渲染一遍场景内所有带有ShadowCaster的可见物体的Pass来实现深度图。

但是场景中的物体在开启ZWrite的时候就把深度写进了Depth Buffer中了,直接获得这个Depth Buffer是不是比近乎DrawCall翻倍的方式更有效率呢?还是Unity在这方面有什么考虑?

另外,问一个更实际的问题:
我们的项目需要渲染场景的中湖水的深度效果,所有不透明的场景物体的材质都是关联同样一个Shader,这个Shader是带有ShadowCaster的。但是只有个别插入水中的物体需要去渲染ShadowCaster的Pass,有没有方法在不增加Shader的情况下,让没有插入水里的物体不渲染Shadow Caster Pass呢?我们用的是Built-in的渲染管线。

A1:第一个问题,可以参考这个问题中Unity官方人员的回复。
里面讲了两个原因,第一是对于非全屏渲染的情况,本来是想拿对应相机渲染的深度,但是Depth Buffer是全屏的。第二个原因是因为很多平台不支持直接拿Depth Buffer的数据。

参考网页:
https://forum.unity.com/threads/poor-performance-of-updatedepthtexture-why-is-it-even-needed.197455/

2020年度大赏 | UWA问答精选

 

另外查FrameBufferFetch相关问题的时候看到Unity论坛上另外一个贴子里面的回答。里面说到Unity支持了FrameBufferFetch,但是不支持DepthBuffer的获取。

参考网页:
https://forum.unity.com/threads/pixel-local-storage-and-frame-buffer-fetch-on-mobile-devices.604186/

2020年度大赏 | UWA问答精选

 

第二个问题,如果不增加Shader,目前没想到其他好的方法。
如果可以增加Shader,可以将原来的Shader复制一份,只在ShadowCaster的部分加一个“NeedDepth”这样的Tag,将水下的物体的材质球换成这个Shader,另外做一个只有ShadowCaster并带有“NeedDepth”这个Tag的Shader,这个Shader用来做Replace操作。

额外增加一个Camera,这个Camera跟随主相机,或者作为主相机的子节点,创建一个RT,让这个Camera渲染到这个RT,在Update里面使用ReplaceShader去画一下,那么只有有那个Tag的ShadowCaster会进行深度渲染,后续可以对这个RT进行编码等操作,这个RT记录的就是水下物体的深度。整个过程看上去没有特别多的额外工作,觉得可以一试(我没有做过测试,但理论上是可行的)。

感谢Xuan@UWA问答社区提供了回答

A2:最近自己试着升级项目到URP,发现Game View的湖水深度效果没有了,Scene View的是正常的,做了很多实验发现了两个现象。

我之前的湖水Shader的Queue是Geometry + 150,保证自己在其他不透明物体之后渲染。别的物体有Shadow Caster的Pass,湖水没有,这样在别的不透明物体渲染完成后自己能直接用到正确的_CameraDepthTexture。但是在URP下我必须将Render Queue设置到Transparent层才有正确效果。

发现勾选了MSAA抗锯齿后,就有跟Scene View一样正确的深度效果了。

URP默认优先使用Copy的方式在所有不透明Queue的物体渲染完后把深度Copy到_CameraDepthTexture上,我的湖水Queue设置在不透明层了,即使是最后渲染的,他的深度也进入了深度图中,因此效果没了。勾选MSAA会正常是因为MSAA会影响管线无法用Copy的方式把深度图拷出来(需要Resolve解析),所以URP默认在这种情况下使用老方式通过渲染Depth Only (原Shadow Caster)的Pass得到深度图,因此回到了老方式,我的Shader就又起效果了。

其实自己如果当时看到Framedebugger的时候,是用心读的而不是只是草草看一眼就花心思在自认为的问题原因上会更容易得到答案。

感谢题主安日天@UWA问答社区提供了回答


Q6:渲染大面积草地时,如何降低消耗

请问下大家,渲染大面积草地时,如何降低消耗呢?

A1:回答如下:

  1. 使用DrawMeshInstance;
  2. 上面这个API是不会进行视距剔除、视锥体剔除和遮挡剔除的。

下面有两种方案:
a. 将草地按区域分组,用每组的中心点计算视距,依据距离切换网格LOD或剔除;还能用向量点乘简单剔除在相机后方的草地(注意临界问题)。
b. 借助CullingGroup。
CullingGroup.onStateChanged事件绑定,通过事件触发调整传入;DrawMeshInstanced的Matrix顺序和渲染数量(但是DrawMeshInstanced只能指定渲染前几个Matrix);
通过cullingGroup.SetBoundingSpheres实现视锥体剔除和遮挡剔除;
通过cullingGroup.SetBoundingDistances实现视距剔除和LOD。
这个方案最好也进行区域分组,不然CullingGroup的事件监听占用会比较高,在中端机上4000个监听会占约2ms的大小。

以后如果有对比两种方案的性能,我再进行补充。

附:

  1. 《CullingGroup API的使用说明》
  2. 《Unity 3D研究院之Lightmap支持GPU Instancing》
  3. 《如何高效使用GPU Instancing技术来进行草丛渲染》
  4. 升级Unity 2018,DrawMeshInstanced不生效的问题

感谢题主李先生@UWA问答社区提供了回答

A2:使用Indirect模式的Instancing,配合Compute Shader实现视锥剔除和遮挡剔除。

感谢邹春毅@UWA问答社区提供了回答

A3:推荐一个使用URP制作的草海效果,亲测可在Mobile端使用。
Unity URP Mobile Draw Mesh Instanced Indirect Example性能测试:

  • can handle 10 million instances on Samsung Galaxy A70 (GPU =
    adreno612, not a strong GPU), 50~60fps, performance mainly affected
    by visible grass count on screen(draw distance = 125)
  • can handle 10 million instances on Lenovo S5 (GPU = adreno506, a weak GPU), 30fps, performance mainly affected by visible grass count on screen(draw distance = 75)

感谢Vest@UWA问答社区提供了回答


Q7:Packages目录下的Shader打包AssetBundle

Unity引入了Package Manager来进行管理插件管理,例如URP引入Packages之后会有目录Packages/com.unity.render-pipelines.universal@7.3.1。请教一下各位,如何对Packages目录下的资源进行AssetBundle打包?

例如,工程目录中有材质球引用到URP的Shader,那么材质球打成AssetBundle之后会将Shader包含进去,会有Shader解析耗时。

A1:我这边是只使用SBP而不用Addressable,这样通过使用AssetBundleBuild是可以将Packages中的资源也打包成AssetBundle的。

将所有依赖到的Shader(包括Packages中的)都使用AssetBundleBuild设置到同一个shader.bundle的,打包后也解包确认了,Packages中的Shader也打包在shader.bundle而不会被包含在材质AssetBundle中。

感谢黄晓文@UWA问答社区提供了回答

A2:我在尝试将现有项目转成URP的时候,遇到和Addressable系统有些不兼容问题。
在打包引用了URP的Shader的Material时会发生Shader被重复打包现象。
如果想把URP的Shader单独打包,又会发现因为不在Assets目录内,Addressable管不到的问题。

我的解决方案是将用到的URP的Shader拷出来,放到Assets目录下通用Shader目录。
当然需要将该Shader改名,并且要注意将内部引用的Shader也一并拷出管理。

不过一般项目中使用的Shader往往还是会自己编写,直接使用官方提供总会遇到这种那种问题。因此我也会考虑尽量不用官方默认Shader,这时对于URP而言自然更加需要将Shader拷出来进行改造了。

感谢黄程@UWA问答社区提供了回答

A3:经过 黄晓文 的思路,已经解决。
打包AssetBundle最重要的,就是指定资源Path的源路径,以及去往的目的AssetBundle地址,这个问题关键是需要知道资源在Packages中的源路径。

例如一个Packages下的Shader资源,Lit.shader,通过AssetDatabase.GetAssetPath可以发现路径是:Packages/com.unity.render-pipelines.universal/Shaders/Lit.shader,这个是正确的路径,用它即可。

而错误的路径分别是:

  1. Unity中看到的:Packages/Universal RP/Shaders/Lit.shader 错误。
  2. 在文件目录中看到的:Packages/com.unity.render-pipelines.universal@7.3.1/Shaders/Lit.shader错误

所以得出结论:Packages 下的资源打包,去除一下 @x.y.z 即可。

感谢题主一刀@UWA问答社区提供了回答

A4:可以试试使用Scriptableobject或Material引用到Shader文件,然后把ScriptableObject或Material打到AssetBundle里。

感谢上午八点@UWA问答社区提供了回答


Q8:Shared UI Mesh内存占用过高

缓存池中的UI如果不隐藏,Shared UI Mesh会比较高;如果隐藏,Shared UI Mesh会比较低,但是UI SetActive又有性能消耗,该如何权衡呢?

隐藏缓存池中的UI时,Shared UI Mesh内存占用:

2020年度大赏 | UWA问答精选

 

不隐藏缓存池UI时Shared UI Mesh内存占用:

2020年度大赏 | UWA问答精选

 

A1:Shared UI Mesh是源自UGUI框架中的一个静态全局变量Graphic.workerMesh:

2020年度大赏 | UWA问答精选

 

而workerMesh主要在以下代码中使用:

2020年度大赏 | UWA问答精选

 

该函数是在Rebuild单个UI元素的顶点信息,红框里的FillMesh就是将更新后的顶点属性数组设置到workerMesh上,且每次调用都会先进行Clear操作。

看逻辑,这个workerMesh的内存大小应该只和单个UI元素的顶点量有关,但实际测试下来,是和当前所有激活UI元素的顶点总量相关的。

所以,Shared UI Mesh很大,表示当前所有激活UI元素的顶点总量很高。需要对部分复杂元素进行简化。

常见的复杂元素有:

  1. Tiled模式的Image:该模式下会根据UI元素的区域和纹理分辨率的大小,自动生成适当数量的四边形,一旦纹理分辨率很小,而区域很大时,就会产生大量的顶点。
  2. Outline效果的Text:Outline效果会将Text文本原来的顶点数放大为5倍。
  3. RichText,且包含了较多样式的Text:样式标签部分也会产生顶点数。
    注:2和3同时使用时,样式标签部分的顶点数也会放大。

定位的方法:

  1. 初步定位:直接在SceneView下换到线框模式,肉眼找一下复杂元素;
  2. 通过Profiler的UI面板,查看各个Canvas下各个Batch产生的顶点数,并检查对应的GameObject即可。
    2020年度大赏 | UWA问答精选

需要注意的是,Canvas组件被禁用的情况下,Profiler里是看不到的,但其下的激活UI元素依然会影响Shared UI Mesh的大小。

该回答由UWA提供

A2:如果只有SetActive才能降低Shared UI Mesh,好像就没有其他选择了;但是如果切换layer可以降低,可以选择该办法。

感谢青麈@UWA问答社区提供了回答

A3:也可以试试把Canvas的Enable设置为False。

感谢Crazy_Liu@UWA问答社区提供了回答


Q9:Addressable如何删除旧资源

目前计划使用Addressable来实现资源热更新,实际真机测试发现当资源更新后,旧的资源Addressable并不会把它删除,同时可以看到App占用的数据文件会越来越大。请问有什么办法可以把指定的Group或Label的资源删除吗?

试了Addressable.ClearDependencyCacheAsync也不行。实际测试这个接口只能删除最新版本的资源。当本地已经是最新版本资源时这个接口确实有效;但是如果本地需要更新资源时,这个接口应该也是尝试去删除最新资源,然而本地并没有最新版的资源,所以大概就无效了。

A:调用Addressable.ClearDependencyCacheAsync实质是调用了 “Caching.ClearAllCachedVersions();”。事实上是使用了Unity的Caching系统。

在Windows编辑器环境测试了一下。
Caching的目录为“C:\Users\UserName\AppData\LocalLow\Unity\ProjectFolder”,当正常下载AssetBundle以后,该目录内就出现 “stage01_298bd883434eedb69ea7316cb23e0b0d\662ab7a0d2aa99bc7a2dbb7baec63872” 之类的目录,并保存着当前的AssetBundle版本,当更新AssetBundle并执行下载以后,该目录也会出现其他AssetBundle的Caching目录。

在执行下载之前,先执行了一下“Caching.ClearCache();”,这时会发现Caching目录内已经被清空,所有版本的AssetBundle都没有了。下载完成后,该目录只保留了最新的AssetBundle资源。由此可推,即使不通过Addressable系统,仍然可以通过Caching把所有的资源都清理掉。

于是继续进行第二个实验,连续更新几次AssetBundle以后,Caching目录内已经有多个版本的AssetBundle目录了,当有新的更新后执行 “Addressables.ClearDependencyCacheAsync(key);”,发现的确并没有将旧版本的AssetBundle都删除。因为“Caching.ClearAllCachedVersions”的参数是对应的AssetBundle名字,而Addressables的管理AssetBundle包名是带Hash的,因为每个版本的AssetBundle文件名都不一样的,Caching系统也就无法分辨了。

继续做实验,将打包名字去掉Hash,Caching目录内的AssetBundle目录名也不带Hash了,然后连续更新几个版本后发现,该AssetBundle目录内多了几个不同Hash版本的目录,内部才是真正的AssetBundle。于是走“Addressables.ClearDependencyCacheAsync(key)”,这时就能正确地删除旧版本,然后再更新新版本了。

确实不勾选Hash打包可以成功删除了,这种方式貌似就是覆盖式的打包,不知道会不会有其他隐患,目前来看够用。

A:隐患就是如果按照Label来做更新检查,本来可以只下载差异部分,但是因为同样使用Label做清除Caching的工作就会造成重复下载原本不必要更新的部分。于是就需要遍历所有的Location然后去检查更新,并将有更新的AssetBundle放入列表,然后再依次清除旧缓存,重新下载。这样就和传统方案没太大区别了。

请问下不勾选Hash其实就不用清除了吧?名字一样不是会直接覆盖吗?

A:不勾选Hash,只是在Cache的目录内第一级资源同名子目录是一致的,但是里面保存具体数据的子目录是递增的,因为有不同版本。每个版本都会有一个子目录。这个是Caching系统管理的。

如果不勾选Hash,CDN有可能不会更新文件,所以要结合自己的项目使用的CDN情况来确定如何管理这块。

我是用Addressables.ClearDependencyCacheAsync(key)并没有清除Cache多次更新后越来越多。上面所说的“将打包名字去掉Hash”,是指配置中Bundle Naming选择Fliename选项嘛?所使用的Key是指本次更新的列表吗?

A:就是“配置中Bundle Naming选择Fliename”,这个Key其实是应该是这个Group名字,对应到打包后的Bundle名字,让Caching系统搜索。貌似1.15.X后面这块有一些更新,还没确认过。

感谢黄程@UWA问答社区提供了回答

A:在论坛里看到一个方案(11楼),修改了Addressables.ClearDependencyCacheAsync(key)的实现:

首先在Addressables.ClearDependencyCacheForKey中对当前使用的资源进行Cache标记,然后在Addressables.ClearDependencyCacheAsync里清除游戏运行之后未使用的资源。

修改AddressablesImpl.cs文件中的以下四个方法:

  • ClearDependencyCacheForKey(object key)
  • ClearDependencyCacheAsync(object key)
  • ClearDependencyCacheAsync(IList locations)
  • ClearDependencyCacheAsync(IList keys)

需要注意调用Addressables.ClearDependencyCacheAsync的时机。

        internal void ClearDependencyCacheForKey(object key)
        {
#if ENABLE_CACHING
            IList<IResourceLocation> locations;
            if (key is IResourceLocation && (key as IResourceLocation).HasDependencies)
            {
                foreach (var dep in (key as IResourceLocation).Dependencies)
                    Caching.ClearAllCachedVersions(Path.GetFileName(dep.InternalId));
            }
            else if (GetResourceLocations(key, typeof(object), out locations))
            {
                foreach (var loc in locations)
                {
                    if (loc.HasDependencies)
                    {
              foreach (var dep in loc.Dependencies){
              // added by Lukas
                            AssetBundleRequestOptions options;
                            if ((options = dep.Data as AssetBundleRequestOptions) != null)
                            {
                   //对当前依赖资源进行标记
                                Caching.MarkAsUsed(dep.InternalId, Hash128.Parse(options.Hash));
                            }
                            //原方法无法删除旧版本的ab
              //Caching.ClearAllCachedVersions(Path.GetFileName(dep.InternalId));
                        }   
                    }
                }
            }
#endif
        }

  

        public AsyncOperationHandle<bool> ClearDependencyCacheAsync(object key)
        {
            if (ShouldChainRequest)
                return ResourceManager.CreateChainOperation(ChainOperation, op => ClearDependencyCacheAsync(key));

            ClearDependencyCacheForKey(key);
            // added to ClearCache 
        Caching.ClearCache((int) Time.realtimeSinceStartup + 10);
            var completedOp = ResourceManager.CreateCompletedOperation(true, string.Empty);
            Release(completedOp);
            return completedOp;
        }

  

感谢小魔女纱代酱@UWA问答社区提供了回答

A:我们用的是覆盖式更新的流程(不是增量更新)。Addressables版本是1.15.1。

在把Bundle Naming设置为Filename后发现,在Caching中的AssetBundle目录还是带有Hash值的,这个和楼上的解释不一致,不知道是不是版本的原因。

2020年度大赏 | UWA问答精选

 

图中可看到AssetBundle包名已经是Group的名字了,但是下载到Caching中还是有 Hash。

然后我们是这么解决的。还是开启文件名的Hash,将Caching中的AssetBundle文件夹名保存到PlayerPrefs中,当检测到有下载的时候,读出PlayerPrefs中的值,把旧的对应AssetBundle包删除,并更新PlayerPrefs。

获取当前Catalog中所有AssetBundle文件夹名的方法,是从Addressables中复制出来的。

    // 获得当前catalog中所有 assetbundle 保存的文件夹名
    // 这个函数中引用到的方法没有列出,可以去 addressables 中源码中找 
    // 示例:CollectBundleNames(new string[]{ "SkllIcons", "ItemIcons", "AvatarIcons" })
    private static List<string> CollectBundleNames(object[] keys)
    {
        List<string> result = new List<string>();
#if ENABLE_CACHING
        foreach(var key in keys)
        {
            IList<IResourceLocation> locations;
            if (key is IResourceLocation resourceLocation && resourceLocation.HasDependencies)
            {
                foreach (var dep in resourceLocation.Dependencies)
                {
                    if (dep.Data is AssetBundleRequestOptions options)
                    {
                        result.Add(options.BundleName);
                    }
                }
            }
            else if (GetResourceLocations(key, typeof(object), out locations))
            {
                var deps = GatherDependenciesFromLocations(locations);
                foreach (var dep in deps)
                {
                    if (dep.Data is AssetBundleRequestOptions options)
                    {
                        result.Add(options.BundleName);
                    }
                }
            }
        }
#endif
        return result;
    }

  

删除AssetBundle包文件夹的方法:

// 这里的 bundleName 就是 CollectBundleNames 的返回值
private static void ClearCacheForBundle(string bundleName)
    {
        List<Hash128> hashList = new List<Hash128>();
        Caching.GetCachedVersions(bundleName, hashList);
        foreach (Hash128 hash in hashList)
        {
            Caching.ClearCachedVersion(bundleName, hash);
        }
    }

  

需要注意的是调用CollectBundleNames的时机,如果已经更新了Catalog,那么返回的是即将要写入Caching中的AssetBundle文件夹名。如果要得到当前AssetBundle文件夹名,要在更新Catalog之前调用。

感谢jim@UWA问答社区提供了回答


Q10:LuaJIT性能热点函数优化

项目中的这个函数耗时非常严重,有什么优化的方法吗?

2020年度大赏 | UWA问答精选

 

A1:这个是table.get,对应获取字段或者访问数组时调用的函数:

  1. 优先使用连续数组而不是英文名字段,可以显著提升访问效率并降低内存消耗,很多团队喜欢使用Class的写法,可以这样改造:
    local obj = ClassA.New()
    obj.abc = 1
    obj.cde = "test"
    变成:
    local obj = ClassA.New()
    obj1 = 1
    obj2 = "test"
    这个方法可以针对使用频率较高的代码进行改造。

  2. 自己开发工具,在编译Lua之前,将Lua代码中的常量从英文名变量转换为数值,这个可以结合1使用,就可以在开发期写英文名字段名,然后编译时转换为数组。

感谢招文勇@UWA问答社区提供了回答

A2:字符串应该是哈希值计算的消耗,这样的开销应该是很频繁地调用了。

感谢王欢@UWA问答社区提供了回答

 

今天的分享就到这里。当然,生有涯而知无涯。在漫漫的开发周期中,您看到的这些问题也许都只是冰山一角,我们早已在UWA问答网站上准备了更多的技术话题等你一起来探索和分享。欢迎热爱进步的你加入,也许你的方法恰能解别人的燃眉之急;而他山之“石”,也能攻你之“玉”。

 

官网:www.uwa4d.com
官方技术博客:blog.uwa4d.com
官方问答社区:answer.uwa4d.com
UWA学堂:edu.uwa4d.com
官方技术QQ群:793972859(原群已满员)

上一篇:本地资源检测,特效检测中Overdraw相关问题


下一篇:UI节点对运行效率的影响