简易打AB包工具
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using HybridCLR.Editor;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using System.Text;
using System.Security.Cryptography;
public class ABTool : Editor
{
static string dllpath = Application.dataPath + "/../" + HybridCLRSettings.Instance.hotUpdateDllCompileOutputRootDir;
static string aotpath = Application.dataPath + "/../" + HybridCLRSettings.Instance.strippedAOTDllOutputRootDir;
public static List<string> AOTMetaAssemblyNames { get; } = new List<string>()
{
"mscorlib.dll",
"System.dll",
"System.Core.dll",
};
[MenuItem("ABTool/生成AB包")]
public static void CreatAB()
{
BuildTarget target = EditorUserBuildSettings.activeBuildTarget;
//找到对应目录
DirectoryInfo direction = new DirectoryInfo(dllpath + "/" + target);
//获取目录下所有文件
FileInfo[] files = direction.GetFiles("*", SearchOption.AllDirectories);
//把所有dll文件拷贝到s目录下
foreach (var item in files)
{
if (item.Name.EndsWith(".dll"))
{
string dllPath = $"{dllpath + "/" + target}/{item.Name}";
string dllBytesPath = $"{Application.streamingAssetsPath}/{item.Name}.bytes";
File.Copy(dllPath, dllBytesPath, true);
Debug.Log($"hotfix dll {dllPath} -> {dllBytesPath}");
}
}
//预制体
List<AssetBundleBuild> abs = new List<AssetBundleBuild>();
//找到预制体目录下的所有目录
AddABPath(abs, $"{Application.dataPath}/Prefabs/");
AddABPath(abs, $"{Application.dataPath}/Resources/");
//构建AB包放到S目录下
BuildPipeline.BuildAssetBundles(Application.streamingAssetsPath, abs.ToArray(), BuildAssetBundleOptions.None, target);
//加载补充元数据
CopyAOTAssembliesToStreamingAssets();
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
}
public static void CopyAOTAssembliesToStreamingAssets()
{
var target = EditorUserBuildSettings.activeBuildTarget;
string aotAssembliesSrcDir = SettingsUtil.GetAssembliesPostIl2CppStripDir(target);
string aotAssembliesDstDir = Application.streamingAssetsPath;
foreach (var dll in AOTMetaAssemblyNames)
{
string srcDllPath = $"{aotAssembliesSrcDir}/{dll}";
if (!File.Exists(srcDllPath))
{
Debug.LogError($"ab中添加AOT补充元数据dll:{srcDllPath} 时发生错误,文件不存在。裁剪后的AOT dll在BuildPlayer时才能生成,因此需要你先构建一次游戏App后再打包。");
continue;
}
string dllBytesPath = $"{aotAssembliesDstDir}/{dll}.bytes";
File.Copy(srcDllPath, dllBytesPath, true);
Debug.Log($"[CopyAOTAssembliesToStreamingAssets] copy AOT dll {srcDllPath} -> {dllBytesPath}");
}
}
public static void AddABPath(List<AssetBundleBuild> abs, string path)
{
DirectoryInfo directionInfo = new DirectoryInfo(path);
DirectoryInfo[] directions = directionInfo.GetDirectories();
//每个目录打一个AB包
for (int i = 0; i < directions.Length; i++)
{
List<string> prefabAssets = new List<string>();
Debug.Log("打包文件夹:" + directions[i].Name);
FileInfo[] fileinfos = directions[i].GetFiles("*", SearchOption.AllDirectories);
Debug.Log("目录下文件个数:" + fileinfos.Length);
foreach (var item in fileinfos)
{
if (item.Name.EndsWith(".meta"))
{
continue;
}
Debug.Log("打包文件:" + item.Name);
prefabAssets.Add(item.FullName.Replace("\\", "/"));
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
}
AssetBundleBuild PrefabsAb = new AssetBundleBuild
{
assetBundleName = directions[i].Name,
assetNames = prefabAssets.Select(s => ToReleateAssetPath(s)).ToArray(),
};
abs.Add(PrefabsAb);
}
}
public static string ToReleateAssetPath(string s)
{
string path = s.Substring(s.IndexOf("Assets/"));
Debug.Log(path);
return path;
}
[MenuItem("ABTool/生成version")]
public static void MakeVersion()
{
string json = AssetDatabase.LoadAssetAtPath<TextAsset>("Assets/Resources/Config/Config.json").text;
Config config = JsonConvert.DeserializeObject<Config>(json);
List<VersionData> versionDatas = new List<VersionData>();
//第一条是假数据,用来判断是否需要热更新
VersionData version = new VersionData();
//判断使用测试版还是正式版热更地址
config.upid++;
version.abName = config.isBate ? config.hotPath_Bate : config.hotPath;
version.len = config.upid;//更新号
version.md5 = config.version;//版本号
versionDatas.Add(version);
json = JsonConvert.SerializeObject(config);
File.WriteAllText(Application.dataPath +"/Resources/Config/Config.json", json);
//找到S目录下所有文件
string[] files = Directory.GetFiles(Application.streamingAssetsPath, ".", SearchOption.AllDirectories);
foreach (var item in files)
{
//取后缀名
string extensionName = Path.GetExtension(item);
//排除.meta和.manifest文件
if (extensionName.Contains("meta") || extensionName.Contains("manifest")) continue;
Debug.Log(item);
string abName = item.Replace(Application.dataPath, "Assets");
abName = abName.Replace("\\", "/");
string assetBundleName = "";
assetBundleName = abName.Replace("Assets/StreamingAssets/", string.Empty);
VersionData versionData = new VersionData();
versionData.abName = assetBundleName;
versionData.len = File.ReadAllBytes(item).Length;
versionData.md5 = FileMD5(item);
versionDatas.Add(versionData);
}
//写入文件
string versionInfo = JsonConvert.SerializeObject(versionDatas);
File.WriteAllText(Application.streamingAssetsPath + "/version.txt", versionInfo, Encoding.UTF8);
AssetDatabase.Refresh();
Debug.Log("version.txt创建成功");
}
static string FileMD5(string filepath)
{
FileStream fileStream = new FileStream(filepath, FileMode.Open);
MD5 mD5 = new MD5CryptoServiceProvider();
byte[] bytes = mD5.ComputeHash(fileStream);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.Length; i++)
{
sb.Append(bytes[i].ToString("x2"));
}
return sb.ToString();
}
}
简易AB包管理器
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
public class ABData
{
public string abname;
public AssetBundle ab;
public int num;
}
public class ABManager : Singleton<ABManager>
{
public Dictionary<string, List<string>> reflist = new Dictionary<string, List<string>>();
public string SPath = Main.config.isEditor ? Application.streamingAssetsPath + "/" : Application.persistentDataPath + "/AB/";
public Dictionary<string, byte[]> assetDatas = new Dictionary<string, byte[]>();
public Dictionary<string, ABData> abDic = new Dictionary<string, ABData>();
Assembly ass;
public byte[] GetAssetData(string dllName)
{
return assetDatas[dllName];
}
public AssetBundle LoadAB(string dllName)
{
return AssetBundle.LoadFromMemory(GetAssetData(dllName));
}
public T Load<T>(string refname, string abName, string assetName) where T : UnityEngine.Object
{
AssetBundle ab;
if (abDic.ContainsKey(abName))
{
ab = abDic[abName].ab;
abDic[abName].num++;
}
else
{
ab = AssetBundle.LoadFromFile(SPath + abName);
ABData data = new ABData();
data.abname = abName;
data.ab = ab;
data.num = 1;
abDic.Add(abName, data);
}
if (ab != null)
{
if (!reflist.ContainsKey(refname))
{
reflist.Add(refname, new List<string>());
}
reflist[refname].Add(abName);
}
return ab.LoadAsset<T>(assetName);
}
public GameObject Load(string abName, string assetName)
{
AssetBundle ab;
if (abDic.ContainsKey(abName))
{
ab = abDic[abName].ab;
abDic[abName].num++;
}
else
{
ab = AssetBundle.LoadFromFile(SPath + abName);
ABData data = new ABData();
data.abname = abName;
data.ab = ab;
data.num = 1;
abDic.Add(abName, data);
}
if (ab != null)
{
if (!reflist.ContainsKey(assetName))
{
reflist.Add(assetName, new List<string>());
}
reflist[assetName].Add(abName);
}
return ab.LoadAsset<GameObject>(assetName);
}
public void InitScript()
{
ass = Assembly.Load(GetAssetData("Script.dll.bytes"));
}
public Type LoadScript(string assetName)
{
if (ass == null)
{
ass = Assembly.Load(GetAssetData("Script.dll.bytes"));
}
return ass.GetType(assetName);
}
public void Release(string refname)
{
if (reflist.ContainsKey(refname))
{
foreach (var abName in reflist[refname])
{
if (abDic.ContainsKey(abName))
{
abDic[abName].num--;
if (abDic[abName].num <= 0)
{
ScheduleOnce.Ins.AddSchedule(9, (obj) =>
{
object[] arr = obj as object[];
ABData data = arr[0] as ABData;
if (data.num <= 0)
{
data.ab.Unload(true);
abDic.Remove(abName);
Debug.Log("释放成功");
}
}, abDic[abName]);
}
}
}
reflist.Remove(refname);
}
}
}
热更脚本加载器
using HybridCLR;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using UnityEngine;
using UnityEngine.Networking;
public class LoadDll : MonoBehaviour
{
string SPath;
string PPath;
void Start()
{
SPath = Application.streamingAssetsPath;
PPath = Application.persistentDataPath + "/AB";
StartCoroutine(this.DownLoadAssets( ()=>
{
#if !UNITY_EDITOR
ABManager.Ins.InitScript();
#endif
Main.main.gameObject.AddComponent(ABManager.Ins.LoadScript("Language"));
Instantiate(ABManager.Ins.Load("Login", "Notice"), Main.GetUILayout(UILayout.Tips));
Instantiate(ABManager.Ins.Load("Login", "Login"), Main.GetUILayout(UILayout.UIScene));
}));
}
IEnumerator DownLoadAssets(Action action)
{
//找到对应目录
DirectoryInfo direction = new DirectoryInfo(Main.config.isEditor ? SPath : PPath);
//获取目录下所有文件
FileInfo[] files = direction.GetFiles("*", SearchOption.AllDirectories);
List<string> AOTMetaAssemblyNames = new List<string>();
//加载所有dll文件
foreach (var item in files)
{
Debug.Log("文件:" + item.Name);
if (item.Name.EndsWith(".bytes"))
{
Debug.Log("开始加载:" + item.Name);
string dllPath = item.FullName.Replace("\\", "/");
UnityWebRequest www = UnityWebRequest.Get(dllPath);
yield return www.SendWebRequest();
if (www.result != UnityWebRequest.Result.Success)
{
Debug.Log(www.error);
}
else
{
byte[] data = www.downloadHandler.data;
ABManager.Ins.assetDatas.Add(item.Name, data);
Debug.Log("加载成功:" + item.Name);
AOTMetaAssemblyNames.Add(item.Name);
}
}
}
LoadMetadataForAOTAssemblies(AOTMetaAssemblyNames);
action();
}
private static void LoadMetadataForAOTAssemblies(List<string> AOTMetaAssemblyNames)
{
// 不限补充元数据dll文件的读取方式,你可以从ab、StreamingAssets、或者裸文件下载等办法获得
HomologousImageMode mode = HomologousImageMode.SuperSet;
foreach (var aotDllName in AOTMetaAssemblyNames)
{
Debug.Log(aotDllName);
byte[] dllBytes = ABManager.Ins.GetAssetData(aotDllName); // 获得某个aot dll文件所有字节
Debug.Log(dllBytes);
// 加载assembly对应的dll,会自动为它hook。一旦aot泛型函数的native函数不存在,用解释器版本代码
LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(dllBytes, mode);
Debug.Log($"LoadMetadataForAOTAssembly:{aotDllName}. mode:{mode} ret:{err}");
}
}
}
版本数据
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class VersionData
{
public string abName;
public int len;
public string md5;
}
简易热更框架
using Newtonsoft.Json;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
public class HotUpDate : MonoBehaviour
{
public Scrollbar scrollbar;
public Text text;
// Start is called before the first frame update
string streamingAssetsPath;
string persistentDataPath;
// Start is called before the first frame update
void Start()
{
Debug.Log("开始热更新");
text.text = "开始热更新";
streamingAssetsPath = Application.streamingAssetsPath + "/";
//p目录在unity运行时就会自动创建,咱们要在下面新建一个目录使用
persistentDataPath = Application.persistentDataPath + "/AB/";
//没有p目录,创建p目录,并把s目录下的拷贝到p目录
//有p目录,直接热更新
//拷贝跟热更都是使用协程异步完成的
if (!Directory.Exists(persistentDataPath))
{
Directory.CreateDirectory(persistentDataPath);
// -- todo S 拷贝到P
StartCoroutine(Copy());
}
else
{
// todo 更新
StartCoroutine(CheckUpdate());
}
}
public void Init()
{
GameObject.Find("Main").AddComponent<LoadDll>();
Destroy(gameObject);
}
IEnumerator Copy()
{
Debug.Log("S 拷贝到P");
//找到版本文件地址
string localVersion = streamingAssetsPath + "version.txt";
//读取版本文件
string localVersionContent = File.ReadAllText(localVersion);
//解析版本文件
List<VersionData> localVersionDataList = JsonConvert.DeserializeObject<List<VersionData>>(localVersionContent);
for (int i = 1; i < localVersionDataList.Count; i++)
{
text.text = "验证本地文件";
//更新进度条
scrollbar.size = (float)i / (float)localVersionDataList.Count;
//ab包地址
string tempS = streamingAssetsPath + localVersionDataList[i].abName;
//获取文件名
string fileName = Path.GetFileName(tempS);
//获取文件夹名称
string dir = Path.GetDirectoryName(tempS) + "/";
dir = dir.Replace("\\", "/");//Assets/StreamingAssets/ABTest/xxxx/xxxxx
//把对应的s目录替换成p目录
dir = dir.Replace(streamingAssetsPath, persistentDataPath);
//创建对应目录
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
//拷贝内容
File.Copy(tempS, dir + fileName);
}
//p目录下写入版本文件
File.WriteAllText(persistentDataPath + "version.txt", localVersionContent);
yield return null;
//从p目录下开始热更新
StartCoroutine(CheckUpdate());
}
IEnumerator CheckUpdate()
{
Debug.Log("检查更新");
//检查p目录下的版本文件
string localVersion = persistentDataPath + "version.txt";
string localVersionContent = File.ReadAllText(localVersion);
List<VersionData> localVersionDataList = JsonConvert.DeserializeObject<List<VersionData>>(localVersionContent);
//创建版本对应字典
Dictionary<string, VersionData> dicVersion = new Dictionary<string, VersionData>();
for (int i = 1; i < localVersionDataList.Count; i++)
{
text.text = "检查更新";
//更新进度条
scrollbar.size = (float)i / (float)localVersionDataList.Count;
//把p目录下的版本信息全部存入字典
dicVersion.Add(localVersionDataList[i].abName, localVersionDataList[i]);
}
//获取热更新服务器上的版本文件地址
string serverUrl = localVersionDataList[0].abName + "version.txt";
Debug.Log(serverUrl);
//创建用于接收版本文件的字符串
string serverVesionContent = string.Empty;
Debug.Log(serverVesionContent);
//从热更新服务器上获取版本文件
UnityWebRequest unityWebRequest = UnityWebRequest.Get(serverUrl);
yield return unityWebRequest.SendWebRequest();
//判断是否获取成功
if (unityWebRequest.result == UnityWebRequest.Result.ConnectionError)
{
text.text = "无法连接更新服务器";
Debug.Log(unityWebRequest.error);
yield break;
}
else
{
//获取成功后,给字符串赋值
serverVesionContent = unityWebRequest.downloadHandler.text;
//Debug.Log(serverVesionContent);
//把热更版本文件写入p目录
File.WriteAllText(persistentDataPath + "serverVesion.txt", serverVesionContent);
}
//Debug.Log(persistentDataPath);
//重新读取热更版本文件
string serverVesion = File.ReadAllText(persistentDataPath + "serverVesion.txt");
//解析热更版本文件
List<VersionData> serverVersionDataList = JsonConvert.DeserializeObject<List<VersionData>>(serverVesion);
//创建一个需要更新的文件集合
List<VersionData> updateList = new List<VersionData>();
//本地文件更新号小于网络文件更新号,说明需要更新
if (localVersionDataList[0].len < serverVersionDataList[0].len) //需要更新
{
// 不一样的要更新?????md5 不一样
//新增的文件
for (int i = 1; i < serverVersionDataList.Count; i++)
{
text.text = "查找需要更新的资源";
scrollbar.size = (float)i / (float)serverVersionDataList.Count;
Debug.Log("资源验证" + i);
//判断本地是否有相同名称的资源
if (dicVersion.ContainsKey(serverVersionDataList[i].abName))
{
//有该资源,则通过MD5码判断该资源是否修改过,修改过则加入更新集合
if (dicVersion[serverVersionDataList[i].abName].md5 != serverVersionDataList[i].md5)
{
updateList.Add(serverVersionDataList[i]);
}
}
else
{
//没有该资源,直接加入更新集合
updateList.Add(serverVersionDataList[i]);
}
}
}
else
{
Debug.Log("localVersionDataList[0].len " + localVersionDataList[0].len);
Debug.Log("serverVersionDataList[0].len " + serverVersionDataList[0].len);
Debug.Log("不更新");
//初始化游戏
Init();
//到此结束,停止该协程
yield break;
}
//循环更新集合
for (int i = 0; i < updateList.Count; i++)
{
text.text = "资源下载中";
scrollbar.size = (float)i / (float)updateList.Count;
Debug.Log("资源下载中" + i);
//下载0是网络地址 + 资源i的名字
UnityWebRequest unityWebRequest1 = UnityWebRequest.Get(localVersionDataList[0].abName + updateList[i].abName);
yield return unityWebRequest1.SendWebRequest();
//判断下载是否成功
if (unityWebRequest.result == UnityWebRequest.Result.ConnectionError)
{
Debug.Log(unityWebRequest1.error);
}
else
{
//创建一个p目录下的地址
string filePath = persistentDataPath + updateList[i].abName;
//获取相对路径
string dir = Path.GetDirectoryName(filePath);
dir = dir.Replace("\\", "/");
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
//写入p目录下的地址
File.WriteAllBytes(filePath, unityWebRequest1.downloadHandler.data);
}
}
//重新写入版本文件
File.WriteAllText(persistentDataPath + "version.txt", serverVesionContent);
yield return null;
Debug.Log("更新完成");
Init();
}
}