unity 移动平台优化

2015年01月18日 10:48 0 点赞 0 评论 更新于 2025-11-21 14:41

在移动平台开发中,Unity的优化至关重要。本文将深入探讨Unity移动平台的优化方法,特别是动态加载机制和内存管理,帮助开发者更好地控制资源使用,提升游戏性能。

动态加载机制

Unity3D中有两种主要的动态加载机制:Resources.LoadAssetBundle。虽然两者功能相似,但仍存在一些差异。

1. Resources.Load

Resources.Load是从默认打进程序包里的AssetBundle中加载资源。这种方式相对简单,但灵活性较低,因为资源必须预先打包到程序中。

2. AssetBundle

AssetBundle允许开发者在运行时动态加载资源,可以指定资源的路径和来源。开发者需要手动创建AssetBundle文件。

加载过程

  • AssetBundle运行时加载
  • CreateFromFile:这是最快的加载方法,但只能用于Standalone程序。它不会将整个硬盘的AssetBundle文件加载到内存,而是建立一个文件操作句柄和缓冲区,需要时才实时加载,节省资源。
  • CreateFromMemory(byte[])byte[]可以来自文件读取的缓冲、WWW的下载或其他方式。
  • WWW.assetBundle:内部数据读取完后会自动创建一个AssetBundle。创建完成后,只是将硬盘或网络的文件读到内存的一个区域,形成AssetBundle内存镜像数据块,此时还没有Assets的概念。
  • Assets加载
  • AssetBundle.Load(同Resources.Load):从AssetBundle的内存镜像中读取并创建一个Asset对象,同时分配相应内存用于存放(反序列化)。
  • AssetBundle.LoadAsync:异步读取Asset对象。
  • AssetBundle.LoadAll:一次读取多个Asset对象。
  • AssetBundle的释放
  • AssetBundle.Unload(false):释放AssetBundle文件的内存镜像,但不包含通过Load创建的Asset内存对象。
  • AssetBundle.Unload(true):释放AssetBundle文件的内存镜像,并销毁所有通过Load创建的Asset内存对象。

实例加载与引用关系

当从AssetBundle中加载一个Prefab时,它可能包含Gameobjecttransformmeshtexturematerialshaderscript等各种Assets。实例化一个Prefab是一个克隆(Clone)和引用结合的过程:

  • GameObjecttransform是克隆生成的新对象。
  • TextureTerrainData等是纯引用关系,只是一个简单的指针指向已经加载的Asset对象。
  • MeshmaterialPhysicMaterial等存在引用和复制同时存在的情况。

对于Script Asset,Unity里每个Script都是一个封闭的Class定义,需要克隆一个script asset(相当于new一个class实例),并将其挂到Unity主线程的调用链里,Class实例里的OnUpdateOnStart等方法才会被执行。

当销毁一个实例时,只会释放克隆对象,不会释放引用对象和克隆的数据源对象。只有当没有任何游戏场景物体使用这些Assets时,它们才成为无用的Assets,可以通过Resources.UnloadUnusedAssets来释放。

内存管理

Unity3D的内存占用问题一直是开发者关注的焦点,特别是在移动平台上。以下是一些有效的内存管理策略:

通用内存管理方法

  • 创建时
  • 先建立一个AssetBundle,可以从www、文件或内存中创建。
  • 使用AssetBundle.load加载需要的asset
  • 加载完成后立即调用AssetBundle.Unload(false),释放AssetBundle文件本身的内存镜像,但不销毁加载的Asset对象。
  • 释放时
  • Instantiate的对象进行Destroy操作。
  • 在合适的地方调用Resources.UnloadUnusedAssets,释放已经没有引用的Asset
  • 如果需要立即释放内存,可以加上GC.Collect()

不同加载和初始化方式总结

  • AssetBundle.CreateFrom.....:创建一个AssetBundle内存镜像,注意同一个assetBundle文件在没有Unload之前不能再次使用。
  • WWW.AssetBundle:与上述方法类似,需要先new一个WWW对象,然后使用yield return,最后才能使用。
  • AssetBundle.Load(name):从AssetBundle读取一个指定名称的Asset并生成Asset内存对象,多次加载同名对象,除第一次外都只会返回已经生成的Asset对象。
  • Resources.Load(path&name):从默认的位置加载Asset
  • Instantiate(object):克隆一个对象的完整结构,包括其所有Component和子物体,是浅拷贝,不复制所有引用类型。可以用于完整拷贝一个引用类型的Asset,但要拷贝的Texture必须设置为Read/Write able

不同释放方式总结

  • Destroy:主要用于销毁克隆对象,也可用于场景内的静态物体,但不会自动释放该对象的所有引用。用于销毁从文件加载的Asset对象时会销毁相应的资源文件;如果销毁的Asset是复制的或用脚本动态生成的,只会销毁内存对象。
  • AssetBundle.Unload(false):释放AssetBundle文件内存镜像。
  • AssetBundle.Unload(true):释放AssetBundle文件内存镜像,同时销毁所有已经加载的Assets内存对象。
  • Resources.UnloadAsset(Object):显式释放已加载的Asset对象,只能卸载从磁盘文件加载的Asset对象。
  • Resources.UnloadUnusedAssets:用于释放所有没有引用的Asset对象。
  • GC.Collect():强制垃圾收集器立即释放内存,Unity的GC功能不算好,没把握时可以强制调用。

示例分析

示例1

常见错误:从某个AssetBundle里加载一个prefab并克隆,然后只使用Destroy销毁实例,以为就释放干净了。实际上,通过Load加载的所有引用、非引用Assets对象仍在内存中。此时应在Destroy后使用AssetBundle.Unload(true)彻底释放;如果AssetBundle要反复读取,可以在Destroy后使用Resources.UnloadUnusedAssets()

示例2

从磁盘读取一个.unity3d文件到内存并建立一个AssetBundle对象,从中读取并创建一个Texture Asset,将其赋值给多个对象的主贴图。当卸载AssetBundle时,不同的卸载方式会有不同的结果。通过这个示例可以理解引用对象的管理和内存释放过程。

其他开发者观点

Hog认为Unity内存管理比较复杂,特别是涉及Texture时。使用AssetBundle加载的asset可以用Resources.UnloadUnusedAssets卸载,但必须先调用AssetBundle.Unload,这些对象才会被识别为无用的asset。保险的做法与上述通用内存管理方法一致。

深入理解Unity加载和内存管理机制

动态加载Prefab方式的差异

存在三种加载prefab的方式:

  • 静态引用:创建一个public的变量,在Inspector里将prefab拖上去,使用时进行instantiate操作。
  • Resource.Load:加载后进行instantiate操作。
  • AssetBundle.Load:加载后进行instantiate操作。

前两种方式,引用对象textureinstantiate时加载;而assetBundle.Load会将prefab的全部assets都加载,instantiate时只是生成克隆对象。因此,前两种方式如果不提前加载相关引用对象,第一次instantiate时会包含加载引用类assets的操作,导致第一次加载有延迟。

AssetBundle创建方式的差异

  • CreateFromFile:最节省资源,只需要Asset对象的内存,但只能在PC/Mac Standalone程序中使用。
  • CreateFromMemory和www.assetBundle:会将AssetBundle文件整个镜像到内存中,文件多大就需要多大的内存,加载Asset对象时还需要额外内存。

UnusedAssets的判断

UnusedAssets不仅要没有被实际物体引用,还要没有被生命周期内的变量所引用。例如,创建并销毁一个Prefab实例后,如果相关变量仍然引用着该Prefab,直接调用Resources.UnloadUnusedAssets不会释放内存,需要将变量设为null后再调用。

复杂示例分析

通过一个复杂的代码示例,展示了从加载AssetBundleTexturePrefab,到实例化、销毁实例,再到卸载AssetBundle和释放无用资源的整个过程。通过内存分析可以更直观地了解每个步骤的内存变化。

Unity 3D中的内存种类及优化

内存种类

Unity游戏使用的内存主要有三种:

  • 程序代码:包括所有的Unity引擎、使用的库以及开发者编写的游戏代码。编译后的运行文件加载到设备中执行,占用一定内存,这部分内存无法管理,只能通过减少使用的库来优化。
  • 托管堆(Managed Heap):由Mono使用,用于存放类的实例。Mono应该自动调整堆的大小,并定时进行垃圾回收。但开发者需要及时清除对不再使用的内存的引用,以确保回收机制正常运行。
  • 本机堆(Native Heap):Unity引擎进行申请和操作的地方,如贴图、音效、关卡数据等。Unity有自己的内存管理机制,但自动加载资源容易导致大量内存占用。

优化方法

优化程序代码的内存占用

主要通过减少打包时的引用库来实现。在Player Setting面板中,可以调整Api Compatibility LevelStripping Level,选择合适的库剥离选项。但需要注意,过度剥离可能导致程序崩溃,可使用第三方轻量级类库来解决依赖问题。

托管堆优化

  • 尽早去除不需要的引用,让垃圾回收机制及时清理内存。但要注意内存清理可能导致游戏卡顿,尽量选择合适的时间进行大量内存回收。
  • 对于大量需要频繁创建和销毁的对象,可以采用对象重用的方法,将不需要的对象隐藏并放入重用数组,需要时再取出使用。
  • 尽量减少在游戏进行过程中对GameObjectInstantiate()Destroy()调用,可选择在合适的时间批量销毁和回收内存。也可以手动调用System.GC.Collect()建议系统进行垃圾回收。

本机堆的优化

  • 减少在Hierarchy中对资源的直接引用,使用Resource.Load方法在需要时从硬盘读取资源,并及时使用Resource.UnloadAsset()Resources.UnloadUnusedAssets()卸载资源。
  • 避免在DontDestroyOnLoadGameObject上挂载包含大量资源的脚本,防止这些资源在场景切换时无法卸载。
  • 减少代码的耦合和对其他脚本的依赖,手动对不再使用的引用对象调用Destroy()或设置为null

总之,要实现Unity移动平台的优化,需要深入理解其加载和内存管理机制,根据具体情况选择合适的优化方法,并参考Unity3D的技术手册,以达到最佳的优化效果。