U3D客户端框架(资源管理篇)之自动化打Assetbundle包管理器

news2024/12/29 8:28:38

一、AssetBundle介绍

AssetBundle是将资源使用Unity提供的一种用于存储资源的压缩格式打包后的集合,它可以存储任何一种Unity可以识别的资源,如模型,纹理图,音频,场景等资源。也可以加载开发者自定义的二进制文件。他们的文件类型是.assetbundle/.unity3d,他们先前被设计好,很容易就下载到我们的游戏或者场景当中。

一般情况下AssetBundle的具体开发流程如下:
(1)创建Asset bundle,开发者在unity编辑器中通过脚本将所需要的资源打包成AssetBundle文件。
(2)上传服务器。开发者将打包好的AssetBundle文件上传至服务器中。使得游戏客户端能够获取当前的资源,进行游戏的更新。
(3)下载AssetBundle,首先将其下载到本地设备中,然后再通过AsstBudle的加载模块将资源加到游戏之中。
(4)加载,通过Unity提供的API可以加载资源里面包含的模型、纹理图、音频、动画、场景等来更新游戏客户端。
(5)卸载AssetBundle,卸载之后可以节省内存资源,并且要保证资源的正常更新。

二、AssetBundle多平台打包

2.1使用Unity自带的编辑器打包AssetBundle

(1)只有在Asset窗口中的资源才可以打包,我们单击GameObject->Cube,然后在Asset窗口创建一个预设体,命名为cubeasset,讲Cube拖到该预设体上。

(2)单击刚创建的预制件cubeasset,在编辑器界面右下角的属性窗口底部有一个名为”AssetBundle”的创建工具。接下来创建即可,空的可以通过单击菜单选项”New…”来创建,将其命名为”cubebundle”,名称固定为小写,如果使用了大写字母之后,系统会自动转换为小写格式。
在这里插入图片描述

2.2 使用自定义的打包器 打包AssetBundle

我们使用的是自定义的Assetbundle资源打包器,没有使用unity自带的那个打标签的那种方式。因为使用自定义打包管理器方式有如下优点:1.对AssetBundle打包功能更方便拓展;2.可控性跟强一些;3.效率更高打包速度更快;
文件夹是否为一个资源包:如果勾选一个文件打包成一个ab包;否则每个文件打包成一个ab包
是否是初始资源:在文件
是否加密:
在这里插入图片描述

三、代码实现

代码实现部分是按照流程去讲的。很细致末节的函数这里不会写出来,在完整代码部分可以找到这些引用到的但是又没有写出来的函数。主要会详细讲解每一个步骤的做的事情。会详细讲这个流程/这个函数,是做什么的,做了什么,函数的功能。

1.缓存打包资源信息 BuildAssetBundleForPath

函数主要功能:添加打包资源信息(设置:包名、后缀、变体名。把打包信息缓存下来);根据编辑器内的设置,决定一个文件夹打成一个包,还是每个文件打成一个包,在设置打包文件的时候过滤掉所有的Meta文件;可以根据项目的需求决定是否要设置包的变体:变体的目的主要是为了作用之一是为了在低中高端机器上加载不同的包,以达到优化资源的效果;重新设置路径:让文件从Assets/目录开始(unity API打包需要),最后把过滤后的文件信息添加到编译链表里。

  private void BuildAssetBundleForPath(string path, bool overall)
    {
        //拼接完整路径
        string fullPath = string.Format("{0}/{1}", Application.dataPath, path);

        //1.拿到文件夹下的所有文件
        DirectoryInfo directoryInfo = new DirectoryInfo(fullPath);

        //拿到文件夹下所有文件
        FileInfo[] arrFiles = directoryInfo.GetFiles("*", SearchOption.AllDirectories);

        if (overall)
        {
            //打成一个资源包
            AssetBundleBuild build = new AssetBundleBuild();

            //ab name = 相对路径.ab
            build.assetBundleName = path + ".ab";
 
            //build.assetBundleVariant = "y";

            //过滤掉meta文件;
            //把目录更改成相对目录:以Assets开始(可能是unity打包流水线的要求)
            string[] arr = GetValidateFiles(arrFiles);
            build.assetNames = arr;
            builds.Add(build);
        }
        else
        {
            //每个文件打成一个包
            string[] arr = GetValidateFiles(arrFiles);

            for (int i = 0; i < arr.Length; ++i)
            {
                AssetBundleBuild build = new AssetBundleBuild();
                build.assetBundleName = arr[i].Substring(0, arr[i].LastIndexOf(".")).Replace("Assets/", "") + ".ab";
                //build.assetBundleVariant = "y";
                build.assetNames = new string[] { arr[i] };

                //add到builds里面
                builds.Add(build);
            }
        }
    }

2.使用Unity API 打包资源 BuildAssetBundles

根据用户在编辑器内的设置,把路径信息、打包信息、AssetBundle打包设置、打包平台,传至UnityAPI,然后调用BuildPipeline.BuildAssetBundles 函数进行打包。

//调用unity 自带的API 把builds里的路径都打包放到TempPath里 
BuildPipeline.BuildAssetBundles(TempPath, builds.ToArray(), Options, GetBuildTarget());

3.拷贝文件 临时目录拷贝到正式目录 CopyFile

用户的输入总是不安全的,不能让用户指定最终保存的目录和文件名,所以首先得找个地方存,然后通过代码代码里指定目标路径和文件名,所以最终总是要有一个move的操作。

    private void CopyFile(string oldPath)
    {
        //如果输出目录存在,先删掉
        if (Directory.Exists(OutPath))
        {
            Directory.Delete(OutPath, true);
        }

        //从临时目录拷贝到正式目录
        IOUtil.CopyDirectory(oldPath, OutPath);

        //拿到文件夹信息
        DirectoryInfo directoryInfo = new DirectoryInfo(OutPath);

        //拿到文件夹下的所有文件,搜索.y文件
        FileInfo[] arrFiles = directoryInfo.GetFiles("*.ab", SearchOption.AllDirectories);

        int len = arrFiles.Length;

        for (int i = 0; i < len; ++i)
        {
            //C#没有直接修改文件名字的方法,只能通过file.move去进行移动式修改
            FileInfo fileInfo = arrFiles[i];
            File.Move(fileInfo.FullName, fileInfo.FullName.Replace(".ab", ".assetbundle"));
        }
    }

