- 前言
- 一、思路概述
- 二、Unity主工程部分
- 1.GameLunch的源码
- 2.ResMgr的源码
- 3.ILRuntimeWrapper的源码
- 三、Addressable部分
- 1.打AA包
- 1.分组问题
- 2.打包和加载包的路径选择
- 2.dll文件和pdb文件的转换
- 1.dll文件转换成byte文件
- 四、ILRuntime部分
- 1.一些工具类
- 1.HotFixSingleton 用于单例,只要继承他就可以了
- 2.MainBehaviour 实现mono的生命周期
- 3.AllUIModel 主要用来存储各种实例
- 2.具体的ui逻辑类
- 1.ContentUIPanelView, 就是具体到单个的Panel的prefab要实现的ui逻辑
- 2.MainUIPanelView 就是具体到单个的Panel的prefab要实现的ui逻辑
- 五、Addressable和ILRuntime的整合
- 六、工程源码
- 总结
- 版权声明
前言
最近在看Unity的Addressable打包和ILRuntime的热更,顺便整理相关过程,记录一下。老规矩,先上效果图
主要演示的内容是通过代码和资产的热更,实现ui的如图交互逻辑。对Addressable 和 ILRuntime基础还不太了解的小伙伴可以先看下我之前的相关的系列文章,先了解下相关的技术,这篇文章是基于这两个的基础进行的。下面我就来详细的说下相关内容吧。
一、思路概述
我的思路是这样的:
把逻辑代码放到ILRuntime里面,项目主工程里面只有启动的入口,剩下的所有逻辑全部放到ILRuntime里面封装成dll文件。
执行顺序大概是这样的
就是把具体要热更哪些资产,也放到ILRuntime里面去执行,主工程里面只是个入口。拿最终效果图来举例的话,就是说我可以随时热更里面的ui资产,随时热更ui交互逻辑。
二、Unity主工程部分
主工程里主要涉及这3块相关的代码
1.GameLunch的源码在主工程里挂载了GameLunch用于游戏启动,加载dll,还会添加两个脚本,ResMgr 和 ILRuntimeWrapper
using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Security; using System.Threading.Tasks; using RenderHeads.Media.AVProVideo; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.Networking; ///2.ResMgr的源码/// 加载方式 /// public enum LoadingMode { ByLocalDll, ByLocalAddressable } public class GameLunch : MonoSingleton{ //public Transform father; [Tooltip("dll文件的加载方式")] public LoadingMode loadingMode=LoadingMode.ByLocalAddressable; public override void Awake() { base.Awake(); Caching.ClearCache(); //初始化游戏框架 //资源管理 gameObject.AddComponent (); gameObject.AddComponent (); //gameObject.AddComponent (); StartHotFix(); //LoadAddressables(); } /// /// 测试加载AA /// ///public async Task LoadAddressables() { //GameObject target= await Addressables.LoadAssetAsync ("Canvas").Task; // var target = await ResMgr.Instance.GetAssetCache ("Canvas"); // GameObject.Instantiate(target); Caching.ClearCache(); // var canvas= Addressables.InstantiateAsync("Canvas"); // await canvas.Task; // await Addressables.InstantiateAsync("MainUIPanel",canvas.Result.transform).Task; } /// /// 加载dll /// ///public async Task StartHotFix() { //去服务器上下载最新的aa包资源 //下载热更代码 //string m_url=null; byte[] dll=new byte[]{}; byte[] pdb = new byte[] {}; if (loadingMode == LoadingMode.ByLocalDll) { //StartCoroutine(CheckHotUpdate(dll,pdb)); } else if (loadingMode==LoadingMode.ByLocalAddressable) { TextAsset assetDll= await Addressables.LoadAssetAsync ("HotFix_Project_dll_res").Task; dll = assetDll.bytes; TextAsset assetPdb= await Addressables.LoadAssetAsync ("HotFix_Project_pdb_res").Task; pdb = assetPdb.bytes; ILRuntimeWrapper.Instance.LoadHotFixAssembly(dll,pdb); ILRuntimeWrapper.Instance.EnterGame(); } // ILRuntimeWrapper.Instance.LoadHotFixAssembly(dll,pdb); // ILRuntimeWrapper.Instance.EnterGame(); } }
主要是为热更工程提供了几个工具接口,用于获取unity的组件;通过addressable实例化ui;通过addressable加载资源到内存
using System; using System.Collections; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; public class ResMgr : MonoSingleton3.ILRuntimeWrapper的源码{ [SerializeField] public Transform parents; public List loadPool; private string m_names; public override void Awake() { base.Awake(); loadPool=new List (); parents = GameObject.Find("Canvas").transform; } /// /// 获取组件 /// /// 名字 ///组件 ///public T GetTarget (string names) where T : UnityEngine.Object { var target = GameObject.Find(names).GetComponent (); return target; } /// /// 通过addressable实例化ui /// /// ui预制体的名字 public async void InstantiateOfAddressables(string names) { //var target = Addressables.InstantiateAsync(names,parents); //await target.Task; //loadPool.Add(names); //m_names = names; //ILRuntimeWrapper.Instance.IsGameStart = false; Addressables.InstantiateAsync(names, parents).Completed += (handle) => OnCompleted(handle,names); //ILRuntimeWrapper.Instance.finishLoadAction("主工程里面的委托@@@@"); } private void OnCompleted(AsyncOperationHandleobj,string names) { Debug.Log($"{names}加载完了"); obj.Result.name = names; //loadPool.Remove(names); //ILRuntimeWrapper.Instance.IsGameStart = true; //ILRuntimeWrapper.Instance.appDomain.Invoke("HotFix_Project.Main", "FixedUpdate", null, obj.Result.name); ILRuntimeWrapper.Instance.TestActionDelegate(names,obj.Result); } private void OnCompleted (AsyncOperationHandle > obj,string names)where T :UnityEngine.Object { Debug.Log($"{names}加载完了"); //obj.Result[0].name = names; //loadPool.Remove(names); //ILRuntimeWrapper.Instance.IsGameStart = true; //ILRuntimeWrapper.Instance.appDomain.Invoke("HotFix_Project.Main", "FixedUpdate", null, obj.Result.name); ILRuntimeWrapper.Instance.TestActionDelegate(names,obj.Result[0]); } /// /// 加载资源到内存 /// /// ////// public async void LoadByAddressable (string names)where T :UnityEngine.Object { //var target = await Addressables.LoadAssetsAsync (names, null, Addressables.MergeMode.None).Task; Addressables.LoadAssetsAsync (names, null).Completed+= (handle) => OnCompleted(handle,names); } }
这个里面把mono的生命周期反射到了ILruntime里面,详细的加载dll和pdb文件,还有就是委托转换器和委托适配器
using System; using System.Collections; using System.Collections.Generic; using System.IO; using ILRuntime.CLR.Method; using ILRuntime.CLR.TypeSystem; using UnityEngine; using ILRuntime.Runtime.Enviorment; using ILRuntime.Runtime.Intepreter; using AppDomain = ILRuntime.Runtime.Enviorment.AppDomain; public class ILRuntimeWrapper : MonoSingleton{ public Action TestActionDelegate; public AppDomain appDomain; private string bindClass; private IType classType; private ILTypeInstance instance; private IMethod updateMethod,fixedUpdateMethod,lateUpdateMethod, awakeMethod, startMethod, onDestroyMethod; private System.IO.MemoryStream m_fs, m_p; private bool m_isGameStart,m_startUpdate; public bool IsGameStart { get => m_isGameStart; set => m_isGameStart = value; } public override void Awake() { base.Awake(); IsGameStart = false; m_startUpdate = false; appDomain = new ILRuntime.Runtime.Enviorment.AppDomain(); } private void FixedUpdate() { appDomain.Invoke(fixedUpdateMethod, instance); } private void Update() { if (IsGameStart) { appDomain.Invoke(updateMethod, instance); } } private void LateUpdate() { if (IsGameStart) { appDomain.Invoke(lateUpdateMethod, instance); } } private void OnDestroy() { if (IsGameStart) { appDomain.Invoke(onDestroyMethod, instance); } } /// /// 加载dll,pdb /// /// /// public void LoadHotFixAssembly(byte[] dll, byte[] pdb) { m_fs = new MemoryStream(dll); //m_p = new MemoryStream(pdb); try { appDomain.LoadAssembly(m_fs, null, new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider()); } catch { Debug.LogError("加载热更DLL失败,请确保已经通过VS打开Assets/Samples/ILRuntime/1.6/Demo/HotFix_Project/HotFix_Project.sln编译过热更DLL"); return; } appDomain.DebugService.StartDebugService(56000); InitializeILRuntime(); } private void InitializeILRuntime() { #if DEBUG && (UNITY_EDITOR || UNITY_ANDROID || UNITY_IPHONE) //由于Unity的Profiler接口只允许在主线程使用,为了避免出异常,需要告诉ILRuntime主线程的线程ID才能正确将函数运行耗时报告给Profiler appDomain.UnityMainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId; #endif //这里做一些ILRuntime的注册,HelloWorld示例暂时没有需要注册的 //Action的参数为一个string Debug.Log("主工程里注册委托"); appDomain.DelegateManager.RegisterMethodDelegate (); //unityAction的委托转换器 appDomain.DelegateManager.RegisterDelegateConvertor ((act) => { return new UnityEngine.Events.UnityAction(() => { ((Action)act).Invoke(); }); }); } /// /// 进入游戏 /// public void EnterGame() { //HelloWorld,第一次方法调用 //appDomain.Invoke("HotFix_Project.InstanceClass", "StaticFunTest", null, null); appDomain.DelegateManager.RegisterMethodDelegate(); IsGameStart = true; //开始调用热更工程 InitHotFixMethod(); //开始执行热更工程 //appDomain.Invoke("HotFix_Project.MainBehaviour","Awake",null,null); } public void InitHotFixMethod() { bindClass = "HotFix_Project.MainBehaviour"; if (IsGameStart) { classType = appDomain.LoadedTypes[bindClass]; instance = (classType as ILType).Instantiate(); awakeMethod = classType.GetMethod("Awake", 0); startMethod = classType.GetMethod("Start", 0); updateMethod = classType.GetMethod("Update", 0); onDestroyMethod = classType.GetMethod("OnDestroy", 0); fixedUpdateMethod = classType.GetMethod("FixedUpdate", 0); lateUpdateMethod = classType.GetMethod("LateUpdate", 0); if (awakeMethod!=null) { appDomain.Invoke(awakeMethod, instance); } } //开始调用热更工程的start appDomain.Invoke(startMethod, instance); } }
三、Addressable部分
之前一直在用AB包,了解到AA包,就尝试的用下,发现确实好用很多。
1.打AA包 1.分组问题我是分成了3个部分来打的AA包,
Dll组 主要是热更工程的代码封装成的dll文件,
UI组 主要是几个uiPanel的Prefab
最后这个组是用的AA包自带的分析工具,自动生成的。
注意:AA包是无法直接打dll文件的,需要把dll文件转换成byte文件然后再打AA包,读取的时候再解析成dll文件。
2.打包和加载包的路径选择因为公司这边的服务器无法直接通过url上传文件,所以我之能把AA包打在本地,然后手动上传服务器,加载AA包的路径还是直接写的服务器的url
这是我打包成功之后,可以看到的相关的AA包文件,我会手动的上传到服务器
刚才也说过了,dll文件是无法直接打成AA包的,不受支持的文件类型,所以我们要把dll文件转成byte文件,从而能打成AA包。
1.dll文件转换成byte文件先上脚本源码:
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using UnityEngine.AddressableAssets;
public class DllToBytes
{
public static string normalPath =Application.dataPath+ "/ILRunTimeAndAddressable/AssetsPackage/HotFixDll";
public static string normalPathToSave =Application.dataPath+ "/ILRunTimeAndAddressable/AssetsPackage/HotFixDllToBytes";
[MenuItem("MyMenu/ILRuntime/DllToByte")]
public static void DllToByte()
{
DllToByte(true);
}
[MenuItem("MyMenu/ILRuntime/DllToByteChoose")]
public static void DllToByteChoose()
{
DllToByte(false);
}
private static void DllToByte(bool autoChoosePath)
{
string folderPath,savePath;
if (autoChoosePath)
{
folderPath = normalPath;
}
else
{
folderPath = EditorUtility.OpenFolderPanel("dll所在的文件夹", Application.dataPath + "/addressable/IlRuntime", string.Empty);
}
if (string.IsNullOrEmpty(folderPath))
{
return;
}
DirectoryInfo directoryInfo=new DirectoryInfo(folderPath);
FileInfo[] fileInfos = directoryInfo.GetFiles();
List listDll=new List();
List listPdb=new List();
for (int i = 0; i
if (fileInfos[i].Extension==".dll")
{
listDll.Add(fileInfos[i]);
}
else if (fileInfos[i].Extension==".pdb")
{
listPdb.Add(fileInfos[i]);
}
}
if (listDll.Count+listPdb.Count<=0)
{
Debug.Log("文件夹下没有dll文件");
}
else
{
Debug.Log("路径为:"+folderPath);
}
if (autoChoosePath)
{
savePath = normalPathToSave;
}
else
{
savePath= EditorUtility.OpenFolderPanel("dll要保存的文件夹", Application.dataPath + "/addressable/IlRuntime", string.Empty);
}
Debug.Log("-----开始转换dll文件------");
string path = string.Empty;
for (int i = 0; i < listDll.Count; i++)
{
//$ 符号的作用:等同于string.Format(),不用写占位符了,直接拼起来就可以了
path = $"{savePath}/{Path.GetFileNameWithoutExtension(listDll[i].Name)}_dll_res.bytes";
Debug.Log(path);
BytesToFile(path,FileToBytes(listDll[i]));
}
Debug.Log("------dll文件转换结束---------");
Debug.Log("-----开始转换pdb文件------");
for (int i = 0; i < listPdb.Count; i++)
{
//$ 符号的作用:等同于string.Format(),不用写占位符了,直接拼起来就可以了
path = $"{savePath}/{Path.GetFileNameWithoutExtension(listPdb[i].Name)}_pdb_res.bytes";
BytesToFile(path,FileToBytes(listPdb[i]));
}
Debug.Log("------pdb文件转换结束---------");
AssetDatabase.Refresh();
}
///
/// file转byte
///
///
///
private static byte[] FileToBytes(FileInfo fileInfo)
{
return File.ReadAllBytes(fileInfo.FullName);
}
///
/// byte转文件
///
///
///
private static void BytesToFile(string path, byte[] bytes)
{
Debug.Log($"路径为:{path}nlength:{bytes.Length}");
File.WriteAllBytes(path,bytes);
}
}
此脚本放到Editor下面,在unity的工具栏会有对应的选项
有两个功能,第一个是默认转换固定路径下的dll和pdb文件为byte文件,第二是手动选取文件目录
四、ILRuntime部分 1.一些工具类 1.HotFixSingleton 用于单例,只要继承他就可以了
using UnityEngine; public class HotFixSingleton2.MainBehaviour 实现mono的生命周期where T : class, new() { private static T m_instance; public static T Instance { get { if (m_instance == null) { T t = new T(); if (t is MonoBehaviour) { //Debug.LogError("该类不是普通类,需要继承MonoSingleton"); return null; } m_instance = t; } return m_instance; } } }
有了这个类,在热更里面也可以用mono的生命周期了,而且性能还是有保障的,用了无GC的反射来做的。
因为AA的加载和实例都是支线程的异步加载,所以我在这个脚本里面加了个资源加载完成的委托,用的也是Action,来降低性能消耗。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
using HotFix_Project.UI;
namespace HotFix_Project
{
public class MainBehaviour:HotFixSingleton
{
public void Awake()
{
Debug.Log("开始Awake");
//声明委托
ILRuntimeWrapper.Instance.TestActionDelegate = FinishLoad;
//HotFixEventDispatcher.GameEvent.DispatchEvent(1000);
UIManager.Instance.LoadUI("MainUIPanel");
UIManager.Instance.LoadUI("ContentUIPanel");
//ResMgr.Instance.InstantiateOfAddressables("MainUIPanel");
//ResMgr.Instance.InstantiateOfAddressables("ContentUIPanel");
}
///
/// 添加游戏逻辑
///
public void Start()
{
Debug.Log("开始Start");
// HotFixEventDispatcher.GameEvent.DispatchEvent(1001);
}
public void Update()
{
// HotFixEventDispatcher.GameEvent.DispatchEvent(1002);
}
public void LateUpdate()
{
//Debug.Log("LateUpdate");
}
public void FixedUpdate()
{
// Debug.Log("FixedUpdate");
}
public void OnDestroy()
{
//HotFixEventDispatcher.GameEvent.DispatchEvent(1003);
}
///
/// 实例完prefab的回调
///
///
///
public void FinishLoad( string methodName,object target)
{
Debug.Log($"开始回调——————{target}");
//HotFixEventDispatcher.GameEvent.DispatchEvent(2000);
switch (methodName)
{
case "MainUIPanel":
AllUIModel.Instance.MainUIPanel = (GameObject)target;
MainUIPanelView.Instance.InitUI();
break;
case "ContentUIPanel":
AllUIModel.Instance.ContentUIPanel = (GameObject)target;
ContentUIPanelView.Instance.InitUI();
break;
case "睡觉":
ContentUIPanelView.Instance.texture1 = (Sprite)target;
break;
case "1000":
ContentUIPanelView.Instance.texture2 = (Sprite)target;
break;
default:
break;
}
}
}
}
3.AllUIModel 主要用来存储各种实例
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
namespace HotFix_Project.UI
{
///
/// 主要是存放各种UIPanel的实例
///
class AllUIModel : HotFixSingleton
{
///
/// 主页面的panel,内容页面的panel
///
public GameObject MainUIPanel, ContentUIPanel;
public AllUIModel()
{
MainUIPanel = new GameObject();
ContentUIPanel = new GameObject();
}
}
}
2.具体的ui逻辑类
1.ContentUIPanelView, 就是具体到单个的Panel的prefab要实现的ui逻辑
其中一个是主界面的uiPanel,一个是显示界面的uiPanel
ContentUIPanelView
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
namespace HotFix_Project.UI
{
class ContentUIPanelView:HotFixSingleton
{
///
/// 切换按钮,关闭按钮
///
public Button cutButton, closeButton;
public Image changeImage;
public Sprite texture1, texture2;
public void InitUI()
{
//base.InitUI();
Debug.Log("开始ContentUIPanel的初始化");
cutButton= ResMgr.Instance.GetTarget
2.MainUIPanelView 就是具体到单个的Panel的prefab要实现的ui逻辑
MainUIPanelView
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
namespace HotFix_Project.UI
{
class MainUIPanelView:HotFixSingleton
{
public Button showButton;
public void InitUI()
{
//base.InitUI();
Debug.Log("开始MainUIPanel的初始化");
showButton= ResMgr.Instance.GetTarget("ShowButton");
showButton.onClick.AddListener(OnShowButton);
AllUIModel.Instance.MainUIPanel.SetActive(true);
}
///
/// 显示按钮的事件
///
private void OnShowButton()
{
Debug.Log("按下了显示按键");
AllUIModel.Instance.ContentUIPanel.SetActive(true);
AllUIModel.Instance.MainUIPanel.SetActive(false);
}
}
}
五、Addressable和ILRuntime的整合
如果单纯用ILRuntime的话,就是直接执行他生成的dll文件就可以了,现在加上AA打包,就是把dll文件打成AA包放在服务器,工程启动的时候是先从服务器下载打成AA包的dll文件,解析后执行dll文件里面的逻辑,dll文件内的逻辑再从服务器下载指定的AA包并且实例,加载完成后,再执行相关的逻辑,比如一个button,需要确实这个panel的Prebab已经从AA包实例完了,再去添加监听事件。
如果要修改内容,重新运行的流程是这样的:
1.修改内容
2.ILRuntime热更工程重新生成dll文件
3.在主工程里手动把dll文件转换成byte
4.addressable重新打包
5.确认服务器的aa包内容是否更新
6.运行unity
六、工程源码
工程源码等上传hub再来补充链接。
总结
欢迎大佬多多来给萌新指正,欢迎大家来共同探讨。
如果各位看官觉得文章有点点帮助,跪求各位给点个“一键三连”,谢啦~
声明:本博文章若非特殊注明皆为原创原文链接
https://blog.csdn.net/Wrinkle2017/article/details/122668665
————————————————————————————————
版权声明:本博客为非营利性个人原创
所刊登的所有作品的著作权均为本人所拥有
本人保留所有法定权利,违者必究!
对于需要复制、转载、链接和传播博客文章或内容的
请及时和本博主进行联系
对于经本博主明确授权和许可使用文章及内容的
使用时请注明文章或内容出处并注明网址
转载请附上原文出处链接及本声明



