unityAB包自动打包框架
新时空
2021年12月19日 17:03
收录于文集
共109篇

主要功能:

  1. 对项目中指定文件夹下的资源进行打包,并生成独立于manifest的打包结果文档(全依赖、全被依赖、包和文件等信息),加载包时可依据全依赖信息文档加载依赖包。这里的文档是针对manifest在一般增量打包时只会生成打出包的信息所采取的的处理。

  2. 支持增量打包。某资源的删减、添加、修改(MD5改变)都会导致该资源、该资源依赖的所有资源、该资源被依赖的所有资源被打包。

  3. 不用手动选择包,程序会分析出所需要打的包。分析规则如上。

注意事项:

  1. 文件夹的删除不会导致打包,必须完整打包。

  2. 添加文件夹时,如果不和已有文件产生依赖关系,正常打包;否则必须完整打包。

知识细节:

  1. DirectoryInfo folder = new DirectoryInfo() 路径不分大小写

  2. string.replace()会生成新的字符串

配置类

代码块
C#
自动换行
复制代码
/// <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;
}
复制成功

代码块
C#
自动换行
复制代码
/// <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> ();
}
复制成功

代码块
C#
自动换行
复制代码
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;
}
复制成功

代码块
C#
自动换行
复制代码
/// <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;
    }
}
复制成功

核心

代码块
C#
自动换行
复制代码
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;
    }
}
复制成功