热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

相关收集animation,aseetbundle的优化

Animation如何降低动画文件的浮点数精度?动画文件后处理可以做两件事,1)精度压缩,2)scale曲

Animation//

如何降低动画文件的浮点数精度 ?

动画文件后处理可以做两件事,1)精度压缩,2)scale曲线剔除。比起用工具修改原始fbx文件,这样比较灵活。实际测试,在开启Optimal压缩的情况下,加上这个后处理,能再节省40%左右。

红色框内即是BlobSize,在我的理解,FileSize是指文件在硬盘中占的大小,BlobSize是从文件反序列化出来的对象的二进制大小。Editor下的MemorySize不仅有序列化后的内存大小,还维护了一份原文件的内存。就像我们在Editor下加载一张Texture内存是双份一样,而真机下就约等于BlobSize。真机下的MemorySize和Inspector里的BlobSize非常接近,BlobSize可以认为是真机上的内存大小,这个大小更有参考意义

同时,我也对去除Scale曲线的方法进行了实验。下图这个动画文件原来Inspector中Scale的值为4,即有Scale曲线,原始文件BlobSize为10.2KB,去除Scale曲线后,Blob Size变为7.4KB,所以BlobSize减小了27%。

Curve减少导致内存减小

从上面的实验可以看出来,只裁剪动画文件的压缩精度,没有引起Curve减少。BlobSize是不会有任何变化的,因为每个浮点数固定占32bit。而文件大小、AB大小、Editor下的内存大小,压缩精度后不管有没有引起Curve的变化,都会变小。

裁剪动画文件的精度,意味着点的位置发生了变化,所以Constant Curve和Dense Curve的数量也有可能发生变化。由于是裁剪精度所以动画的点更稀疏了,而连续相同的点更多了。所以Dense Curve是减少了,Constant Curve是增多了,总的内存是减小了。

Constant Curve只需要最左边的点就可以描述一个曲线段。

裁剪精度后,大小为2.1kb,ConstantCurve为7(100%),Stream为0(0%)。裁剪完精度后导致ConstantCurve增加了3,Stream(Optimal模式下即为Dense)减少了3,BlobSize减小了0.1kb。
 

通过精度优化降低内存的方式,其实质是将曲线上过于接近的数值(例如相差数值出现在小数点4位以后)直接变为一致,从而使部分曲线变为constant曲线来降低内存消耗。



总结

隔壁项目组对他们项目中所有的动画文件都进行了优化。其中文件大小从820MB->225MB, ab大小从72MB->64MB, 内存大小从50MB->40MB。总的来说动画文件的scale越多优化越明显。



取BlobSize代码

AnimationClip aniClip = AssetDatabase.LoadAssetAtPath (path);

var fileInfo = new System.IO.FileInfo(path);

Debug.Log(fileInfo.Length);//FileSize

Debug.Log(Profiler.GetRuntimeMemorySize (aniClip));//MemorySize

 

Assembly asm = Assembly.GetAssembly(typeof(Editor));

MethodInfo getAnimationClipStats = typeof(AnimationUtility).GetMethod("GetAnimationClipStats", BindingFlags.Static | BindingFlags.NonPublic);

Type aniclipstats = asm.GetType("UnityEditor.AnimationClipStats");

FieldInfo sizeInfo = aniclipstats.GetField ("size", BindingFlags.Public | BindingFlags.Instance);

 

var stats = getAnimationClipStats.Invoke(null, new object[]{aniClip});

Debug.Log(EditorUtility.FormatBytes((int)sizeInfo.GetValue(stats)));//BlobSize

一个简单的工具:

 

using System;

using System.Collections.Generic;

using UnityEngine;

using System.Reflection;

using UnityEditor;

using System.IO;

 

namespace EditorTool

{

    class AnimationOpt

    {

        static Dictionary _FLOAT_FORMAT;

        static MethodInfo getAnimationClipStats;

        static FieldInfo sizeInfo;

        static object[] _param = new object[1];

 

        static AnimationOpt ()