4.加密资源包 AssetBundleEncrypt

根据用户在编辑器内的设置,决定这个资源包是否要加密。又分为加密文件夹和文件,加密文件夹时会拿到加该目录内的所有函数,最终还是会调用加密单文件函数。然后读取出来文件流(bytes数组),对文件流使用异或因子进行异或加密,把加密过的文件流数据重新写入文件,加密完成。

private void AssetBundleEncrypt()
    {
        int len = Datas.Length;

        for (int i = 0; i < len; ++i)
        {
            AssetBundleData assetBundleData = Datas[i];
         
            if (assetBundleData.IsEncrypt)
            {
                
                for (int j = 0; j < assetBundleData.Path.Length; ++j)
                {
                    string path = OutPath + "/" + assetBundleData.Path[j];

                    //打成一个包
                    if (assetBundleData.Overall)
                    {
                        //不是遍历文件夹打包,这个路径就是一个包
                        path = path + ".assetbundle";
                        AssetBundleEncryptFile(path);
                    }
                    else
                    {
                        AssetBundleEncryptFolder(path);
                    }
                }
            }
        }
    }

5.生成依赖关系文件 CreateDependenciesFile

函数主要作用是缓存好资源包的依赖关系,加载时候可根据文件内的配置决定先后加载顺序。最终会生成两个文件:json文件:会记录{文件信息:依赖列表{a,b,c}};这个主要是用于web访问的时候使用,直接通过http获取文件依赖信息,也方便调试的时候查看依赖信息;bytes二进制文件:这个的主要作用是放到本地,在比对资源的时候加载。

   private void CreateDependenciesFile()
    {
        //第一次循环   把所有的Asset存储到一个列表里
        //临时列表
        List<AssetEntity> lstTemp = new List<AssetEntity>();

        //循环设置文件夹包括子文件里面的项
        int len = Datas.Length;
        for (int i = 0; i < len; ++i)
        {
            AssetBundleData assetBundleData = Datas[i];

            for (int j = 0; j < assetBundleData.Path.Length; ++j)
            {
                //assets为根目录
                string path = Application.dataPath + "/" + assetBundleData.Path[j];

                //把所有的文件存到一个链表中
                CollectFileInfo(lstTemp, path);
            }
        }

        //获取临时链表的长度
        len = lstTemp.Count;

        //资源链表,会遍历临时list,然后在临时链表的基础上 加上这个文件的依赖信息
        List<AssetEntity> assetList = new List<AssetEntity>();
        for (int i = 0; i < len; ++i)
        {
            AssetEntity entity = lstTemp[i];

            //new一个新的AssetEntity
            AssetEntity newEntity = new AssetEntity();
            newEntity.Category = entity.Category;

            //找到最后一个/,因为最后一个/后面就是Asset的名字
            newEntity.AssetName = entity.AssetFullName.Substring(entity.AssetFullName.LastIndexOf("/") + 1);

            //去掉拓展名
            int iLastIndexOf = newEntity.AssetName.LastIndexOf(".");
            if (iLastIndexOf > -1)
            {
                newEntity.AssetName = newEntity.AssetName.Substring(0, newEntity.AssetName.LastIndexOf("."));
            } 

            newEntity.AssetFullName = entity.AssetFullName;
            newEntity.AssetBundleName = entity.AssetBundleName;

            assetList.Add(newEntity);

            //场景不需要检查依赖项
            if (entity.Category == AssetCategory.Scenes)
                continue;
                                                                                                                                                     
            newEntity.ListDependsAsset = new List<AssetDependsEntity>();
            //string fullAssetPath = entity.AssetFullName;
            string[] arr = AssetDatabase.GetDependencies(entity.AssetFullName);
            foreach (string str in arr)
            {
                //依赖的assetbundle是AssetFullName,并且是Asset
                if (!str.Equals(newEntity.AssetFullName, StringComparison.CurrentCultureIgnoreCase) &&
                    IsContainAssetList(lstTemp, str))
                {
                    AssetDependsEntity assetDepend = new AssetDependsEntity();
                    assetDepend.Category = GetAssetCategory(str);
                    assetDepend.AssetFullName = str;

                    //把依赖的资源 加入到资源依赖列表中
                    newEntity.ListDependsAsset.Add(assetDepend);
                }
            }
        }


        //生成一个json文件
        string targetPath = OutPath;
        if (!Directory.Exists(targetPath))
            Directory.CreateDirectory(targetPath);

        //版本文件路径+AssetInfo.json
        string strJsonFilePath = targetPath + "/AssetInfo.json";
        IOUtil.CreateTextFile(strJsonFilePath, LitJson.JsonMapper.ToJson(assetList));
        //生成log
        Debug.Log("生成 AssetInfo.json完毕");

        //生成二进制数据
        MMO_MemoryStream ms = new MMO_MemoryStream();

        //assetList的长度
        len = assetList.Count;
        ms.WriteInt(len);

        for (int i = 0; i < len; ++i)
        {
            AssetEntity entity = assetList[i];
            ms.WriteByte((byte)entity.Category);
            ms.WriteUTF8String(entity.AssetFullName);
            ms.WriteUTF8String(entity.AssetBundleName);

            if (entity.ListDependsAsset != null)
            {
                //添加依赖资源
                //获取依赖资源数量
                int depLen = entity.ListDependsAsset.Count;
                for (int j = 0; j < depLen; ++j)
                {
                    //依赖资源的信息
                    AssetDependsEntity assetDependsEntity = entity.ListDependsAsset[j];
                    ms.WriteByte((byte)assetDependsEntity.Category);
                    ms.WriteUTF8String(assetDependsEntity.AssetFullName);
                }
            }
            else
            {
                ms.WriteInt(0);
            }

            string filePath = targetPath + "/AssetInfo.bytes";
            byte[] buffer = ms.ToArray();
            buffer = ZlibHelper.CompressBytes(buffer);
            FileStream fs = new FileStream(filePath, FileMode.Create);
            fs.Write(buffer, 0, buffer.Length);
            fs.Close();
            fs.Dispose();
            Debug.Log("生成AssetInfo.bytes文件 完毕");
        }
    }

6.生成版本文件 CreateVersionFile

