Bootstrap

Unity - 性能优化 - 包体,内存 - 偏静态资源的优化


如果 产品 制作出来不经过优化,那不叫 产品,准确的说是:DEMO


此文偏 静态资源的优化 内容
如果是 unity 动态画质设置,可以参考目录中:运行时的品质调整
传送门链接,可以参考我之前写的:Unity - 画质设置


之前(2022.1月初左右的时候)做了一些项目分析,发现某个项目 性能方面 惨不忍睹

下面更多的是简单的调用 Unity API 来做的优化

还有其他更多的是优化经验上的数值

初步判断可以做的优化点:

(下面是简述版本,详细的就不说了,免得影响可读性)

---
- 资源压缩:
  - 纹理(格式,尺寸)- 减少 内存 包体
  - 模型(点面数量这个之前说 扒出来的资源,不好改,其实 *.obj, *.fbx 都可以放到 3D MAX 修改后重新导出即可,但是也要工作量) - 减少包体
  - 动画精度压缩,动画帧删减(可能会导致部分动作精度不足,会产生小量的抖动) - 减少 内存 包体
  - 烘焙(烘焙纹理精度降低,方向光光贴删除,一般需要近距离的才会用到,也可以减少用到烘焙的地方的纹理大小) - 减少 内存 包体


---
- 画质分档(低中高):
  - 分档规则:
    - 不同项目的分档规则都是不同的(CPU 通用算力、内存大小,Graphics API 的最高支持版本,GPU算力、显存大小,系统版本,如:Android 多少版本)
    - 具体要看使用到什么版本的功能(压缩格式,Graphics API 版本特性,对内存的需求量,等)
    - 在游戏第一次运行的时候,进去游戏前,就要给玩家设备定档
    - 在定档完毕自动帮玩家选择 低中高 对应的画质级别
  - 纹理:
    - 纹理默认 mipmap 级别 - 低中高:1/4 mipmap;1/21,看情况;1/1 原始 mipmap 值0- 提升缓存命中率
  - 渲染分辨率:
    - 低中高(75%, 85%, 100%- 减少 片段着色器压力,和 绘制 内存、显存、带宽的占用
  - 阴影:
    - 低中高:无;低质量,阴影距离小;原始质量,阴影距离原始值 - 减少:内存、显存 占用
  - 抗锯齿:
    - 低中高:无;无;x2;- 减少:内存、显存 占用
  - 后效:
    - 低中高:全无;删减部分;全部保留; - 减少:内存、显存 占用
  - Shader LOD:(但是这个会增加 shader 变体,看情况而选择是否需要优化,如果本身 shader 效果简陋、消耗能接受,就不需要处理这步)
    - 低中高:最大限度的删减 部分光照、或是逐片段挪到逐顶点光照 效果;删减部分 光照效果;保留所有效果
  - 分档后有什么优缺点:
    - 优点:
      - 让部分机型的兼容性、运行流畅性 提高
    - 缺点:
      - 工作量增加(美术资源、程序代码都要增加,这些都需要时间)
      - 包体额外增加,shaderlab LOD, model LOD, texture mipmap 的增加大概会让包体增加 1/3
    - 整体来说是利大于弊的(毕竟 硬性指标:兼容性、流畅性 摆在那里)


一般低端机,可以再 15~24 FPS,中端机再 30 FPS,高端机要:30~60 FPS

(极高的:60FPS+,我们没有极高画质,所以就算了)


---
- 缓存策略:
  - 现在发现 GUI 实例都没有删减,那么就会可能导致 内存占用巨大
  - 可以看下列方式来优化:
    - 按 LFU, FIO, LRU 都可以(未确定哪种)
      - FIFO(First In First out):先见先出,淘汰最先近来的页面,新进来的页面最迟被淘汰,完全符合队列。
      - LRU(Least recently used):最近最少使用,淘汰最近不使用的页面
      - LFU(Least frequently used): 最近使用次数最少, 淘汰使用次数最少的页面
    - 可以参考:三种常见的缓存过期策略LFU,FIFO,LRU说明 https://blog.csdn.net/weixin_42449534/article/details/100541192
  - 不单指 GUI 要这么做缓存策略,其他 缓存也是一样可以这样管理的


---
- 部分资源的 制作调整:
  - 现在的 shaderlab 变体方式:
    - 我们这个项目的所有 shader 都放在: Always Include Shaders 列表中,那么就有个问题,所有变体都会添加进去
    - 这种方式会导致 shader 变体不可优化,会在发包的时候,生成 并 添加所有的变体在 包体中(增加包体)
    - 运行是还会增加导致 shader 变体数量过多而 导致 内存占用变多(增加内存占用)
  - 需要优化的方式:
    - 将 母包需要的 shader 直接放 Always Include Shaders
    - 将 其余非母包资源 都应该独立在一个 shaders.ab AB 包中(如果项目体量巨大,还得 shader 资源分包加载)
    - 然后在程序运行后,初始化时加要先加载 shaders.ab 资源、

---
- GC 优化(内存垃圾回收)- (跳过)- 影响流畅性
  - 目前我们的 GC 间歇性卡顿也是有点严重的,用 profiler 分析即可知道
  - 本想使用 unity 的 increasement gc,但是发现这个就项目使用的是 unity 2018 还没有这个功能
  - 代码大改就算了,目前时间不允许
  - 这个在平时制作时就需要比较严格的 code review 中可以避免大部分的 GC 问题,或时 后期项目 时间允许情况下,并且添加人力集中时间去优化才行


---
- 代码写法、算法问题(暂时不处理)
- 特效(暂时不处理)
- GUI 纹理图集的合理划分(暂时不处理)


---
各个部分优化时的建议

每个部分不要等到全部的量处理完才去验证,而是要在每个具有 代表性的最简优化后,马上测试
这样才能及时发现问题,将问题影响范围压缩到最小


---
另外在项目阶段接近尾部时,才做优化的话,尽量按项目需求来选择,而不是所有都要做,这个都需要时间
前面制作有多 “快”(挖坑) 后续就需要多少 代价 来填补前面 挖的坑
坑大到一定程度才考虑优化,那么无异于重新制作项目,所以平时在制作项目时,尽量不要以为快就是好
要质量和速度中选一个能接受的,尤其现在大浪淘沙的游戏发展期更加要权衡好 速度 与 质量

最后的节奏就是每个功能在开发的时候,都要让参与管理的伙伴了解,这里头开发的速度、质量的影响,让大家选一个合理的,能接受的节奏来开发

这个经验,我在多个项目都有过这些教训,这对以后制作任何一个项目都是有帮助的

以上就给项目组的一些建议


然后我自己着手做了一些优化:

粗略记下好几点,便于后续自己查阅

  • Profiler
    • Unity Profiler - Profiler overview
      • Memory Take Sample - 可以快速定位及优化 99% 以上的 unity 资源问题
    • CPU 可以定位 70% 左右的 CSharp 源码级别的性能问题,因为 一些 黑箱 API 就无法优化
    • GPU 不建议使用,使用其他的 Profiler 工具更佳

其中 unity 资源、性能问题基本上好几个大块:

  • 纹理 - 可以使用脚本扫描输出日志,但是不精准,只能模糊的定位,如果提量不大的项目,可以使用 Unity Profiler -> Memory -> 左下角 Simple 改 Detail -> Take Sample: Editor,然后一个个过目即可

    • 格式

    • 尺寸

      • UI 的还注意是否可以 3/4/6/9 宫格来减少尺寸
      • 部分特效为了效果制作了很大尺寸的,就很不划算了,效果 性能要权衡好,有些时候,把 尺寸压小一些,特学同学也时可以接受的范围,就放心的压缩特效纹理尺寸,不然 overdraw 很严重
    • Mipmap - 当指定使用 最高清的 Mipmap 层级时,可以有效的减少内存、显存,提升缓存命中率,可以通过设置:QualitySettings.masterTextureLimit 来达到效果,另外 UI 纹理不要开启 mipmap,可以节省包体,内存,显存
      另外 如果 3D 场景的镜头不会有镜头远景的效果(比如:正交投影,不会有大小控制;或是透视投影下的镜头不会前后移动),那么同样不需要 Mipmap

    • 烘焙贴图

      • 尺寸 及 效果的权衡
      • 部分场景如果不是需要近距离的观察物体的,可以不需要 DirectionMode,这样可以减少 dir 贴图
    • 冗余资源

      • 可以使用 UPR Asset Checker 来分析是否又冗余,一般判断原理是:字节大小、尺寸,如果都相同,很大程度上是同一个纹理的图像,再不够精准也可以使用图像像素的数据来做 MD5 判断,Asset Checker 都帮我们做了这些分析
  • 模型

    • Readable/Writeable - 一般 CPU 层不需要读写网格数据的话,注意不要开启 R/W - 低版本的 unity 甚至默认是开启的,如果开启了,会再 CPU 主存,GPU 显存都会占用各一份
    • Optimize Mesh - 尽量开启(High, Medium, Low, Off),减少不必要的顶点 和 索引缓存的数据,减少包体和内存、显存
    • BlendShape - 没有表情动画的都去掉
    • Cameras - 一般去掉
    • Lights - 一般去掉
    • Visibility - 一般去掉
    • Animation Compress - 一般Optimize
    • Generate Lightmap UVs - 如果不参与烘焙的不要勾选
    • Normals, Tangents - 法线,切线的确定不需要的都可以去掉,如:UI 上的特效网格
  • 模型动画

  • 音频

    • 下面的经验数值参考:《Unity性能优化》第壹节——静态资源优化(1)——Audio导入设置检查与优化
    • 音频在内存与CPU上的性能考量:
      • 小于 200kb,使用 Decompress on load 再加载时,一次性解压到内存,节省 CPU 中途解压数据的消耗,但是,但内存会占用变大,比如:Vorbis 编码的音频,再解压到内存,内存会翻 10倍,具体参考官方文档说明:Audio Clip
      • 大于 200kb 时长超5秒,使用 Compress in Memory,一次性加载到内存的数据都是压缩过的,所以需要实时解压播放
      • 时长比较长的使用 Streaming 方式,可节省内存,因为是按流式加载,每播放一段音频缓存的数据,才从硬盘中读取,并解压,所以才比较省内存,但是,CPU会额外占用在另一个线程的处理:流式文件数据读取 + 内存解压
    • 音频资源大小考量:
      • 一般手游的话,采样率使用 22 MHZ (22050 Hz) 就够了,而不必使用 44MHZ (44100Hz),这样文件也会小很多
      • ForceToMono:如果左右声道的数据都是一样的,但是又占用了双声道,那么内存必然浪费,这种音频,设置一下 ForceToMono 设置为单声道 即可,资源大小,与内存都可以减少;但是 这个音频的双声道是否一致,可以使用 UPR 的 Asset Checker 来分析,人耳来分析就太费时间了,也费耳朵
  • AA

    • 可以通过 AA,查看抓帧的性能消耗
    • 最简单的在低端机(用过一台低端测试机:CPU: 1.4 GHz, 4 Cores, RAM: 1.8 GB, GRAM: 512 MB),测试 开启和 关闭 AA 后的性能对比是相当明显的
  • Physics

    • 能用 SphereCollider,BoxCollider 等简单的 Collider 就不用 MeshCollider
    • 特别是动态对象的 Collider 也是非常消耗性能的,所以重点关注一下,动态对象的 Collider 是否足够简单
  • 合批 (优先级从上往下)

    • 合批这里 指 DC,也有 SetPassCall 的合批,具体理解区别可以参考我之前写的:Unity - DrawCall, Batch, SetPassCall区别
    • 能 instancing 尽量 instancing batching
    • 部分网格数据体量(点、面 数量)小的,材质一致,图集一致的,静态不动的物体,尽量 static batch
      • 这部分会占用额外的内存,如果内存非常吃紧,就再考量考量
    • 然后才是 dynamic batching
      • 需要相同的材质
      • 一般用于顶点数小,顶点属性少的比较适合,如:粒子
      • 会消耗 CPU 计算,因为会申请 大的 VERTEX BUFFER,并将:positionOS -> positionWS,然后再设置到 VERTEX BUFFER 中的 POSITION
  • 包体大小

  • 内存


没整的部分

  • AB 分类 - 没去整,因为惨不忍睹
  • ShaderLab 变体删减 - 没去整,也是惨不忍睹,全都放 always includes 列表里(所以后面大部分的 shader lab 变体优化我都时直接针对单个 shader 直接减去不并要的变体,具体可以参考:Unity Shader - Built-in管线下优化 multi_compile_fwdbase、multi_compile_fog 变体
  • 缓存机制 - 惨不忍睹,没什么机制,特别时 UI 模块 全都常驻内存

经过上面的操作,就删减了近 400~500 MB 的内存消耗


静态资源优化 - AssetPostprocessor

通常游戏资源中,占用 包体、内存 最大的部分都是:

  • 纹理
  • 音频
  • 模型动画、网格

下面就 针对这 三中类型的资源来处理


Texture 压缩

在 AssetPostProcessor 中对纹理处理:

// jave.lin : 纹理压缩
    static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
    {
        if (importedAssets.Length > 0)
        {
            OnHandleTexImportProperties(importedAssets);
        }
        if (movedAssets.Length > 0)
        {
            OnHandleTexImportProperties(importedAssets);
        }
    }

    void OnPreprocessTexture()
    {
        OnHandleTexImportProperties((TextureImporter)assetImporter);
    }
        
	public static void OnHandleTexImportProperties(string[] assetPaths)
    {
        foreach (var assetPath in assetPaths)
        {
            var importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
            if (importer != null) OnHandleTexImportProperties(importer);
        }
    }

    public static void OnHandleTexImportProperties(TextureImporter importer)
    {
        var assetPath = importer.assetPath;
        if (assetPath.StartsWith("Assets/xxxxx"))
        {
            SetTexAndroidSetting(importer, TextureImporterFormat.ETC_RGB4Crunched, 50, 512);
        }
        else if (assetPath.StartsWith("Assets/xxxxx"))
        {
            SetTexAndroidSetting(importer, TextureImporterFormat.ETC2_RGBA8Crunched, 50);
        }
        else if (assetPath.StartsWith("Assets/xxxxxxx"))
        {
            SetTexAndroidSetting(importer, TextureImporterFormat.Automatic, 50, 512);
        }
        else if (assetPath.StartsWith("Assets/xxxxx"))
        {
            SetTexAndroidSetting(importer, TextureImporterFormat.Automatic, 50, 512);
        }
        else if (assetPath.StartsWith("Assets/xxx/Scenes/xxx"))
        {
            if (assetPath.Contains("comp_dir"))
            {
                SetTexAndroidSetting(importer, TextureImporterFormat.ETC2_RGBA8, 50, 512);
            }
            else if (assetPath.Contains("comp_light"))
            {
                SetTexAndroidSetting(importer, TextureImporterFormat.ETC_RGB4, 50, 512);
            }
            else if (assetPath.Contains("shadowmask"))
            {
                SetTexAndroidSetting(importer, TextureImporterFormat.ARGB16, 50, 512);
            }
        }
        else if(assetPath.StartsWith("Assets/xxxx"))
        {
            SetTexAndroidSetting(importer, TextureImporterFormat.Automatic, 50, 256);
        }
    }

    private static void SetTexAndroidSetting(TextureImporter importer, TextureImporterFormat format, int compressionQuality, int maxTexSize = -1, TextureImporterType? texType = null, bool? sRGB = null)
    {
        try
        {
            TextureImporterPlatformSettings originalSettings = importer.GetPlatformTextureSettings("Android");
            var new_format = format == TextureImporterFormat.Automatic ? originalSettings.format : format;
            var new_maxTextureSize = maxTexSize == -1 ? originalSettings.maxTextureSize : maxTexSize;

            if (originalSettings.name != "Android" ||
                originalSettings.overridden != true ||
                (texType.HasValue && (importer.textureType != texType.Value)) ||
                originalSettings.format != new_format ||
                originalSettings.compressionQuality != compressionQuality ||
                originalSettings.maxTextureSize != new_maxTextureSize ||
                (sRGB.HasValue && (importer.sRGBTexture != sRGB.Value))
                )
            {
                originalSettings.name = "Android";
                originalSettings.overridden = true;
                importer.textureType = texType.HasValue ? texType.Value : importer.textureType;
                originalSettings.format = new_format;
                originalSettings.compressionQuality = compressionQuality;
                originalSettings.maxTextureSize = new_maxTextureSize;
                importer.sRGBTexture = sRGB.HasValue ? sRGB.Value : importer.sRGBTexture;
                importer.SetPlatformTextureSettings(originalSettings);
                importer.SaveAndReimport();
            }
        }
        catch (System.Exception er)
        {
            Debug.LogError(" SetTextureAndroidSetting Error : assetPath : " + importer.assetPath + "\n" + er);
        }
    }

Model 网格、动画 压缩

	// jave.lin : 模型、动画压缩
    static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
    {
        if (importedAssets.Length > 0)
        {
            OnHandleModelImportProperties(importedAssets);
        }
        if (movedAssets.Length > 0)
        {
            OnHandleModelImportProperties(importedAssets);
        }
    }

    public void OnPostprocessAnimation(GameObject go, AnimationClip clip)
    {
        try
        {
            CompressAnimationClip(clip);
        }
        catch (System.Exception e)
        {
            Debug.LogError("CompressAnimationClip Failed !!! animationPath :" + assetPath + "error: " + e);
        }
    }
    public static void CompressAnimationClip(AnimationClip clip, bool removeScaleAnima = true, string precision = "f4")
    {
        if (removeScaleAnima)
        {
            var editorCurves = AnimationUtility.GetCurveBindings(clip);
            if (editorCurves != null)
            {
                foreach (EditorCurveBinding editorCurve in editorCurves)
                {
                    string name = editorCurve.propertyName.ToLower();
                    if (name.Contains("scale"))
                    {
                        AnimationUtility.SetEditorCurve(clip, editorCurve, null);
                    }
                }
            }
        }

        AnimationClipCurveData[] curves = AnimationUtility.GetAllCurves(clip);
        for (int ii = 0; ii < curves.Length; ++ii)
        {
            AnimationClipCurveData curveDate = curves[ii];
            if (curveDate.curve == null || curveDate.curve.keys == null)
            {
                continue;
            }
            Keyframe[] keyFrames = curveDate.curve.keys;
            for (int i = 0; i < keyFrames.Length; i++)
            {
                Keyframe key = keyFrames[i];
                key.value = float.Parse(key.value.ToString(precision));
                key.inTangent = float.Parse(key.inTangent.ToString(precision));
                key.outTangent = float.Parse(key.outTangent.ToString(precision));
                keyFrames[i] = key;
            }
            curveDate.curve.keys = keyFrames;
            clip.SetCurve(curveDate.path, curveDate.type, curveDate.propertyName, curveDate.curve);
        }
    }

    public static void OnHandleModelImportProperties(string[] assetPaths)
    {
        foreach (var assetPath in assetPaths)
        {
            var importer = AssetImporter.GetAtPath(assetPath) as ModelImporter;
            if (importer != null) OnHandleModelImportProperties(importer);
        }
    }

    public static void OnHandleModelImportProperties(ModelImporter importer)
    {
        var assetPath = importer.assetPath;
        if (assetPath == "Assets/xxxx/xxxx.fbx")
        {
            OnHandleModelImporterProperties(importer, false, ModelImporterMeshCompression.Medium, null, ModelImporterAnimationType.None);
        }
        else if (assetPath.StartsWith("Assets/xxxxx/Mode"))
        {
            OnHandleModelImporterProperties(importer, false, ModelImporterMeshCompression.High, null, ModelImporterAnimationType.None);
        }
        else if (assetPath.StartsWith("Assets/xxxx/Role"))
        {
            OnHandleModelImporterProperties(importer, false, ModelImporterMeshCompression.High, ModelImporterIndexFormat.UInt16, ModelImporterAnimationCompression.Optimal);
        }
    }

    private static void OnHandleModelImporterProperties(
        ModelImporter importer,
        bool? readable = null,
        ModelImporterMeshCompression? meshCompression = null,
        ModelImporterIndexFormat? indexFormat = null,
        ModelImporterAnimationType? animaType = null,
        ModelImporterAnimationCompression? animaCompression = null
        )
    {
        var changed = false;
        // jave.lin : model 属性
        if (meshCompression.HasValue && importer.meshCompression != meshCompression.Value)
        {
            importer.meshCompression = meshCompression.Value;
            changed = true;
        }
        if (indexFormat.HasValue && importer.indexFormat != indexFormat.Value)
        {
            importer.indexFormat = indexFormat.Value;
            changed = true;
        }
        if (readable.HasValue && importer.isReadable != readable.Value)
        {
            importer.isReadable = readable.Value;
            changed = true;
        }
        if (importer.optimizeMesh != true)
        {
            importer.optimizeMesh = true;
            changed = true;
        }
        if (importer.importBlendShapes != false)
        {
            importer.importBlendShapes = false;
            changed = true;
        }
        if (importer.importVisibility != false)
        {
            importer.importVisibility = false;
            changed = true;
        }
        if (importer.importCameras != false)
        {
            importer.importCameras = false;
            changed = true;
        }
        if (importer.importLights != false)
        {
            importer.importLights = false;
            changed = true;
        }
        if (animaType.HasValue && importer.animationType != animaType.Value)
        {
            importer.animationType = animaType.Value;
            changed = true;
        }
        if (animaCompression.HasValue && importer.animationCompression != animaCompression.Value)
        {
            importer.animationCompression = animaCompression.Value;
            changed = true;
        }
        if (changed)
        {
            importer.SaveAndReimport();
        }
    }

音频压缩

	// jave.lin : 音频压缩
    void OnPostprocessAudio(AudioClip clip)
    {
        AudioImporter importer = (AudioImporter)assetImporter;
        OnHandleAndroidAudioImporterProperties(importer, clip);
    }
    public static void OnHandleAndroidAudioImporterProperties(AudioImporter importer, AudioClip clip)
    {
        var changed = false;
        const string platform = "Android";
        var originalSettings = importer.GetOverrideSampleSettings(platform);
        if (originalSettings.compressionFormat != AudioCompressionFormat.Vorbis)
        {
        	// 手机端建议用 vorbis
            originalSettings.compressionFormat = AudioCompressionFormat.Vorbis;
            changed = true;
        }
        if (originalSettings.quality != 0.2f)
        {
        	// 压缩到的品质 .2F 就够了
            originalSettings.quality = 0.2f;
            changed = true;
        }
        if (originalSettings.sampleRateSetting != AudioSampleRateSetting.OverrideSampleRate)
        {
        	// 设置 采样率 的设置方式为:覆盖设置 的方式
            originalSettings.sampleRateSetting = AudioSampleRateSetting.OverrideSampleRate;
            changed = true;
        }
        if (originalSettings.sampleRateOverride != 22050)
        {
        	// 覆盖 采样率 的频率,采样频率越低,那使用数据量就越少,文件压缩就越小,手游一般 22050 就够了
            originalSettings.sampleRateOverride = 22050;
            changed = true;
        }
        // jave.lin : 
        // 按照上面的优化建议,200 KB 以下的使用 DecompressOnLoad 就可以了
        // 然后我看了一下 AudioClip 里面没有文件原始大小的信息,只有描述
        // 然后我观察了一下多个文件,大概 44 MHz 的采样率下的文件大小为规律为:60 秒 约等于 1 MB
        // 所以 200 KB 左右的,大概在 12 秒时长左右
        // 1 mb ~~= 60 s
        // 200 kb ~~= 12 s
        // 大概小于 200 KB 的
        if (clip.length < 12)
        {
        	// 使用 DecompressOnLoad 即可
            if (originalSettings.loadType != AudioClipLoadType.DecompressOnLoad)
            {
                originalSettings.loadType = AudioClipLoadType.DecompressOnLoad;
                changed = true;
            }
            // 可以预先加载
            if (importer.preloadAudioData != true)
            {
                importer.preloadAudioData = true;
                changed = true;
            }
        }
        // 200 KB ~ 300 KB 范围内的
        else if (clip.length > 12 && clip.length < 20)
        {
        	// 使用 CompressedInMemory
            if (originalSettings.loadType != AudioClipLoadType.CompressedInMemory)
            {
                originalSettings.loadType = AudioClipLoadType.CompressedInMemory;
                changed = true;
            }
            // 也可以预加载
            if (importer.preloadAudioData != false)
            {
                importer.preloadAudioData = false;
                changed = true;
            }
        }
        // 时长大约 20 秒的,使用 流式加载
        else if(clip.length >= 20)
        {
        	// 使用 Streaming
            if (originalSettings.loadType != AudioClipLoadType.Streaming)
            {
                originalSettings.loadType = AudioClipLoadType.Streaming;
                changed = true;
            }
            // 不要预加载
            if (importer.preloadAudioData != false)
            {
                importer.preloadAudioData = false;
                changed = true;
            }
        }
        if (changed)
        {
            importer.SetOverrideSampleSettings("Android", originalSettings);
        }
        // 强制设置为 单声道,减去 双声道的数据占用(根据项目实际情况确定是否需要有双声道的)
        if (importer.forceToMono != true)
        {
            importer.forceToMono = true;
            changed = true;
        }
        if (changed)
        {
            importer.SaveAndReimport();
        }
    }

可以将原来 321个 音频文件(含部分 BGM,大部分的 effect 音频)

从原来的:Imported Size : 12.0 MB 压缩到 4.8 MB,如下图:

在这里插入图片描述


内存优化,在简单的处理了资源部分,大部分是纹理资源:尺寸、格式问题,冗余纹理问题
再加上少部分的,网格压缩、音频压缩

从原来登录后占用:700 MB+ 减少至:300 MB

还没有做 程序上的缓存优化(这个项目内存对象很多都是常驻方式,实在是太无语)


经过对纹理、模型、音效、(Shader 变体就不整了,时间来不及)、部分冗余资源的删减

最后包体减少了近 200 MB(而且还是有部分冗余资源没有删除干净,这些不好处理,因为部分资源是动态加载的,可能配置在 excel表、写在lua 脚本、写在cs 脚本中,如果删除干净了,估计只有 350~450 MB 左右的 APK)
在这里插入图片描述

后面再经过我和策划将一些冗余资源再删除一遍
然后将一些未 9宫格化得,统统处理一遍
又减少了近 100MB
在这里插入图片描述

在这里插入图片描述

包体:
最后我将 项目的包体 从最开始的 780 MB 压缩到了 350 MB (减去了一倍多的包体亮,如果做成分包加载还可以将母包制作的更小)

内存:
之前登录到主城后,仅仅纹理占用的内存占用了:800MB±
经过静态资源的优化后,仅仅纹理占用的内存只有 100 MB± (还有很多优化空间,比如:还是由常驻的对象太多,-_-!, 但是这些涉及太多的业务多级代码 和 底层资源管理、加载的代码调整,然后主要负责的人员又离职了;所以就没去整了,因为 快要上线了,给 的优化 时间是有限的,因此权衡下来,还是不要大改;让我想起一张图:能看着运行,就不要修改,就算想水管交错的,但是刚刚好 BUG 之间互补抵消了,-_-!)


纹理的优化经验


尺寸

在这里插入图片描述
本身图像颜色很糊的
可以压缩小一些也不会又太多影响
上面的烟雾 256x256,还带有噪点问题
可以使用 photoshop ,先高斯模糊,再将尺寸压缩到 128x128,这样不会又噪点,效果也不会丢失,包体、内存都会更小

总结:

图像本身的糊的,并有明显的噪点、和色阶的问题,都可以高斯模糊一下,再压缩
这些是制作特效纹理的一些优化经验


通道

在这里插入图片描述

类似这种,黑色底的贴图
导出 jpg/jpeg 就好了(只要 RGB 三个通道,不需要 A(alpha) 通道)
这样程序这边就可以写脚本统一处理 纹理压缩格式
处理方式未:

  • jpg/jpeg 等用 RGB 三通道的数据
  • png/tga 等等 RGBA 四通道的数据
    这样内存、包体都可以更小

另外,静态资源的优化,在 unity 中有多种方式来处理(多种工具),本质一样的,都是对 assets importer 的设置

  • AssetPostprocessor - 需要有专人对资源维护(目录划分,资源规格,等)
  • AssetGraph - unity 后续出的插件,就是基于 PlayableGraph 类 开发的插件,也是比较美观,推荐可以使用这种方法,我暂时没去使用过
  • Presets - 的配置批量设置,暂时没使用过

文章是以前写的,后续(2022/11/30)在别的公司,别的项目也有类似的静态资源压缩处理参考如下:
在这里插入图片描述

在这里插入图片描述

优化前后对比:
效果图:优化前的细节是比较高的,优化后细节丢失的部分可以接受
优化前:25 MB
优化后:1.704MB
压缩率:6.816%
压缩比:93.186%


发布出来的包资源再次分析

这里以 apk (android) 为例,分析 apk 内的一些资源大小的步骤如下:

  • 将 xxx.apk 的后缀修改为:xxx.zip
  • 解压到 xxx 文件夹
  • 再 打开 assets 目录,并再 assets 目录下过滤:*.* 的通配符
  • 然后使用 大小 的降序来排序

然后从大到小的资源包分析里面的使用是否有不合规格的资源
使用 AssetStudioGUI 来分析,可以自行去下载:AssetStudio

如下图:在这里插入图片描述


如何工具快速定位静态资源问题

虽然上述的方法可以手动定位热点问题

如果手动定位的热点问题处理得差不多了

那么可以使用工具来分析定位,以免 漏网之鱼

一般市面上 unity 引擎比较主流的有: UWAUPR
两个工具个有千秋

比如下面是 UPR 为例:UPR/Download,打开网页,下载 UPR Asset Checker

在这里插入图片描述

如何使用 UPR Asset Checker 可以查看 UPR 文档:UPR/AssetChecker/Docs
在这里插入图片描述

使用根据得到静态资源的分析报告后,即可逐条酌情处理


运行时的品质调整

上面基本是基于静态资源的优化处理

运行时的画质调整,可以参考之前写的一篇:Unity - 画质设置


References

;