unity-doc-HybridCLR

对比

lua:是创建一个lua虚拟机来跑热更代码,会有跨域行为。
Ilruntime:创建一个c#虚拟机来跑热更代码,对mono等支持不好,而且还会有跨域行为
hybridclr:在IL2CPP VM中内置一个.net 字节码解释器,同时还会把.net里面的数据对象映射到native的数据对象(内存对其),不会有跨域行为,对unity代码完全支持,而且不需要生成任何中间代码。

例子

git下载入门项目

https://github.com/focus-creative-games/hybridclr_trial

操作

  1. 打开项目
  2. 打开安装菜单,HybirdCLR->installer,点击安装按钮
  3. 生成必要文件,HybridCLR->Generate->All
  4. 构建一次项目,File->Build Settings构建一个自己需要测试的平台的项目。

修改代码

  1. 修改LoadDll.cs,改成加载服务器上的代码,并且修改热更启动代码为HotCodeLaunch
  2. 修改BuildAssetsCommand.cs,热更代码编译生成路径修改,改到Editor下。
LoadDll.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
using HybridCLR;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEngine.Networking;

public class LoadDll : MonoBehaviour
{
public static List<string> AOTMetaAssemblyNames { get; } = new List<string>()
{
"mscorlib.dll",
"System.dll",
"System.Core.dll",
};

void Start()
{
StartCoroutine(DownLoadAssets(this.StartGame));
}

private static Dictionary<string, byte[]> s_assetDatas = new Dictionary<string, byte[]>();

public static byte[] GetAssetData(string dllName)
{
return s_assetDatas[dllName];
}

private string GetWebRequestPath(string asset)
{
var path = $"http://nginx1.gameorse.com/test1/"+asset;
//if (!path.Contains("://"))
//{
// path = "file://" + path;
//}
if (path.EndsWith(".dll"))
{
path += ".bytes";
}
return path;
}

IEnumerator DownLoadAssets(Action onDownloadComplete)
{
var assets = new List<string>
{
"prefabs",
"Assembly-CSharp.dll",
}.Concat(AOTMetaAssemblyNames);

foreach (var asset in assets)
{
string dllPath = GetWebRequestPath(asset);
Debug.Log($"start download asset:{dllPath}");
UnityWebRequest www = UnityWebRequest.Get(dllPath);
yield return www.SendWebRequest();

#if UNITY_2020_1_OR_NEWER
if (www.result != UnityWebRequest.Result.Success)
{
Debug.Log(www.error);
}
#else
if (www.isHttpError || www.isNetworkError)
{
Debug.Log(www.error);
}
#endif
else
{
// Or retrieve results as binary data
byte[] assetData = www.downloadHandler.data;
Debug.Log($"dll:{asset} size:{assetData.Length}");
s_assetDatas[asset] = assetData;
}
}

onDownloadComplete();
}


void StartGame()
{
LoadMetadataForAOTAssemblies();

#if !UNITY_EDITOR
var gameAss = System.Reflection.Assembly.Load(GetAssetData("Assembly-CSharp.dll"));
#else
var gameAss = AppDomain.CurrentDomain.GetAssemblies().First(assembly => assembly.GetName().Name == "Assembly-CSharp");
#endif

//AssetBundle prefabAb = AssetBundle.LoadFromMemory(GetAssetData("prefabs"));
//GameObject testPrefab = Instantiate(prefabAb.LoadAsset<GameObject>("HotUpdatePrefab.prefab"));

const string LAUNCH_MONO_NAME = "HotCodeLaunch";
Type type = gameAss.GetType(LAUNCH_MONO_NAME);
GameObject go = new GameObject(LAUNCH_MONO_NAME, type);
}

private static void LoadMetadataForAOTAssemblies()
{
HomologousImageMode mode = HomologousImageMode.SuperSet;
foreach (var aotDllName in AOTMetaAssemblyNames)
{
byte[] dllBytes = GetAssetData(aotDllName);
LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(dllBytes, mode);
Debug.Log($"LoadMetadataForAOTAssembly:{aotDllName}. mode:{mode} ret:{err}");
}
}
}
BuildAssetsCommand.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
using HybridCLR.Editor.Commands;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;