收集资源包信息,把资源包的名字、资源名MD5、字节大小、是否是母包资源、是否加密信息写成一行数据写入到版本文件中。在资源热更时需要使用到资源包的MD5和CDN上的文件MD5做一致性匹配,如果不匹配会优先使用CDN中的资源下载到本地,所以版本文件是必须需要的文件,在打包时候就会生成,然后写入到打包目录中。

private void CreateVersionFile()
    {
        string path = OutPath;
        if (!Directory.Exists(path))
        {
            Directory.CreateDirectory(path);
        }
        
        //拼接版本文件路径
        string strVersionFilePath = path + "/VersionFile.txt";

        //如果存在版本文件,则删除
        IOUtil.DeleteFile(strVersionFilePath);

        StringBuilder sbContent = new StringBuilder();

        DirectoryInfo directoryInfo = new DirectoryInfo(path);

        //拿到文件夹下所有文件
        FileInfo[] arrFiles = directoryInfo.GetFiles("*",SearchOption.AllDirectories);

        //开始append信息
        //append资源版本信息
        sbContent.AppendLine(this.ResourceVersion);

        //1.循环所有可写区的文件,保存文件信息到sbContent
        for (int i = 0; i < arrFiles.Length; ++i)
        {
            FileInfo file = arrFiles[i];

            //manifest文件跳过
            if (file.Extension == ".manifest")
                continue;

            //包路径的全名
            string fullName = file.FullName;

            //相对路径
            //在打包路径下,搜索当前平台的路径
            //去除掉IOS/、Android/、Windows/ 路径字符串
            string name = fullName.Substring(fullName.IndexOf(CurrBuildTarget.ToString()) + CurrBuildTarget.ToString().Length + 1);

            //计算文件完整路径的md5
            string md5 = EncryptUtil.Md5(fullName);

            if (null == md5)
                continue;

            //计算文件大小(字节)
            string size = file.Length.ToString();

            //是否是初始化数据
            bool isFirstData = false;

            //该文件是否加密
            bool isEncrypt = false;

            //break标记
            bool isBreak = false;

            for (int j = 0; j < Datas.Length; ++j)
            {
                foreach (string tempPath in Datas[j].Path)
                {
                    //\\ rep to /
                    name = name.Replace("\\","/");
                    
                    //在相对平台名字里面,找资源看路径
                    //拿到打包信息
                    if (name.IndexOf(tempPath, StringComparison.CurrentCultureIgnoreCase) != -1)
                    {
                        isFirstData = Datas[j].IsFirstData;
                        isEncrypt = Datas[j].IsEncrypt;
                        
                        //如果在打包配置中找到了配置的文件信息,获取完有用的信息后,就跳出内两层循环
                        isBreak = true;
                    }
                }
                
                //在打包配置中找到了这个文件的配置信息
                if (isBreak)
                    break;
            }

            //format assetbundle文件信息
            string strLine = string.Format("{0}|{1}|{2}|{3}|{4}", name, md5, size, isFirstData ? 1 : 0,isEncrypt?1:0) ;
            sbContent.AppendLine(strLine);        
        }

        //2.循环完成,已经保存完所有文件的 assetName、md5、size、isFirstData、isEncryptData
        //创建文件,把所有的文件信息写入VersionFile.txt文件中
        IOUtil.CreateTextFile(strVersionFilePath,sbContent.ToString());

        MMO_MemoryStream ms = new MMO_MemoryStream();
        
        //删除掉多余的空格
        string str = sbContent.ToString().Trim();

        //使用\n 切割内容到一个数组里
        string[] arr = str.Split('\n');

        int len = arr.Length;

        //长度信息写入内存流
        ms.WriteInt(len);

        //循环保存
        for (int i = 0; i < len; ++i)
        {
            //第一个是版本信息 单独处理
            if (0 == i)
            {
                ms.WriteUTF8String(arr[i]);
            }
            else
            {
                //其他的都是文件信息,再次分割,拿到具体信息
                string[] arrInner = arr[i].Split('|');
                string name= arrInner[0];
                string md5= arrInner[1];
                ulong size = ulong.Parse(arrInner[2]);
                byte isFirstData = byte.Parse(arrInner[3]);
                byte isEncrypt = byte.Parse(arrInner[4]);

                //写成二进制数据
                ms.WriteUTF8String(name);
                ms.WriteUTF8String(md5);
                ms.WriteULong(size);
                ms.WriteByte(isFirstData);
                ms.WriteByte(isEncrypt);
            }
        }
        
        //版本文件路径
        string filePath = path + "/VersionFile.bytes";
        
        //拿到字节数组
        byte[] buffer = ms.ToArray();
        ms.Dispose();
        ms.Close();

        //对字节数组压缩
        buffer = ZlibHelper.CompressBytes(buffer);

        //将压缩过的二进制数据流,写入文件
        using (FileStream fs = new FileStream(filePath, FileMode.Create))
        {
            //写入文件
            fs.Write(buffer,0,buffer.Length);

            //关闭文件占用
            fs.Close();

            //释放fs内的资源
            fs.Dispose();
        }
    }

完整代码

[CreateAssetMenu]
class AssetBundleSetting : SerializedScriptableObject
{
    //必须加上可序列化标记
    [Serializable]
    public class AssetBundleData
    {
        //assetBundle的名称
        [LabelText("名称")]
        public string Name;

        //是否把文件夹打包成一个资源包(Overall:总的,全面的;又或者说 打包的这个是不是一个文件夹)
        [LabelText("文件夹为一个资源包")]
        public bool Overall;
         
        //这个assetbundle是否是初始资源
        [LabelText("是否是初始资源")]
        public bool IsFirstData;

        //是否加密(用啥加密算法)
        [LabelText("是否加密")]
        public bool IsEncrypt;

        //资源根节点路径(一个目录打多个包)
        [FolderPath(ParentFolder = "Assets")]
        public string[] Path;
    }

    //自定义打包平台
    //只支持打包自定义的这些平台
    public enum CusBuildTarget
    {
        Windows,
        Android,
        IOS,
    }

    //资源版本号
    [HorizontalGroup("Common", LabelWidth = 70)]
    [VerticalGroup("Common/Left")]
    [LabelText("资源版本号")]
    public string ResourceVersion = "1.0.1";


    //打包平台枚举
    [PropertySpace(10)]
    [VerticalGroup("Common/Left")]
    [LabelText("目标平台")]
    public CusBuildTarget CurrBuildTarget;

