游戏热更新资源加密的必要性
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开发高能技巧,请关注: 微信公众号:独立游戏之路