资源管理|02 打包策略分析
打包策略
- 制作编辑器工具,统一设置AB包名,与路径管理
- 根据依赖关系生成不冗余AB包
- 根据Asset全路径生成依赖关系表
- 根据依赖关系表加载资源 / 编辑器下直接通过AssetDataBase加载资源
配置文件
打包一共有两种配置
- 以文件夹为单位,一个文件夹打成一个包
- 文件夹下的所有文件打成单独的一个包(一般是Prefab)
使用ScriptableObject制作配置表
public class AssetBundleBuildConfig : ScriptableObject
{
public const string buildConfigName = "AssetBundleBuildConfig";
// 是否生成XML文件(默认配置文件是bytes,xml用于debug)
public bool buildXML = true;
// 配置文件的保存路径,以及配置文件的名称
public string configPath = "Assets/Resources";
public string configName = "AssetBundleConfig";
// 打包路径
public string targetPath;
// 该文件夹路径下的所有Prefab都会被单独打成一个AB包,包名即Prefab名
public List<string> prefabList;
// 该文件夹会被打成一个AB包,文件夹名即包名
public List<string> assetList;
}
生成配置表的Editor脚本,保证全局只有一个配置表,并且只能生成在Resources目录下
public class CreateConfig
{
[MenuItem("Assets/Create/AssetModule/Create Config")]
private static void Create()
{
var config = Resources.Load<AssetBundleBuildConfig>(AssetBundleBuildConfig.buildConfigName);
// 说明没有配置表
if (config == null)
{
if (!Directory.Exists("Assets/Resources"))
Directory.CreateDirectory("Assets/Resources");
var asset = ScriptableObject.CreateInstance<AssetBundleBuildConfig>();
AssetDatabase.CreateAsset(asset, $"Assets/Resources/{AssetBundleBuildConfig.buildConfigName}.asset");
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
EditorGUIUtility.PingObject(asset);
}
else
{
EditorGUIUtility.PingObject(config);
Debug.Log("资源管理 - 配置文件已经存在");
}
}
}
稍微修改一下编辑器界面
[CustomEditor(typeof(AssetBundleBuildConfig))]
public class AssetBundleBuildConfigInspector : Editor
{
#region Field
private AssetBundleBuildConfig script;
private SerializedProperty buildXML;
private SerializedProperty configName;
private SerializedProperty targetPath;
private SerializedProperty prefabs;
private SerializedProperty assets;
private ReorderableList prefabList;
private ReorderableList assetList;
// 所有的文件夹AB包,key是包名,value是路径
private Dictionary<string, string> folderBundles = new Dictionary<string, string>();
// 所有单独打包的AB包
private Dictionary<string, List<string>> prefabBundles = new Dictionary<string, List<string>>();
// 过滤
private List<string> filter = new List<string>();
// XML的过滤
private List<string> xmlFilter = new List<string>();
#endregion
#region 生命周期
private void OnEnable()
{
script = target as AssetBundleBuildConfig;
buildXML = serializedObject.FindProperty(nameof(AssetBundleBuildConfig.buildXML));
configName = serializedObject.FindProperty(nameof(AssetBundleBuildConfig.configName));
targetPath = serializedObject.FindProperty(nameof(AssetBundleBuildConfig.targetPath));
prefabs = serializedObject.FindProperty(nameof(AssetBundleBuildConfig.prefabList));
assets = serializedObject.FindProperty(nameof(AssetBundleBuildConfig.assetList));
prefabList = new ReorderableList(serializedObject, prefabs);
assetList = new ReorderableList(serializedObject, assets);
RegisterList(prefabList,prefabs,"以下文件夹内的每一个Prefab都会被打成一个AB包");
RegisterList(assetList,assets,"以下文件夹会被打成一个AB包");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
Draw();
EditorGUILayout.Space();
EditorGUILayout.Space();
prefabList.DoLayoutList();
EditorGUILayout.Space();
EditorGUILayout.Space();
assetList.DoLayoutList();
serializedObject.ApplyModifiedProperties();
Build();
}
#endregion
#region 面板
private void RegisterList(ReorderableList list, SerializedProperty property,string title)
{
list.drawHeaderCallback = rect =>
{
EditorGUI.LabelField(rect,title);
};
list.drawElementCallback = (rect, index, active, focused) =>
{
var item = property.GetArrayElementAtIndex(index);
var folderRect = new Rect(rect)
{
y = rect.y + 2.5f,
width = (rect.width - 5f) / 4f,
height = rect.height - 5,
};
var folder = EditorGUI.ObjectField(folderRect, AssetDatabase.LoadAssetAtPath<DefaultAsset>(item.stringValue), typeof(DefaultAsset), true);
item.stringValue = AssetDatabase.GetAssetPath(folder);
var pathRect = new Rect(rect)
{
y = rect.y + 2.5f,
x = rect.x + folderRect.width + 5,
width = (rect.width - 5) / 4f * 3f,
height = rect.height-5
};
GUI.enabled = false;
EditorGUI.TextField(pathRect,item.stringValue);
GUI.enabled = true;
};
}
private void Draw()
{
// AssetBundle的保存路径
EditorGUILayout.BeginHorizontal();
GUI.enabled = false;
EditorGUIUtility.labelWidth = 55f;
EditorGUILayout.TextField("保存路径",targetPath.stringValue);
GUI.enabled = true;
if (GUILayout.Button("...", GUILayout.Width(100)))
{
var tempPath = EditorUtility.OpenFolderPanel("保存路径", targetPath.stringValue, "");
// 只能选择StreamingAsset下的目录
var streamingAssetsPath = Application.streamingAssetsPath;
if (!tempPath.Contains(streamingAssetsPath))
{
Debug.LogError("保存路径必须位于项目路径以下");
targetPath.stringValue = "";
return;
}
var index = tempPath.IndexOf("Assets");
targetPath.stringValue = tempPath.Substring(index);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
EditorGUILayout.Space();
// 是否生成XML,以及配置文件的名称
EditorGUILayout.BeginHorizontal();
EditorGUIUtility.labelWidth = 80f;
buildXML.boolValue = EditorGUILayout.Toggle("生成XML文件", buildXML.boolValue, GUILayout.Width(120f));
configName.stringValue = EditorGUILayout.TextField("配置文件名称", configName.stringValue);
EditorGUILayout.LabelField(".bytes",GUILayout.Width(40f));
EditorGUILayout.EndHorizontal();
}
#endregion
}
生成
1.不冗余AB包
有3个目的
- 根据单个文件和文件夹设置AB包
- 剔除冗余AB包
- 生成AB包配置表
如果每一个Prefab都单独打成一个包,那么我应该把Prefab依赖的所有资源也打入这个包内
但是,比如Prefab依赖于某一张贴图A,但是这张贴图A与其他贴图B C D E都已经被打入了某一个贴图AB包内,此时我们打包Prefab时就不应该再继续打包这张贴图了
首先,先要知道哪些文件夹会被打成AB包
// 文件夹名:文件夹路径
private Dictionary<string, string> folderBundles = new Dictionary<string, string>();
// 过滤
private List<string> filter = new List<string>();
private void Build()
{
if (GUILayout.Button("打包", GUILayout.Height(40)))
{
folderBundles.Clear();
filter.Clear();
// 所有资源
for (int i = 0; i < script.assetList.Count; i++)
{
var path = script.assetList[i];
var bundleName = path.Substring(path.LastIndexOf("/")+1);
if (!folderBundles.ContainsKey(bundleName))
{
folderBundles.Add(bundleName, path);
filter.Add(path);
}
else
{
throw new Exception($"重复的文件夹:{bundleName}");
}
}
}
}
关键在于filter,我们会把每一个文件夹的路径都添加到filter里
如果某一个Prefab的依赖资源包含该路径,则说明该依赖资源已经被打入资源包内,不需要再次被打入Prefab包
// 所有的文件夹AB包,key是包名,value是路径
private Dictionary<string, string> folderBundles = new Dictionary<string, string>();
// 所有单独打包的AB包
private Dictionary<string, string> prefabBundles = new Dictionary<string, string>();
// 过滤
private List<string> filter = new List<string>();
private void Build()
{
if (GUILayout.Button("打包", GUILayout.Height(40)))
{
...
// 所有单独打包的Prefab
var allFolderAssetGUID = AssetDatabase.FindAssets("t:prefab", script.prefabList.ToArray());
for (int i = 0; i < allFolderAssetGUID.Length; i++)
{
var path = AssetDatabase.GUIDToAssetPath(allFolderAssetGUID[i]);
var bundleName = path.Substring(path.LastIndexOf("/") + 1);
bundleName = bundleName.Substring(0, bundleName.Length - 7);
var depend = AssetDatabase.GetDependencies(path);
var tempDepend = new List<string>();
for (int j = 0; j < depend.Length; j++)
{
if (!ContainAsset(depend[j]) && !depend[j].EndsWith(".cs"))
{
filter.Add(depend[j]);
tempDepend.Add(depend[j]);
}
}
if (!prefabBundles.ContainsKey(bundleName))
prefabBundles.Add(bundleName,tempDepend);
else
throw new Exception($"重复的Prefab:{bundleName}");
}
}
}
// 如果一个资源的路径中包含有资源文件夹的路径,说明该资源肯定已经被打入资源包了
// A/B/CC/TEST.prefab是会包含 A/B/C 这个目录的,所有&&是有必要的
private bool ContainAsset(string path)
{
for (int i = 0; i < filter.Count; i++)
{
if (path == filter[i] || (path.Contains(filter[i]) && path.Replace(filter[i], "")[0] == '/'))
return true;
}
return false;
}
然后设置标签,并且在最开始我们应该清楚所有的标签
// 所有的文件夹AB包,key是包名,value是路径
private Dictionary<string, string> folderBundles = new Dictionary<string, string>();
// 所有单独打包的AB包
private Dictionary<string, List<string>> prefabBundles = new Dictionary<string, List<string>>();
// 过滤
private List<string> filter = new List<string>();
private void Build()
{
if (GUILayout.Button("打包", GUILayout.Height(40)))
{
Clear();
...
// 设置Bundle
foreach (var bundle in folderBundles)
SetBundle(bundle.Key, bundle.Value);
foreach (var bundle in prefabBundles)
SetBundle(bundle.Key, bundle.Value);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
// 测试
BuildPipeline.BuildAssetBundles(script.targetPath, BuildAssetBundleOptions.None,
BuildTarget.StandaloneWindows);
}
}
private void SetBundle(string name, string path)
{
var asset = AssetImporter.GetAtPath(path);
if (asset == null)
throw new Exception($"不存在{path}");
asset.assetBundleName = name;
}
private void SetBundle(string name, List<string> paths)
{
for (int i = 0; i < paths.Count; i++)
SetBundle(name, paths[i]);
}
private void Clear()
{
var bundles = AssetDatabase.GetAllAssetBundleNames();
for (int i = 0; i < bundles.Length; i++)
AssetDatabase.RemoveAssetBundleName(bundles[i], true);
}
此时我们的资源引用关系与打包关系如下
再观察Mainfest文件
符合预期
2.Crc32验证
我们这个框架加载资源时是以资源全路径为加载目录的,例如:
AssetModule.Load("Assets/Test/Cube.prefb");
此时我们肯定需要去配置文件中读取相关的配置,而能够和配置文件相关联的只有这个资源的路径,即”Assets/Test/Cube.prefb”
但是我们有必要把这么长一串(实际项目只可能更长)字符串存入到配置文件中去吗?那会非常的浪费
所以可以采用一种压缩格式,对资源路径进行压缩(也是为了方便校验)
这里采用Crc32校验,核心原理是把一个字符串转译为一串uint数字
public class CRC32
{
static UInt32[] crcTable =
{
0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005,
0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd,
0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75,
0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd,
0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5,
0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81, 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d,
0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95,
0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d,
0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae, 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072,
0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca,
0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02,
0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066, 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba,
0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692,
0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a,
0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e, 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2,
0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a,
0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb,
0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f, 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53,
0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b,
0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623,
0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7, 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b,
0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3,
0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b,
0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f, 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3,
0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c,
0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24,
0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30, 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec,
0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654,
0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c,
0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18, 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4,
0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c,
0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4
};
public static uint GetCRC32(string str)
{
var bytes = Encoding.UTF8.GetBytes(str);
return GetCRC32(bytes);
}
private static uint GetCRC32(byte[] bytes)
{
uint iCount = (uint)bytes.Length;
uint crc = 0xFFFFFFFF;
for (uint i = 0; i < iCount; i++)
{
crc = (crc << 8) ^ crcTable[(crc >> 24) ^ bytes[i]];
}
return crc;
}
}
3.配置文件
XML所需要的属性
[Serializable]
public class AssetConfig
{
public uint crc; // 资源CRC32验证
public string bundleName; // 所属Bundle
public string assetName; // 资源名称
public List<string> dependence; // 依赖的Bundle Name
}
[Serializable]
public class AssetBundleConfig
{
public List<AssetConfig> bundleList;
}
生成XML配置文件
private void Build()
{
if (GUILayout.Button("打包", GUILayout.Height(40)))
{
...
BuildConfig();
BuildBundle();
}
}
private void BuildConfig()
{
// 所有的BundleName
var bundleNames = AssetDatabase.GetAllAssetBundleNames();
// 资源路径:资源所属Bundle
var assetInBundle = new Dictionary<string, string>();
for (int i = 0; i < bundleNames.Length; i++)
{
// 某一个Bundle下所有的资源路径
var assetPath = AssetDatabase.GetAssetPathsFromAssetBundle(bundleNames[i]);
for (int j = 0; j < assetPath.Length; j++)
{
if (assetPath[j].EndsWith(".cs") || !IsValidPath(assetPath[j]))
continue;
assetInBundle.Add(assetPath[j], bundleNames[i]);
}
}
AssetBundleConfig config = new AssetBundleConfig();
config.bundleList = new List<AssetConfig>();
foreach (var assetInfo in assetInBundle)
{
var asset = new AssetConfig();
// assetInfo.key 是资源路径,我们不会把资源路径放入配置文件,而是放入对应的CRC32验证码
asset.crc = CRC32.GetCRC32(assetInfo.Key);
asset.bundleName = assetInfo.Value;
asset.assetName = assetInfo.Key.Remove(0, assetInfo.Key.LastIndexOf("/", StringComparison.Ordinal) + 1);
asset.dependence = new List<string>();
var tempDep = AssetDatabase.GetDependencies(assetInfo.Key);
for (int i = 0; i < tempDep.Length; i++)
{
if (tempDep[i] == assetInfo.Key || tempDep[i].EndsWith(".cs"))
continue;
if (assetInBundle.TryGetValue(tempDep[i], out var depBundle))
{
if (depBundle == assetInfo.Value)
continue;
if (!asset.dependence.Contains(depBundle))
asset.dependence.Add(depBundle);
}
}
config.bundleList.Add(asset);
}
if (Directory.Exists(script.configPath))
Directory.CreateDirectory(script.configPath);
// XML
if (buildXML.boolValue)
{
var xmlPath = $"{script.configPath}/{script.configName}.xml";
if (File.Exists(xmlPath))
File.Delete(xmlPath);
using (var stream = new FileStream(xmlPath, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
using (var writer = new StreamWriter(stream, Encoding.UTF8))
{
var xs = new XmlSerializer(typeof(AssetBundleConfig));
xs.Serialize(writer, config);
}
}
}
// bytes
var bytesPath = $"{script.configPath}/{script.configName}.bytes";
if (File.Exists(bytesPath))
File.Delete(bytesPath);
using (var stream = new FileStream(bytesPath, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
var bf = new BinaryFormatter();
bf.Serialize(stream, config);
}
}
还有一点问题,此时会把我们项目里的所有资源都写入XML中,但实际上一部分资源我们是永远不会手动加载的,比如资源文件夹下的资源,那都是加载Prefab时自动加载的,我们并不关心,我们只关心那些我们真正会手动加载的资源
// XML的过滤
private List<string> xmlFilter = new List<string>();
private void Build()
{
if (GUILayout.Button("打包", GUILayout.Height(40)))
{
...
xmlFilter.Clear();
for (int i = 0; i < script.assetList.Count; i++)
{
...
if (!folderBundles.ContainsKey(bundleName))
{
...
xmlFilter.Add(path);
}
...
}
for (int i = 0; i < allFolderAssetGUID.Length; i++)
{
...
xmlFilter.Add(path);
...
}
BuildXML();
...
}
}
private void BuildXML()
{
...
for (int i = 0; i < bundleNames.Length; i++)
{
...
for (int j = 0; j < assetPath.Length; j++)
{
if (assetPath[j].EndsWith(".cs") || !IsValidPath(assetPath[j])) // <---
continue;
...
}
}
...
}
private bool IsValidPath(string path)
{
for (int i = 0; i < xmlFilter.Count; i++)
{
if (path.Contains(xmlFilter[i]))
return true;
}
return false;
}
打包后测试一下加载
打印所有资源
public class ResourceLoadTest : MonoBehaviour
{
void Start()
{
var buildConfig = Resources.Load<AssetBundleBuildConfig>(AssetBundleBuildConfig.buildConfigName);
var bundleConfig = Resources.Load<TextAsset>(buildConfig.configName);
using (var stream = new MemoryStream(bundleConfig.bytes))
{
var bf = new BinaryFormatter();
var test = bf.Deserialize(stream) as AssetBundleConfig;
foreach (var bundle in test.bundleList)
{
Debug.Log(bundle.bundleName);
}
}
}
}