游戏资源差异化热更新及加密全攻略(Assetbundle 打包 AssetBundle 加载 AssetBundle 加密)

游戏热更新资源加密的必要性

unity中资源热更新还是Assetbundle为主,资源使用越来越广泛,ab包里可以包含图片、视频或者脚本,都是游戏的知识财产,如果被破解者或者竞争对手解开,拿到里面的内容,对游戏是个很大的损失.

Ab资源被解密抓取,除了知识财产损失外,也会对游戏运营产生很大影响。比如游戏包内的活动资源,被提前解开剧透,会大大影响活动效果.另外资源如果被修改还会产生外挂效果.
在这里插入图片描述

上图就是修改了资源包的材质,达到了透视的效果.

游戏资源的加密以及差异化更新

首先准备一些资源,放在工程中.因为打包这种简单的东西,我就不和各位浪费时间了,直接截图给各位看一下.首先是文件夹的创建.

在这里插入图片描述

如果你有一个好的开发习惯,应该能知道这些文件夹的作用.

随便设置两个模型成为ab文件:

在这里插入图片描述

然后是打包的代码编写.首先需要一个PathConfig脚本,用来存放一些路径,文件名等等.目前因为比较简单,只放了一个打包的路径以及版本号,还有平台的路径:

 using System.Collections;
 using System.Collections.Generic;
 using UnityEditor;
 using UnityEngine;
 
 public static class PathConfig 
 {
 
     public static readonly string localUrl = Application.persistentDataPath;

    //unity打包的地址
    public static readonly string buildAssetPath = Application.streamingAssetsPath;

    //版本号 
    public static string ProductVersion = "V1.0";



#if UNITY_EDITOR
   /// <summary>
    /// 构建的路径
    /// </summary>
   /// <param name="buildTarget">构建的平台</param>
   /// <returns></returns>
    public static string GetBuildTargetPath(BuildTarget buildTarget)
    {
        var version = ProductVersion;
        switch (buildTarget)
        {
            case BuildTarget.iOS:
                return "IOS/" + version;
            case BuildTarget.Android:
                return "Android/" + version;
            case BuildTarget.WebGL:
                return "WebGL/" + version;
            default:
                return "Others/" + version;
        }
    }
#endif
}

下面在编写一个可以打包的代码,因为太过简单,所以这里不做解释

 using System.Collections;
 using System.Collections.Generic;
 using System.IO;
 using UnityEditor;
 using UnityEngine;
 
 public class CreatAssetBundle : EditorWindow
 {
     //构建的路径
    public static string GetAssetBundlePath(BuildTarget buildTarget)
    {
        var path = PathConfig.buildAssetPath + "/" + PathConfig.GetBuildTargetPath(buildTarget) + "/";

        //当在硬盘目录结构里不存在该路径时,创建文件夹
        if (!Directory.Exists(path))
        {
            Directory.CreateDirectory(path);
        }
        return path;
    }

    [MenuItem("构建AB/构建Windows平台")]
    public static void BuildAB()
    {
        BuildBundle(BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows);
        Debug.Log("bundle完成....");
    }

    private static void BuildBundle(BuildAssetBundleOptions bundleOptions, BuildTarget buildTarget)
    {
        //包裹存储的路径...
        string outputPath = GetAssetBundlePath(EditorUserBuildSettings.activeBuildTarget);
        if (!Directory.Exists(outputPath))
            Directory.CreateDirectory(outputPath);
        //打包过程..
        BuildPipeline.BuildAssetBundles(outputPath, bundleOptions, buildTarget);

        Debug.Log("打包完成!位置: " + outputPath);
        AssetDatabase.Refresh();

    }
}

这时,在菜单栏打包即可发现,打包成功了.成功如下图所示:
在这里插入图片描述
当然,这个从本地加载也是没有任何问题的.

我们来写个代码加载一下.我们先写一个加载的管理类,然后设置成单例,这样就可以在任何地方任何环境下加载.不用继承自Mono.

