unity游戏框架学习-AssetBundle

AssetBundle官网链接:https://docs.unity3d.com/Manual/AssetBundles-Workflow.html

一、为什么要使用AssetBundle

AssetBundle是Unity推荐的资源管理方式,热更新必须使用此方式。

二、AssetBundle是什么?

First is the actual file on disk. This we call the AssetBundle archive, or just archive for short in this document. 
The archive can be thought of as a container, like a folder, that holds additional files inside of it.
These additional files consist of two types; the serialized file and resource files. The serialized file contains your assets broken out into their individual objects and written out to this single file.
The resource files are just chunks of binary data stored separately for certain assets (textures and audio) to allow us to load them from disk on another thread efficiently. Second is the actual AssetBundle object you interact with via code to load assets from a specific archive.
This object contains a map of all the file paths of the assets you added to this archive to the objects that belong to that asset that need to be loaded when you ask for it.

1.如上,第一部分是我们在unity里看到的,生成出来的ab包文件,你可以理解成一种特殊的文件夹。我们将chariot打成ab包”animation_2d_chariot.unity3d“,然后用UnityStudio解压”animation_2d_chariot.unity3d“后的结构大概是下面这样子的

unity游戏框架学习-AssetBundle

他所包含的资源列表如下:

unity游戏框架学习-AssetBundle

你也可以通过.mainfest文件查看

ManifestFileVersion: 0
CRC: 2785811640
Hashes:
  AssetFileHash:
    serializedVersion: 2
    Hash: 05555bf8d49a3c8fc690e4913454de28
  TypeTreeHash:
    serializedVersion: 2
    Hash: 0317c6c2e1c00c8e914e7d09d8b3e9b0
HashAppended: 0
ClassTypes:
- Class: 1
  Script: {instanceID: 0}
- Class: 4
  Script: {instanceID: 0}
- Class: 21
  Script: {instanceID: 0}
- Class: 28
  Script: {instanceID: 0}
- Class: 48
  Script: {instanceID: 0}
- Class: 114
  Script: {fileID: 11500000, guid: 24fd26203f8ea48f1b25f24fc3663d1c, type: 3}
- Class: 114
  Script: {fileID: 11500000, guid: c93168c4c5e9f49bfa80fc75bd465a40, type: 3}
- Class: 114
  Script: {fileID: 11500000, guid: a6791178c999f426a8618ef42eac4275, type: 3}
- Class: 115
  Script: {instanceID: 0}
- Class: 212
  Script: {instanceID: 0}
- Class: 213
  Script: {instanceID: 0}
Assets:
- Assets/Data/animation/2d/chariot/chariot_5.prefab
- Assets/Data/animation/2d/chariot/chariot_6.prefab
Dependencies:
- F:/ALClient/Assets/Temp/data/shader.unity3d

2.第二部分是脚本中使用的,例如通过unity api AssetBundle.LoadFromFile可以从指定路径加载一个AssetBundle 对象,这个要加载的对象就是上面我们说的unity里面的Asset(你可以理解成AssetBundle是一种特殊的资源,如prefabs),通过这个脚本的AssetBundle,我们可以加载出unity的AssetBundle所包含的文件

三、如何生成AssetBundle文件?

生成AssetBundle文件分两步,第一步标记你要生成AssetBundle的文件,你可以在unity面板直接指定AB包名字,如下

To assign a given Asset to an AssetBundle, follow these steps:

Select the asset you want to assign to a bundle from your Project View
Examine the object in the inspector
At the bottom of the inspector
, you should see a section to assign AssetBundles and Variants:
The left-hand drop down assigns the AssetBundle while the right-hand drop down assigns the variant
Click the left-hand drop down where it says “None” to reveal the currently registered AssetBundle names
Click “New…” to create a new AssetBundle
Type in the desired AssetBundle name. Note that AssetBundle names do support a type of folder structure depending on what you type. To add sub folders, separate folder names by a “/”. For example: AssetBundle name “environment/forest” will create a bundle named forest under an environment sub folder
Once you’ve selected or created an AssetBundle name, you can repeat this process for the right hand drop down to assign or create a Variant name, if you desire. Variant names are not required to build the AssetBundles