    //参数设置
    [PropertySpace(10)]
    [VerticalGroup("Common/Left")]
    [LabelText("参数")]
    public BuildAssetBundleOptions Options;

    //资源包保存路径
    [LabelText("资源包保存路径")]
    [FolderPath]
    public string AssetBundleSavePath;

    //编辑开关
    [LabelText("勾选进行编辑")]
    public bool IsCanEditor;

    //assetBundle设置
    [EnableIf("IsCanEditor")]
    [BoxGroup("AssetBundleSettings")]
    public AssetBundleData[] Datas;

    //要收集的资源包
    List<AssetBundleBuild> builds = new List<AssetBundleBuild>();

    #region 临时变量
    //临时目录
    public string TempPath
    {
        get
        {
            return Application.dataPath + "/../" + AssetBundleSavePath + "/" + ResourceVersion + "_Temp/" + CurrBuildTarget;
        }
    }

    //输出目录(就是临时目录去掉temp)
    public string OutPath
    {
        get
        {
            return TempPath.Replace("_Temp", "");
        }
    }

    #endregion

    public BuildTarget GetBuildTarget()
    {
        switch (CurrBuildTarget)
        {
            default:
            case CusBuildTarget.Windows:
                return BuildTarget.StandaloneWindows;
            case CusBuildTarget.Android:
                return BuildTarget.Android;
            case CusBuildTarget.IOS:
                return BuildTarget.iOS;
        }
    }


    //更新版本号(点击之后版本号+1)
    [VerticalGroup("Common/Right")]
    [Button(ButtonSizes.Medium)]
    [LabelText("更新版本号")]
    public void UpdateResourceVersion()
    {
        //拿到完整版本字符串
        //分割成三部分
        string version = ResourceVersion;
        string[] arr = version.Split('.');

        int shortVersion = 0;
        //拿到第三个参数,解析出来,转换成int类型
        int.TryParse(arr[2], out shortVersion);
        version = string.Format("{0}.{1}.{2}", arr[0], arr[1], ++shortVersion);
        ResourceVersion = version;
    }

    #region 打包函数

    //验证文件
    //这个相当于一个过滤器函数:1.过滤掉meta文件;2.让文件从Assets/目录开始(unity API打包需要)
    private string[] GetValidateFiles(FileInfo[] arrFiles)
    {
        List<string> lst = new List<string>();
        int iLen = arrFiles.Length;

        for (int i = 0; i < iLen; ++i)
        {
            FileInfo file = arrFiles[i];
            if (!file.Extension.Equals(".meta", StringComparison.CurrentCultureIgnoreCase))
            {
                //1.先把\\替换成/
                //2.把dataPath删除 D:/XXX/Assets/ 删除这个路径
                //3.拼接上Assets
                lst.Add("Assets" + file.FullName.Replace("\\", "/").Replace(Application.dataPath, ""));
            }
        }
        return lst.ToArray();
    }

    //加密文件
    private void AssetBundleEncryptFile(string filePath, bool isDelete = false)
    {
        FileInfo fileInfo = new FileInfo(filePath);
        byte[] buffer = null;

        //打开文件
        //拿到字节流(文件字节数据)
        using (FileStream fs = new FileStream(filePath, FileMode.Open))
        {
            buffer = new byte[fs.Length];
            fs.Read(buffer, 0, buffer.Length);
        }

        //对数据进行Xor运算
        buffer = SecurityUtil.Xor(buffer);

        //重新把字节流数据进文件(二进制流数据)
        using (FileStream fs = new FileStream(filePath, FileMode.Create))
        {
            fs.Write(buffer, 0, buffer.Length);
            fs.Flush();
        }
    }

    //加密文件夹下所有文件
    private void AssetBundleEncryptFolder(string folderPath, bool isDelete = false)
    {
        //文件夹信息
        DirectoryInfo directoryInfo = new DirectoryInfo(folderPath);

        //拿到文件夹下的所有文件信息
        FileInfo[] arrFiles = directoryInfo.GetFiles("*", SearchOption.AllDirectories);

        foreach (FileInfo fileInfo in arrFiles)
        {
            AssetBundleEncryptFile(fileInfo.FullName, isDelete);
        }
    }