首先是加载类:

 using System.Collections;
 using System.Collections.Generic;
 using System.IO;
 using UnityEngine;
 
 public class AssetBundleLoaderMgr  
 {
     //首先是依赖文件
     private AssetBundleManifest m_manifest;
    //bundle的缓存
    private Dictionary<string, AssetBundle> m_abDic = new Dictionary<string, AssetBundle>();
    private static AssetBundleLoaderMgr s_instance;
    public static AssetBundleLoaderMgr instance
    {
        get
        {
            if (null == s_instance)
                s_instance = new AssetBundleLoaderMgr();
            return s_instance;
        }
    }
    //这里全局唯一,不能多次实例
    public void Init()
    {
        //先从本地加载,也就是StreamingAsset文件夹
        //string streamingAssetsAbPath = Path.Combine(PathConfig.localUrl, "Others/SamJanAsset_1.0/SamJanAsset_1.0"); 
        string streamingAssetsAbPath = Path.Combine(Application .streamingAssetsPath , "Others/V1.0/V1.0");
        Debug.Log(streamingAssetsAbPath);
        AssetBundle streamingAssetsAb = AssetBundle.LoadFromFile(streamingAssetsAbPath ); 
        m_manifest = streamingAssetsAb.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
    }


    public T LoadAsset<T>(string abName, string assetName) where T : Object
    {
        AssetBundle ab = LoadAssetBundle(abName);
        if (ab == null)
        {
            Debug.Log("加载名为: " + abName + " 的AB资源失败!");
            return null;
        }
        T t = ab.LoadAsset<T>(assetName);
        if (t == null)
        {
            Debug.Log("加载名为: " + assetName + " 的预设资源失败!");
            return null;
        }
        return t;
    }



    public AssetBundle LoadAssetBundle(string abName)
    {
        Debug.Log("Bundle名字:" + abName);
        AssetBundle ab = null;
        if (!m_abDic.ContainsKey(abName))
        {
            string abResPath = Path.Combine(Application .streamingAssetsPath  + "/" + "Others/V1.0" + "/", abName);
            Debug.Log("Bundle加载路径: "+ abResPath); 
            ab = AssetBundle.LoadFromFile(abResPath); 
            m_abDic[abName] = ab;
        }
        else
        {
            ab = m_abDic[abName];
        }

        //加载依赖
        string[] dependences = m_manifest.GetAllDependencies(abName);
        int dependenceLen = dependences.Length;
        if (dependenceLen > 0)
        {
            for (int i = 0; i < dependenceLen; i++)
            {
                string dependenceAbName = dependences[i];
                if (!m_abDic.ContainsKey(dependenceAbName))
                {
                    AssetBundle dependenceAb = LoadAssetBundle(dependenceAbName);

                    m_abDic[dependenceAbName] = dependenceAb;
                }
            }
        }

        return ab;
    }
}

这个是加载在StreamingAsset文件夹下的资源,保证我们打包的热更新资源没有任何问题.

然后我们再新建一个脚本用来加载模型.只需要两句即可:

 using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 
 public class LoadRes : MonoBehaviour
 {
 
     private void Awake()
     {
        AssetBundleLoaderMgr.instance.Init();

    }
    // Start is called before the first frame update
    void Start()
    {
        GameObject go = AssetBundleLoaderMgr.instance.LoadAsset<GameObject>("dragon.model", "dragon");
        GameObject Model = Instantiate(go);
    }


}

挂载脚本之后即可运行,看到场景中实例出来的模型:

在这里插入图片描述

以上呢,是各位可以在网上搜到的内容,那么如果要做商业化的项目,仅仅做到这个地步是远远不够的,接下来,我们完善一下差异化更新以及加密,那么要做到差异化更新呢用的方法就是比对同名文件的md5的值 ,使用AES方式来进行资源的加密解密:

首先是差异化更新.那么,我们如何知道资源变化了呢?最有效的办法就是记录每一个文件的MD5文件,那么什么是MD5呢?网上的全名叫 MD5信息摘要算法 简单来说,就是为了信息传输的一致性.那么这里就正好用在我们打包的时候去记录下来.一般是txt文件中.所以,我们在打包完成的时候,就去遍历文件夹内的文件,并获得文件的MD5值,再将这些数据保存在txt文本中.