1.选中该Project中要导出ab包的文件。2在Inspect底部设置AssetBundle

unity游戏框架学习-AssetBundle

但是一般不推荐这种手撸的方法(大项目动辄几万个文件。。。),一般使用脚本动态进行设置,如下,主要就是针对你要导出的文件、文件夹调用AssetImporter.SetAssetBundleNameAndVariant方法动态设置ab包,你可以进一步封装,例如文件夹xxx目录下的所有文件都设置成单独的ab包或者只有子目录设置ab包等等,这种方法会比手撸效率高很多,只需要设置一次需要导出abbb包的文件,然后在每次打包都调用指定的方法进行ab包设置就好了

// 设置单个文件(或目录)的ABName
    private static void ImportSingleFile(string Path, string abName)
    {
        AssetImporter importer = AssetImporter.GetAtPath(Path);

        if (importer == null)
        {
            Debugger.LogError("[路径错误] path:{0}", Path);
            return;
        }
        abName = abName.Replace ('\\', '_').Replace ('/','_');
        importer.SetAssetBundleNameAndVariant(abName, BaseDef.AB_SUFFIX);
    }

第二步,调用unity的生成ab包的接口(依赖第一步设置完的ab包名字和属性)

BuildPipeline.BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);

outputPath:输出路径
assetBundleOptions:压缩模式等,unity提供三种压缩模式,官方说明:https://docs.unity3d.com/Manual/AssetBundles-Building.html

BuildAssetBundleOptions.None:LZMA格式压缩,使用的时候要整包解压到内存,最大的压缩比(意味着压缩后的文件最小),但第一次的加载需要更长的时间,unity会将解压后的LZMA重新压缩成LZ4格式并保存在硬盘里,意味着第二次加载将会拥有和LZ4压缩相同的加速度,unity推荐在下载时使用(如热更、高清资源),这样可以节省用户流量,加快下载速度

BuildAssetBundleOptions.UncompressedAssetBundle:不压缩,最大的文件,最快的加载速度
BuildAssetBundleOptions.ChunkBasedCompression:LZ4压缩,使用的时候不需要整包解压,即只解压当前需要的块,这是unity推荐的压缩方式(母包)

Using ChunkBasedCompression has comparable loading times to uncompressed bundles with the added benefit of reduced size on disk.

如果觉得lz4的压缩格式导致包体过大,可以将一部分ab包在压缩成lzma(将要压缩的ab包放在文件夹Temp,再将Temp压缩成LZMA格式,只在用户初次进入游戏时整个解压就好了),相当于压缩了两次

targetPlatform:目标平台android/ios等

四、AssetBundle如何分组?

AssetBundle 数量太少:
  会增加运行时内存使用,因为可能加载了当前功能不需要使用的资源
  会增加加载时间,虽然lz4压缩格式不需要整包解压,但还是会把文件头加载进来的
  需要下载大量数据,包体太大,导致细分度不够,可能其中一个对象更新了会导致其他对象也更新,对热更不友好。

有太多的 AssetBundle:
  会增加构建的时间
  会加大开发的复杂性
  会增加总的加载时间:一个大文件的解压时间和多个小文件的解压时间 ,文件总大小一致的话,肯定是大文件快

官方说明:https://docs.unity3d.com/Manual/AssetBundles-Preparing.html

1.按逻辑实体(功能)分组,例如英雄界面相关的预知体一个包,副本界面相关的预制体打一个包,对热更支持最高

2.按类型分组,例如音效、shader、本地化文件等都单独打1-n个包,对热更版本不友好,因为包体相对会被比较大

3.不相干(concurrent)内容分组,将需要同时加载和使用内容分组到同一个 AssetBundle 的策略。这种策略最常用在强本地相关属性的内容上,也就是说内容很少或者基本不可能在应用特定的位置或者时间之外出现,例如某个副本关卡用到的独特的资源、模型等

官方的分组建议如下:

Regardless of the strategy you follow, here are some additional tips that are good to keep in mind across the board:

