主要功能:
对项目中指定文件夹下的资源进行打包,并生成独立于manifest的打包结果文档(全依赖、全被依赖、包和文件等信息),加载包时可依据全依赖信息文档加载依赖包。这里的文档是针对manifest在一般增量打包时只会生成打出包的信息所采取的的处理。
支持增量打包。某资源的删减、添加、修改(MD5改变)都会导致该资源、该资源依赖的所有资源、该资源被依赖的所有资源被打包。
不用手动选择包,程序会分析出所需要打的包。分析规则如上。
注意事项:
文件夹的删除不会导致打包,必须完整打包。
添加文件夹时,如果不和已有文件产生依赖关系,正常打包;否则必须完整打包。
知识细节:
DirectoryInfo folder = new DirectoryInfo() 路径不分大小写
string.replace()会生成新的字符串
配置类
/// <summary>
/// 文件校验
/// </summary>
public class VerifyData
{
/// <summary>
/// 版本号
/// </summary>
public int version;
/// <summary>
/// 最新的打包时间
/// </summary>
public string builddate;
/// <summary>
/// 文件总表
/// </summary>
public List<FileMsg> filemap = new List<FileMsg>();
/// <summary>
/// 包总表
/// </summary>
public List<BundleMsg> bagmap = new List<BundleMsg>();
}
[Serializable]
public class FileMsg
{
/// <summary>
/// 文件路径(含尾缀)
/// </summary>
public string Path;
public string MD5;
}
[Serializable]
public class BundleMsg
{
public string bagname;
public int num;
}
/// <summary>
/// 依赖关系信息
/// </summary>
public class DependData
{
public List<DependMsg> depsmap = new List<DependMsg>();
}
[Serializable]
public class DependMsg
{
public string selfbag;
public List<string> depends = new List<string>();
}
/// <summary>
/// 被依赖关系信息
/// </summary>
public class BeDependData
{
public List<BeDependMsg> bedepsmap=new List<BeDependMsg> ();
}
[Serializable]
public class BeDependMsg
{
public string selfbag;
public List<string> bedepends=new List<string> ();
}
public class ABConfig
{
/// <summary>
/// 输出路径
/// </summary>
public static string outputpath = Application.dataPath.Substring(0, Application.dataPath.Length - 6) + "AB"; //最后是主包名字
/// <summary>
/// 项目AB包文件夹
/// </summary>
public static string abroot = "ABres";
/// <summary>
/// 校验文件路径
/// </summary>
public static string verifypath = Application.dataPath + "/Editor/version.json";
/// <summary>
/// 全被依赖文件路径
/// </summary>
public static string allbedependpath = Application.dataPath + "/Editor/allbedependdata.json";
/// <summary>
/// 全依赖文件路径
/// </summary>
public static string alldependpath = Application.dataPath + "/Editor/alldependdata.json";
/// <summary>
/// 打包压缩设置
/// </summary>
public static BuildAssetBundleOptions ABOptions = BuildAssetBundleOptions.ChunkBasedCompression;
/// <summary>
/// 打包平台
/// </summary>
public static BuildTarget ABPlatform = BuildTarget.StandaloneWindows;
}
/// <summary>
/// 校验工具
/// </summary>
public class ToolVerify
{
private static MD5 md5 = new MD5CryptoServiceProvider();
private static StringBuilder sb = new StringBuilder();
/// <summary>
/// 获取文件的MD5码
/// </summary>
/// <param name="fileName">传入的文件名(含路径及后缀名)</param>
/// <returns></returns>
public static string GetFileMD5(string fileName)
{
try
{
sb.Clear();
FileStream file = new FileStream(fileName, FileMode.Open);
byte[] retVal = md5.ComputeHash(file);
file.Close();
for (int i = 0; i < retVal.Length; i++)
{
sb.Append(retVal[i].ToString("x2")); //x表示16进制 2表示显示两位,即 0x1A
}
return sb.ToString();
}
catch (Exception ex)
{
throw new Exception("获取MD5出错:" + ex.Message);
}
}
/// <summary>
/// 比较指定文件的MD5值与目标值是否相等,并返回文件当前md5
/// </summary>
/// <param name="fileName"></param>
/// <param name="Template"></param>
/// <param name="curmd5">文件当前的md5</param>
/// <returns>true 没修改 false 修改</returns>
public static bool Compare(string fileName,StringBuilder Template)
{
string newmd5 = GetFileMD5(fileName);
if (newmd5.Equals(Template.ToString()))
{
Template.Clear();
Template.Append(newmd5);
return true;
}
Template.Clear();
Template.Append(newmd5);
return false;
}
/// <summary>
/// 比较指定文件的MD5值与目标值是否相等
/// </summary>
/// <param name="fileName"></param>
/// <param name="Template"></param>
/// <returns></returns>
public static bool Compare(string fileName, string Template)
{
string newmd5 = GetFileMD5(fileName);
if (newmd5.Equals(Template))
{
return true;
}
return false;
}
} 核心
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using System;
using System.Linq;
using System.Text;
public class ExportAB : MonoBehaviour
{
#region 全局变量
/// <summary>
/// 当次打包总数
/// </summary>
private static int bagnum;
/// <summary>
/// 文件校验对象
/// </summary>
private static VerifyData versiondata;
/// <summary>
/// 全被依赖关系对象
/// </summary>
private static BeDependData allbedependData;
/// <summary>
/// 全依赖关系对象
/// </summary>
private static DependData alldependData;
/// <summary>
/// 文件MD5表
/// </summary>
private static Dictionary<string, string> filedict;
/// <summary>
/// AB包单包所含文件个数表
/// </summary>
private static Dictionary<string, int> bagdict;
/// <summary>
/// 全被依赖关系表
/// </summary>
private static Dictionary<string, List<string>> allbedependdict;
/// <summary>
/// 全依赖关系表
/// </summary>
private static Dictionary<string, List<string>> alldependdict;
/// <summary>
/// 已打包表
/// </summary>
private static Dictionary<string, int> builtbagdict;
/// <summary>
/// 待打包表:由于依赖关系追加的
/// </summary>
private static Dictionary<string, int> addbagdict;
#endregion
[MenuItem("AB包/导出")]
private static void Export()
{
DateTime starttime = DateTime.Now;
#region 打包前数据初始化
bagnum = 0;
filedict = new Dictionary<string, string>();
bagdict = new Dictionary<string, int>();
allbedependdict = new Dictionary<string, List<string>>();
alldependdict = new Dictionary<string, List<string>>();
builtbagdict = new Dictionary<string, int>();
addbagdict = new Dictionary<string, int>();
//读取校验文件和被依赖信息,将其中的数据读进字典中
versiondata = new VerifyData();
allbedependData = new BeDependData();
alldependData = new DependData();
if (File.Exists(ABConfig.verifypath))
{
string json = File.ReadAllText(ABConfig.verifypath);
versiondata = JsonUtility.FromJson<VerifyData>(json);
}
for (int i = 0; i < versiondata.filemap.Count; i++)
{
filedict.Add(versiondata.filemap[i].Path, versiondata.filemap[i].MD5);
}
for (int i = 0; i < versiondata.bagmap.Count; i++)
{
bagdict.Add(versiondata.bagmap[i].bagname, versiondata.bagmap[i].num);
}
if (File.Exists(ABConfig.allbedependpath))
{
string json = File.ReadAllText(ABConfig.allbedependpath);
allbedependData = JsonUtility.FromJson<BeDependData>(json);
}
for (int i = 0; i < allbedependData.bedepsmap.Count; i++)
{
allbedependdict.Add(allbedependData.bedepsmap[i].selfbag, allbedependData.bedepsmap[i].bedepends);
}
if (File.Exists(ABConfig.alldependpath))
{
string json = File.ReadAllText(ABConfig.alldependpath);
alldependData = JsonUtility.FromJson<DependData>(json);
}
for (int i = 0; i < alldependData.depsmap.Count; i++)
{
alldependdict.Add(alldependData.depsmap[i].selfbag, alldependData.depsmap[i].depends);
}
#endregion
//遍历项目中AB包文件夹目录,设置里面各个文件的包名
DirectoryInfo rootfolder = new DirectoryInfo(Application.dataPath + string.Concat("/", ABConfig.abroot)); ;
SetBundlesName(rootfolder);
if (bagnum == 0)
{
Debug.LogError("没有需要打包的文件");
return;
}
else
{
//剔除待打包表中已打包的
for (int i = 0; i < addbagdict.Count; i++)
{
if (builtbagdict.ContainsKey(addbagdict.ElementAt(i).Key))
{
addbagdict.Remove(addbagdict.ElementAt(i).Key);
i--;
}
}
//给待打包中的包设置包名
StringBuilder md5 = new StringBuilder();
for (int i = 0; i < addbagdict.Count; i++)
{
string path = string.Concat(Application.dataPath, "/", addbagdict.ElementAt(i).Key);
if (!Directory.Exists(path)) continue;
DirectoryInfo folder = new DirectoryInfo(path);
if (FinalSetName(folder, addbagdict.ElementAt(i).Key, md5,false))
{
Debug.Log("追加设置包名:" + addbagdict.ElementAt(i).Key);
bagnum++;
}
md5.Clear();
}
Debug.Log("<size=15>" + string.Concat("【设置包名】 ", (DateTime.Now - starttime).TotalSeconds) + "</size>");
//最终打包
if (Build())
{
Debug.Log("导出AB包成功");
Debug.Log($"共打包{bagnum}个");
Debug.Log("<size=15>" + string.Concat("【打包】 ", (DateTime.Now - starttime).TotalSeconds) + "</size>");
UpdateVersion();
Debug.Log("校验文件 依赖文件 被依赖文件 保存成功");
Debug.Log("<size=15>"+string.Concat("【总计用时】 " , (DateTime.Now - starttime).TotalSeconds)+ "</size>");
}
}
}
/// <summary>
/// 打包最后一步 注意生成后的实际文件路径全是小写
/// </summary>
private static bool Build()
{
try
{
//检测路径是否存在,没有则创建
if (!Directory.Exists(ABConfig.outputpath))
{
Directory.CreateDirectory(ABConfig.outputpath);
}
//导出ab包 ab包存储位置,打包设置,平台
BuildPipeline.BuildAssetBundles(ABConfig.outputpath, ABConfig.ABOptions,ABConfig.ABPlatform);
}
catch
{
Debug.LogError("打包出现异常");
return false;
}
return true;
}
/// <summary>
/// 增加,修改,修改都会会导致该文件所在包被记录
/// </summary>
/// <param name="folder"></param>
private static void SetBundlesName(DirectoryInfo folder)
{
string newbagname=folder.FullName.
Substring(folder.FullName.IndexOf(ABConfig.abroot)).Replace("\\", "/").ToLower(); // abres/...
StringBuilder md5 = new StringBuilder();
int checknum=0; //记录当前目录检测完毕的文件个数(处理文件删减情况)
bool Nofile=false; //是否有文件
//遍历文件夹
foreach (DirectoryInfo child in folder.GetDirectories())
{
if (!Nofile) Nofile = true;
SetBundlesName(child);
}
//文件夹和文件不会并存
if (Nofile) return;
//如果自己在待打包表中,将其去除,并直接打包
if (addbagdict.ContainsKey(newbagname))
{
addbagdict.Remove(newbagname);
if(FinalSetName(folder, newbagname,md5))
{
Debug.Log("设置包名:" + newbagname);
bagnum++;
}
return;
}
//遍历文件
foreach (FileInfo file in folder.GetFiles())
{
//todo 排除某些文件
if (file.Extension.Equals(".meta"))
{
continue;
}
md5.Clear();
//校验当前文件的更新情况
//如果dict中有当前文件的记录,进行比较
if (filedict.ContainsKey(file.FullName))
{
md5.Append(filedict[file.FullName]);
//如果MD5一样,说明该文件没有被修改
if (ToolVerify.Compare(file.FullName, md5.ToString()))
{
//取消其包名
DealSingleABName(file,string.Empty,md5);
checknum++;
continue;
}
//如果该文件被修改了,直接进行对该AB包设置名字
else
{
md5.Clear();
if (FinalSetName(folder, newbagname,md5))
{
Debug.Log("设置包名:" + newbagname);
bagnum++;
}
//结束对该目录的操作
return;
}
}
//该文件是新文件,直接进行对该AB包设置名字
else
{
md5.Clear();
if (FinalSetName(folder, newbagname,md5))
{
Debug.Log("设置包名:" + newbagname);
bagnum++;
}
//结束对该目录的操作
return;
}
}
//该目录下没有文件
if (checknum == 0) return;
//走到这里说明当前目录所有文件都没被修改,因此检测是否有文件删减
if (bagdict.ContainsKey(newbagname))
{
if (bagdict[newbagname] != checknum)
{
md5.Clear();
if (FinalSetName(folder, newbagname,md5))
{
Debug.Log("设置包名:" + newbagname);
bagnum++;
}
}
}
//理论上走不到这个分支
else
{
bagdict.Add(newbagname, checknum);
md5.Clear();
if (FinalSetName(folder, newbagname,md5))
{
Debug.Log("设置包名:" + newbagname);
bagnum++;
}
}
}
/// <summary>
/// 具体设置单个资源的包名
/// </summary>
/// <param name="file">文件对象</param>
/// <param name="bagname">包名</param>
/// <param name="assetsfilepath">用于检索文件的路径变量</param>
private static void DealSingleABName(FileInfo file,string newbagname,StringBuilder assetsfilepath)
{
assetsfilepath.Clear();
assetsfilepath.Append(file.FullName.Substring(file.FullName.IndexOf("Assets")));
//找到文件
AssetImporter res = AssetImporter.GetAtPath(assetsfilepath.ToString());
//设置包名
if (res != null)
{
res.assetBundleName = newbagname;
}
else
{
Debug.LogError("设置包名异常:" + assetsfilepath.ToString());
}
}
/// <summary>
/// 更新校验文件信息,依赖信息,被依赖信息
/// </summary>
private static void UpdateVersion()
{
#region 更新校验文件数据并写入
versiondata.filemap.Clear();
versiondata.bagmap.Clear();
for (int i = 0; i < filedict.Count; i++)
{
FileMsg filemsg = new FileMsg();
filemsg.Path = filedict.ElementAt(i).Key;
filemsg.MD5 = filedict.ElementAt(i).Value;
versiondata.filemap.Add(filemsg);
}
for (int i = 0; i < bagdict.Count; i++)
{
BundleMsg bagmsg = new BundleMsg();
bagmsg.bagname = bagdict.ElementAt(i).Key;
bagmsg.num = bagdict.ElementAt(i).Value;
versiondata.bagmap.Add(bagmsg);
}
versiondata.version++;
versiondata.builddate = DateTime.Now.ToString();
if (!File.Exists(ABConfig.verifypath))
{
File.Create(ABConfig.verifypath).Dispose();
}
string json = JsonUtility.ToJson(versiondata, true);
File.WriteAllText(ABConfig.verifypath, json);
#endregion
#region 读取主包
//读取打包后的主包
AssetBundle main = AssetBundle.LoadFromFile(ABConfig.outputpath + "/AB");
AssetBundleManifest fest = main.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
#endregion
#region 更新依赖数据 生成被依赖数据
//更新全依赖信息数据
for (int i = 0; i < builtbagdict.Count; i++)
{
string bagname = builtbagdict.ElementAt(i).Key;
string[] alldeps = fest.GetAllDependencies(bagname);
//如果全依赖信息中有打出的这个包,将新的依赖信息覆盖原信息
if (alldependdict.ContainsKey(bagname))
{
alldependdict[bagname].Clear();
if (alldeps.Length != 0) alldependdict[bagname].AddRange(alldeps);
}
//如果全依赖信息中没有打出的这个包
else
{
//不记录没有依赖的包
if (alldeps.Length != 0)
{
List<string> temp = new List<string>();
temp.AddRange(alldeps);
alldependdict.Add(bagname, temp);
}
}
}
//用最新的全依赖信息,生成全被依赖信息
allbedependdict = new Dictionary<string, List<string>>();
for (int i = 0; i < alldependdict.Count; i++)
{
List<string> values = alldependdict.ElementAt(i).Value;
string key = alldependdict.ElementAt(i).Key;
for (int p = 0; p < values.Count; p++)
{
//如果这个被依赖包已经登记了
if (allbedependdict.ContainsKey(values[p]))
{
allbedependdict[values[p]].Add(key);
}
//如果这个被依赖包还没登记,是个新的被依赖包
else
{
List<string> temp = new List<string>();
temp.Add(key);
allbedependdict.Add(values[p], temp);
}
}
}
#endregion
#region 将依赖信息和被依赖信息写入文件
BeDependData allbedependlist = new BeDependData();
DependData alldependlist = new DependData();
foreach (KeyValuePair<string, List<string>> kv in alldependdict)
{
DependMsg alldependmsg = new DependMsg();
alldependmsg.selfbag = kv.Key;
alldependmsg.depends = kv.Value;
alldependlist.depsmap.Add(alldependmsg);
}
foreach (KeyValuePair<string, List<string>> kv in allbedependdict)
{
BeDependMsg allbedependmsg = new BeDependMsg();
allbedependmsg.selfbag = kv.Key;
allbedependmsg.bedepends = kv.Value;
allbedependlist.bedepsmap.Add(allbedependmsg);
}
if (!File.Exists(ABConfig.alldependpath))
{
File.Create(ABConfig.alldependpath).Dispose();
}
json = JsonUtility.ToJson(alldependlist, true);
File.WriteAllText(ABConfig.alldependpath, json);
if (!File.Exists(ABConfig.allbedependpath))
{
File.Create(ABConfig.allbedependpath).Dispose();
}
json = JsonUtility.ToJson(allbedependlist, true);
File.WriteAllText(ABConfig.allbedependpath, json);
#endregion
main.Unload(true);
}
/// <summary>
/// 最终遍历,设置包名
/// </summary>
/// <param name="folder"></param>
/// <param name="bagname"></param>
/// <param name="assetsfilepath"></param>
/// <param name="check">是否要检测待打包表</param>
private static bool FinalSetName(DirectoryInfo folder,string newbagname,StringBuilder md5,bool check=true)
{
if (check)
{
//将自己被依赖的和依赖的全添加到待打包表中
List<string> bedeps;
if (allbedependdict.TryGetValue(newbagname, out bedeps))
{
for (int i = 0; i < bedeps.Count; i++)
{
if (!addbagdict.ContainsKey(bedeps[i]))
{
addbagdict.Add(bedeps[i], 0);
}
}
}
List<string> deps;
if (alldependdict.TryGetValue(newbagname, out deps))
{
for (int i = 0; i < deps.Count; i++)
{
if (!addbagdict.ContainsKey(deps[i]))
{
addbagdict.Add(deps[i], 0);
}
}
}
}
int checknum = 0;
//最终遍历该AB包的所有文件,并更新AB校验文件中的信息
foreach (FileInfo tar in folder.GetFiles())
{
//todo 排除某些文件
if (tar.Extension.Equals(".meta"))
{
continue;
}
md5.Clear();
//如果dict中有当前文件的记录,进行比较
if (filedict.ContainsKey(tar.FullName))
{
md5.Append(filedict[tar.FullName]);
//如果MD5一样,说明该文件没有被修改
if (ToolVerify.Compare(tar.FullName,md5))
{
//设置包名
DealSingleABName(tar, newbagname, md5);
}
//如果该文件被修改了
else
{
//设置包名
DealSingleABName(tar, newbagname, md5);
//更新校验文件中该文件的md5
filedict[tar.FullName] = md5.ToString();
}
}
//如果dict中没有当前文件的记录,添加信息
else
{
//设置包名
DealSingleABName(tar, newbagname, md5);
//添加当前文件的校验信息
filedict.Add(tar.FullName, ToolVerify.GetFileMD5(tar.FullName));
}
checknum++;
}
//可能没文件
if (checknum == 0) return false;
//更新当前包的文件个数
if (bagdict.ContainsKey(newbagname))
{
if (bagdict[newbagname] != checknum) bagdict[newbagname] = checknum;
}
else
{
bagdict.Add(newbagname, checknum);
}
//登记打包
builtbagdict.Add(newbagname, 0);
return true;
}
}