您的位置:首页 > 移动开发 > Unity3D

Unity AssetBundle 冗余检测与资源分析

2017-09-30 13:10 465 查看

原因

在使用 Unity 进行开发项目时,通常使用 AssetBundle 来进行资源打包,虽然在 Unity 5.x 版本里提供了更加智能的依赖自动管理,即如果依赖的资源没有显式设置 AssetBundle 名称,那么就会被隐式地打包到同一个 AssetBundle 包里面。而如果已经设置的话,那么就会自动生成依赖关系。

那么当被依赖的资源没有独立打包时,而此时又存在两个或以上 AssetBundle 依赖此资源的话,这个资源就会被同时打包到这些 AssetBundle 包里面,造成资源冗余,增大 AssetBundle 包的体积,增加游戏加载 AssetBundle 时所需的内存。

于是,检测 AssetBundle 资源的冗余,才好方便对其进行优化。检测冗余可以在未打包前对将要打包的资源做分析,但是这无法完全保证打包之后的 AssetBundle 完全无冗余,一是分析时无法保证正确无冗余,二是引用的内置资源无法剔除冗余,所以对打包之后的 AssetBundle 包进行检测才真正检查到所有的冗余。

优点

通过查找 AssetBundle 里冗余的资源,就能方便对其进行优化。优化之后,AssetBundle 包的大小也会相应的变小,作为初始包的话,包体也会变小。另外,游戏运行时加载 AssetBundle 时所占用的内存也会降低。而资源分析,能够发现到资源的错误设置引起的各种问题,方便纠正。

实现过程

检测 AssetBundle 资源的冗余,要分两种情况,一种是非场景打包的 AssetBundle 文件,一种是场景打包的 AssetBundle 文件。这两种类型的 AssetBundle 文件存储的方式有所不同,加载用的 API 也不同,所以分两种情况来处理。

非场景打包的 AssetBundle

问题一:如何取出每个 AssetBundle 文件里面的所有资源?

对于显式设置 AssetBundle 名称的资源,可以通过 API 来直接获取,比如:一个材质引用了一张贴图,对材质设置 AssetBundle 名称,如下所示:



使用
AssetBundle.LoadAllAssets
来获取所有的资源,结果如下:



可以看到,只能得到有设置 AssetBundle 名称的资源对象,无法直接获取到所有的资源对象。

解决:我们知道贴图资源是被材质所引用了,那么只要获取材质对象,然后通过材质的接口就可以获取到贴图对象了,如下所示:



可以看到,贴图对象在材质的
mainTexture
属性,着色器对象在材质的
shader
属性上。这种方式可以获取到所引用的对象,但是太繁琐,需要对每个类进行处理,我们可以学习 Unity 编辑器检视器窗口的处理,把每个可序列化的对象都通过
SerializedObject
来进行处理。具体流程伪代码如下:

public static void AnalyzeObjectReference(AssetBundleFileInfo info, Object o)
{
var serializedObject = new SerializedObject(o);
var it = serializedObject.GetIterator();
while (it.NextVisible(true))
{
// 如果是引用类型的属性,则递归查询
if (it.propertyType == SerializedPropertyType.ObjectReference && it.objectReferenceValue != null)
{
AnalyzeObjectReference(info, it.objectReferenceValue);
}
}
}


这样就可以查询到所有被依赖的资源。

额外情况:对于
AssetDatabase.AddObjectToAsset
方式合并多个对象到一个资产的话,并且没有任何其他对象进行引用的话,是无法获取得到的,比如
AnimatorController
组件,里面关联的动画片段文件无法用
SerializedObject
方式来获取得到,其检视器窗口也是空空的,如下所示:



对于这种情况,只能特定处理,通过其外部接口去获取引用的对象。还好这种情况比较少,目前也就
AnimatorController
组件如此。

问题二:如何确定资源的唯一性?

我们知道在编辑器下的话,每个资源都有个
GUID
唯一标识符,但是这个标识符没有直接保存到 AssetBundle 里面,而是经过 Unity 计算过后的一个唯一值,通过解包工具可以看到:



其中的
Path ID
就是资源的标识符,但没有提供 API 可以直接访问这个变量,就无法知道资源是否冗余,因为资源重名太常见了,不能仅因为相同的资源名称就认为是冗余。