1.Split frequently updated objects into AssetBundles separate from objects that rarely change
2.Group objects that are likely to be loaded simultaneously. Such as a model, its textures, and its animations
3.If you notice multiple objects across multiple AssetBundles are dependant on a single asset from a completely different AssetBundle, move the dependency to a separate AssetBundle. 
If several AssetBundles are referencing the same group of assets in other AssetBundles, it may be worth pulling those dependencies into a shared AssetBundle to reduce duplication. 4.If two sets of objects are unlikely to ever be loaded at the same time, such as Standard and High Definition assets, be sure they are in their own AssetBundles. 6.Consider splitting apart an AssetBundle if less that 50% of that bundle is ever frequently loaded at the same time 7.Consider combining AssetBundles that are small (less that 5 to 10 assets) but whose content is frequently loaded simultaneously 8.If a group of objects are simply different versions of the same object, consider AssetBundle Variants

1.把经常更新的资源放在一个单独的包里面,跟不经常更新的包分离
2.把需要同时加载的资源放在一个包里面,如同一个功能模块。如果两个对象不太可能同时加载,比如一个纹理的高清和标清版本,可以将他们分配到不同的 AssetBundle 中
3.可以把其他包共享的资源放在一个单独的包里面,例如UI界面里面会有很多按钮、弹窗,而这些资源一般是所有界面通用的,那就可以把它们打1-3个图集
4.控制ab包体的大小,太大了,热更的话,要更新很大的文件,太小的话,io次数会很高,对性能不好

五、如何加载AssetBundle?

1.从包含AssetBundle数据的bytes 里读取

unity游戏框架学习-AssetBundle

2.本地加载最快的接口,如果是lzma的压缩格式,会先解压到内存里(占用内存)

unity游戏框架学习-AssetBundle

3.从网络加载(也可以从本地加载)

IEnumerator InstantiateObject()

    {
        string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;        
     UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequest.GetAssetBundle(uri, 0); yield return request.Send(); AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request); GameObject cube = bundle.LoadAsset<GameObject>("Cube"); GameObject sprite = bundle.LoadAsset<GameObject>("Sprite"); Instantiate(cube); Instantiate(sprite); }

六、如何从AssetBundle中加载文件?

同步加载单个对象:T objectFromBundle = bundleObject.LoadAsset<T>(assetName);

异步加载单个对象:

AssetBundleRequest request = loadedAssetBundleObject.LoadAssetAsync<GameObject>(assetName);
yield return request;
var loadedAsset = request.asset;

同步加载ab包里的所有对象:Unity.Object[] objectArray = loadedAssetBundle.LoadAllAssets();

异步加载ab包里的所有对象:

AssetBundleRequest request = loadedAssetBundle.LoadAllAssetsAsync(); 
yield return request;
var loadedAssets = request.allAssets;

七、如何使用AssetBundleManifest,什么叫做依赖包?

AssetBundles can become dependent on other AssetBundles if one or more of the UnityEngine.Objects contains a reference to a UnityEngine.Object located in another bundle. A dependency does not occur if the UnityEngine.Object contains a reference to a UnityEngine.Object that is not contained in any AssetBundle. In this case, a copy of the object that the bundle would be dependent on is copied into the bundle when you build the AssetBundles. If multiple objects in multiple bundles contain a reference to the same object that isn’t assigned to a bundle, every bundle that would have a dependency on that object will make its own copy of the object and package it into the built AssetBundle.

Should an AssetBundle contain a dependency, it is important that the bundles that contain those dependencies are loaded before the object you’re attempting to instantiate is loaded. Unity will not attempt to automatically load dependencies.

Consider the following example, a Material in Bundle 1 references a Texture in Bundle 2:

In this example, before loading the Material from Bundle 1, you would need to load Bundle 2 into memory. It does not matter which order you load Bundle 1 and Bundle 2, the important takeaway is that Bundle 2 is loaded before loading the Material from Bundle 1. In the next section, we’ll discuss how you can use the AssetBundleManifest objects we touched on in the previous section to determine, and load, dependencies at runtime.

ab包依赖情况有以下两种:

1.ab包引用到另一个ab包里面的资源,即ab包依赖了另一个ab包,例如Bundle1材质A引用了Bundle2的贴图B,那么Bundle1就是依赖Bundle2的,在加载材质A前,你必须先加载Bundle2到内存,unity不会自动加载依赖项。也就是说在你使用某个ab包时,必须先加载他依赖的ab包。

