AssetBundle是一个存档文件,是Unity提供的一种用于存储资源的资源压缩包,可以包含模型、贴图、音频、预制体等。
Unity中的AssetBundle系统是对资源管理的一种扩展,通过将资源分布在不同的AB包中可以最大程度地减少运行时的内存压力,可以动态地加载和卸载AB包,继而有选择地加载内容。
而最重要的是AssetBundle可以用于热更新,是Unity更新非代码内容的主要工具。
AssetBundle 主要由两部分组成:文件头和数据段
文件头包含了id、压缩类型、索引清单,该索引清单是与 Resources 相同的记录了序列化文件中的字节偏移量的查找表。对于大部分平台该表为平衡搜索树,对 Windows 和 OSX 系列(包括 iOS)则为红黑树,随着 AssetBundle 中对象的增加,构造清单所需时间的增长速度将超过线形增长速度
数据段包含了 Asset 经过序列化的原始数据,数据还可选择是否压缩,若使用 LZMA 压缩,则将所有 Asset 的字节数组整体压缩;若使用 LZ4 压缩,则将每个 Asset 单独压缩;若不压缩,则数据保持原始字节流
BuildPipeline.BuildAssetBundles(string outputPath, AssetBundleBuild[] builds, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);
BuildPipeline.BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);
第1种方式打包时要手动收集所有AssetBundleBuild
第2种方式事先生成或手动填写所有需要打包的AssetBundleBuild
推荐第2种方式,可以在Unity编辑器看到所有要打包的AB,比较直观。
一般控制在10MB以下,最好不要超过20MB,相比总大小,控制单个ab的资源数量尤为重要。如果项目中大部分ab都只包含单个资源,太过分散需要加载的ab过多,导致io压力增加;如果单个ab包含太多资源,首先热更新时容易增加下载资源量,也容易导致每次加载都有大部分用不到的资源,并且导致ab文件头复杂增加,解释时间也相对增加。所以要根据项目情况而具体分析,平衡才是最好的,通过经验一般单个ab包含10个左右资源为宜。
以每个文件夹为一个ab包,ab名就以文件夹路径为名,在这样的规则下,工程组织也可按文件夹分类,较为清晰。也可加入一些灵活的规则,如过滤一些不需要打包的文件后缀或文件夹,某文件夹的每个文件都为单独一个AB。
当我们调用Unity的API BuildPipeline.BuildAssetBundles去打AssetBundle的时候,实际上有很多的参数可以供我们选择。如果没有选择合适的参数,就可能会导致在包体,内存以及加载时间等方面造成很多的浪费。
实际上我们经常用到的有这么几个:
我们可以使用四种不同的方法来加载 AssetBundle。
AssetBundle.LoadFromMemory
从内存区域创建一个AssetBundle,可以通过byte[]把AB包完整的加载出来。一般用于需要高度加密,或者WebGL,缺点:内存占用高,会占用两份内存。
AssetBundle.LoadFromFile
该方法可高效地从硬盘加载未压缩或 LZ4 压缩的 Assetbundle,加载 LZMA 压缩包时会先解压再加载到内存,加载 LZ4 它只会加载AB包的Header,之后需要什么资源再加载那部分的AB包chunk。从这可以看出使用 LZ4 构建AssetBundle的优势。
public class LoadFromFileExample : MonoBehaviour {
function Start() {
var myLoadedAssetBundle
= AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "myassetBundle"));
if (myLoadedAssetBundle == null) {
Debug.Log("Failed to load AssetBundle!");
return;
}
var prefab = myLoadedAssetBundle.LoadAsset.("MyObject");
Instantiate(prefab);
}
}
AssetBundle加载完后,还不能直接使用,还需要从该ab中加载需要的Asset
LoadAsset从ab包中加载指定的Asset,返回的是Object
LoadAllAssets从ab包中加载所有Asset,返回的是Object[],如在加载Sprite图集可以使用
不管是AssetBundle加载或者是AssetBundle里Asset的加载Unity都提供同步异步的加载方式,那么应如何选择呢?
实际上这只是一个策略的问题,并没有哪个更好。同步最大的优点是快,因为在这一帧里面主线程所有的CPU全都会归你用,所有的时间片全都归你用,它可以一门心思的把这件事情做完,再做其他的事情。但是同步的问题就是会造成主线程卡顿。异步可以简单的理解为多线程(其实还是有点区别的),最大的优点是不怎么会造成主线程的卡顿(也不是完全不卡顿),主线程可以尽量不卡顿的去跑。
也就是说卡顿不敏感的情况下可以使用同步,卡顿敏感的场景如战斗场景可以使用异步。
推荐做法:
在Unity Editor下运行一般不使用AssetBundle加载资源,一方面是AssetBundle需要提前打包,另一方面是大部分shader显示有问题。
那么在Editor下一般用AssetDatabase.LoadMainAssetAtPath或AssetDatabase.LoadAllAssetsAtPath,对应于AssetBundle.LoadAsset和AssetBundle.LoadAllAssets,
AssetDatabase的加载需要知道资源路径,可通过AssetDatabase.GetAssetPathsFromAssetBundleAndAssetName获得
AssetDatabase只有同步加载,那么对于AssetBundle异步加载,在Editor下最好模拟AssetDatabase的异步加载,如可以等待几帧。
Resources加载
Resources.Load只能加载Resources文件夹的资源,而Resources文件夹必须包含在首包,且不可热更,最重要的是启动时就自动加载,导致启动时间增加。因此,Resources.Load缺点非常明显,一般可用于首包必须要用到的资源加载,如配置资源。
使用AssetBundle一个非常有用的特性是AssetBundle的依赖,在Unity中当有资源需要复用时,可将该资源生成一个复用的ab包,这样在AssetBundle构建时其它引用该资源的ab包会自动关联依赖它。
那AssetBundle依赖对加载有什么影响呢?
当一个ab包有其它依赖包时,如果只加载该ab包,那么实例化的对象会出现资源丢失的现象。因此在ab加载Asset前必须加载它所有的依赖ab,注意是所有,要一直递归。
AssetBundleManifest.GetAllDependencies 可获取 AssetBundle 的所有依赖层级,manifest为主包
参数分true和false,如果是true那就是把AssetBundle和它加载出来的Asset全都一起干掉。这个在不合适的时机就有可能发生资源丢失,出现粉色现象。如果是false,那么只是把AssetBundle给丢掉,Asset是不会被扔掉的。那么当你第二次去加载同一个AssetBundle的时候,在内存中就会有两份Asset,因为当AssetBundle被卸载的时候,它和对应的Asset的关系就被切割掉了。所以AssetBundle不知道之前的Asset是不是还在内存中,是不是从自己这加载出来的,容易导致内存泄漏。所以使用AssetBundle.Unload就很考验游戏的规划。
推荐使用AssetBundle.Unload(true),理由:程序应当自己管理维护AssetBundle,仅当引用AssetBundle的所有Asset都移除后才应该卸载该AssetBundle。
如果应用程序必须使用 AssetBundle.Unload(false),或者Resources.Load加载的Asset移除后,可使用Resources.UnloadUnusedAssets,它可以卸载掉那些没用的Asset,把它从内存中清除掉。Resources.UnloadUnusedAssets可能容易造成卡顿,需要注意调用时机,Unity在切换Scene的时候会自动调用一次UnloadUnusedAssets。
要处理好AssetBundle的加载与卸载,少不了要有一套完善的管理系统。程序在实际使用时只关心Asset的获取,AssetBundle应做到完全透明,因此AssetBundle和Asset的管理要独立开。
推荐做法:
Unity的AssetBundle非常容易破解,轻易就可获得原始资源,如AssetStudio工具,因此成熟的项目都需要考虑AssetBundle的加密。一种比较直接又可以自定义的加密方式是使用AssetBundle.LoadFromMemory(Async)
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
public class ExampleClass : MonoBehaviour
{
byte[] MyDecription(byte[] binary)
{
byte[] decrypted = new byte[1024];
return decrypted;
}
IEnumerator Start()
{
var uwr = UnityWebRequest.Get("http://myserver/myBundle.unity3d");
yield return uwr.SendWebRequest();
byte[] decryptedBytes = MyDecription(uwr.downloadHandler.data);
AssetBundle.LoadFromMemory(decryptedBytes);
}
}
但AssetBundle.LoadFromMemory(Async)的使用成本非常高昂,不但内存增加,解密也需要耗时,一般不推荐使用。
推荐做法:
AssetBundle LoadFromFile(string path, uint crc, ulong offset);
offset参数可以自定义,是指AssetBundle内容的偏移量,只要在AssetBundle构建后
foreach (var abName in manifest.GetAllAssetBundles())
{
string filePath = outputPath + abName;
int offset = Utility.GetAssetBundlesOffset(abName);
var fileContent = File.ReadAllBytes(filePath);
int filelen = offset + fileContent.Length;
byte[] buffer = new byte[filelen];
fileContent.CopyTo(buffer, offset);
FileStream fs = File.OpenWrite(filePath);
fs.Write(buffer, 0, filelen);
fs.Close();
}
Utility.GetAssetBundlesOffset 是自定义offset的算法,这里是根据ab名字来计算,也可以根据其它参数来计算,例如hashcode
基于offset加载AssetBundle:
public AssetBundle LoadAssetBundle(string abName)
{
string path = abPath + abName;
int offset = Utility.GetAssetBundlesOffset(abName);
AssetBundle ab = AssetBundle.LoadFromFile(path, 0, (ulong)offset);
return ab;
}
这样就达到了加密的效果,只要不知道offset的算法,就无法破解了,并且这种做法只是做了一下偏移,基本不会增加性能消耗。