    //获取资源分类(自定义的资源分类)
    private AssetCategory GetAssetCategory(string filePath)
    {
        AssetCategory category = AssetCategory.None;

        if (filePath.IndexOf("Reporter", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.Reporter;
        }
        else if (filePath.IndexOf("Audio", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.Audio;
        }
        else if (filePath.IndexOf("CusShaders", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.CusShaders;
        }
        else if (filePath.IndexOf("DataTable", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.DataTable;
        }
        else if (filePath.IndexOf("EffectSources", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.EffectSources;
        }
        else if (filePath.IndexOf("RoleEffectPrefab", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.RoleEffectPrefab;
        }
        else if (filePath.IndexOf("UIEffectPrefab", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.UIEffectPrefab;
        }
        else if (filePath.IndexOf("RolePrefab", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.RolePrefab;
        }
        else if (filePath.IndexOf("RoleSources", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.RoleSources;
        }
        else if (filePath.IndexOf("Scenes", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.Scenes;
        }
        else if (filePath.IndexOf("UIFont", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.UIFont;
        }
        else if (filePath.IndexOf("UIPrefab", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.UIPrefab;
        }
        else if (filePath.IndexOf("UIRes", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.UIRes;
        }
        else if (filePath.IndexOf("xLuaLogic", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.xLuaLogic;
        }

        return category;
    }

    //获取资源包的名称
    private string GetAssetBundleName(string newPath)
    {
        // \\ -> /
        string path = newPath.Replace("\\", "/");

        int len = Datas.Length;

        for (int i = 0; i < len; ++i)
        {
            AssetBundleData assetBundleData = Datas[i];

            for (int j = 0; j < assetBundleData.Path.Length; ++j)
            {
                //path里包含 assetBundleData.path 吗
                if (path.IndexOf(assetBundleData.Path[j], StringComparison.CurrentCultureIgnoreCase) > -1)
                {
                    if (assetBundleData.Overall)
                    {
                        return assetBundleData.Path[j].ToLower();
                    }
                    else
                    {
                        return path.Substring(0, path.LastIndexOf('.')).ToLower().Replace("assets/", "");
                    }
                }
            }
        }
        return null;
    }

    //收集文件信息
    private void CollectFileInfo(List<AssetEntity> lstTemp, string folderPath)
    {
        DirectoryInfo directoryInfo = new DirectoryInfo(folderPath);

        //拿到文件夹下的所有文件
        FileInfo[] arrFiles = directoryInfo.GetFiles("*", SearchOption.AllDirectories);
        for (int i = 0; i < arrFiles.Length; ++i)
        {
            FileInfo file = arrFiles[i];
            if (file.Extension == ".meta")
                continue;

            //拿到完整路径
            string filePath = file.FullName;//全名

            //找到asset\\的开始位置
            int idx = filePath.IndexOf("Assets\\", StringComparison.CurrentCultureIgnoreCase);

            //删除Assets\\ 前面的路径
            //拿到新路径
            string newPath = filePath.Substring(idx);
            if (newPath.IndexOf(".idea") != -1)
                continue;

            AssetEntity entity = new AssetEntity();
            
            entity.AssetFullName = newPath.Replace("\\", "/");

            entity.Category = GetAssetCategory(newPath.Replace(file.Name, ""));

            entity.AssetFullName = GetAssetBundleName(newPath);

            //push到临时链表里去
            lstTemp.Add(entity);
        }
    }

    //判断某个资源是否存在于资源列表中
    private bool IsContainAssetList(List<AssetEntity> lstTemp, string assetFullName)
    {
        int len = lstTemp.Count;
        for (int i = 0; i < len; ++i)
        {
            AssetEntity entity = lstTemp[i];
            if (entity.AssetFullName.Equals(assetFullName, StringComparison.CurrentCultureIgnoreCase))
            {
                return true;
            }
        }
        return false;
    }


    //步骤:

    /*
     * 函数功能:
     * 1.添加打包资源信息(设置:包名、后缀、变体名。把打包信息缓存下来)
     * path:资源相对路径
     * overall:达成一个资源包
     */
    private void BuildAssetBundleForPath(string path, bool overall)
    {
        //拼接完整路径
        string fullPath = string.Format("{0}/{1}", Application.dataPath, path);

        //1.拿到文件夹下的所有文件
        DirectoryInfo directoryInfo = new DirectoryInfo(fullPath);

        //拿到文件夹下所有文件
        FileInfo[] arrFiles = directoryInfo.GetFiles("*", SearchOption.AllDirectories);

        if (overall)
        {
            //打成一个资源包
            AssetBundleBuild build = new AssetBundleBuild();

            //ab name = 相对路径.ab
            build.assetBundleName = path + ".ab";

            //过滤掉meta文件;
            //把目录更改成相对目录:以Assets开始(可能是unity打包流水线的要求)
            string[] arr = GetValidateFiles(arrFiles);
            build.assetNames = arr;
            builds.Add(build);
        }
        else
        {
            //每个文件打成一个包
            string[] arr = GetValidateFiles(arrFiles);

            for (int i = 0; i < arr.Length; ++i)
            {
                AssetBundleBuild build = new AssetBundleBuild();

                //里面拼接上了Asset
                //外面为啥要把Asset/去了
                //1.先把拓展名删除;2.删除Asset/前缀(估计是直接用文件名asset/a/c a/c作为包名);3.拓展名改成.ab
                build.assetBundleName = arr[i].Substring(0, arr[i].LastIndexOf(".")).Replace("Assets/", "") + ".ab";
                //build.assetBundleVariant = "y";
                build.assetNames = new string[] { arr[i] };

                //add到builds里面
                builds.Add(build);
            }
        }
    }


    //2.拷贝文件
    //从临时路径 拷贝文件 到正式目录 
    //顺便把ab.y 后缀改成.assetbundle
    private void CopyFile(string oldPath)
    {
        //如果输出目录存在,先删掉
        if (Directory.Exists(OutPath))
        {
            Directory.Delete(OutPath, true);
        }

        //从临时目录拷贝到正式目录
        IOUtil.CopyDirectory(oldPath, OutPath);

        //拿到文件夹信息
        DirectoryInfo directoryInfo = new DirectoryInfo(OutPath);

        //拿到文件夹下的所有文件,搜索.y文件
        FileInfo[] arrFiles = directoryInfo.GetFiles("*.ab", SearchOption.AllDirectories);

        int len = arrFiles.Length;

        for (int i = 0; i < len; ++i)
        {
            //C#没有直接修改文件名字的方法,只能通过file.move去进行移动式修改
            FileInfo fileInfo = arrFiles[i];
            File.Move(fileInfo.FullName, fileInfo.FullName.Replace(".ab", ".assetbundle"));
        }
    }

    //3.资源加密
    private void AssetBundleEncrypt()
    {
        int len = Datas.Length;

        for (int i = 0; i < len; ++i)
        {
            AssetBundleData assetBundleData = Datas[i];
            //加密想要机密的
            if (assetBundleData.IsEncrypt)
            {
                //加密想要加密的
                for (int j = 0; j < assetBundleData.Path.Length; ++j)
                {
                    string path = OutPath + "/" + assetBundleData.Path[j];

                    //打成一个包
                    if (assetBundleData.Overall)
                    {
                        //不是遍历文件夹打包,这个路径就是一个包
                        path = path + ".assetbundle";
                        AssetBundleEncryptFile(path);
                    }
                    else
                    {
                        AssetBundleEncryptFolder(path);
                    }
                }
            }
        }
    }


    private void CreateDependenciesFile()
    {
        //第一次循环   把所有的Asset存储到一个列表里
        //临时列表
        List<AssetEntity> lstTemp = new List<AssetEntity>();

        //循环设置文件夹包括子文件里面的项
        int len = Datas.Length;
        for (int i = 0; i < len; ++i)
        {
            AssetBundleData assetBundleData = Datas[i];

            for (int j = 0; j < assetBundleData.Path.Length; ++j)
            {
                //assets为根目录
                string path = Application.dataPath + "/" + assetBundleData.Path[j];

                //把所有的文件存到一个链表中
                CollectFileInfo(lstTemp, path);
            }
        }

        //获取临时链表的长度
        len = lstTemp.Count;

        //资源链表,会遍历临时list,然后在临时链表的基础上 加上这个文件的依赖信息
        List<AssetEntity> assetList = new List<AssetEntity>();
        for (int i = 0; i < len; ++i)
        {
            AssetEntity entity = lstTemp[i];

            //new一个新的AssetEntity
            AssetEntity newEntity = new AssetEntity();
            newEntity.Category = entity.Category;

            //找到最后一个/,因为最后一个/后面就是Asset的名字
            newEntity.AssetName = entity.AssetFullName.Substring(entity.AssetFullName.LastIndexOf("/") + 1);

            //去掉拓展名
            int iLastIndexOf = newEntity.AssetName.LastIndexOf(".");
            if (iLastIndexOf > -1)
            {
                newEntity.AssetName = newEntity.AssetName.Substring(0, newEntity.AssetName.LastIndexOf("."));
            }
            else
            {
                int x1 = 11;
            }

            newEntity.AssetFullName = entity.AssetFullName;
            newEntity.AssetBundleName = entity.AssetBundleName;

            assetList.Add(newEntity);

            //场景不需要检查依赖项
            if (entity.Category == AssetCategory.Scenes)
                continue;
                                                                                                                                                     
            newEntity.ListDependsAsset = new List<AssetDependsEntity>();
            //string fullAssetPath = entity.AssetFullName;
            string[] arr = AssetDatabase.GetDependencies(entity.AssetFullName);
            foreach (string str in arr)
            {
                //依赖的assetbundle是AssetFullName,并且是Asset
                if (!str.Equals(newEntity.AssetFullName, StringComparison.CurrentCultureIgnoreCase) &&
                    IsContainAssetList(lstTemp, str))
                {
                    AssetDependsEntity assetDepend = new AssetDependsEntity();
                    assetDepend.Category = GetAssetCategory(str);
                    assetDepend.AssetFullName = str;

                    //把依赖的资源 加入到资源依赖列表中
                    newEntity.ListDependsAsset.Add(assetDepend);
                }
            }
        }


        //生成一个json文件
        string targetPath = OutPath;
        if (!Directory.Exists(targetPath))
            Directory.CreateDirectory(targetPath);

        //版本文件路径+AssetInfo.json
        string strJsonFilePath = targetPath + "/AssetInfo.json";
        IOUtil.CreateTextFile(strJsonFilePath, LitJson.JsonMapper.ToJson(assetList));
        //生成log
        Debug.Log("生成 AssetInfo.json完毕");

        //生成二进制数据
        MMO_MemoryStream ms = new MMO_MemoryStream();

        //assetList的长度
        len = assetList.Count;
        ms.WriteInt(len);

        for (int i = 0; i < len; ++i)
        {
            AssetEntity entity = assetList[i];
            ms.WriteByte((byte)entity.Category);
            ms.WriteUTF8String(entity.AssetFullName);
            ms.WriteUTF8String(entity.AssetBundleName);

            if (entity.ListDependsAsset != null)
            {
                //添加依赖资源
                //获取依赖资源数量
                int depLen = entity.ListDependsAsset.Count;
                for (int j = 0; j < depLen; ++j)
                {
                    //依赖资源的信息
                    AssetDependsEntity assetDependsEntity = entity.ListDependsAsset[j];
                    ms.WriteByte((byte)assetDependsEntity.Category);
                    ms.WriteUTF8String(assetDependsEntity.AssetFullName);
                }
            }
            else
            {
                ms.WriteInt(0);
            }

            //生成AssetInfo.bytes文件
            string filePath = targetPath + "/AssetInfo.bytes";
            byte[] buffer = ms.ToArray();
            buffer = ZlibHelper.CompressBytes(buffer);
            FileStream fs = new FileStream(filePath, FileMode.Create);
            fs.Write(buffer, 0, buffer.Length);
            fs.Close();
            fs.Dispose();
            Debug.Log("生成AssetInfo.bytes文件 完毕");
        }
    }

    //5.生成版本文件
    private void CreateVersionFile()
    {
        string path = OutPath;
        if (!Directory.Exists(path))
        {
            Directory.CreateDirectory(path);
        }
        
        //拼接版本文件路径
        string strVersionFilePath = path + "/VersionFile.txt";

        //如果存在版本文件,则删除
        IOUtil.DeleteFile(strVersionFilePath);

        StringBuilder sbContent = new StringBuilder();

        DirectoryInfo directoryInfo = new DirectoryInfo(path);

        //拿到文件夹下所有文件
        FileInfo[] arrFiles = directoryInfo.GetFiles("*",SearchOption.AllDirectories);

        //开始append信息
        //append资源版本信息
        sbContent.AppendLine(this.ResourceVersion);

        //1.循环所有可写区的文件,保存文件信息到sbContent
        for (int i = 0; i < arrFiles.Length; ++i)
        {
            FileInfo file = arrFiles[i];

            //manifest文件跳过
            if (file.Extension == ".manifest")
                continue;

            //包路径的全名
            string fullName = file.FullName;

            //相对路径
            //在打包路径下,搜索当前平台的路径
            //去除掉IOS/、Android/、Windows/ 路径字符串
            string name = fullName.Substring(fullName.IndexOf(CurrBuildTarget.ToString()) + CurrBuildTarget.ToString().Length + 1);

            //计算文件完整路径的md5
            string md5 = EncryptUtil.Md5(fullName);

            if (null == md5)
                continue;

            //计算文件大小(字节)
            string size = file.Length.ToString();

            //是否是初始化数据
            bool isFirstData = false;

            //该文件是否加密
            bool isEncrypt = false;

            //break标记
            bool isBreak = false;

            for (int j = 0; j < Datas.Length; ++j)
            {
                foreach (string tempPath in Datas[j].Path)
                {
                    //\\ rep to /
                    name = name.Replace("\\","/");
                    
                    //在相对平台名字里面,找资源看路径
                    //拿到打包信息
                    if (name.IndexOf(tempPath, StringComparison.CurrentCultureIgnoreCase) != -1)
                    {
                        isFirstData = Datas[j].IsFirstData;
                        isEncrypt = Datas[j].IsEncrypt;
                        
                        //如果在打包配置中找到了配置的文件信息,获取完有用的信息后,就跳出内两层循环
                        isBreak = true;
                    }
                }
                
                //在打包配置中找到了这个文件的配置信息
                if (isBreak)
                    break;
            }

            //format assetbundle文件信息
            string strLine = string.Format("{0}|{1}|{2}|{3}|{4}", name, md5, size, isFirstData ? 1 : 0,isEncrypt?1:0) ;
            sbContent.AppendLine(strLine);        
        }

        IOUtil.CreateTextFile(strVersionFilePath,sbContent.ToString());

        MMO_MemoryStream ms = new MMO_MemoryStream();
        
        //删除掉多余的空格
        string str = sbContent.ToString().Trim();

        //使用\n 切割内容到一个数组里
        string[] arr = str.Split('\n');

        int len = arr.Length;

        //长度信息写入内存流
        ms.WriteInt(len);

        //循环保存
        for (int i = 0; i < len; ++i)
        {
            //第一个是版本信息 单独处理
            if (0 == i)
            {
                ms.WriteUTF8String(arr[i]);
            }
            else
            {
                //其他的都是文件信息,再次分割,拿到具体信息
                string[] arrInner = arr[i].Split('|');
                string name= arrInner[0];
                string md5= arrInner[1];
                ulong size = ulong.Parse(arrInner[2]);
                byte isFirstData = byte.Parse(arrInner[3]);
                byte isEncrypt = byte.Parse(arrInner[4]);

                //写成二进制数据
                ms.WriteUTF8String(name);
                ms.WriteUTF8String(md5);
                ms.WriteULong(size);
                ms.WriteByte(isFirstData);
                ms.WriteByte(isEncrypt);
            }
        }
        
        //版本文件路径
        string filePath = path + "/VersionFile.bytes";
        
        //拿到字节数组
        byte[] buffer = ms.ToArray();
        ms.Dispose();
        ms.Close();

        //对字节数组压缩
        buffer = ZlibHelper.CompressBytes(buffer);

        //将压缩过的二进制数据流,写入文件
        using (FileStream fs = new FileStream(filePath, FileMode.Create))
        {
            //写入文件
            fs.Write(buffer,0,buffer.Length);

            //关闭文件占用
            fs.Close();

            //释放fs内的资源
            fs.Dispose();
        }
    }

    #endregion


    //清空资源包
    [VerticalGroup("Common/Right")]
    [Button(ButtonSizes.Medium)]
    [LabelText("清空资源包")]
    public void ClearAssetBundle()
    {
        if (Directory.Exists(TempPath))
        {
            Directory.Delete(TempPath, true);
        }

        EditorUtility.DisplayDialog("", "清空完毕", "确定");
    }

    [VerticalGroup("Common/Right")]
    [Button(ButtonSizes.Medium)]
    [LabelText("打包")]
    public void BuildAssetBundle()
    {
        //每次打包前,先clear assetBundleBuild信息。防止打包两次
        builds.Clear();

        //拿到配置里的assetBundle的长度
        int len = Datas.Length;

        //打包
        //如果写了多路径,又写了Overall,配置中的路径会每个路径打一个包
        //如果写了多路径,没写Overall,会把这些路径里的所有的文件都打包成assetbundle包
        for (int i = 0; i < len; ++i)
        {
            AssetBundleData assetBundleData = Datas[i];
            int lenPath = assetBundleData.Path.Length;

            //一个设置,可以设置打多个包
            for (int j = 0; j < lenPath; ++j)
            {
                //打包路径/文件
                string path = assetBundleData.Path[j];

                //1.往builds里添加打包信息
                BuildAssetBundleForPath(path, assetBundleData.Overall);
            }
        }

        //如果不存在临时写入目录,就创建一个
        if (!Directory.Exists(TempPath))
        {
            Directory.CreateDirectory(TempPath);
        }

        if (builds.Count == 0)
        {
            Debug.Log("未找到需要打包的内容");
            return;
        }
        Debug.Log(" builds count:" + builds.Count);

        //2.调用unity 自带的API 把builds里的路径都打包放到TempPath里 
        BuildPipeline.BuildAssetBundles(TempPath, builds.ToArray(), Options, GetBuildTarget());
        Debug.Log("临时资源打包完毕");

        //3.拷贝文件
        CopyFile(TempPath);
        Debug.Log("文件拷贝到输出目录完毕");

        //4.使用异或因子加密资源包
        AssetBundleEncrypt();
        Debug.Log("资源包加密完毕");

        //5.生成依赖关系文件
        CreateDependenciesFile();
        Debug.Log("生成依赖关系文件完毕");
        
        //6.生成版本文件完毕
        CreateVersionFile();
        Debug.Log("生成版本文件完毕");
    }

}


使用到的插件

OdinInspector Odin编辑器插件:https://odininspector.com/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/138400.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CleanMyMacX4.12最新免费版苹果电脑系统优化清理软件

CleanMyMac X是一款Mac上优秀的系统清理工具&#xff0c;它可以帮助用户清理垃圾文件、清理缓存、清理日志文件、清理应用程序等&#xff0c;可以让Mac系统保持良好的运行状态&#xff0c;提高Mac的运行速度。CleanMyMac X还可以帮助用户清理垃圾文件、清理缓存、清理日志文件、…

PCB翘曲度标准是多少

PCB翘曲其实也是指电路板弯曲&#xff0c;是指原本平整的电路板&#xff0c;放置桌面时两端或中间出现在微微往上翘起&#xff0c;这种现象被业内人士称为PCB翘曲。 电路板翘曲度计算公式将电路板平放在桌面上&#xff0c;电路板的四个角着地&#xff0c;测量中间拱起的高度&am…

乐观的调优(插入排序希尔排序)

学习目标 写在前面 1.插入排序 2.插入排序实战 3.插入排序的实现 4.插入排序的效率 5.平均情况 6.希尔排序 7.希尔排序的实现 8.希尔排序的效率 9.总结 写在前面 之前我们衡量一个算法的效率时&#xff0c;都是着眼于它在最坏情况下需要多少步。原因很简单&#x…

PyQt6快速入门-开发环境搭建

PyQt6开发环境搭建 文章目录 PyQt6开发环境搭建1、软件安装2 、创建工程2.1 创建Qt工程2.2 创建Python工程2.3 PyQt6启动失败问题解决本文将介绍如何在Windows下创建PyQt6的开发环境。 1、软件安装 第一步,下载并安装Python环境 https://www.python.org/downloads/windows/或…

effective C++读书笔记3

目录 在资源管理中小心copy行为 在资源管理中提供对原始资源的访问 以独立语句将newed对象置入智能指针 这是effectiveC中第三章内容&#xff1a;资源管理 在资源管理中小心copy行为 不是所有资源都是heap-based&#xff0c;对那种资源而言&#xff0c;像auto_ptr和 tr1 :…

【C语言进阶】数据在内存的存储

作者:匿名者Unit 目录一.数据类型分类1.整形2.浮点型3.构造类型4.指针类型5.空类型二.整形的存储1.原、反、补码2.大小端存储三.浮点数的存储一.数据类型分类 根据不同的数据类型&#xff0c;在内存中的人存储方法也有所差异&#xff0c;所以我们先来介绍一下数据类型的分类。…

(二)uboot移植--从零开始自制linux掌上电脑(F1C200S)<嵌入式项目>

目录 一、前言 二、F1C200s上电启动顺序 三、前期准备 四、新建用户 五、交叉编译环境配置 六、uboot简介 七、uboot移植 &#x1f34f; uboot下载 &#x1f34f; uboot默认配置 &#x1f34f; uboot图形界面配置 &#x1f34f; uboot编译 &#x1f34f; 烧录bin…

Python-变量和简单数据类型

目录 1. 字符串 1.1. 修改字符串大小写 1.2. 合并字符串 1.3. 制表符与换行符 1.4. 删除空白 2. 数字 2.1. 使用 2.2. 使用str函数避免类型错误 2.3. 注释 2.4. Python之禅 1. 字符串 1.1. 修改字符串大小写 首字母大写&#xff1a;name.title()全部大写&#xf…

基于DDAUNet的CT食管肿瘤分割

摘要 在CT图像中手动或自动描绘食道肿瘤是非常具有挑战性的。这是由于肿瘤与邻近组织的对比度低,食道的解剖结构变化,以及偶尔存在异物(如喂食管)。 本文提出了一种基于卷积神经网络的全自动端到端食管肿瘤,本文所提出的网络称为扩张密集注意力网络,利用每个密集块中的空…

VScode代码片段尝试

最近开始尝试强迫自己使用 VSCode&#xff0c;毕竟 Webstorm 用了很多年了&#xff0c;依赖性比较严重。但是&#xff0c;最近 Webstorm 越来越难搞到免费永久合法使用方式了。 遂转向 VSCode 。 不过 VSCode 需要调教&#xff0c;不像 Webstorm 那么集成度比较高&#xff0c…

CalDAV网络服务器Baikal

什么是 Baikal &#xff1f; Baikal 是一个免费的开源自托管 CalDav 和 CardDav 服务器&#xff0c;适用于想要管理其数据并确保其数据是私有的用户。 Baikal和群晖套件中的 Synology Calendar 是类似的应用&#xff1b; 什么是 CalDav ? CalDav 是一种互联网标准和协议&…

Qt扫盲-QVector理论总结

QVector理论总结一、概述二、使用1. 声明初始化2. 获取元素和链表信息3. 常用操作4. 迭代Vector三、注意一、概述 QVector是Qt的泛型容器类之一。它将每一个元素存储在相邻的内存位置&#xff0c;并提供快速的基于索引的访问。 QList&#xff0c; QLinkedList&#xff0c; QVe…

eurake原理分析以及搭建

消费者随时能够监控到服务的状态 消费者如何获取服务提供者具体的信息&#xff1f; 1&#xff09;服务提供者启动时间eureka注册自己的信息 2&#xff09;eureka保存这些信息 3&#xff09;消费者根据服务名称向eureka拉取提供者的信息 如果有多个服务提供者&#xff0c;消费者…

VS代码生成工具ReSharper v2022.3官宣首发——支持C# 11

实质上&#xff0c;ReSharper特征可用于C#&#xff0c;VB.net&#xff0c;XML&#xff0c;Asp.net&#xff0c;XAML&#xff0c;和构建脚本。 使用ReSharper&#xff0c;你可以进行深度代码分析&#xff0c;智能代码协助&#xff0c;实时错误代码高亮显示&#xff0c;解决方案范…

网络OSI(七层模型)

OSIOSI是一个理论上的网络通信模型&#xff0c;而TCP/IP则是实际运行的网络协议。TCP/IPTCP/IP (传输控制协议/网际协议) 网络通信模型 以及一整个网络传输协议家族应用层应用层协议: FTP TFTP HTTP SNMP SMTP DNS Telnet表示层 数据格式化 代码转换 数据解密/加密会话层 解除或…

低代码如何构建响应式布局前端页面

“你开发的界面为啥在我的屏幕里这么小啊&#xff1f;” “这个界面为啥在我这里会出现横向滚动条啊&#xff1f;” 大家在进行前端界面开发时&#xff0c;有没有遇到这些类似的问题呢&#xff1f;又是如何解决的呢&#xff1f; 页面响应式 在进行项目交付的场景中&#xf…

JavaEE【Spring】:Spring AOP

文章目录一、概念1、定义2、作用二、Spring AOP1、AOP 组成① 切面&#xff08;Aspect&#xff09;② 连接点&#xff08;Join Point&#xff09;③ 切点&#xff08;Pointcut&#xff09;④ 通知&#xff08;Advice&#xff09;2、实现① 添加 AOP 框架支持② 定义切面和切点Ⅰ…

都有哪些好用的设备维护管理软件?这5款值得一试

都有哪些好用的设备维护管理软件&#xff1f; 速速点进来&#xff0c;这些超高性价比的设备维护管理App/软件不容错过&#xff01; 设备是工厂及企业生存的基础&#xff0c;设备的正常运作是保证工厂及企业存活的前提&#xff0c;而要保证设备的正常运作&#xff0c;就必须要…

NLP预训练小结-从词向量到BERT

图像预训练预训练首先是在图像领域广泛应用的。设计网络结构后&#xff0c;处理图像一般是CNN的多层叠加网络结构&#xff0c;先用训练集对网络预先训练&#xff0c;处理新任务时采取相同的网络结构&#xff0c;在比较浅的几层CNN结构&#xff0c;网络参数初始化的时候可以加载…

思科路由器DHCPv6中继服务配置

配置如下&#xff1a; Router>ena Router#conf t Router(config)#host R1 R1(config)#ipv6 unicast-routing R1(config)#service dhcp R1(config)#int g0/1 R1(config-if)#ipv6 add 2001:1::1/64 R1(config-if)#no sh R1(config-if)#exit R1(config)#ipv6 local …