2.ab包引用到另一个不再任何ab包里的资源,例如Bundle1材质A引用了贴图B,而贴图B没有打进任何ab包里,那么最终打ab包时,unity会拷贝一份贴图B到Bundle1,如果有n个Bundle都引用了贴图B,那么这n个Bundle里都会有贴图的拷贝,会造成资源冗余。

AssetBundleManifest文件包含了所有ab包的依赖关系,在使用ab包前,你需要先加载AssetBundleManifest文件,在通过AssetBundleManifest获取ab包的依赖ab包,AssetBundleManifest的加载:

AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

获取依赖包:

AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
string[] dependencies = manifest.GetAllDependencies("assetBundle"); //Pass the name of the bundle you want the dependencies for.
foreach(string dependency in dependencies)
{
    AssetBundle.LoadFromFile(Path.Combine(assetBundlePath, dependency));
}

七、如何判断AssetBundle是否还被引用?

ab包的引用主要作用有两点:1.缓存ab包,卸载无用的ab包,避免内存泄漏 2.判断ab包的依赖包是否已经被加载

核心点是引用技术,每个AssetBundle都会维护一个引用计数,当该ab包被加载、被依赖时引用计数加1,当依赖包被卸载、加载的资源被卸载时,引用计数减1,当引用计数为0超过一段时间(一般为几分钟)时,认为该ab包已经无用了,卸载该ab包。

以下是一个简单的示例(不涉及到从ab包里加载资源),简单说明引用计数的用法,这个例子分两部分,一个是缓存的ab包实体类AssetBundleCache,该类维护一个自己的引用计数,一个是缓存的控制类ABCachePool,该类用于维护ab包的引用、卸载。

using UnityEngine;

// AssetBundle缓存
public class AssetBundleCache
{
    string m_name;          // AssetBundle name
    int m_referencedCount;  // 引用计数
    float m_unloadTime;     // 释放时间

    public AssetBundleCache(string name, AssetBundle ab, int refCount)
    {
        m_name = name;
        Bundle = ab;
        ReferencedCount = refCount;
    }

    // AssetBundle
    public AssetBundle Bundle
    {
        get;
        private set;
    }

    // 是否常驻,通用资源的ab包不卸载
    public bool Persistent
    {
        get;
        set;
    }

    public string BundleName
    {
        get
        {
            return m_name;
        }
    }

    // 引用计数
    public int ReferencedCount
    {
        get
        {
            return m_referencedCount;
        }

        set
        {
            m_referencedCount = value;
            if (m_referencedCount <= 0)
            {
                m_unloadTime = Time.realtimeSinceStartup;
            }
            else
            {
                m_unloadTime = 0;
            }
            if (m_referencedCount < 0)
            {
                Debug.LogWarningFormat("AssetBundleCache reference count < 0, name:{0}, referencecount:{1}", m_name, m_referencedCount);
            }
        }
    }

    // 是否可以删除
    public bool IsCanRemove
    {
        get
        {
            // 常驻资源
            if (Persistent) return false;

            // 非常驻,并且引用计数为0
            if (!Persistent && ReferencedCount <= 0)
            {
                return true;
            }

            return false;
        }
    }

    // 缓存时间到
    public bool IsTimeOut
    {
        get
        {
            return Time.realtimeSinceStartup - m_unloadTime >= Config.Instance.AssetCacheTime;//这个时间自己定义
        }
    }

    //卸载ab包
    public void Unload()
    {
        if (Bundle != null)
        {
            Bundle.Unload(false);
            Bundle = null;
        }
    }
}
public class ABCachePool
{
    #region Instance
    private static ABCachePool m_Instance;
    public static ABCachePool Instance
    {
        get { return m_Instance ?? (m_Instance = new ABCachePool()); }
    }
    #endregion
    Dictionary<string, AssetBundleCache> m_AssetBundleCaches = new Dictionary<string, AssetBundleCache>();  // 缓存队列
    HashSet<string> m_persistentABs = new HashSet<string>();