namespace HybridCLR.Editor
{
public static class BuildAssetsCommand
{
public static string HybridCLRBuildCacheDir => Application.dataPath + "/HybridCLRBuildCache";

public static string AssetBundleOutputDir => $"{HybridCLRBuildCacheDir}/AssetBundleOutput";

public static string AssetBundleSourceDataTempDir => $"{HybridCLRBuildCacheDir}/AssetBundleSourceData";


public static string GetAssetBundleOutputDirByTarget(BuildTarget target)
{
return $"{AssetBundleOutputDir}/{target}";
}

public static string GetAssetBundleTempDirByTarget(BuildTarget target)
{
return $"{AssetBundleSourceDataTempDir}/{target}";
}

public static string ToRelativeAssetPath(string s)
{
return s.Substring(s.IndexOf("Assets/"));
}

/// <summary>
/// 将HotFix.dll和HotUpdatePrefab.prefab打入common包.
/// 将HotUpdateScene.unity打入scene包.
/// </summary>
/// <param name="tempDir"></param>
/// <param name="outputDir"></param>
/// <param name="target"></param>
private static void BuildAssetBundles(string tempDir, string outputDir, BuildTarget target)
{
Directory.CreateDirectory(tempDir);
Directory.CreateDirectory(outputDir);

List<AssetBundleBuild> abs = new List<AssetBundleBuild>();

{
var prefabAssets = new List<string>();
string testPrefab = $"{Application.dataPath}/Prefabs/HotUpdatePrefab.prefab";
prefabAssets.Add(testPrefab);
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
abs.Add(new AssetBundleBuild
{
assetBundleName = "prefabs",
assetNames = prefabAssets.Select(s => ToRelativeAssetPath(s)).ToArray(),
});
}

BuildPipeline.BuildAssetBundles(outputDir, abs.ToArray(), BuildAssetBundleOptions.None, target);
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
}

public static void BuildAssetBundleByTarget(BuildTarget target)
{
BuildAssetBundles(GetAssetBundleTempDirByTarget(target), GetAssetBundleOutputDirByTarget(target), target);
}

[MenuItem("HybridCLR/Build/BuildAssetsAndCopyToStreamingAssets")]
public static void BuildAndCopyABAOTHotUpdateDlls()
{
// BuildTarget target = EditorUserBuildSettings.activeBuildTarget;
// BuildAssetBundleByTarget(target);
// CompileDllCommand.CompileDll(target);
// CopyABAOTHotUpdateDlls(target);

CompileDllCommand.CompileDllActiveBuildTarget();
CopyAOTAssembliesToStreamingAssets();
CopyHotUpdateAssembliesToStreamingAssets();
AssetDatabase.Refresh();
}

public static void CopyABAOTHotUpdateDlls(BuildTarget target)
{
CopyAssetBundlesToStreamingAssets(target);
CopyAOTAssembliesToStreamingAssets();
CopyHotUpdateAssembliesToStreamingAssets();
}


//[MenuItem("HybridCLR/Build/BuildAssetbundle")]
public static void BuildSceneAssetBundleActiveBuildTargetExcludeAOT()
{
BuildAssetBundleByTarget(EditorUserBuildSettings.activeBuildTarget);
}

public static void CopyAOTAssembliesToStreamingAssets()
{
var target = EditorUserBuildSettings.activeBuildTarget;
string aotAssembliesSrcDir = SettingsUtil.GetAssembliesPostIl2CppStripDir(target);
// string aotAssembliesDstDir = Application.streamingAssetsPath;
string aotAssembliesDstDir = Application.dataPath + "/Editor/HybridCLR/HotDlls";

foreach (var dll in LoadDll.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 CopyHotUpdateAssembliesToStreamingAssets()
{
var target = EditorUserBuildSettings.activeBuildTarget;

string hotfixDllSrcDir = SettingsUtil.GetHotUpdateDllsOutputDirByTarget(target);
// string hotfixAssembliesDstDir = Application.streamingAssetsPath;
string hotfixAssembliesDstDir = Application.dataPath + "/Editor/HybridCLR/HotDlls";
foreach (var dll in SettingsUtil.HotUpdateAssemblyFiles)
{
string dllPath = $"{hotfixDllSrcDir}/{dll}";
string dllBytesPath = $"{hotfixAssembliesDstDir}/{dll}.bytes";
File.Copy(dllPath, dllBytesPath, true);
Debug.Log($"[CopyHotUpdateAssembliesToStreamingAssets] copy hotfix dll {dllPath} -> {dllBytesPath}");
}
}

public static void CopyAssetBundlesToStreamingAssets(BuildTarget target)
{
string streamingAssetPathDst = Application.streamingAssetsPath;
Directory.CreateDirectory(streamingAssetPathDst);
string outputDir = GetAssetBundleOutputDirByTarget(target);
var abs = new string[] { "prefabs" };
foreach (var ab in abs)
{
string srcAb = ToRelativeAssetPath($"{outputDir}/{ab}");
string dstAb = ToRelativeAssetPath($"{streamingAssetPathDst}/{ab}");
Debug.Log($"[CopyAssetBundlesToStreamingAssets] copy assetbundle {srcAb} -> {dstAb}");
AssetDatabase.CopyAsset( srcAb, dstAb);
}
}
}
}

增加热更测试代码