解决:在进行尝试的过程中,发现可以通过获取
Local Identfier In File
的方式来获取得到,这个属性在检视器的
Debug
模式下,如下所示:



对资源的
SerializedObject
对象进行设置
Debug
模式,代码如下:

public static void AnalyzeObjectReference(AssetBundleFileInfo info, Object o)
{
var serializedObject = new SerializedObject(o);
if (inspectorMode == null)
{
// 反射获取模式属性
inspectorMode = typeof(SerializedObject).GetProperty("inspectorMode", BindingFlags.NonPublic | BindingFlags.Instance);
}
inspectorMode.SetValue(serializedObject, InspectorMode.Debug, null);

var it = serializedObject.GetIterator();
while (it.NextVisible(true))
{
if (it.propertyType == SerializedPropertyType.ObjectReference && it.objectReferenceValue != null)
{
AnalyzeObjectReference(info, it.objectReferenceValue);
}
}
}


获取唯一标识符的方式如下:



可以看到
m_LocalIdentfierInFile
属性值就是
Path ID
值,就可以确定资源的唯一性。

问题三:如何获取 AssetBundle 之间的依赖关系?

解决:如果使用的是 Unity5 自动生成的
AssetBundleManifest
依赖关系记录文件的话,那么直接使用
AssetBundleManifest
的 API 接口就可以获取每个 AssetBundle 包的依赖关系了。

allDepends = assetBundleManifest.GetAllDependencies(bundle)


如果是自己维护的依赖关系文件的话,那么只要实现自己的加载方式即可,类似代码如下:

public static void MyAnalyzeCustomDepend()
{
AssetBundleFilesAnalyze.analyzeCustomDepend = directoryPath =>
{
List<AssetBundleFileInfo> infos = new List<AssetBundleFileInfo>();

// 添加每个 AssetBundle 信息
AssetBundleFileInfo info = new AssetBundleFileInfo
{
//name = bundle,
//path = path,
//rootPath = directoryPath,
//size = new FileInfo(path).Length,
//directDepends = assetBundleManifest.GetDirectDependencies(bundle),
//allDepends = assetBundleManifest.GetAllDependencies(bundle)
};
infos.Add(info);

return infos;
};
}


如果都不是的话,那么就会加载文件夹下的所有 AssetBundle 文件,不分析依赖关系,只分析资源冗余。

场景打包的 AssetBundle

问题四:如何分析场景里的资源?

场景打包的 AssetBundle 无法像普通 AssetBundle 那样去加载分析,普通的 AssetBundle 可以在编辑器下,不进入播放模式,直接进行使用
ab.LoadAllAssets
接口去加载分析。但场景打包的 AssetBundle 无法使用这个接口,它只能在播放模式下,加载 AssetBundle 文件,使用
SceneManager.LoadScene
方式去加载场景。而且无法获取到场景里资源的唯一标识符,解包工具可以看到:



这里打包进场景的贴图就是之前使用到的贴图文件,但是在这里的
Path ID
只是在场景里的顺序索引而已。

解决:故不能检测冗余,只能做资源分析。在播放模式下,一个接一个地加载 AssetBundle 文件,载入场景,伪代码如下:

private void LoadNextBundleScene()
{
BundleSceneInfo info = m_BundleSceneInfos.Peek();
info.ab = AssetBundle.LoadFromFile(info.fileInfo.path);
SceneManager.LoadScene(info.sceneName, LoadSceneMode.Additive);
}

private IEnumerator AnalyzeBundleScene(Scene scene)
{
BundleSceneInfo info = m_BundleSceneInfos.Peek();
AssetBundleFilesAnalyze.AnalyzeObjectReference(info.fileInfo, RenderSettings.skybox);
GameObject[] gos = scene.GetRootGameObjects();
foreach (var go in gos)
{
AssetBundleFilesAnalyze.AnalyzeObjectComponent(info.fileInfo, go);
}
AssetBundleFilesAnalyze.AnalyzeObjectsCompleted(info.fileInfo);
SceneManager.SetActiveScene(defaultScene);

info.ab.Unload(true);
info.ab = null;
SceneManager.UnloadScene(scene);
}


最好是场景打包的 AssetBundle 单独进行分析,这样不会干扰非场景打包的 AssetBundle 分析,使用的代码开关如下:

AssetBundleFilesAnalyze.analyzeOnlyScene = true;