    public Dictionary<string, AssetBundleCache> AssetBundleCaches
    {
        get
        {
            return m_AssetBundleCaches;
        }
    }

  //只有在退出游戏时会调用这个接口 public void ClearAllCache() { foreach(KeyValuePair<string, AssetBundleCache> keyval in m_AssetBundleCaches) { keyval.Value.Unload(); } m_AssetBundleCaches.Clear(); }
  //是否存在ab包缓存 public bool IsExistCache(string abName) { return m_AssetBundleCaches.ContainsKey (abName); } // 引用这个bundle public AssetBundleCache ReferenceCacheByName(string abName) { AssetBundleCache cache = null; m_AssetBundleCaches.TryGetValue (abName, out cache); if(cache!=null) { ++cache.ReferencedCount; } return cache; } // 获取ABCache 不增加引用 public AssetBundleCache GetABCacheByName(string abName) { AssetBundleCache cache = null; m_AssetBundleCaches.TryGetValue (abName, out cache); return cache; } public AssetBundleCache AddCache(string abName, AssetBundle bundle, int refCount) { if(m_AssetBundleCaches.ContainsKey (abName)) { Debugger.LogWarning ("AssetBundleCache already contains key:{0}, it will be cover by new value.", abName); } AssetBundleCache cache = new AssetBundleCache (abName, bundle, refCount); m_AssetBundleCaches [abName] = cache; if(m_persistentABs.Contains (abName)) { cache.Persistent = true; } return cache; } // immediate 只有场景是立刻卸载 public AssetBundleCache UnReferenceCache(string abName, bool immediate = false) { AssetBundleCache cache = null; if (!m_AssetBundleCaches.TryGetValue(abName, out cache)) { return null; } if(cache.Persistent) { return null; } --cache.ReferencedCount; if (immediate && cache.IsCanRemove) { RemoveCache (abName); } return cache; }
public void RemoveCache(string abName) { AssetBundleCache cache = m_AssetBundleCaches [abName]; cache.Unload (); m_AssetBundleCaches.Remove(abName); } private List<string> m_lstRm = new List<string>(); // 清除无引用的AssetBundle缓存 public void ClearNoneRefCache(bool mustTimeout) { foreach(KeyValuePair<string, AssetBundleCache> keyval in m_AssetBundleCaches) { AssetBundleCache item = keyval.Value; // 只清除引用计数为0的 if (item.IsCanRemove && (!mustTimeout || item.IsTimeOut)) { m_lstRm.Add(keyval.Key); } } for(int i=0; i<m_lstRm.Count; i++) { RemoveCache(m_lstRm[i]); } m_lstRm.Clear(); } /// <summary> /// 常驻ab包设置 /// </summary> /// <param name="arrAB"></param> public void SetPersistentABs(string[] arrAB) { m_persistentABs.Clear(); for (int i = 0; i < arrAB.Length; i++) { string strAB = FileHelper.GenBundlePath (arrAB[i]); m_persistentABs.Add(strAB); AssetBundleCache abCache; m_AssetBundleCaches.TryGetValue(strAB, out abCache); if (abCache!=null) { abCache.Persistent = true; } } } }

八、如何卸载AssetBundle?

Most projects should use AssetBundle.Unload(true) and adopt a method to ensure that Objects are not duplicated. Two common methods are:

Having well-defined points during the application’s lifetime at which transient AssetBundles are unloaded, such as between levels or during a loading screen.

Maintaining reference-counts for individual Objects and unload AssetBundles only when all of their constituent Objects are unused. This permits an application to unload & reload individual Objects without duplicating memory.

If an application must use AssetBundle.Unload(false), then individual Objects can only be unloaded in two ways:

Eliminate all references to an unwanted Object, both in the scene and in code. After this is done, call Resources.UnloadUnusedAssets.

Load a scene non-additively. This will destroy all Objects in the current scene and invoke Resources.UnloadUnusedAssets automatically.

AssetBundle.Unload可以卸载一个AssetBundle,下面会说到这个方法

Resources.UnloadUnusedAssets会卸载所有不被引用的资源,具体如下图所示

unity游戏框架学习-AssetBundle

 