首先我们需要一个能计算出文件MD5的值的代码:

 using System;
 using System.IO;
 using System.Security.Cryptography;
 
 public class MD5Checker
 {
     public delegate void AsyncCheckHeadler(AsyncCheckEventArgs e);
    public event AsyncCheckHeadler AsyncCheckProgress;
     //支持所有哈希算法
     private HashAlgorithm hashAlgorithm;
     //文件读取流
     private Stream inputStream;
     //缓存
     private byte[] asyncBuffer;
     public AsyncCheckState CompleteState { get; private set; }
     public float Progress { get; private set; }
     public string GetMD5 { get; private set; }
     /// <summary>
     /// 返回指定文件的MD5值
     /// </summary>
     /// <param name="path">文件的路径</param>
     /// <returns></returns>
     public static string Check(string path)
     {
         try
         {
             var fs = new FileStream(path, FileMode.Open);
             MD5CryptoServiceProvider md5Provider = new MD5CryptoServiceProvider();
             byte[] buffer = md5Provider.ComputeHash(fs);
             string resule = BitConverter.ToString(buffer);
             resule = resule.Replace("-", "");
             fs.Close();
             return resule;
         }
         catch (ArgumentException aex)
         {
             throw new ArgumentException(string.Format("<{0}>, 不存在: {1}", path, aex.Message));
         }
         catch (Exception ex)
         {
             throw new Exception(string.Format("读取文件 {0} ,MD5失败: {1}", path, ex.Message));
         }
     }
 
     public static string Check_Stream(string path)
     {
         try
         {
             int bufferSize = 1024 * 256;//自定义缓冲区大小256K
             var buffer = new byte[bufferSize];
             Stream inputStream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
             HashAlgorithm hashAlgorithm = new MD5CryptoServiceProvider();
             int readLength = 0;//每次读取长度
             var output = new byte[bufferSize];
             while ((readLength = inputStream.Read(buffer, 0, buffer.Length)) > 0)
             {
                 //计算MD5
                 hashAlgorithm.TransformBlock(buffer, 0, readLength, output, 0);
             }
             //完成最后计算,必须调用(由于上一部循环已经完成所有运算,所以调用此方法时后面的两个参数都为0)
             hashAlgorithm.TransformFinalBlock(buffer, 0, 0);
             string md5 = BitConverter.ToString(hashAlgorithm.Hash);
             hashAlgorithm.Clear();
             inputStream.Close();
             md5 = md5.Replace("-", "");
             return md5;
         }
         catch (ArgumentException aex)
         {
             throw new ArgumentException(string.Format("<{0}>, 不存在: {1}", path, aex.Message));
         }
         catch (Exception ex)
         {
             throw new Exception(string.Format("读取文件 {0} ,MD5失败: {1}", path, ex.Message));
        }
     }
 
     public void AsyncCheck(string path)
     {
         CompleteState = AsyncCheckState.Checking;
         try
         {
             int bufferSize = 1024 * 256;//缓冲区大小,1MB 1048576
 
             asyncBuffer = new byte[bufferSize];
 
             //打开文件流
             inputStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None, bufferSize, true);
             hashAlgorithm = new MD5CryptoServiceProvider();
 
             //异步读取数据到缓冲区
             inputStream.BeginRead(asyncBuffer, 0, asyncBuffer.Length, new AsyncCallback(AsyncComputeHashCallback), null);
         }
         catch (ArgumentException aex)
         {
             throw new ArgumentException(string.Format("<{0}>, 不存在: {1}", path, aex.Message));
         }
         catch (Exception ex)
         {
            throw new Exception(string.Format("读取文件{0} ,MD5失败: {1}", path, ex.Message));
        }
    }

    private void AsyncComputeHashCallback(IAsyncResult result)
    {
        int bytesRead = inputStream.EndRead(result);
        //检查是否到达流末尾
        if (inputStream.Position < inputStream.Length)
        {
            //输出进度
            Progress = (float)inputStream.Position / inputStream.Length;
            string pro = string.Format("{0:P0}", Progress);
            AsyncCheckProgress?.Invoke(new AsyncCheckEventArgs(AsyncCheckState.Checking, pro));

            var output = new byte[asyncBuffer.Length];
            //分块计算哈希值
            hashAlgorithm.TransformBlock(asyncBuffer, 0, asyncBuffer.Length, output, 0);

            //异步读取下一分块
            inputStream.BeginRead(asyncBuffer, 0, asyncBuffer.Length, new AsyncCallback(AsyncComputeHashCallback), null);
            return;
        }
        else
        {
            //计算最后分块哈希值
            hashAlgorithm.TransformFinalBlock(asyncBuffer, 0, bytesRead);
        }
        Progress = 1;
        string md5 = BitConverter.ToString(hashAlgorithm.Hash).Replace("-", "");
        CompleteState = AsyncCheckState.Completed;
        GetMD5 = md5;
        AsyncCheckProgress?.Invoke(new AsyncCheckEventArgs(AsyncCheckState.Completed, GetMD5));
        inputStream.Close();
    }
}
//异步检查状态
public enum AsyncCheckState
{
    Completed,
    Checking
}

public class AsyncCheckEventArgs : EventArgs
{
    public string Value { get; private set; }

    public AsyncCheckState State { get; private set; }

    public AsyncCheckEventArgs(AsyncCheckState state, string value)
    {
        Value = value; State = state;
    }
}

然后,我们新建一个FileIO的脚本,这个脚本主要控制一些数据的输入输出.我们首先就写一个获取文件MD5值的方法:

 using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 
 public static class FileIO 
 { 
 
     public static string MD5File(string file)
     {
        try
        {
            return MD5Checker.Check(file);
        }
        catch (System.Exception ex)
        {
            Debug.Log(ex.Message);
            return string.Empty;
        }
    }
}