即可在播放模式下,只分析场景资源。

资源的分析

在对 AssetBundle 文件进行加载分析的时候,可以获取到资源对象,在这里主要分析比较会引起性能问题的资源,比如:Mesh、Texture、AnimationClip、AudioClip等。分析的方法,主要通过资源对象的接口和
SerializedObject
序列化方式获取属性,比如:纹理的分析代码如下:

private static List<KeyValuePair<string, object>> AnalyzeTexture2D(Texture2D tex, SerializedObject serializedObject)
{
var propertys = new List<KeyValuePair<string, object>>
{
new KeyValuePair<string, object>("宽度", tex.width),
new KeyValuePair<string, object>("高度", tex.height),
new KeyValuePair<string, object>("格式", tex.format.ToString()),
new KeyValuePair<string, object>("MipMap功能", tex.mipmapCount > 1 ? "True" : "False")
};

var property = serializedObject.FindProperty("m_IsReadable");
propertys.Add(new KeyValuePair<string, object>("Read/Write", property.boolValue.ToString()));

property = serializedObject.FindProperty("m_CompleteImageSize");
propertys.Add(new KeyValuePair<string, object>("内存占用", property.intValue));

return propertys;
}


其他资源的分析也是类似如此。

资源的导出

既然可以获取到资源对象,那么就可以将某些资源导出来,这在分析其他 Unity 项目的时候比较有用,目前实现了纹理的导出,默认不开启功能,开启的代码如下:

AssetBundleFilesAnalyze.analyzeExport = true;


开启后,在分析的时候,会将资源自动保存到以
Export
结尾的同名目录下。

输出最终结果

在分析完毕之后,输出最终结果为 Excel 报表。使用 Excel 可以实现跳转链接的效果,也可以实现多个分页报告的效果。



第一页为 AssetBundle 文件列表,显示所有的 AssetBundle 文件,以及每个 AssetBundle 文件的大小,依赖的 AssetBundle 数量,存在的冗余资源数量,以及包含各类型资源的数量。点击表格的 AssetBundle 名称,即可跳转到第二页相对应的所包含资源信息。



第二页为每个 AssetBundle 文件所包含的具体资源信息,以及所依赖的 AssetBundle 文件列表,和被依赖的 AssetBundle 文件列表。若此 AssetBundle 包含冗余资源,则资源名称会以红色进行显示。点击资源名称,即可跳转到第三页相对应的资源信息,如果是具体分析的资源,如:Mesh、Texture2D、Material、AnimationClip、AudioClip类型的话,会跳转到相应的资源类型分页。



第三页为所有资源的列表,以及资源类型,被包含的 AssetBundle 文件数量和具体的文件名称。



第四页为网格资源列表,以及顶点数、面数、子网格数、网格压缩、Read/Write等参数信息。



第五页为纹理资源列表,以及宽度、高度、格式、MipMap功能、Read/Write、内存占用等参数信息。

第六页为材质资源列表,以及依赖Shader、依赖纹理等参数信息。

第七页为动画片段资源列表,以及总曲线数、Constant曲线数、Dense曲线数、Stream曲线数、事件数、内存占用等参数信息。

第八页为音频片段资源列表,以及加载方式、预加载、频率、长度、格式等参数信息。

每一页都可以对每一列进行排序或查找定位,方便直接定位到有问题的资源,如下图所示:



使用说明

将插件包导入到工程,打包 AssetBundle 之后,调用检测的接口,如下所示:

/// <summary>
/// 分析打印 AssetBundle
/// </summary>
/// <param name="bundlePath">AssetBundle 文件所在文件夹路径</param>
/// <param name="outputPath">Excel 报告文件保存路径</param>
/// <param name="completed">分析打印完毕后的回调</param>
public static void AnalyzePrint(string bundlePath, string outputPath, UnityAction completed = null)


传入所需的参数即可,等待输出报告。另外注意一点,打包完 AssetBundle 就立即检测,这样才能在分析 AssetBundle 的时候,获取到正确的自定义脚本类信息,才能分析完全。过后再检测的话,自定义的脚本类可能被其他人所修改,那么就无法分析正确。Unity 5.4+ 支持场景资源分析,Unity 4.X ~ Unity 5.3 只支持非场景资源分析。

源码地址

https://github.com/akof1314/AssetBundleReporter
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: