1,AssetBundle的概念与作用

AssetBundle是一个存档文件,是Unity提供的一种用于存储资源的资源压缩包,可以包含模型、贴图、音频、预制体等。
Unity中的AssetBundle系统是对资源管理的一种扩展,通过将资源分布在不同的AB包中可以最大程度地减少运行时的内存压力,可以动态地加载和卸载AB包,继而有选择地加载内容。
而最重要的是AssetBundle可以用于热更新,是Unity更新非代码内容的主要工具。

2,AssetBundle组成

AssetBundle 主要由两部分组成:文件头和数据段

unity 删除VS文件 unity怎么删除没用的文件_AssetBundle

文件头包含了id、压缩类型、索引清单,该索引清单是与 Resources 相同的记录了序列化文件中的字节偏移量的查找表。对于大部分平台该表为平衡搜索树,对 Windows 和 OSX 系列(包括 iOS)则为红黑树,随着 AssetBundle 中对象的增加,构造清单所需时间的增长速度将超过线形增长速度
数据段包含了 Asset 经过序列化的原始数据,数据还可选择是否压缩,若使用 LZMA 压缩,则将所有 Asset 的字节数组整体压缩;若使用 LZ4 压缩,则将每个 Asset 单独压缩;若不压缩,则数据保持原始字节流

3,压缩模式

  • AssetBundle 提供了三种压缩格式
  • NoCompression:不压缩,解压快,包较大,不建议使用。
  • LZMA: 压缩最小,解压慢,用一个资源要解压包下所有资源。
  • LZ4: 压缩稍大,解压快,用什么解压什么,内存占用低,一般建议使用这种。

4,AssetBundle的构建

4.1 两种构建方式:

BuildPipeline.BuildAssetBundles(string outputPath, AssetBundleBuild[] builds, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);
BuildPipeline.BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);

第1种方式打包时要手动收集所有AssetBundleBuild
第2种方式事先生成或手动填写所有需要打包的AssetBundleBuild

unity 删除VS文件 unity怎么删除没用的文件_打包_02

推荐第2种方式,可以在Unity编辑器看到所有要打包的AB,比较直观。

4.2 分包策略

  • 将需要同时加载的对象分为一组,如一个模型,其所需的材质和动画分为一组
  • 若多个 AssetBundle 中的多个对象引用了其他 AssetBundle 中的单个 Asset,则将依赖项分离到单独的包中以减少重复
  • 确保两组完全不可能同时加载的对象不在用一包中,如低清和高清材质包
  • 若一个包中只有低于一半的对象被频繁加载,可将其拆分
  • 将一些同时加载的小包(资源少于5到10个)合并
单个ab包多大合适?

一般控制在10MB以下,最好不要超过20MB,相比总大小,控制单个ab的资源数量尤为重要。如果项目中大部分ab都只包含单个资源,太过分散需要加载的ab过多,导致io压力增加;如果单个ab包含太多资源,首先热更新时容易增加下载资源量,也容易导致每次加载都有大部分用不到的资源,并且导致ab文件头复杂增加,解释时间也相对增加。所以要根据项目情况而具体分析,平衡才是最好的,通过经验一般单个ab包含10个左右资源为宜。

推荐的一种分包策略:

以每个文件夹为一个ab包,ab名就以文件夹路径为名,在这样的规则下,工程组织也可按文件夹分类,较为清晰。也可加入一些灵活的规则,如过滤一些不需要打包的文件后缀或文件夹,某文件夹的每个文件都为单独一个AB。

4.3 AssetBundle的参数(BuildAssetBundleOptions)

当我们调用Unity的API BuildPipeline.BuildAssetBundles去打AssetBundle的时候,实际上有很多的参数可以供我们选择。如果没有选择合适的参数,就可能会导致在包体,内存以及加载时间等方面造成很多的浪费。
实际上我们经常用到的有这么几个:

  • ChunkBasedCompression:这个参数是压缩AssetBundle的用的。前面提到Android的StreamingAssets是不压缩的。为了减小包体大小,可以使用该参数对AssetBundle进行压缩。它实际上是一个由Unity改良过的LZ4,使它的算法更符合Unity的使用方式。
  • DisableWriteTypetree:这个其实是会被很多开发者忽略的一个参数,它非常有用,可以帮我们减小AssetBundle包体的大小,同时也可以减小内存,以及减少我们加载这个AssetBundle时的CPU时间。
  • DisableLoadAssetByFileName,DisableLoadAssetByFileNameWithExtension:当我们加载好一个AssetBundle然后使用LoadAsset加载Asset的时候,需要传递Asset的路径名称。

4.4 自动构建流程

  • 构建前检查,如各种资源格式的检查,是否符合约定
  • lua脚本打包
  • 自动生成AssetBundle名字
  • 构建生成AssetBundle包
  • AssetBundle包加密
  • 生成AssetBundle md5
  • 删除旧ab资源
  • 拷贝首包ab资源到StreamingAssets
  • 生成各平台包,exe、apk、ios工程等

5,AssetBundle加载

5.1 加载方式

我们可以使用四种不同的方法来加载 AssetBundle。

  • AssetBundle.LoadFromMemory与AssetBundle.LoadFromMemoryAsync
  • AssetBundle.LoadFromFile与AssetBundle.LoadFromFileAsync(推荐)
  • WWW.LoadfromCacheOrDownload(5.6 及以前版本)
  • UnityWebRequestAssetBundle 和 DownloadHandlerAssetBundle(WebGL或需要网络加载的)

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.<GameObject>("MyObject");
        Instantiate(prefab);
    }
}