然后我们再在打包完成之后,调用生成MD5值的方法,那么我们就需要一个可以遍历文件夹里面内容的函数,并能将文件夹中的文件挨个读出来,计算号MD5的值,然后保存在一个文本文档中.

     public static void CreateVersion(string resPath, string name)
     {
         name += ".txt";
         // 获取Res文件夹下所有文件的相对路径和MD5值
         string[] files = Directory.GetFiles(resPath, "*", SearchOption.AllDirectories);
         //Debug.Log(resPath + PathConfig.ProductVersion);
         Debug.Log(resPath + name);
         StringBuilder versions = new StringBuilder();
         for (int i = 0, len = files.Length; i < len; i++)
        {
            string filePath = files[i];

            if (filePath == (resPath + PathConfig.ProductVersion))
            {
                string relativePath = filePath.Replace(resPath, "").Replace("\\", "/");
                string md5 = FileIO.MD5File(filePath);
                versions.Append(relativePath).Append(",").Append(md5).Append("\r\n");
            }

            //所有AB包打包后的格式的文件都在这里进行筛选
            if (filePath.Contains("."))
            {
                string extension = filePath.Substring(files[i].LastIndexOf("."));

                //依赖文件(必须要)
                if (extension == ".manifest")
                {
                    string relativePath = filePath.Replace(resPath, "").Replace("\\", "/");
                    string md5 = FileIO.MD5File(filePath);
                    versions.Append(relativePath).Append(",").Append(md5).Append("\r\n");
                }

                //模型
                else if (extension == ".model")
                {
                    string relativePath = filePath.Replace(resPath, "").Replace("\\", "/");
                    string md5 = FileIO.MD5File(filePath);
                    versions.Append(relativePath).Append(",").Append(md5).Append("\r\n");
                }

            }
            else
            {
                string test = filePath.Substring(files[i].LastIndexOf("/") + 1);
                if (test == PathConfig.GetBuildTargetPath(EditorUserBuildSettings.activeBuildTarget))
                {
                   string relativePath = filePath.Replace(resPath, "").Replace("\\", "/");
                    string md5 = FileIO.MD5File(filePath);
                    versions.Append(relativePath).Append(",").Append(md5).Append("\r\n");
                }
            }
        }

        // 生成配置文件
        FileStream stream = new FileStream(resPath + name, FileMode.Create);

        byte[] data = Encoding.UTF8.GetBytes(versions.ToString());
        stream.Write(data, 0, data.Length);
        stream.Flush();
        stream.Close();
    }

打包完成后的代码处调用即可

   private static void BuildBundle(BuildAssetBundleOptions bundleOptions, BuildTarget buildTarget)
     {
         //包裹存储的路径...
         ...
         CreateVersion(outputPath, "version");
 
         Debug.Log("打包完成!位置: " + outputPath);
         AssetDatabase.Refresh();

    }

我们再次打包一次,发现会有一个version,txt文件,打开之后,就会看到每个ab文件的名字,以及MD5的值.

在这里插入图片描述
这个时候如果我们修改一下room的属性,比如,修改一下旋转的位置.就会发现,不仅room的MD5的值变了,连V1.0的值都变了.因为V1.0记录着文件所有的信息包括依赖项.

我们再看一下只修改room的文件:
在这里插入图片描述
而上面的dragon文件MD5的值是没有被修改的.

好的,这个文件就是实现差异化更新的关键.到这里我们就不能在本地实现更新了,或者说不能再在StreaminAsset文件夹中进行热更新了.测试的话可以放在其他的盘中,而在这里.我就直接放到云端了.然后将地址记录到pathconfig脚本中方便调用.

这是通过xftp来管理的服务器地址

在这里插入图片描述
我们首先从云端将version.txt下载下来,记录里面的文件和文件的MD5值,然后和本地的version.txt里面的文件和文件的MD5的值相对比.得出不同项,那么不同的几条数据就是需要我们更新的.

那么.我们首先需要一个下载txt文件的方法.下载下来还不能立即就放到文件夹中,要先读取里面的元素.

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;

 //更新全部资源 
 public class UpdateAssets : MonoBehaviour
 {
     //保存本地的assetbundle名和对应的MD5值
     private Dictionary<string, string> LocalResVersion;
     private Dictionary<string, string> ServerResVersion;
 
     //保存需要更新的AssetBundle名
     private List<string> NeedDownFiles;
 
     private bool NeedUpdateLocalVersionFile = false;
 
     private string _localUrl;
     private string _serverUrl;
 
     //启动差异化更新
     private void Start()
     {
         StartCoroutine(OnStart());
     }
 
     public IEnumerator OnStart()
     {
         yield return Init();
     }
 
     private IEnumerator Init()
     {
         LocalResVersion = new Dictionary<string, string>();
         ServerResVersion = new Dictionary<string, string>();
         NeedDownFiles = new List<string>();
         //加载本地version配置
         _localUrl = PathConfig.localUrl + "/";
         yield return DownLoad("file:///" + _localUrl, "version.txt", LocalVersionCallBack);
 
         //加载服务端version配置
         var serverUrl = PathConfig.serverUrl;
         _serverUrl = serverUrl +"V1.0";
         //Debug.Log("下载文件服务器的地址: " + _serverUrl);
         //下载vision文件
         yield return DownLoad(_serverUrl, "version.txt", ServerVersionCallBack);
     }
 
     private IEnumerator LocalVersionCallBack(UnityWebRequest request, string param = "")
     {
         //保存本地的version
         var context = request.downloadHandler.text;  
         ParseVersionFile(context, LocalResVersion);
         yield return ClearIncompleteFile();
     }
 
     private IEnumerator ServerVersionCallBack(UnityWebRequest request, string param = "")
     {
         //保存服务端version
         var context = request.downloadHandler.text;
         ParseVersionFile(context, ServerResVersion);
         //过滤下载文件
         FilterDownLoadFile();
 
         //计算出需要重新加载的资源
         CompareVersion();
         if (NeedUpdateLocalVersionFile)
         {
             //DownLoadProgress.Instance.ShowDownLoad(NeedDownFiles.Count);
             Debug.Log("需要更新的资源个数为:" + NeedDownFiles.Count);
         }
         //加载需要更新的资源
         yield return DownLoadRes();
     }
     //对比本地配置,清除缺失文件
     private IEnumerator ClearIncompleteFile()
     {
         if (LocalResVersion != null)
         {
             List<string> removeKey = new List<string>();
             //--------------------更新文件列表-----------------------
             foreach (var local in LocalResVersion)
             {
                 //第一层路径
                 string filePath = _localUrl + local.Key;
                 if (!Directory.Exists(_localUrl))
                 {
                     Directory.CreateDirectory(_localUrl);
                 }
                 if (!File.Exists(filePath))
                 {
                     removeKey.Add(local.Key);
                 }
                 else
                 {
                     //异步
                    yield return MD5FileAsync(filePath, delegate (string md5)
                    {
                        if (md5 != local.Value)
                        {
                            File.Delete(filePath);
                            removeKey.Add(local.Key);
                       }
                    });
                }
            }
            foreach (var key in removeKey)
            {
                if (LocalResVersion.ContainsKey(key))
                    LocalResVersion.Remove(key);
            }
        }
    }

    //过滤服务器下载文件,根据项目名下载项目
    private void FilterDownLoadFile(string ProduceName = "")
    { 
        Dictionary<string, string> root = new Dictionary<string, string>(); 
        //将root文件夹中的文件先记录下来 (文件夹只能有一层)
        foreach (var item in ServerResVersion)
        {
            root.Add(item.Key, item.Value);
        } 
        foreach (var item in ServerResVersion)
        {
            Debug.Log(item.Key + "----" + item.Value);
        }
        //将带有项目名的文件筛选出来
        Dictionary<string, string> filterServer = new Dictionary<string, string>();

        filterServer = root; 
        ServerResVersion = filterServer;
    }

    //依次加载需要更新的资源

    private IEnumerator DownLoadRes()
    {
        if (NeedDownFiles.Count == 0)
        {
            UpdateLocalVersionFile();
            yield break;
        }

        string file = NeedDownFiles[0];

        NeedDownFiles.RemoveAt(0);
        yield return DownLoad(_serverUrl, file, DownLoadCallBack);
    }

    private IEnumerator DownLoadCallBack(UnityWebRequest request, string param = "")
    {
        //将下载的资源替换本地就的资源
        var download = request.downloadHandler;
        if (!request.isNetworkError && !request.isHttpError)
        {
            ReplaceLocalRes(param, download.data);
            if (ServerResVersion.ContainsKey(param))
            {
                if (LocalResVersion.ContainsKey(param))
                    LocalResVersion[param] = ServerResVersion[param];
                else
                    LocalResVersion.Add(param, ServerResVersion[param]);
            }
        }
        yield return DownLoadRes();
    }

    //替换本地资源(多层文件夹结构)
    private void ReplaceLocalRes(string fileName, byte[] data)
    {
        string filePath = _localUrl + fileName;
        string sublocalUrl = "";

        if (!Directory.Exists(_localUrl))
        {
            Directory.CreateDirectory(_localUrl);
        }

        if (fileName.Contains("/"))
        {
            sublocalUrl = _localUrl;
            string[] _sublocalUrl = fileName.Split('/');
            for (int i = 0; i < _sublocalUrl.Length - 1; i++)
            {
                sublocalUrl += _sublocalUrl[i] + "/";
            }
            if (!Directory.Exists(sublocalUrl))
            {
                Directory.CreateDirectory(sublocalUrl);
            }
        }

        FileStream stream = new FileStream(filePath, FileMode.Create);
        stream.Write(data, 0, data.Length);
        stream.Flush();
        stream.Close();
    }

    //更新本地的version配置
    private void UpdateLocalVersionFile()
    {
        if (NeedUpdateLocalVersionFile)
        {
            StringBuilder versions = new StringBuilder();
            foreach (var item in LocalResVersion)
            {
                versions.Append(item.Key).Append(",").Append(item.Value).Append("\r\n");
            }
            if (!Directory.Exists(_localUrl))
            {
               Directory.CreateDirectory(_localUrl);
           }
            FileStream stream = new FileStream(_localUrl + "version.txt", FileMode.Create);
            byte[] data = Encoding.UTF8.GetBytes(versions.ToString());
            stream.Write(data, 0, data.Length);
            stream.Flush();
            stream.Close();
        }

        //加载显示对象
        //StartCoroutine(Show());
    }


    //比对文件
    private void CompareVersion()
    {
        foreach (var version in ServerResVersion)
        {
            string fileName = version.Key;                 //assetbundleName
            string serverMd5 = version.Value;              // asset MD5值

            //新增的资源
            if (!LocalResVersion.ContainsKey(fileName))
            {
                NeedDownFiles.Add(fileName);
            }
            else
            {
                //需要替换的资源
                string localMd5;

                LocalResVersion.TryGetValue(fileName, out localMd5);
                if (!serverMd5.Equals(localMd5))
                {
                    NeedDownFiles.Add(fileName);
                }
            }
        }

        if (NeedDownFiles.Count > 0)
        {
            for (int i = 0; i < NeedDownFiles.Count; i++)
            {
                Debug.Log("需要更新的资源:" + NeedDownFiles[i]);
            }
        }
        //本次有更新,同时更新本地的version.txt
        NeedUpdateLocalVersionFile = NeedDownFiles.Count > 0;
   }

    //比对资源差异
    private void ParseVersionFile(string content, Dictionary<string, string> dict)
    {
        if (content == null || content.Length == 0)
        {
            return;
        }
        string[] items = content.Split('\n');
        foreach (string item in items)
        {
            string str = item.Replace("\r", "").Replace("\n", "").Replace(" ", "");
            string[] info = str.Split(',');
            if (info != null && info.Length == 2)
            {
                dict.Add(info[0], info[1]);
            }
        }
    }

    //下载文件
    private IEnumerator DownLoad(string url, string fileName, HandleFinishDownload finishFun)
    {
       url = url.Replace('\\', '/').TrimEnd('/') + '/';
        Debug.Log(url);
        var request = UnityWebRequest.Get(url + fileName);
        if (NeedUpdateLocalVersionFile)
        {
            yield return LoadRegularRequest(request);
        }
        else
        {
            yield return request.SendWebRequest();
        }

        if (finishFun != null && request.isDone)
        {
            yield return finishFun(request, fileName);
        }
        if (request.isNetworkError)
        {
            Debug.LogError("更新资源出错: " + url + " error: " + request.error);
        }
        request.Dispose();
    }

    public delegate IEnumerator HandleFinishDownload(UnityWebRequest request, string param = "");

    //异步生成MD5值
   private IEnumerator MD5FileAsync(string file, Action<string> action)
    {
        var asyncChecker = new MD5Checker();
        asyncChecker.AsyncCheck(file);
        var endframe = new WaitForEndOfFrame();
        while (asyncChecker.CompleteState == AsyncCheckState.Checking)
        {
            //SeerLogger.Log("load...{0:P0}" + asyncChecker.Progress);
            yield return endframe;
        }
        action(asyncChecker.GetMD5);
    }
    //整齐的下载资源
    public IEnumerator LoadRegularRequest(UnityEngine.Networking.UnityWebRequest request)
    {
        var ao = request.SendWebRequest();
        bool downError = false;
        //ao.allowSceneActivation = false;
        while (true)
        {
            if (downError) break; 
            if (ao.webRequest.isNetworkError || ao.webRequest.isHttpError)
            {
                downError = true;
            }
            else if (ao.isDone)
                break;
        }
        yield return new WaitForEndOfFrame();
    }

}

那么我们将它挂在场景中测试一下:

第一次打开:

在这里插入图片描述
第二次打开:

在这里插入图片描述
然后我们修改一下dragon.model.打包后放到服务器上,然后再次运行:
在这里插入图片描述

那么差异化更新我们已经完成了

接下来就是加密了,其实,AB加密有3种,第一种就是往AB钱塞几个字符,然后加载AB的时候往后偏移几个字符就可以了,但是,这个只是防君子不妨小人.

第二种是对于字节进行字节加密,有在网上看到这样的:

 public static void Encypt(ref byte[] targetData, byte m_key)
    {
       //加密,与key异或,解密的时候同样如此
        int dataLength = targetData.Length;
        for (int i = 0; i < dataLength; ++i)
        {
            targetData[i] = (byte)(targetData[i] ^ m_key);
        }
    }

其实这个也可以,不过只能用在小项目中,因为亦或的处理方式比较消耗性能

我选用的是AES加密. 这个加密调用自身的dll.

我们在打包完成的时候执行加密,最后再根据加密后的AB包,再生成txt文件,然后在加载之前执行解密

首先是加上密钥,密钥也可以放在一个比较隐藏的位置,或者这个文件也是要通过某个解密才能拿到,都可以.

 1using System.Collections;
 2using System.Collections.Generic;
 3using System.IO;
 4using System.Security.Cryptography;
 5using System.Text;
 6using UnityEditor;
 7using UnityEngine;
 8
 9public class CreatAssetBundle : EditorWindow
