Game resource differentiation hot update and encryption strategy (Assetbundle packaging AssetBundle loading AssetBundle encryption)

Necessity of game hot update resource encryption

The hot update of resources in Unity is mainly Assetbundle, and the use of resources is becoming more and more extensive. The ab package can contain pictures, videos or scripts, which are the intellectual property of the game. If it is unlocked by a cracker or a competitor, you can get the content inside , is a big loss to the game.

Ab resources are decrypted and captured, in addition to the loss of intellectual property, it will also have a great impact on game operations. For example, if the event resources in the game package are unlocked in advance, the spoiler will greatly affect the effect of the event. In addition, if the resources are modified, there will be a plug-in effect.
insert image description here

The picture above is to modify the material of the resource pack to achieve the effect of perspective.

Encryption of game resources and differential updates

First of all, prepare some resources and put them in the project. Because of the simple things like packaging, I won’t waste time with you, and I will take a screenshot for you to see. The first is the creation of the folder.

insert image description here

If you have a good development habit, you should be able to know what these folders do.

Randomly set two models to become ab files:

insert image description here

Then it is packaged code writing. First, a PathConfig script is needed to store some paths, file names, etc. Currently, because it is relatively simple, only a packaged path, version number, and platform path are placed:

 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
}

The following is writing a code that can be packaged, because it is too simple, so I will not explain it here

 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();

    }
}

At this time, you can find that the package is successful in the menu bar. The success is shown in the figure below:
insert image description here
Of course, there is no problem with loading from the local.

Let's write a code to load it. We first write a loaded management class, and then set it as a singleton, so that it can be loaded anywhere and in any environment. No need to inherit from Mono.

The first is to load the class:

 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;
    }
}

This is the resource loaded in the StreamingAsset folder, to ensure that there is no problem with the hot update resources we packaged.

Then we create a new script to load the model. It only needs two sentences:

 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);
    }


}

After the script is mounted, it can be run, and you can see the model instanced in the scene:

insert image description here

The above is what you can find on the Internet, so if you want to do a commercial project, it is not enough to just do this. Next, let’s improve the differentiated update and encryption, then we must achieve the difference The method used for updating is to compare the md5 value of the file with the same name, and use the AES method to encrypt and decrypt resources:

The first is differential update. So, how do we know that resources have changed? The most effective way is to record the MD5 file of each file, so what is MD5? The full name of the online MD5 information summary algorithm is simply for Consistency of information transmission. Then it is just used here to record when we pack. Usually in a txt file. Therefore, when the pack is completed, we traverse the files in the folder and obtain the MD5 value of the file , and then save the data in the txt text.

First we need a code that can calculate the MD5 value of the file:

 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;
    }
}

Then, we create a new FileIO script, which mainly controls the input and output of some data. We first write a method to obtain the MD5 value of the file:

 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;
        }
    }
}

Then we call the method of generating MD5 value after the packaging is completed, then we need a function that can traverse the contents of the folder, and can read the files in the folder one by one, calculate the value of MD5, and then save it in in a text document.

     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();
    }

After the packaging is completed, the code can be called

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

    }

We pack it again and find that there will be a version, txt file. After opening, you will see the name of each ab file and the value of MD5.

insert image description here
At this time, if we modify the properties of the room, for example, modify the position of the rotation, we will find that not only the MD5 value of the room has changed, but also the value of V1.0. Because V1.0 records all the files Information includes dependencies.

Let's look at the file that only modifies the room:
insert image description here
the MD5 value of the dragon file above has not been modified.

Ok, this file is the key to implementing differential updates. At this point we can no longer implement updates locally, or we can no longer perform hot updates in the StreaminAsset folder. For testing, it can be placed on other disks, and in Here. I put it directly in the cloud. Then record the address in the pathconfig script for easy calling.

This is the server address managed by xftp

insert image description here
We first download version.txt from the cloud, record the files in it and the MD5 value of the file, and then compare them with the files in the local version.txt and the MD5 value of the file. Get different items, then a few different items The data is what we need to update.

So. First of all, we need a method to download txt files. After downloading, we can’t put them in the folder immediately, but we need to read the elements inside.

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();
    }

}

Then let's hang it in the scene and test it:

Open for the first time:

insert image description here
Open it a second time:

insert image description here
Then we modify the dragon.model. Package it and put it on the server, and then run it again:
insert image description here

Then the differential update we have done

The next step is encryption. In fact, there are three types of AB encryption. The first one is to insert a few characters into AB money, and then offset a few characters back when loading AB. However, this is just to prevent gentlemen. people.

The second is to encrypt bytes by bytes, and I have seen this on the Internet:

 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);
        }
    }

In fact, this is also possible, but it can only be used in small projects, because the processing method of OR is more performance-consuming

I choose AES encryption. This encryption calls its own dll.

We perform encryption when the packaging is complete, and finally generate a txt file according to the encrypted AB package, and then perform decryption before loading

The first is to add the key. The key can also be placed in a relatively hidden location, or the file can only be obtained through a certain decryption.

 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}

Then pack it and find that the encryption has been completed:

insert image description here
Then the next step is to decrypt when loading. The previous method of loading directly from the file file can no longer be used. We have to read the byte stream information of the file first, and then decrypt it according to the key. We are loading Write a similar decryption method in the method.

  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}

Let's try again with the loading method:

 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}

It was found that it can be parsed out:

insert image description here
We tried to read the ab resource in a normal way, and found that it could not be read:

You can use AssetbundBrower to verify it. This is the ab packaging tool that comes with unity:

insert image description here
abstudio.exe also cannot preview

insert image description here
For more high-energy skills in unity development, please pay attention to: WeChat public account: The road to independent games
insert image description here

Guess you like

Origin blog.csdn.net/weixin_41590778/article/details/129421308
Recommended