  1. 增加HotCodeLaunch.cs 作为代码热更启动入口
  2. 增加TestClass.cs 测试普通类的热更
  3. 增加TestMono.cs 测试mono类的热更
HotCodeLaunch.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using UnityEngine;

/// <summary>
/// 热更启动代码
/// </summary>
public class HotCodeLaunch : MonoBehaviour
{
void Start()
{
Debug.Log("======= HotCodeLaunch Start =======");
TestClass testClass = new TestClass();
testClass.Test1();

GameObject testMono = new GameObject("TestMono");
testMono.AddComponent<TestMono>();
}
}
TestClass.cs
1
2
3
4
5
6
7
8
9
using UnityEngine;

public class TestClass
{
public void Test1()
{
Debug.Log("======= TestClass Test1 =======");
}
}
TestMono.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TestMono : MonoBehaviour
{
private int num = 5;

void Start()
{
Debug.Log("======= TestMono Start Hot =======");
}

void Update()
{
if (num > 0)
{
Debug.LogFormat("======= TestMono Update Hot {0}=======", num);
num--;
}
}
}

发布测试

android

  1. 切换到android平台。
  2. 生成热更代码,HybridCLD/Build/BuildAssetsAndCopyToStreamingAssets,将生成的代码字节文件上传到服务器。
  3. 安装apk测试,
  4. 修改热更代码,HotCodeLaunch.cs、TestClass.cs、TestMono.cs 或新加代码等。
  5. 重新生成热更代码,将生成的代码字节文件上传到服务器。
  6. 重新运行apk测试,看是否是热更后的日志等。

ios

  1. 切换到ios平台
  2. 进入工程目录xxx/HybridCLRData/iOSBuild/
  3. 命令行执行bash ./build_libil2cpp.sh,看build目录下是否有libil2cpp.a且大于60m,有就是成功,否则出错。
  4. 发布xcode项目
  5. 用新生成的libil2cpp.a替换xcode项目Librarieslibil2cpp.a

新工程测试

环境

  • Unity 2021.3.19f1
  • package的manifest.json 添加com.focus-creative-games.hybridclr_unity": "https://gitee.com/focus-creative-games/hybridclr_unity.git,打开packagemanager查阅版本号Version 2.1.0
  • 修改ProjectSettingScripting Backend改成IL2CPPApi Compatiblity Level改为.NET Framework
  • 安装hybridclr+il2cpp_plus代码到本地目录,HybirdCLR->installer,点击安装按钮
  • 生成必要文件,HybridCLR->Generate->All
  • 修改设置,如果打开设置界面失败,去例子工程复制HybridCLRSettings.asset放入到ProjectSetting目录
  • 构建一次项目,File->Build Settings构建一个自己需要测试的平台的项目。

补充

安装HybridCLR package

  1. 打开unity的packageMgr
  2. 点击+号,Add package from git URL,将git或gitee的地址里面的git地址拷贝进去,会自行下载安装。
  3. 或者在:package目录的manifest.json目录添加"com.focus-creative-games.hybridclr_unity": "https://gitee.com/focus-creative-games/hybridclr_unity.git",

错误

Editor代码引用了游戏代码会失败
将游戏代码复制一份到Editor,或先将Editor删除了

Mono.Linker.LinkerFatalErrorException: /Users/popolin/files/git/hhf/unity-go-mhsg/client/Assets/Scripts/BaseCode/GameLauncher.cs(22,9): error IL1005: .GameLauncher.Awake(): Error processing method ‘.GameLauncher.Awake()’ in assembly ‘Assembly-CSharp.dll’

问题出现在非热更代码使用到了热更代码。

方法1:删掉对应的xxx热更模块的Assembly。
方法2:删掉对应引用了热更模块的代码。
方法3:将引用了热更模块代码的代码都放到热更代码模块里面。

Exception: OBSOLETE - Providing Android resources in Assets/Plugins/Android/assets was removed, please move your resources to an AAR or an Android Library. See “AAR plug-ins and Android Libraries” section of the Manual for more details.

方法1:先删除对应的文件夹,等生成完成在加回来。

挂载载资源上的脚本出现Scrip Missing

方法1:资源需要打AssetBundle方式加载,不能直接放到Resource下加载。

相关地址

huatuo入门github
hybridclr国内
hybridclr国外
hybridclr热更框架地址
hybridclr官网文档
HybridCLR入门代码改 https://pan.baidu.com/s/1uZz0ezccXskH3X3xQy6GVQ 提取吗prbs