        {

            _FLOAT_FORMAT = new Dictionary ();

            for (uint i &#61; 1; i <6; i&#43;&#43;) {

                _FLOAT_FORMAT.Add (i, "f" &#43; i.ToString ());

            }

            Assembly asm &#61; Assembly.GetAssembly (typeof(Editor));

            getAnimationClipStats &#61; typeof(AnimationUtility).GetMethod ("GetAnimationClipStats", BindingFlags.Static | BindingFlags.NonPublic);

            Type aniclipstats &#61; asm.GetType ("UnityEditor.AnimationClipStats");

            sizeInfo &#61; aniclipstats.GetField ("size", BindingFlags.Public | BindingFlags.Instance);

        }

 

        AnimationClip _clip;

        string _path;

 

        public string path { get{ return _path;} }

 

        public long originFileSize { get; private set; }

 

        public int originMemorySize { get; private set; }

 

        public int originInspectorSize { get; private set; }

 

        public long optFileSize { get; private set; }

 

        public int optMemorySize { get; private set; }

 

        public int optInspectorSize { get; private set; }

 

        public AnimationOpt (string path, AnimationClip clip)

        {

            _path &#61; path;

            _clip &#61; clip;

            _GetOriginSize ();

        }

 

        void _GetOriginSize ()

        {

            originFileSize &#61; _GetFileZie ();

            originMemorySize &#61; _GetMemSize ();

            originInspectorSize &#61; _GetInspectorSize ();

        }

 

        void _GetOptSize ()

        {

            optFileSize &#61; _GetFileZie ();

            optMemorySize &#61; _GetMemSize ();

            optInspectorSize &#61; _GetInspectorSize ();

        }

 

        long _GetFileZie ()

        {

            FileInfo fi &#61; new FileInfo (_path);

            return fi.Length;

        }

 

        int _GetMemSize ()

        {

            return Profiler.GetRuntimeMemorySize (_clip);

        }

 

        int _GetInspectorSize ()

        {

            _param [0] &#61; _clip;

            var stats &#61; getAnimationClipStats.Invoke (null, _param);

            return (int)sizeInfo.GetValue (stats);

        }

 

        void _OptmizeAnimationScaleCurve ()

        {

            if (_clip !&#61; null) {

                //去除scale曲线

                foreach (EditorCurveBinding theCurveBinding in AnimationUtility.GetCurveBindings(_clip)) {

                    string name &#61; theCurveBinding.propertyName.ToLower ();

                    if (name.Contains ("scale")) {

                        AnimationUtility.SetEditorCurve (_clip, theCurveBinding, null);

                        Debug.LogFormat ("关闭{0}的scale curve", _clip.name);

                    }

                }

            }

        }

 

        void _OptmizeAnimationFloat_X (uint x)

        {

            if (_clip !&#61; null && x > 0) {

                //浮点数精度压缩到f3

                AnimationClipCurveData[] curves &#61; null;

                curves &#61; AnimationUtility.GetAllCurves (_clip);

                Keyframe key;

                Keyframe[] keyFrames;

                string floatFormat;

                if (_FLOAT_FORMAT.TryGetValue (x, out floatFormat)) {

                    if (curves !&#61; null && curves.Length > 0) {

                        for (int ii &#61; 0; ii

                            AnimationClipCurveData curveDate &#61; curves [ii];

                            if (curveDate.curve &#61;&#61; null || curveDate.curve.keys &#61;&#61; null) {

                                //Debug.LogWarning(string.Format("AnimationClipCurveData {0} don&#39;t have curve; Animation name {1} ", curveDate, animationPath));

                                continue;

                            }

                            keyFrames &#61; curveDate.curve.keys;

                            for (int i &#61; 0; i

                                key &#61; keyFrames [i];

                                key.value &#61; float.Parse (key.value.ToString (floatFormat));

                                key.inTangent &#61; float.Parse (key.inTangent.ToString (floatFormat));

                                key.outTangent &#61; float.Parse (key.outTangent.ToString (floatFormat));

                                keyFrames [i] &#61; key;

                            }

                            curveDate.curve.keys &#61; keyFrames;

                            _clip.SetCurve (curveDate.path, curveDate.type, curveDate.propertyName, curveDate.curve);

                        }

                    }

                } else {

                    Debug.LogErrorFormat ("目前不支持{0}位浮点", x);

                }

            }

        }

 

        public void Optimize (bool scaleOpt, uint floatSize)

        {

            if (scaleOpt) {

                _OptmizeAnimationScaleCurve ();

            }

            _OptmizeAnimationFloat_X (floatSize);

            _GetOptSize ();

        }

 

        public void Optimize_Scale_Float3 ()

        {

            Optimize (true, 3);

        }

 

        public void LogOrigin ()

        {

            _logSize (originFileSize, originMemorySize, originInspectorSize);

        }

 

        public void LogOpt ()

        {

            _logSize (optFileSize, optMemorySize, optInspectorSize);

        }

 

        public void LogDelta ()

        {

 

        }

 

        void _logSize (long fileSize, int memSize, int inspectorSize)

        {

            Debug.LogFormat ("{0} \nSize&#61;[ {1} ]", _path, string.Format ("FSize&#61;{0} ; Mem->{1} ; inspector->{2}",

                EditorUtility.FormatBytes (fileSize), EditorUtility.FormatBytes (memSize), EditorUtility.FormatBytes (inspectorSize)));

        }

    }

 

    public class OptimizeAnimationClipTool

    {

        static List _AnimOptList &#61; new List ();

        static List _Errors &#61; new List();

        static int _Index &#61; 0;

 

        [MenuItem("Assets/Animation/裁剪浮点数去除Scale")]

        public static void Optimize()

        {

            _AnimOptList &#61; FindAnims ();

            if (_AnimOptList.Count > 0)

            {

                _Index &#61; 0;

                _Errors.Clear ();

                EditorApplication.update &#61; ScanAnimationClip;

            }

        }

 

        private static void ScanAnimationClip()