10{
11    //构建的路径
12    public static string GetAssetBundlePath(BuildTarget buildTarget)
13    {
14        var path = PathConfig.buildAssetPath + "/" + PathConfig.GetBuildTargetPath(buildTarget) + "/";
15
16        //当在硬盘目录结构里不存在该路径时,创建文件夹
17        if (!Directory.Exists(path))
18        {
19            Directory.CreateDirectory(path);
20        }
21        return path;
22    }
23
24    [MenuItem("构建AB/构建Windows平台")]
25    public static void BuildAB()
26    {
27        BuildBundle(BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows);
28        Debug.Log("bundle完成....");
29    }
30
31    private static void BuildBundle(BuildAssetBundleOptions bundleOptions, BuildTarget buildTarget)
32    {
33        //包裹存储的路径...
34        string outputPath = GetAssetBundlePath(EditorUserBuildSettings.activeBuildTarget);
35        if (!Directory.Exists(outputPath))
36            Directory.CreateDirectory(outputPath);
37        //打包过程..
38        BuildPipeline.BuildAssetBundles(outputPath, bundleOptions, buildTarget);
39
40
41        Debug.Log("打包完成!位置: " + outputPath);
42        //加密
43        AES_EncryptionAB(); 
44
45    }
46
47
48    public static void CreateVersion(string resPath, string name)
49    {
50        ...
51    }
52
53
54    //加密 
55    static void AES_EncryptionAB()
56    {
57        string[] files = Directory.GetFiles(GetAssetBundlePath(EditorUserBuildSettings.activeBuildTarget), "*", SearchOption.AllDirectories);
58        foreach (var item in files)
59        {
60            string filePath = item;
61            if (filePath.Contains(".meta")||filePath .Contains ("version.txt"))
62            { 
63                continue;
64            }
65            FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
66            byte[] buffur = new byte[fs.Length];
67            fs.Read(buffur, 0, buffur.Length);
68            byte[] bytes = buffur;
69            byte[] key = Encoding.UTF8.GetBytes(PathConfig.key);
70            byte[] iv = Encoding.UTF8.GetBytes(PathConfig.iv);
71
72            fs.Dispose();
73
74            Aes aes = Aes.Create();
75            MemoryStream memoryStream = new MemoryStream();
76            CryptoStream cryptoStream = new CryptoStream(memoryStream, aes.CreateEncryptor(key, iv), CryptoStreamMode.Write);
77            cryptoStream.Write(bytes, 0, bytes.Length);
78            cryptoStream.FlushFinalBlock();
79            File.WriteAllBytes(filePath, memoryStream.ToArray());
80            Debug.Log("名为 " + item + " 的文件加密完成.");
81
82        }
83        string outputPath = GetAssetBundlePath(EditorUserBuildSettings.activeBuildTarget);
84        CreateVersion(outputPath, "version");
85
86        AssetDatabase.Refresh();
87    }
88
89}

那么打包一下,发现加密已经完成:

在这里插入图片描述
那么接下来就是加载的时候的解密了,以前的直接从file文件中加载的方法就不能再用了.我们得先读取文件得字节流信息,然后按照密钥来解密,我们在加载的方法中写上类似的解密方法.

  1using System.Collections;
  2using System.Collections.Generic;
  3using System.IO;
  4using System.Security.Cryptography;
  5using System.Text;
  6using UnityEngine;
  7
  8public class AssetBundleLoaderMgr  
  9{
 10    //首先是依赖文件
 11    private AssetBundleManifest m_manifest;
 12    //bundle的缓存
 13    private Dictionary<string, AssetBundle> m_abDic = new Dictionary<string, AssetBundle>();
 14    private static AssetBundleLoaderMgr s_instance;
 15    public static AssetBundleLoaderMgr instance
 16    {
 17        get
 18        {
 19            if (null == s_instance)
 20                s_instance = new AssetBundleLoaderMgr();
 21            return s_instance;
 22        }
 23    }
 24    //这里全局唯一,不能多次实例
 25    public void Init()
 26    {
 27        //先从本地加载,也就是StreamingAsset文件夹
 28        //string streamingAssetsAbPath = Path.Combine(PathConfig.localUrl, "Others/SamJanAsset_1.0/SamJanAsset_1.0"); 
 29        string streamingAssetsAbPath = Path.Combine(Application .streamingAssetsPath , "Others/V1.0/V1.0");
 30        //AssetBundle streamingAssetsAb = AssetBundle.LoadFromFile(streamingAssetsAbPath ); 
 31        AssetBundle streamingAssetsAb = LoadAesAB(streamingAssetsAbPath);
 32        m_manifest = streamingAssetsAb.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
 33    }
 34
 35
 36    public T LoadAsset<T>(string abName, string assetName) where T : Object
 37    {
 38        AssetBundle ab = LoadAssetBundle(abName);
 39        if (ab == null)
 40        {
 41            Debug.Log("加载名为: " + abName + " 的AB资源失败!");
 42            return null;
 43        }
 44        T t = ab.LoadAsset<T>(assetName);
 45        if (t == null)
 46        {
 47            Debug.Log("加载名为: " + assetName + " 的预设资源失败!");
 48            return null;
 49        }
 50        return t;
 51    }
 52
 53
 54
 55    public AssetBundle LoadAssetBundle(string abName)
 56    {
 57        Debug.Log("Bundle名字:" + abName);
 58        AssetBundle ab = null;
 59        if (!m_abDic.ContainsKey(abName))
 60        {
 61            string abResPath = Path.Combine(Application .streamingAssetsPath  + "/" + "Others/V1.0" + "/", abName);
 62            Debug.Log("Bundle加载路径: "+ abResPath); 
 63            //ab = AssetBundle.LoadFromFile(abResPath);
 64            ab = LoadAesAB(abResPath);
 65            m_abDic[abName] = ab;
 66        }
 67        else
 68        {
 69            ab = m_abDic[abName];
 70        }
 71
 72        //加载依赖
 73        string[] dependences = m_manifest.GetAllDependencies(abName);
 74        int dependenceLen = dependences.Length;
 75        if (dependenceLen > 0)
 76        {
 77            for (int i = 0; i < dependenceLen; i++)
 78            {
 79                string dependenceAbName = dependences[i];
 80                if (!m_abDic.ContainsKey(dependenceAbName))
 81                {
 82                    //AssetBundle dependenceAb = LoadAssetBundle(dependenceAbName);
 83                    AssetBundle dependenceAb = LoadAesAB(dependenceAbName);
 84                    m_abDic[dependenceAbName] = dependenceAb;
 85                }
 86            }
 87        }
 88
 89        return ab;
 90    }
 91
 92
 93    //解密AB包   16位密码(一层)
 94    AssetBundle LoadAesAB(string path)
 95    {
 96        string filePath = path;
 97        FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
 98        byte[] buffer = new byte[fs.Length];
 99        fs.Read(buffer, 0, buffer.Length);
100        byte[] bytes = buffer;
101        byte[] key = Encoding.UTF8.GetBytes(PathConfig.key);
102        byte[] iv = Encoding.UTF8.GetBytes(PathConfig.iv);
103        Aes aes = Aes.Create();
104        MemoryStream memoryStream = new MemoryStream();
105        CryptoStream cryptoStream = new CryptoStream(memoryStream, aes.CreateDecryptor(key, iv), CryptoStreamMode.Write);
106        cryptoStream.Write(bytes, 0, bytes.Length);
107
108        cryptoStream.FlushFinalBlock();//加密多次会报错
109
110        AssetBundle Decode_AB = AssetBundle.LoadFromMemory(memoryStream.ToArray());
111
112        //释放资源
113        cryptoStream = null;
114        memoryStream = null;
115        aes = null;
116        fs.Dispose();
117        //cryptoStream.Close();
118        return Decode_AB;
119
120    }
121}

我们再次用加载的方法来试一试:

 1using System.Collections;
 2using System.Collections.Generic;
 3using UnityEngine;
 4
 5public class LoadRes : MonoBehaviour
 6{
 7
 8    private void Awake()
 9    {
10        AssetBundleLoaderMgr.instance.Init();
11
12    }
13    // Start is called before the first frame update
14    void Start()
15    {
16        GameObject go = AssetBundleLoaderMgr.instance.LoadAsset<GameObject>("dragon.model", "dragon");
17        GameObject Model = Instantiate(go);
18    }
19
20
21}

发现可以解析出来:

在这里插入图片描述
我们尝试用普通的方法去读取ab资源,发现已经读取不出来了:

可以用AssetbundBrower来验证一下,这个是unity自带的ab打包工具:

在这里插入图片描述
abstudio.exe 同样也是预览不了

在这里插入图片描述
更多unity开发高能技巧,请关注: 微信公众号:独立游戏之路
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_41590778/article/details/129421308