 九、注意事项

1.AssetBundle.Unload(bool unloadAllLoadedObjects)

unloadAllLoadedObjects为true时会卸载所有从这个ab包里加载的对象(不包括instantiation对象),例如材质M加载自Bundle1,当Bundle1调用Unload(true)时,材质M也会被删除,对象会在场景中显示红色(缺失)

unloadAllLoadedObjects为false时不会卸载从这个ab包里加载的对象,但会断开和这个对象的联系,例如材质M加载自Bundle1,当Bundle1调用Unload(false)时,材质M不会被删除,当用户再次加载Bundle1的时候不会重新建立和材质M的联系,而是会重新创建一份引用,造成材质M的冗余,如下图,内存里会存在两个材质M,冗余了一份

unity游戏框架学习-AssetBundle

 

unity游戏框架学习-AssetBundle

 关于true跟false,unity官方时间以使用true的,这样不会造成冗余,但是你必须清楚的知道什么时候可以卸载这个ab包

Most projects should use AssetBundle.Unload(true) and adopt a method to ensure that Objects are not duplicated. Two common methods are:

Having well-defined points during the application’s lifetime at which transient AssetBundles are unloaded, such as between levels or during a loading screen.

Maintaining reference-counts for individual Objects and unload AssetBundles only when all of their constituent Objects are unused. This permits an application to unload & reload individual Objects without duplicating memory.

If an application must use AssetBundle.Unload(false), then individual Objects can only be unloaded in two ways:

Eliminate all references to an unwanted Object, both in the scene and in code. After this is done, call Resources.UnloadUnusedAssets.

Load a scene non-additively. This will destroy all Objects in the current scene and invoke Resources.UnloadUnusedAssets automatically.

2.一个没有被分配到任何ab包中的资源A,任何引用资源A的ab包都会产生一份资源A的拷贝,这会导致游戏的ab包大小变大(资源A冗余了),如果这两个ab包都被加载到内存,那么还会导致内存里存在两份完全一样资源A。

Any Object that is not explicitly assigned in an AssetBundle will be included in all AssetBundles that contain 1 or more Objects that reference the untagged Object.

If two different Objects are assigned to two different AssetBundles, but both have references to a common dependency Object, then that dependency Object will be copied into both AssetBundles. 
The duplicated dependency will also be instanced, meaning that the two copies of the dependency Object will be considered different Objects with a different identifiers.
This will increase the total size of the application’s AssetBundles. This will also cause two different copies of the Object to be loaded into memory if the application loads both of its parents.

解决这个问题的最优解是:把所有ab包引用的资源都打到ab包里,即ab包不引用任何不再ab包里的资源,但是这样做需要程序在加载ab包时,确认该ab包的所有依赖包都已经加载完成了(ab包缓存、ab依赖包加载)

3.图集:首先我们需要大概知道图集在AssetBundle里是以什么形式存在的。如下图所示,一个图集的ab包里包含了这个图集的图片资源以及图集的信息(下图的SpriteAtlasTexture-ui_atlas_elf-1024x1024-fmt12)

unity游戏框架学习-AssetBundle

需要注意的是:

1.如果一个图集包含的Sprite资源被包含在多个AssetBundle里,那么所有包含该图集的Sprite的ab包都会有一份图集信息(SpriteAtlasTexture-ui_atlas_elf-1024x1024-fmt12),从上图我们看到,这个文件还是很大的

2.如果一个图集包含的Sprite资源不再任何ab包里,那么图集信息(SpriteAtlasTexture-ui_atlas_elf-1024x1024-fmt12)也不会在任何的ab包里

综上,如果图集分散到多个ab包,会造成资源冗余,会增大包体大小,运行时也会浪费内存,如果不分配到ab包里又无法热更,那么唯一的做法就是一个图集打一个ab包(把相同图集的Sprite放在同一个文件夹,这个文件夹只包含该图集的sprite,在把这个文件夹打成ab包)

4.减少同时加载的AB数量(这个是纯逻辑控制),使用AssetBundle.LoadFromFile接口。使用WWW加载会生成一个新的线程,在移动平台线程多了会导致游戏崩溃,尽量使用UnityWebRequest

上一篇:半期考试 之 SQLite 操作2


下一篇:AssetBundle 详解