5.2 AssetBundle Asset 加载

AssetBundle加载完后,还不能直接使用,还需要从该ab中加载需要的Asset

  • AssetBundle.LoadAsset (LoadAssetAsync)
  • AssetBundle.LoadAllAssets (LoadAllAssetsAsync)

LoadAsset从ab包中加载指定的Asset,返回的是Object
LoadAllAssets从ab包中加载所有Asset,返回的是Object[],如在加载Sprite图集可以使用

5.3 同步与异步加载

不管是AssetBundle加载或者是AssetBundle里Asset的加载Unity都提供同步异步的加载方式,那么应如何选择呢?
实际上这只是一个策略的问题,并没有哪个更好。同步最大的优点是快,因为在这一帧里面主线程所有的CPU全都会归你用,所有的时间片全都归你用,它可以一门心思的把这件事情做完,再做其他的事情。但是同步的问题就是会造成主线程卡顿。异步可以简单的理解为多线程(其实还是有点区别的),最大的优点是不怎么会造成主线程的卡顿(也不是完全不卡顿),主线程可以尽量不卡顿的去跑。
也就是说卡顿不敏感的情况下可以使用同步,卡顿敏感的场景如战斗场景可以使用异步。
推荐做法:

  • 一般的UI界面,ui贴图,音效资源都可使用同步加载,通常不会感应卡顿,对ui逻辑处理也方便
  • 特效,角色模型,场景,背景音乐资源使用异步加载
  • 避免同步与异步同时进行,如一个ab里有多个Asset,用异步加载一个Asset时,同时又用同步加载另一个Asset,最好是不要把同步和异步加载的Asset放在同一个ab包里。
     

5.4 Editor加载资源与Resources加载

在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缺点非常明显,一般可用于首包必须要用到的资源加载,如配置资源。
 

6,AssetBundle依赖

使用AssetBundle一个非常有用的特性是AssetBundle的依赖,在Unity中当有资源需要复用时,可将该资源生成一个复用的ab包,这样在AssetBundle构建时其它引用该资源的ab包会自动关联依赖它。

unity 删除VS文件 unity怎么删除没用的文件_打包_03

那AssetBundle依赖对加载有什么影响呢?
当一个ab包有其它依赖包时,如果只加载该ab包,那么实例化的对象会出现资源丢失的现象。因此在ab加载Asset前必须加载它所有的依赖ab,注意是所有,要一直递归。
AssetBundleManifest.GetAllDependencies 可获取 AssetBundle 的所有依赖层级,manifest为主包

unity 删除VS文件 unity怎么删除没用的文件_unity 删除VS文件_04

7,AssetBundle卸载

7.1 AssetBundle.Unload(bool)

参数分true和false,如果是true那就是把AssetBundle和它加载出来的Asset全都一起干掉。这个在不合适的时机就有可能发生资源丢失,出现粉色现象。如果是false,那么只是把AssetBundle给丢掉,Asset是不会被扔掉的。那么当你第二次去加载同一个AssetBundle的时候,在内存中就会有两份Asset,因为当AssetBundle被卸载的时候,它和对应的Asset的关系就被切割掉了。所以AssetBundle不知道之前的Asset是不是还在内存中,是不是从自己这加载出来的,容易导致内存泄漏。所以使用AssetBundle.Unload就很考验游戏的规划。

推荐使用AssetBundle.Unload(true),理由:程序应当自己管理维护AssetBundle,仅当引用AssetBundle的所有Asset都移除后才应该卸载该AssetBundle。

7.2 Resources.UnloadUnusedAssets

如果应用程序必须使用 AssetBundle.Unload(false),或者Resources.Load加载的Asset移除后,可使用Resources.UnloadUnusedAssets,它可以卸载掉那些没用的Asset,把它从内存中清除掉。Resources.UnloadUnusedAssets可能容易造成卡顿,需要注意调用时机,Unity在切换Scene的时候会自动调用一次UnloadUnusedAssets。

7.3 AssetBundle的管理与Asset的管理

要处理好AssetBundle的加载与卸载,少不了要有一套完善的管理系统。程序在实际使用时只关心Asset的获取,AssetBundle应做到完全透明,因此AssetBundle和Asset的管理要独立开。

推荐做法:

  • AssetBundle的管理AssetBundleManager放在C#层,Asset的管理AssetManager放在Lua层
  • AssetBundleManager和AssetManager分别都有自己的一套引用计数算法
  • 获取Asset:通过AssetManager获取Asset,Asset引用计数+1;AssetManager向AssetBundleManager获取Asset,AssetBundleManager计算Asset所属AB,加载对应AB,再通过AB加载对应Asset,AB引用计数+1
  • 释放Asset:通过AssetManager释放Asset,Asset引用计数-1;当该Asset引用计数为0时,可在适当时机通知AssetBundleManager释放Asset,AssetBundleManager计算Asset所属AB,AB引用计数-1,当AB引用计数为0,可在适应时机卸载AB
  • Asset通常只需要一份,如:贴图、音频、字体,文本、动画,AssetManager只需要向AssetBundleManager获取一次,每次使用引用计数+1即可,需要注意每个AssetBundle可能有多个Asset,引用计数应是其总和。另外Prefab是比较常用的一种Asset,它也只需要一份Asset,但每次使用需要实例化GameObject.Instantiate(asset),为避免经常实例化,最好使用对象池管理GameObject。
  • 在内存不紧张的情况下,可在场景切换时进行统一释放Asset,卸载AssetBundle

    

8,AssetBundle加密

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的算法,就无法破解了,并且这种做法只是做了一下偏移,基本不会增加性能消耗。