关闭

Unity AssetBundle 冗余检测与资源分析

标签: unity
198人阅读 评论(0) 收藏 举报
分类:

原因

在使用 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

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:3400120次
    • 积分:35247
    • 等级:
    • 排名:第138名
    • 原创:398篇
    • 转载:88篇
    • 译文:5篇
    • 评论:3409条
    个人说明
    联系方式:
    文章存档
    最新评论