        {

            AnimationOpt _AnimOpt &#61; _AnimOptList[_Index];

            bool isCancel &#61; EditorUtility.DisplayCancelableProgressBar("优化AnimationClip", _AnimOpt.path, (float)_Index / (float)_AnimOptList.Count);

            _AnimOpt.Optimize_Scale_Float3();

            _Index&#43;&#43;;

            if (isCancel || _Index >&#61; _AnimOptList.Count)

            {

                EditorUtility.ClearProgressBar();

                Debug.Log(string.Format("--优化完成--    错误数量: {0}    总数量: {1}/{2}    错误信息↓:\n{3}\n----------输出完毕----------", _Errors.Count, _Index, _AnimOptList.Count, string.Join(string.Empty, _Errors.ToArray())));

                Resources.UnloadUnusedAssets();

                GC.Collect();

                AssetDatabase.SaveAssets();

                EditorApplication.update &#61; null;

                _AnimOptList.Clear();

                _cachedOpts.Clear ();

                _Index &#61; 0;

            }

        }

 

        static Dictionary _cachedOpts &#61; new Dictionary ();

 

        static AnimationOpt _GetNewAOpt (string path)

        {

            AnimationOpt opt &#61; null;

            if (!_cachedOpts.ContainsKey(path)) {

                AnimationClip clip &#61; AssetDatabase.LoadAssetAtPath (path);

                if (clip !&#61; null) {

                    opt &#61; new AnimationOpt (path, clip);

                    _cachedOpts [path] &#61; opt;

                }

            }

            return opt;

        }

 

        static List FindAnims()

        {

            string[] guids &#61; null;

            List path &#61; new List();

            List assets &#61; new List ();

            UnityEngine.Object[] objs &#61; Selection.GetFiltered(typeof(object), SelectionMode.Assets);

            if (objs.Length > 0)

            {

                for(int i &#61; 0; i

                {

                    if (objs [i].GetType () &#61;&#61; typeof(AnimationClip))

                    {

                        string p &#61; AssetDatabase.GetAssetPath (objs [i]);

                        AnimationOpt animopt &#61; _GetNewAOpt (p);

                        if (animopt !&#61; null)

                            assets.Add (animopt);

                    }

                    else

                        path.Add(AssetDatabase.GetAssetPath (objs [i]));

                }

                if(path.Count > 0)

                    guids &#61; AssetDatabase.FindAssets (string.Format ("t:{0}", typeof(AnimationClip).ToString().Replace("UnityEngine.", "")), path.ToArray());

                else

                    guids &#61; new string[]{};

            }

            for(int i &#61; 0; i

            {

                string assetPath &#61; AssetDatabase.GUIDToAssetPath (guids [i]);

                AnimationOpt animopt &#61; _GetNewAOpt (assetPath);

                if (animopt !&#61; null)

                    assets.Add (animopt);

            }

            return assets;

        }

    }

}

 

动画文件后处理可以做两件事&#xff0c;精度压缩&#xff0c;scale曲线剔除。
比起用工具修改原始fbx文件&#xff0c;这样比较灵活。

实际测试&#xff0c;在开启Optimal压缩的情况下&#xff0c;加上这个后处理&#xff0c;能再节省40%左右。

void OnPostprocessModel(GameObject g) {

       // for skeleton animations.

      

              List animationClipList &#61; new List(AnimationUtility.GetAnimationClips(g));

              if (animationClipList.Count &#61;&#61; 0) {

                    AnimationClip[] objectList &#61; UnityEngine.Object.FindObjectsOfType (typeof(AnimationClip)) as AnimationClip[];

                    animationClipList.AddRange(objectList);

              }

             

              foreach (AnimationClip theAnimation in animationClipList)

              {

 

                    try

                    {

                           //去除scale曲线

                           foreach (EditorCurveBinding theCurveBinding in AnimationUtility.GetCurveBindings(theAnimation))

                           {

                                  string name &#61; theCurveBinding.propertyName.ToLower();

                                  if (name.Contains("scale"))

                                  {

                                         AnimationUtility.SetEditorCurve(theAnimation, theCurveBinding, null);

                                  }

                           }

 

                           //浮点数精度压缩到f3

                           AnimationClipCurveData[] curves &#61; null;

                           curves &#61; AnimationUtility.GetAllCurves(theAnimation);

                   Keyframe key;

                   Keyframe[] keyFrames;

                   for (int ii &#61; 0; ii

                   {

                       AnimationClipCurveData curveDate &#61; curves[ii];

                       if (curveDate.curve &#61;&#61; null || curveDate.curve.keys &#61;&#61; null)

                       {

                           //Debug.LogWarning(string.Format("AnimationClipCurveData {0} don&#39;t have curve; Animation name {1} ", curveDate, animationPath));

                           continue;

                       }

                       keyFrames &#61; curveDate.curve.keys;

                       for (int i &#61; 0; i

                       {

                           key &#61; keyFrames[i];

                           key.value &#61; float.Parse(key.value.ToString("f3"));

                           key.inTangent &#61; float.Parse(key.inTangent.ToString("f3"));

                           key.outTangent &#61; float.Parse(key.outTangent.ToString("f3"));

                           keyFrames[i] &#61; key;

                       }

                       curveDate.curve.keys &#61; keyFrames;

                                  theAnimation.SetCurve(curveDate.path, curveDate.type, curveDate.propertyName, curveDate.curve);

                   }

                    }

                    catch (System.Exception e)

                    {

                           Debug.LogError(string.Format("CompressAnimationClip Failed !!! animationPath : {0} error: {1}", assetPath, e));

                    }

              }

             

}

 

测试&#xff1a;不同压缩格式的AnimationClip资源加载效率测试
我们制作了三组测试用例&#xff0c;AnimationClip资源数量分别为10个、30个和50个。同时&#xff0c;每组AnimationClip又根据其压缩格式的不同分为三小组&#xff1a;None Compression、Keyframe Reduction和Optimal。

我们在三种不同档次的机型上加载这些AnimationClip资源&#xff0c;为降低偶然性&#xff0c;每台设备上重复进行十次加载操作并将取其平均值作为最终性能开销。具体测试结果如下表所示。

第1组测试
10个“None Compression”资源、10个“Keyframe Reduction”资源和10个“Optimal”资源&#xff0c;打包成AssetBundle文件后&#xff0c;其文件大小分别为&#xff1a;409KB、172KB和92KB。

第2组测试
30个“None Compression”资源、30个“Keyframe Reduction”资源和30个“Optimal”资源&#xff0c;打包成AssetBundle文件后&#xff0c;其文件大小分别为&#xff1a;1.42MB、514KB和312KB。

第3组测试
50个“None Compression”资源、50个“Keyframe Reduction”资源和50个“Optimal”资源&#xff0c;打包成AssetBundle文件后&#xff0c;其文件大小分别为&#xff1a;2.46MB、858KB和525KB。

通过上述测试&#xff0c;我们可以得到以下结论&#xff1a;

  1. Optimal压缩方式确实可以提升资源的加载效率&#xff0c;无论是在高端机、中端机还是低端机上&#xff1b;
  2. 硬件设备性能越好&#xff0c;其加载效率越高。但随着设备的提升&#xff0c;Keyframe Reduction和Optimal的加载效率提升已不十分明显&#xff1b;
  3. Optimal压缩方式可能会降低动画的视觉质量&#xff0c;因此&#xff0c;是否最终选择Optimal压缩模式&#xff0c;还需根据最终视觉效果的接受程度来决定。




只裁剪精度使BlobSize减小的实例

裁剪精度前&#xff0c;大小为2.2kb&#xff0c;ScaleCurve为0&#xff0c; ConstantCurve为4(57.1%)&#xff0c;Stream(使用Optimal模式这部分数据将储存为Dense)为3(42.9%)。

 

assetbundle//

AssetBundle加载基础

通过AssetBundle加载资源&#xff0c;分为两步&#xff0c;第一步是获取AssetBundle对象&#xff0c;第二步是通过该对象加载需要的资源。而第一步又分为两种方式&#xff0c;下文中将结合常用的API进行详细地描述。

一、获取AssetBundle对象的常用API

&#xff08;1&#xff09;先获取WWW对象&#xff0c;再通过WWW.assetBundle获取AssetBundle对象&#xff1a;

·        publicWWW(string url);
加载Bundle文件并获取WWW对象&#xff0c;完成后会在内存中创建较大的WebStream&#xff08;解压后的内容&#xff0c;通常为原Bundle文件的4~5倍大小&#xff0c;纹理资源比例可能更大&#xff09;&#xff0c;因此后续的AssetBundle.Load可以直接在内存中进行。

 

public static WWW LoadFromCacheOrDownload(stringurl, int version, uint crc &#61; 0);

 

·        加载Bundle文件并获取WWW对象&#xff0c;同时将解压形式的Bundle内容存入磁盘中作为缓存&#xff08;如果该Bundle已在缓存中&#xff0c;则省去这一步&#xff09;&#xff0c;完成后只会在内存中创建较小的SerializedFile&#xff0c;而后续的AssetBundle.Load需要通过IO从磁盘中的缓存获取。

·        publicAssetBundle assetBundle;
通过之前两个接口获取WWW对象后&#xff0c;即可通过WWW.assetBundle获取AssetBundle对象。

&#xff08;2&#xff09; 直接获取AssetBundle&#xff1a;

·        publicstatic AssetBundle CreateFromFile(string path);
通过未压缩的Bundle文件&#xff0c;同步创建AssetBundle对象&#xff0c;这是最快的创建方式。创建完成后只会在内存中创建较小的SerializedFile&#xff0c;而后续的AssetBundle.Load需要通过IO从磁盘中获取。

·        publicstatic AssetBundleCreateRequest CreateFromMemory(byte[] binary);
通过Bundle的二进制数据&#xff0c;异步创建AssetBundle对象。完成后会在内存中创建较大的WebStream。调用时&#xff0c;Bundle的解压是异步进行的&#xff0c;因此对于未压缩的Bundle文件&#xff0c;该接口与CreateFromMemoryImmediate等价。

·        publicstatic AssetBundle CreateFromMemoryImmediate(byte[] binary);
该接口是CreateFromMemory的同步版本。

·        注&#xff1a;5.3下分别改名为LoadFromFile&#xff0c;LoadFromMemory&#xff0c;LoadFromMemoryAsync并增加了LoadFromFileAsync&#xff0c;且机制也有一定的变化&#xff0c;可详见Unity官方文档。

二、从AssetBundle加载资源的常用API

·        publicObject Load(string name, Type type);
通过给定的名字和资源类型&#xff0c;加载资源。加载时会自动加载其依赖的资源&#xff0c;即Load一个Prefab时&#xff0c;会自动Load其引用的Texture资源。

·        publicObject[] LoadAll(Type type);
一次性加载Bundle中给定资源类型的所有资源。

·        publicAssetBundleRequest LoadAsync(string name, Type type);
该接口是Load的异步版本。

·        注&#xff1a;5.x下分别改名为LoadAsset&#xff0c;LoadAllAssets&#xff0c;LoadAssetAsync&#xff0c;并增加了LoadAllAssetsAsync。

AssetBundle加载进阶

一、接口对比&#xff1a;new WWW与WWW.LoadFromCacheOrDownload

&#xff08;1&#xff09;前者的优势

·        后续的Load操作在内存中进行&#xff0c;相比后者的IO操作开销更小&#xff1b;

·        不形成缓存文件&#xff0c;而后者则需要额外的磁盘空间存放缓存&#xff1b;

·        能通过WWW.texture&#xff0c;WWW.bytes&#xff0c;WWW.audioClip等接口直接加载外部资源&#xff0c;而后者只能用于加载AssetBundle

&#xff08;2&#xff09;前者的劣势

·        每次加载都涉及到解压操作&#xff0c;而后者在第二次加载时就省去了解压的开销&#xff1b;

·        在内存中会有较大的WebStream&#xff0c;而后者在内存中只有通常较小的SerializedFile。&#xff08;此项为一般情况&#xff0c;但并不绝对&#xff0c;对于序列化信息较多的Prefab&#xff0c;很可能出现SerializedFile比WebStream更大的情况&#xff09;

二、内存分析


 

 

 

在管理AssetBundle时&#xff0c;了解其加载过程中对内存的影响意义重大。在上图中&#xff0c;我们在中间列出了AssetBundle加载资源后&#xff0c;内存中各类物件的分布图&#xff0c;在左侧则列出了每一类内存的产生所涉及到的加载API&#xff1a;

·        WWW对象&#xff1a;在第一步的方式1中产生&#xff0c;内存开销小&#xff1b;

·        WebStream&#xff1a;在使用new WWW或CreateFromMemory时产生&#xff0c;内存开销通常较大&#xff1b;

·        SerializedFile&#xff1a;在第一步中两种方式都会产生&#xff0c;内存开销通常较小&#xff1b;

·        AssetBundle对象&#xff1a;在第一步中两种方式都会产生&#xff0c;内存开销小&#xff1b;

·        资源&#xff08;包括Prefab&#xff09;&#xff1a;在第二步中通过Load产生&#xff0c;根据资源类型&#xff0c;内存开销各有大小&#xff1b;

·        场景物件&#xff08;GameObject&#xff09;&#xff1a;在第二步中通过Instantiate产生&#xff0c;内存开销通常较小。
在后续的章节中&#xff0c;我们还将针对该图中各类内存物件分析其卸载的方式&#xff0c;从而避免内存残留甚至泄露。

三、注意点

·        CreateFromFile只能适用于未压缩的AssetBundle&#xff0c;而Android系统下StreamingAssets是在压缩目录(.jar)中&#xff0c;因此需要先将未压缩的AssetBundle放到SD卡中才能对其使用CreateFromFile。
Application.streamingAsstsPath &#61; "jar:file://" &#43;Application.dataPath&#43;"!/assets/";

·        iOS系统有256个开启文件的上限&#xff0c;因此&#xff0c;内存中通过CreateFromFile或WWW.LoadFromCacheOrDownload加载的AssetBundle对象也会低于该值&#xff0c;在较新的版本中&#xff0c;如果LoadFromCacheOrDownload超过上限&#xff0c;则会自动改为new WWW的形式加载&#xff0c;而较早的版本中则会加载失败。

·        CreateFromFile和WWW.LoadFromCacheOrDownload的调用会增加RersistentManager.Remapper的大小&#xff0c;而PersistentManager负责维护资源的持久化存储&#xff0c;Remapper保存的是加载到内存的资源HeapID与源数据FileID的映射关系&#xff0c;它是一个Memory Pool&#xff0c;其行为类似Mono堆内存&#xff0c;只增不减&#xff0c;因此需要对这两个接口的使用做合理的规划。

·        对于存在依赖关系的Bundle包&#xff0c;在加载时主要注意顺序。举例来说&#xff0c;假设CanvasA在BundleA中&#xff0c;所依赖的AtlasB在BundleB中&#xff0c;为了确保资源正确引用&#xff0c;那么最晚创建BundleB的AssetBundle对象的时间点是在实例化CanvasA之前。即&#xff0c;创建BundleA的AssetBundle对象时、Load(“CanvasA”)时&#xff0c;BundleB的AssetBundle对象都可以不在内存中。

 

 

 

·        根据经验&#xff0c;建议AssetBundle文件的大小不超过1MB&#xff0c;因为在普遍情况下Bundle的加载时间与其大小并非呈线性关系&#xff0c;过大的Bundle可能引起较大的加载开销。

·        由于WWW对象的加载是异步的&#xff0c;因此逐个加载容易出现下图中CPU空闲的情况&#xff08;选中帧处Vsync占了大部分&#xff09;&#xff0c;此时建议适当地同时加载多个对象&#xff0c;以增加CPU的使用率&#xff0c;同时加快加载的完成。

 

 

 

AssetBundle卸载

前文提到了通过AssetBundle加载资源时的内存分配情况&#xff0c;下面&#xff0c;我们结合常用的API来介绍如何将已分配的内存进行卸载&#xff0c;最终达到清空所有相关内存的目的。

一、内存分析

 

 

 

在上图中的右侧&#xff0c;我们列出了各种内存物件的卸载方式&#xff1a;

·        场景物件&#xff08;GameObject&#xff09;&#xff1a;这类物件可通过Destroy函数进行卸载&#xff1b;

·        资源&#xff08;包括Prefab&#xff09;&#xff1a;除了Prefab以外&#xff0c;资源文件可以通过三种方式来卸载&#xff1a;
1) 通过Resources.UnloadAsset卸载指定的资源&#xff0c;CPU开销小&#xff1b;
2)通过Resources.UnloadUnusedAssets一次性卸载所有未被引用的资源&#xff0c;CPU开销大&#xff1b;
3)通过AssetBundle.Unload(true)在卸载AssetBundle对象时&#xff0c;将加载出来的资源一起卸载。
而对于Prefab&#xff0c;目前仅能通过DestroyImmediate来卸载&#xff0c;且卸载后&#xff0c;必须重新加载AssetBundle才能重新加载该Prefab。由于内存开销较小&#xff0c;通常不建议进行针对性地卸载。

·        WWW对象&#xff1a;调用对象的Dispose函数或将其置为null即可&#xff1b;

·        WebStream&#xff1a;在卸载WWW对象以及对应的AssetBundle对象后&#xff0c;这部分内存即会被引擎自动卸载&#xff1b;

·        SerializedFile&#xff1a;卸载AssetBundle后&#xff0c;这部分内存会被引擎自动卸载&#xff1b;

·        AssetBundle对象&#xff1a;AssetBundle的卸载有两种方式&#xff1a;
1)通过AssetBundle.Unload(false)&#xff0c;卸载AssetBundle对象时保留内存中已加载的资源&#xff1b;
2)通过AssetBundle.Unload(true)&#xff0c;卸载AssetBundle对象时卸载内存中已加载的资源&#xff0c;由于该方法容易引起资源引用丢失&#xff0c;因此并不建议经常使用&#xff1b;

二、注意点

在通过AssetBundle.Unload(false)卸载AssetBundle对象后&#xff0c;如果重新创建该对象并加载之前加载过的资源到内存时&#xff0c;会出现冗余&#xff0c;即两份相同的资源。
被脚本的静态变量引用的资源&#xff0c;在调用Resources.UnloadUnusedAssets时&#xff0c;并不会被卸载&#xff0c;在Profiler中能够看到其引用情况。

 

 

 

UWA推荐方案

通过以上的讲解&#xff0c;相信您对AssetBundle的加载和卸载已经有了明确的了解。下面&#xff0c;我们将简单地做一下API选择上的推荐&#xff1a;

·        对于需要常驻内存的Bundle文件来说&#xff0c;优先考虑减小内存占用&#xff0c;因此对于存放非Prefab资源&#xff08;特别是纹理&#xff09;的Bundle文件&#xff0c;可以考虑使用WWW.LoadFromCacheOrDownload或AssetBundle.CreateFromFile加载&#xff0c;从而避免WebStream常驻内存&#xff1b;而对于存放较多Prefab资源的Bundle&#xff0c;则考虑使用new WWW加载&#xff0c;因为这类Bundle用WWW.LoadFromCacheOrDownload加载时产生的SerializedFile可能会比new WWW产生的WebStream更大。

·        对于加载完后即卸载的Bundle文件&#xff0c;则分两种情况&#xff1a;优先考虑速度&#xff08;加载场景时&#xff09;和优先考虑流畅度&#xff08;游戏进行时&#xff09;。
1&#xff09;加载场景的情况下&#xff0c;需要注意的是避免WWW对象的逐个加载导致的CPU空闲&#xff0c;可以考虑使用加载速度较快的WWW.LoadFromCacheOrDownload或AssetBundle.CreateFromFile&#xff0c;但需要避免后续大量地进行Load资源的操作&#xff0c;引起IO开销&#xff08;可以尝试直接LoadAll&#xff09;。
2&#xff09; 游戏进行的情况下&#xff0c;则需要避免使用同步操作引起卡顿&#xff0c;因此可以考虑使用new WWW配合AssetBundle.LoadAsync来进行平滑的资源加载&#xff0c;但需要注意的是&#xff0c;对于Shader、较大的Texture等资源&#xff0c;其初始化操作通常很耗时&#xff0c;容易引起卡顿&#xff0c;因此建议将这类资源在加载场景时进行预加载。

·        只在Bundle需要加密的情况下&#xff0c;考虑使用CreateFromMemory&#xff0c;因为该接口加载速度较慢。

·        尽量避免在游戏进行中调用Resources.UnloadUnusedAssets()&#xff0c;因为该接口开销较大&#xff0c;容易引起卡顿&#xff0c;可尝试使用Resources.Unload(obj)来逐个进行卸载&#xff0c;以保证游戏的流畅度。

 

 

AssetBundle 打包&#xff08;4.x&#xff09;基础

基本介绍

&#xff08;1&#xff09;常用打包API

public static bool BuildAssetBundle(ObjectmainAsset, Object[] assets,

string pathName, out uint crc,BuildAssetBundleOptions assetBundleOptions,

BuildTarget targetPlatform);

public static stringBuildStreamedSceneAssetBundle(string[] levels,

string locationPath, BuildTarget target, outuint crc, BuildOptions options);

·        BuildPipeline.BuildAssetBundle
对除Scene以外的资源打包&#xff0c;支持单个和多个&#xff1b;

·        BuildPipeline.BuildStreamedSceneAssetBundle
对Scene文件打包&#xff0c;也支持单个和多个。

&#xff08;2&#xff09;常用打包选项&#xff08;BuildAssetBundleOptions&#xff09;

·        CompleteAssets
用于保证资源的完备性。比如&#xff0c;当你仅打包一个Mesh资源并开启了该选项时&#xff0c;引擎会将Mesh资源和相关GameObject一起打入AssetBundle文件中&#xff1b;

·        CollectDependencies
用于收集资源的依赖项。比如&#xff0c;当你打包一个Prefab并开启了该选项时&#xff0c;引擎会将该Prefab用到的所有资源和Component全部打入AssetBundle文件中&#xff1b;

·        DeterministicAssetBundle
用于为资源维护固定ID&#xff0c;以便进行资源的热更新。

以上选项均已在5.x新机制中默认开启。因此在4.x版本中&#xff0c;开发者如果没有深入了解每个选项的意义&#xff0c;我们建议也都开启。

三个选项开启的情况下打包&#xff0c;可以保证在加载并实例化其中的Prefab时不会出现资源引用丢失的情况&#xff0c;因为所有依赖的资源都在包中。这也意味着&#xff0c;如果Prefab-A和Prefab-B引用了同一个Asset-A且分别打包时&#xff0c;两个包中就都会包含Asset-A。

 

 

 

加载到内存后&#xff0c;通过Profiler会发现Asset-A的冗余资源。

 

 

 

然而很多时候&#xff0c;并不希望把两个Prefab打在一个Bundle中&#xff0c;此时&#xff0c;就需要通过依赖性打包来解决。

依赖性打包

依赖性打包的作用在于避免资源冗余&#xff0c;同时提高资源加载和卸载的灵活性&#xff0c;其重要性不言而喻。在4.x版本的AssetBundle打包系统中&#xff0c;涉及一对 BuildPipeline.PushAssetDependencies和BuildPipeline.PopAssetDependencies接口&#xff0c;从官方文档中可以大致了解其用法&#xff1a;http://docs.unity3d.com/ScriptReference/BuildPipeline.PushAssetDependencies.html

你可以简单地认为&#xff0c;PushAssetDependencies是将资源进栈&#xff0c;PopAssetDependencies是让资源出栈&#xff0c;每打一个包&#xff0c;引擎都会检查当前栈中所有的依赖项&#xff0c;查看是否有相同资源已经在栈中。如有&#xff0c;则与其相关的AssetBundle建立依赖关系。机制不难理解&#xff0c;但使用中依然有几个容易忽视的注意点&#xff0c;请移步下文进阶篇。



AssetBundle 打包&#xff08;4.x&#xff09;进阶

注意点

·        进行一次Push&#xff0c;多次Build操作&#xff0c;如依次Build资源Prefab-A&#xff0c;Prefab-B时&#xff0c;可以认为Prefab-A&#xff0c;Prefab-B会依次 进栈&#xff0c;所以如果两者之间也存在共享资源&#xff0c;则后者会依赖前者。具体表现为&#xff0c;运行时先加载Prefab-B会出现共享资源丢失的情况。

 

 

 

·        4.x中脚本也会作为“共享资源”参与依赖性打包&#xff0c;即当Prefab-A和Prefab-B同时挂有脚本M时&#xff0c;如果出现了上一点中的情况&#xff0c;那么后者同样会依赖前者。具体表现为&#xff0c;运行时先加载Prefab-B会出现脚本M丢失。

·        将shader放入GraphicsSettings->Always IncludedShaders中后&#xff0c;打包时会将相应的shader抽离&#xff0c;运行时加载时会自动加载其依赖的shader。同时也意味着&#xff0c;如果修改了Always Included Shaders或在一个新建项目中使用该Bundle&#xff0c;会出现shader丢失的问题。

·        当需要更新bundle内容&#xff0c;但不改变依赖关系时&#xff0c;仍然需要重打其依赖的Bundle包。即如果Bundle-B依赖Bundle-A&#xff0c;那么在更新Bundle-A时可以不需要重打Bundle-B&#xff08;前提是开启了DeterministicAssetBundle&#xff09;&#xff1b;但要更新Bundle-B的话&#xff0c;则必须重打Bundle-A。

AssetBundle打包&#xff08;5.x&#xff09;基础

基本介绍

&#xff08;1&#xff09;唯一API

public static AssetBundleManifestBuildAssetBundles(string outputPath, BuildAssetBundleOptions      assetBundleOptions &#61;BuildAssetBundleOptions.None,BuildTarget targetPlatform &#61;BuildTarget.WebPlayer);

调用BuildPipeline.BuildAssetBundles&#xff0c;引擎将自动根据资源的assetbundleName属性&#xff08;以下简称abName&#xff09;批量打包&#xff0c;自动建立Bundle以及资源之间的依赖关系。

&#xff08;2&#xff09;打包规则

在资源的Inpector界面最下方可设置一个abName&#xff0c;每个abName&#xff08;包含路径&#xff09;对应一个Bundle&#xff0c;即abName相同的资源会打在一个Bundle中。如果所依赖的资源设置了不同的abName&#xff0c;则会与之建立依赖关系&#xff0c;避免出现冗余。

 

 

 

支持增量式发布&#xff0c;即在资源内容改变并重新打包时&#xff0c;会自动跳过内容未变的Bundle。因此&#xff0c;相比4.x&#xff0c;会极大地缩短更新Bundle的时间。

&#xff08;3&#xff09; 新打包选项

除了前文提到的&#xff0c;5.x下默认开启的三个选项&#xff08;CompleteAssets &#xff0c;用于保证资源的完备性&#xff1b;CollectDependencies&#xff0c;用于收集资源的依赖项&#xff1b;DeterministicAssetBundle&#xff0c;用于为资源维护固定ID&#xff09;之外&#xff0c;5.x中新增了以下选项&#xff1a;

·        ForceRebuildAssetBundle
用于强制重打所有AssetBundle文件&#xff1b;

·        IgnoreTypeTreeChanges
用于判断AssetBundle更新时&#xff0c;是否忽略TypeTree的变化&#xff1b;

·        AppendHashToAssetBundleName
用于将Hash值添加在AssetBundle文件名之后&#xff0c;开启这个选项&#xff0c;可以直接通过文件名来判断哪些Bundle的内容进行了更新&#xff08;4.x下普遍需要通过比较二进制等方法来判断&#xff0c;但在某些情况下即使内容不变重新打包&#xff0c;Bundle的二进制也会变化&#xff09;。

与4.x不同的是&#xff0c;对于移动平台&#xff0c;5.x下默认会将TypeTree信息写入AssetBundle&#xff0c;因此在移动平台上DisableWriteTypeTree选项也变得有意义了。

&#xff08;4&#xff09;Manifest文件

在4.x版本中&#xff0c;我们通常需要自行维护配置文件&#xff0c;以记录AssetBundle之间的依赖关系&#xff0c;并供运行时使用。而在5.x版本中&#xff0c;Manifest文件可以免去这一过程。

&#xff08;5&#xff09; Variant参数

Variant参数能够让AssetBundle方便地进行“多分辨率支持”&#xff0c;相关详解请移步下文。



AssetBundle 打包&#xff08;5.x&#xff09;进阶

在新系统中&#xff0c;添加了以下两个实用的新特性&#xff0c;也许能够给开发者带来事半功倍的效果。

&#xff08;1&#xff09; Manifest文件

在打包后生成的文件夹中&#xff0c;每个Bundle都会对应一个manifest文件&#xff0c;记录了Bundle的一些信息&#xff0c;但这类manifest只在增量式打包时才用到&#xff1b;同时&#xff0c;根目录下还会生成一个同名manifest文件及其对应的Bundle文件&#xff0c;通过该Bundle可以在运行时得到一个AssetbundleManifest对象&#xff0c;而所有的Bundle以及各自依赖的Bundle都可以通过该对象提供的接口进行获取。

&#xff08;2&#xff09; Variant参数

在资源的Inspector界面最下方&#xff0c;除了可以指定abName&#xff0c;在其后方还可以指定Variant。打包时&#xff0c;Variant会作为后缀添加在Bundle名字之后。相同abName&#xff0c;不同variant的Bundle中&#xff0c;资源必须是一一对应的&#xff0c;且他们在Bundle中的ID也是相同的&#xff0c;从而可以起到相互替换的作用。

 

 

 

当需要为手机和平板上的某个UI界面使用两套分辨率不同的纹理、Shader&#xff0c;以及文字提示时&#xff0c;借助Variant的特性&#xff0c;只需创建两个文件夹&#xff0c;分别放置两套不同的资源&#xff0c;且资源名一一对应&#xff0c;然后给两个文件夹设置相同的abName和不同的variant&#xff0c;再给UI界面设置abName&#xff0c;然后进行打包即可。运行时&#xff0c;先选择合适的依赖包加载&#xff0c;那么后续加载UI界面时&#xff0c;会根据已加载的依赖包&#xff0c;呈现出相对应的版本。

开发者注意事项

·        abName可通过脚本进行设置和清除&#xff0c;也可以通过构造一个AssetBundleBuild数组来打包。

·        新机制打包无法指定Assetbundle.mainAsset&#xff0c;因此无法再通过mainAsset来直接获取资源。

·        开启DisableWriteTypeTree可能造成AssetBundle对Unity版本的兼容问题&#xff0c;但会使Bundle更小&#xff0c;同时也会略微提高加载速度。

·        Prefab之间不会建立依赖&#xff0c;即如果Prefab-A和Prefab-B引用了同一张纹理&#xff0c;而他们设置了不同的abName&#xff0c;而共享的纹理并未设置abName&#xff0c;那么Prefab-A和Prefab-B可视为分别打包&#xff0c;各自Bundle中都包含共享的纹理。因此在使用UGUI&#xff0c;开启Sprite Packer时&#xff0c;由于Atlas无法标记abName&#xff0c;在设置UI界面Prefab的abName时就需要注意这个问题。

 

 

 

·        5.x中加入了Shader stripping功能,在打包时&#xff0c;默认情况下会根据当前场景的Lightmap及Fog设置对资源中的Shader进行代码剥离。这意味着&#xff0c;如果在一个空场景下进行打包&#xff0c;则Bundle中的Shader会失去对Lightmap和Fog的支持&#xff0c;从而出现运行时Lightmap和Fog丢失的情况.而通过将Edit->ProjectSettings->Graphics下shader Stripping中的modes改为Manual&#xff0c;并勾选相应的mode即可避免这一问题。

 

 

 

 


推荐阅读
  • 本文介绍了在iOS开发中使用UITextField实现字符限制的方法,包括利用代理方法和使用BNTextField-Limit库的实现策略。通过这些方法,开发者可以方便地限制UITextField的字符个数和输入规则。 ... [详细]
  • 生成式对抗网络模型综述摘要生成式对抗网络模型(GAN)是基于深度学习的一种强大的生成模型,可以应用于计算机视觉、自然语言处理、半监督学习等重要领域。生成式对抗网络 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • Oracle seg,V$TEMPSEG_USAGE与Oracle排序的关系及使用方法
    本文介绍了Oracle seg,V$TEMPSEG_USAGE与Oracle排序之间的关系,V$TEMPSEG_USAGE是V_$SORT_USAGE的同义词,通过查询dba_objects和dba_synonyms视图可以了解到它们的详细信息。同时,还探讨了V$TEMPSEG_USAGE的使用方法。 ... [详细]
  • 本文介绍了机器学习手册中关于日期和时区操作的重要性以及其在实际应用中的作用。文章以一个故事为背景,描述了学童们面对老先生的教导时的反应,以及上官如在这个过程中的表现。同时,文章也提到了顾慎为对上官如的恨意以及他们之间的矛盾源于早年的结局。最后,文章强调了日期和时区操作在机器学习中的重要性,并指出了其在实际应用中的作用和意义。 ... [详细]
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • Redis底层数据结构之压缩列表的介绍及实现原理
    本文介绍了Redis底层数据结构之压缩列表的概念、实现原理以及使用场景。压缩列表是Redis为了节约内存而开发的一种顺序数据结构,由特殊编码的连续内存块组成。文章详细解释了压缩列表的构成和各个属性的含义,以及如何通过指针来计算表尾节点的地址。压缩列表适用于列表键和哈希键中只包含少量小整数值和短字符串的情况。通过使用压缩列表,可以有效减少内存占用,提升Redis的性能。 ... [详细]
  • 本文介绍了在处理不规则数据时如何使用Python自动提取文本中的时间日期,包括使用dateutil.parser模块统一日期字符串格式和使用datefinder模块提取日期。同时,还介绍了一段使用正则表达式的代码,可以支持中文日期和一些特殊的时间识别,例如'2012年12月12日'、'3小时前'、'在2012/12/13哈哈'等。 ... [详细]
  • Python爬虫中使用正则表达式的方法和注意事项
    本文介绍了在Python爬虫中使用正则表达式的方法和注意事项。首先解释了爬虫的四个主要步骤,并强调了正则表达式在数据处理中的重要性。然后详细介绍了正则表达式的概念和用法,包括检索、替换和过滤文本的功能。同时提到了re模块是Python内置的用于处理正则表达式的模块,并给出了使用正则表达式时需要注意的特殊字符转义和原始字符串的用法。通过本文的学习,读者可以掌握在Python爬虫中使用正则表达式的技巧和方法。 ... [详细]
  • IOS开发之短信发送与拨打电话的方法详解
    本文详细介绍了在IOS开发中实现短信发送和拨打电话的两种方式,一种是使用系统底层发送,虽然无法自定义短信内容和返回原应用,但是简单方便;另一种是使用第三方框架发送,需要导入MessageUI头文件,并遵守MFMessageComposeViewControllerDelegate协议,可以实现自定义短信内容和返回原应用的功能。 ... [详细]
  • 本文介绍了在MFC下利用C++和MFC的特性动态创建窗口的方法,包括继承现有的MFC类并加以改造、插入工具栏和状态栏对象的声明等。同时还提到了窗口销毁的处理方法。本文详细介绍了实现方法并给出了相关注意事项。 ... [详细]
author-avatar
sferra石_455
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有