打app包的时候,备份一份全量的配置表、lua(用以打增量时做自动差异比较)
资源文件零碎,可以考虑使用人工选择某个资源打增量的方式
你可能会用到的
//using System.IO;
// 获取目录中的所有文件(会遍历子目录)
public static void GetAllFiles(string path, ref List<string> files)
{
var fs = Directory.GetFiles(path);
var dirs = Directory.GetDirectories(path);
foreach(var f in fs)
{
var ext = Path.GetExtension(f);
if(ext.Equals(".meta")) continue;
files.Add(f.Replace('\\','/'));
}
foreach(var d in dirs)
{
GetAllFiles(d, ref files);
}
}
//using UnityEditor;
// 显示进度条
public static void UpdateProgress(int progress, int max, string desc)
{
var title = "Processing...[" + progress + "/" + max + "]";
var value = (float)progress / (float)max;
EditorUtility.DisplayProgressBar(title, desc, value);
if(value >= 1)
EditorUtility.ClearProgressBar();
}
//using System.IO;
//判断文件是否存在
File.Exists(path);
//删除文件
File.Delete(path);
//复制文件
File.Copy(path1, path2, true);
//移动文件
File.Move(path1, path2);
//判断目录是否存在
Directory.Exists(path);
//创建目录
Directory.CreateDirectory(path);
//删除目录
Directory.Delete(path, true);
//获取文件的路径
Path.GetDirectoryName(fpath);
//获取文件名称(带后缀)
Path.GetFileName(fpath);
//using UnityEditor;
//刷新Unity的Assets目录,特别是有文件变更的时候,最好调用一下这个
AssetDatabase.Refresh();
打增量包的时候,需要先比对工程内的文件和之前备份的文件,过滤出有差异的文件,拷贝到一个临时目录(比如叫lua_update
,这个目录必须在Assets目录中,因为打AssetBundle的时候只会识别到Assets目录中的文件),用以打成AssetBundle。
注意.lua后缀是不会被AssetBundle识别的,所以lua文件要加上.bytes后缀,比如Game.lua.bytes,另外,建议打AssetBundle之前做一下加密,不要直接暴露明文的lua和配置
你可能需要的:
//using System;
//using System.IO;
//using System.Security.Cryptography;
// 获取文件的md5值
public static string GetMD5Hash(string pathName)
{
if(!File.Exists(pathName))
{
Debug.LogError("GetMD5Hash Error, file not exist: " + pathName);
return "";
}
string strResult = "";
string strHash = "";
byte[] bytesHash;
FileStream fs = null;
MD5CryptoServiceProvider oMD5Hasher = new MD5CryptoServiceProvider();
try
{
fs = new FileStream(pathName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
bytesHash = oMD5Hasher.ComputeHash(fs);
fs.Close();
strHash = BitConverter.ToString(bytesHash);
strHash = strHash.Replace("-", "");
strResult = strHash;
}
catch (System.Exception ex)
{
Debug.LogError("read md5 file error :" + pathName + " e: " + ex.ToString());
}
return strResult;
}
//using System.Text;
//using System;
//using System.Security.Cryptography;
//using System.IO;
///
/// AES 加密(高级加密标准,是下一代的加密算法标准,速度快,安全级别高,目前 AES 标准的一个实现是 Rijndael 算法)
///
/// 待加密密文
/// 加密密钥
public static byte[] AESEncrypt(byte[] encryptByte, string encryptKey)
{
if (encryptByte.Length == 0)
{
//throw (new Exception("明文不得为空"));
return new byte[0];
}
if (string.IsNullOrEmpty(encryptKey)) { throw (new Exception("密钥不得为空")); }
byte[] bytesrEncrypt;
byte[] btIV = Convert.FromBase64String("CBFdfv7d/ChblnhfqKBto3qbvb=3");
byte[] btSalt = Convert.FromBase64String("chv34qtlerl/VBc4najgnlb");
Rijndael m_providerAES = Rijndael.Create();
try
{
MemoryStream stream = new MemoryStream();
PasswordDeriveBytes pdb = new PasswordDeriveBytes(encryptKey, btSalt);
ICryptoTransform transform = m_providerAES.CreateEncryptor(pdb.GetBytes(32), btIV);
CryptoStream csstream = new CryptoStream(stream, transform, CryptoStreamMode.Write);
csstream.Write(encryptByte, 0, encryptByte.Length);
csstream.FlushFinalBlock();
bytesrEncrypt = stream.ToArray();
stream.Close(); stream.Dispose();
csstream.Close(); csstream.Dispose();
}
catch (IOException ex) { throw ex; }
catch (CryptographicException ex) { throw ex; }
catch (ArgumentException ex) { throw ex; }
catch (Exception ex) { throw ex; }
finally { m_providerAES.Clear(); }
return bytesrEncrypt;
}
///
/// AES 解密(高级加密标准,是下一代的加密算法标准,速度快,安全级别高,目前 AES 标准的一个实现是 Rijndael 算法)
///
/// 待解密密文
/// 解密密钥
public static byte[] AESDecrypt(byte[] decryptByte, string decryptKey)
{
if (decryptByte.Length == 0)
{
//throw (new Exception("密文不得为空"));
return new byte[0];
}
if (string.IsNullOrEmpty(decryptKey)) { throw (new Exception("密钥不得为空")); }
byte[] bytesDecrypt;
byte[] btIV = Convert.FromBase64String("CBFdfv7d/ChblnhfqKBto3qbvb=3");
byte[] btSalt = Convert.FromBase64String("chv34qtlerl/VBc4najgnlb");
Rijndael providerAES = Rijndael.Create();
try
{
MemoryStream stream = new MemoryStream();
PasswordDeriveBytes pdb = new PasswordDeriveBytes(decryptKey, btSalt);
ICryptoTransform transform = providerAES.CreateDecryptor(pdb.GetBytes(32), btIV);
CryptoStream csstream = new CryptoStream(stream, transform, CryptoStreamMode.Write);
csstream.Write(decryptByte, 0, decryptByte.Length);
csstream.FlushFinalBlock();
bytesDecrypt = stream.ToArray();
stream.Close(); stream.Dispose();
csstream.Close(); csstream.Dispose();
}
catch (IOException ex) { throw ex; }
catch (CryptographicException ex) { throw ex; }
catch (ArgumentException ex) { throw ex; }
catch (Exception ex) { throw ex; }
finally { providerAES.Clear(); }
return bytesDecrypt;
}
//LuaFramework.AppConst.LuaSecretKey = "Fd3ht4mlgOhc/=fjC+dk2B";
byte[] allBytes = File.ReadAllBytes(f);
byte[] encryptBytes = CryptoUtil.AESEncrypt(allBytes, LuaFramework.AppConst.LuaSecretKey);
if (File.Exists(newfilePath))
{
File.Delete(newfilePath);
}
File.WriteAllBytes(newfilePath, encryptBytes);
//ab 为lua的AssetBundle
private byte[] ReadBytesFromAssetBundle(string fileName)
{
...
//这个LuaUpdateBundle.bundle要通过AssetBundle.LoadFromFile从下载保存的目录中加载
var ab = LuaUpdateBundle.bundle;
// 以全路径读取,防止重名
var luaFileName = string.Format("assets/lua_update/{0}.bytes", fileName);
TextAsset luaCode = ab.LoadAsset<TextAsset>(luaFileName);
byte[] luaBytes = CryptoUtil.AESDecrypt(luaCode.bytes, LuaFramework.AppConst.LuaSecretKey);
Resources.UnloadAsset(luaCode);
...
}
private string ReadStringFromAssetBundle(string fileName)
{
...
//这个LuaUpdateBundle.bundle要通过AssetBundle.LoadFromFile从下载保存的目录中加载
var ab = LuaUpdateBundle.bundle;
// 以全路径读取,防止重名
var luaFileName = string.Format("assets/lua_update/{0}.bytes", fileName);
TextAsset luaCode = ab.LoadAsset<TextAsset>(luaFileName);
var luaStr = System.Text.Encoding.GetEncoding(65001).GetString(AESDecrypt(luaCode.bytes, LuaFramework.AppConst.LuaSecretKey));
Resources.UnloadAsset(luaCode);
...
}
//打AssetBundle
AssetBundleBuild[] buildMap = new AssetBundleBuild[1];
//因为要通过http或https下载,必须以.zip结尾
buildMap[0].assetBundleName = "lua_update.bundle.zip";
//注意这里的路径是相对Assets的
buildMap[0].assetNames = new string[]{ "Assets/lua_update/Game.lua.bytes", "Assets/lua_update/Hello.lua.bytes" };
string outpath = Application.dataPath + "/../Bin";
BuildPipeline.BuildAssetBundles(outpath, buildMap, BuildAssetBundleOptions.ChunkBasedCompression, BuildTarget.Android);
构建一个update_info.json,格式如下
[
{
"version": "1.15.0",
"update_type": "1",
"update_list": [
{
"name": "mygame.apk",
"size": "606469145",
"md5": "6496571659E53ECD774352E4D309AF31",
"url": "1.15.0/mygame_1.15.0.apk.zip"
}
]
},
{
"version": "1.15.1",
"update_type": "0",
"update_list": [
{
"name": "lua_update.bundle",
"size": "1440",
"md5": "755C4800303C19ECA9206117AAED1478",
"url": "update_1.15.1/lua_update.bundle.zip"
},
{
"name": "cfg_update.bundle",
"size": "2392",
"md5": "A47D1E8D1B31AD81B2FDD1C6A852E474",
"url": "update_1.15.1/cfg_update.bundle.zip"
}
]
}
]
通过WWW远程获取到上面的json文件,通过版本比较和计算得出需要的更新列表
对应的数据类:UpdateInfoList.cs
//UpdateInfoList.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UpdateInfoItem
{
public string name;
public string size;
public string md5;
public string savepath;
///
/// 这个是相对的url
///
public string url;
///
/// 这个才是完整的url
///
public string fullUrl;
public void InitSavePath()
{
#if UNITY_EDITOR
savepath = Application.dataPath + "/update/" + name.ToLower();
#else
savepath = Application.persistentDataPath + "/update/" + name.ToLower();
#endif
}
public void InitFullUrl()
{
fullUrl = "https://linxinfa.game.com/" + url;
}
}
public class UpdateInfoList
{
public Dictionary<string, UpdateInfoItem> updateList;
public List<UpdateInfoItem> GetList()
{
return new List<UpdateInfoItem>(updateList.Values);
}
public UpdateInfoList()
{
updateList = new Dictionary<string, UpdateInfoItem>();
}
public void Clear()
{
updateList.Clear();
}
public int Count()
{
return updateList.Count;
}
public UpdateInfoItem Add(string infoJson, ref bool isApp)
{
var item = JsonMapper.ToObject<UpdateInfoItem>(infoJson);
item.InitSavePath();
item.InitFullUrl();
if (item.name.EndsWith(".apk") || item.name.EndsWith(".ipa"))
{
updateList.Clear();
updateList.Add(item.name, item);
isApp = true;
}
else
{
if(!updateList.ContainsKey(item.name))
{
updateList.Add(item.name, item);
}
isApp = false;
}
return item;
}
///
/// 总大小
///
public long TotalSize()
{
long size = 0;
foreach (var item in updateList.Values)
{
size += long.Parse(item.size);
}
return size;
}
public string TotalSizeFormat()
{
long size = TotalSize();
if (size >= 1024 * 1024)
return (size / 1024f / 1024f).ToString("#0.00MB");
else if (size >= 1024)
return (size / 1024f).ToString("#0.00KB");
else
return size + "B";
}
}
写一个更新的类: GameUpdateHelper.cs,用来监控下载过程
using UnityEngine;
using System.Collections;
using System.IO;
using System.Collections.Generic;
using LitJson;
public enum UpdateResult
{
NONE,
Success, // 更新成功
GetVersionFail, // 获取版本号失败
GetFileListFail, // 获取文件列表失败
DownloadInComplete, // 下载文件不完全
DownloadFail, // 下载文件失败
CopyDataFileFail, // 拷贝文件失败
GenerateVersionFileFail, // 生成版本号文件失败
GenerateFileListFail, // 生成文件列表文件失败
CleanCacheFail, // 生成文件列表文件失败
LoadRomoteFailListError, // 读取下载的文件列表失败
GetCheckFileFail, // 下载MD5对比文件失败
}
public enum UpdateStep
{
NONE,
CheckVersion,
GetFileList,
CompareRes,
AskIsDonwload, // 询问是否下载文件
DownloadRes,
CheckRes,
DecompressRes,
CleanCache,
FINISH,
}
public class GameUpdateHelper : MonoBehaviour
{
private JsonData m_updateInfoJsonData;
///
/// 更新列表
///
private UpdateInfoList m_updateInfoList = new UpdateInfoList();
private DownloadHelper m_downloader = null;
// 更新状态
public UpdateStep CurUpdateStep { get { return m_curUpdateStep; } }
public UpdateResult CurUpdateResult { get { return m_curUpdateResult; } }
private UpdateStep m_curUpdateStep = UpdateStep.NONE;
private UpdateResult m_curUpdateResult = UpdateResult.NONE;
public long AlreadyDownloadSize { get { return (null != m_downloader) ? m_downloader.AlreadyDownloadSize : 0; } }
public long NeedDownloadTotalSize { get { return m_updateInfoList.TotalSize(); } }
private UpdateStep m_lastUpateStep = UpdateStep.NONE;
public void InitUpdateInfoList()
{
var updateInfoTxt = "[]"; //更新列表的json文本,可以通过WWW从http服务器拉取
m_updateInfoJsonData = JsonMapper.ToObject(updateInfoTxt);
//根据json构造UpdateInfoList
if (null == m_updateInfoJsonData)
{
UpdateFinish(UpdateResult.Success);
}
else
{
for (int i = m_updateInfoJsonData.Count - 1; i >= 0; --i)
{
var data = m_updateInfoJsonData[i];
var version = (string)data["version"];
if (CompareVersion(version, m_localAppVersion) > 0)
{
var update_list = data["update_list"];
for (int j = 0, cnt2 = update_list.Count; j < cnt2; ++j)
{
var updateItem = update_list[j];
var item = m_updateInfoList.Add(updateItem.ToJson(), ref m_isUpdateFullApp);
if (m_isUpdateFullApp)
{
// 整包更新的app下载路径
m_updateFullAppSavePath = item.savepath;
break;
}
}
}
}
if (m_updateInfoList.Count() > 0)
{
m_curUpdateStep = UpdateStep.AskIsDonwload;
}
else
{
UpdateFinish(UpdateResult.Success);
}
}
}
void UpdateFinish(UpdateHelper.UpdateResult result)
{
m_curUpdateStep = UpdateHelper.UpdateStep.FINISH;
m_curUpdateResult = result;
}
public void StartDownload()
{
m_downloader = new DownloadHelper(m_updateInfoList.GetList());
m_downloader.StartDownload();
}
void Update()
{
if (m_lastUpateStep != CurUpdateStep)
{
switch (m_curUpdateHelper.CurUpdateStep)
{
case UpdateStep.AskIsDonwload:
{
//TOOD 询问下载,点击确定执行StartDownload()
}
break;
case UpdateStep.FINISH:
{
if (UpdateResult.Success == CurUpdateResult)
{
//TODO 下载完成
}
else
{
//TODO 下载失败
}
}
break;
}
m_lastUpateStep = CurUpdateStep;
}
if (m_lastUpateStep == UpdateHelper.UpdateStep.DownloadRes)
{
Debug.Log("下载中: {0}/{1}", AlreadyDownloadSize, NeedDownloadTotalSize);
}
}
}
下载类:DownloadHelper.cs,开启独立的线程下载
//DownloadHelper.cs
using UnityEngine;
using System.Collections;
using System.IO;
using System.Net;
using System.Threading;
using GCGame;
using System.Collections.Generic;
using System;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
public class DownloadHelper
{
private HttpWebRequest m_request;
private Stream m_fs, m_ns;
private Thread m_thread;
private byte[] m_buffer;
private int m_readSize;
private int m_curDownloadFileIndex;
private List<UpdateInfoItem> m_updateInfoList;
private UpdateInfoItem m_curUpdateInfo;
public long AlreadyDownloadSize { get { return m_alreadyDownloadSize; } }
private long m_alreadyDownloadSize = 0;
public DownloadState downlodadState;
public enum DownloadState
{
None,
Ready,
Ing,
CommonError,
Md5Error,
Finish,
}
public DownloadHelper(List<UpdateInfoItem> updateInfoList)
{
downlodadState = DownloadState.None;
m_updateInfoList = updateInfoList;
m_buffer = new byte[1024 * 8];
}
public void ResetState()
{
downlodadState = DownloadState.None;
}
public void StartDownload()
{
downlodadState = DownloadState.Ready;
if (null == m_updateInfoList || 0 == m_updateInfoList.Count)
{
DownloadFinish();
return;
}
DownloadNext(false);
}
public static string AddTimestampToUrl(string url)
{
return url + "?" + DateTime.Now.Millisecond.ToString();
}
public bool isDone { get { return m_curDownloadFileIndex >= m_updateInfoList.Count; } }
///
/// 创建一个HttpWebRequest,兼容https的身份验证
///
///
///
private HttpWebRequest MakeOneWebRequest(string url)
{
HttpWebRequest request = null;
if (url.StartsWith("https", StringComparison.OrdinalIgnoreCase))
{
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(CheckValidationRequest);
request = WebRequest.Create(url) as HttpWebRequest;
request.ProtocolVersion = HttpVersion.Version11;
}
else
{
request = WebRequest.Create(url) as HttpWebRequest;
}
return request;
}
private bool CheckValidationRequest(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
//直接返回true即可,信任https身份验证
return true;
}
private void HttpRequest(string url, string saveFullPath)
{
try
{
Debug.Log("download from : " + url);
m_request = MakeOneWebRequest(url);
m_request.Timeout = 10000;
var dir = Path.GetDirectoryName(m_curUpdateInfo.savepath);
if(!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
Debug.Log("CreateDirectory, dir: " + dir);
}
if (File.Exists(saveFullPath)) // 下载续传
{
if (null != m_fs) m_fs.Close();
FileInfo tmpF = new FileInfo(saveFullPath);
m_fs = File.OpenWrite(saveFullPath);
if (tmpF.Length < long.Parse(m_curUpdateInfo.size))
{
Debug.LogYellow("文件续传, url: " + url);
m_fs.Seek(tmpF.Length, SeekOrigin.Current);
Debug.Log("m_fs.Seek: " + tmpF.Length);
m_request.AddRange((int)m_fs.Length);
m_alreadyDownloadSize += m_fs.Length;
}
else if (tmpF.Length == long.Parse(m_curUpdateInfo.size))
{
//TODO CheckMd5
if (null != m_fs) m_fs.Close();
var md5 = Utils.GetMD5Hash(saveFullPath);
if(md5 == m_curUpdateInfo.md5)
{
DownloadNext(true);
return;
}
else
{
// 重新下载
if (null != m_fs) m_fs.Close();
m_fs = new FileStream(m_curUpdateInfo.savepath, FileMode.Create);
}
}
else
{
// 重新下载
if (null != m_fs) m_fs.Close();
m_fs = new FileStream(m_curUpdateInfo.savepath, FileMode.Create);
}
}
else
{
m_fs = new FileStream(saveFullPath, FileMode.Create);
}
HttpWebResponse response = (HttpWebResponse)m_request.GetResponse();
Debug.Log("response.StatusCode: " + response.StatusCode.ToString());
if (response.StatusCode != HttpStatusCode.PartialContent)
{
Debug.Log("server not support partial content.");
}
m_ns = response.GetResponseStream();
m_ns.ReadTimeout = 15000;
if (m_thread == null)
{
m_thread = new Thread(DownloadThread);
m_thread.Start();
}
}
catch (Exception ex)
{
DownloadError(ex);
}
}
private void DownloadThread()
{
Debug.Log("download thread start");
while (true)
{
try
{
Write2file();
if (m_readSize == 0)
{
DownloadOneFileEnd();
if (isDone) break;
}
}
catch (Exception ex)
{
DownloadError(ex);
}
}
Debug.Log("download thread stop");
}
private void Write2file()
{
m_readSize = m_ns.Read(m_buffer, 0, m_buffer.Length);
if (m_readSize > 0)
{
m_fs.Write(m_buffer, 0, m_readSize);
m_alreadyDownloadSize += m_readSize;
Thread.Sleep(0);
}
}
private void DownloadOneFileEnd()
{
//TODO check md5
if (null != m_fs) m_fs.Close();
var localMd5 = Utils.GetMD5Hash(m_curUpdateInfo.savepath);
if(localMd5 == m_curUpdateInfo.md5)
{
DownloadNext(true);
}
else
{
if(File.Exists(m_curUpdateInfo.savepath))
{
File.Delete(m_curUpdateInfo.savepath);
}
if (null != m_fs) m_fs.Close();
DownloadNext(false);
}
}
private void DownloadNext(bool next)
{
if(next)
++m_curDownloadFileIndex;
if (m_curDownloadFileIndex < m_updateInfoList.Count)
{
m_curUpdateInfo = m_updateInfoList[m_curDownloadFileIndex];
var url = m_curUpdateInfo.fullUrl;
var savePath = m_curUpdateInfo.savepath;
HttpRequest(url, savePath);
}
else
{
DownloadFinish();
}
}
private void DownloadError(Exception ex)
{
Debug.LogError(ex);
if (m_ns != null) m_ns.Close();
if (m_fs != null) m_fs.Close();
downlodadState = DownloadState.CommonError;
}
private void DownloadFinish()
{
if (m_ns != null) m_ns.Close();
if (m_fs != null) m_fs.Close();
downlodadState = DownloadState.Finish;
}
}