当前位置: 首页 > news >正文

Unity热更新实战:YooAsset与HybridCLR双核框架搭建指南

1. 项目概述:为什么我们需要一个“双核”热更新框架?

在Unity项目开发的中后期,尤其是上线运营阶段,最让开发者头疼的问题之一就是“更新”。传统的整包更新,需要用户重新下载几百兆甚至几个G的安装包,流失率极高。热更新技术应运而生,它允许我们只下载并替换发生变化的资源或代码,实现“静默”或“快速”更新,极大地提升了用户体验和项目迭代的灵活性。

然而,Unity的热更新方案长期以来存在一个“阿喀琉斯之踵”:代码热更。对于资源(如图片、预制体、配置表),我们有AssetBundle这样的成熟方案。但对于C#脚本逻辑的更新,官方并未提供原生支持。社区涌现了Lua、ILRuntime等方案,但它们都存在一些固有痛点:Lua需要学习另一套语法并与C#进行繁琐交互;ILRuntime等解释执行方案在性能上存在损耗,且对现代C#语法支持有限。

直到HybridCLR的出现,它通过引入Mono/IL2CPP的补充元数据,实现了对C#原生代码(dll)的热更新,性能无损,开发体验与编写普通C#代码无异。但这带来了新的问题:如何高效地管理这些需要热更的dll、AssetBundle以及其他资源?如何实现差异化的更新策略?如何构建一个稳定、可维护的更新管线?

这正是YooAsset的用武之地。YooAsset是一个强大的资源管理系统,它提供了从资源收集、打包、分发到加载、卸载的全生命周期管理。将YooAsset与HybridCLR结合,就构成了一个“双核”热更新框架:YooAsset负责资源(包括HybridCLR的热更dll)的打包与分发,HybridCLR负责C#代码的动态加载与执行。两者各司其职,珠联璧合。

这个框架适合所有中大型、需要长期运营的Unity项目,特别是手游、PC/主机平台的在线游戏、工具类应用等。接下来,我将带你从零开始,一步步搭建这个强大的框架,并分享我在实战中踩过的坑和总结的经验。

2. 环境准备与核心工具解析

在动手之前,我们需要明确所需的工具链和它们各自扮演的角色。一个清晰的技术选型是项目成功的基石。

2.1 Unity版本与后端服务选择

Unity版本:强烈建议使用Unity 2021.3 LTS或更高版本。HybridCLR对高版本Unity的IL2CPP支持更完善,且2021.3是一个长期支持版本,稳定性有保障。本实战也将基于此版本进行。

后端服务:你需要一个资源服务器来存放热更包。对于中小团队或个人开发者,有几种高性价比的选择:

  1. 对象存储服务:如阿里云OSS、腾讯云COS、七牛云Kodo等。它们提供高可用、高并发的静态文件存储与分发服务,按量计费,成本极低,是首选方案。
  2. Web服务器:如果你已有服务器,可以简单地使用Nginx或Apache搭建一个静态文件服务器。
  3. 本地测试:在开发阶段,可以使用Python的http.server模块或任何静态文件服务器在本地搭建一个临时服务器。

注意:无论选择哪种,务必确保服务器支持断点续传文件校验(如MD5或CRC),这对于大文件的热更新至关重要。YooAsset内置了对这些功能的支持。

2.2 YooAsset核心概念与导入

YooAsset不是一个简单的AssetBundle打包工具,它是一个完整的资源操作解决方案。你需要从GitHub或Asset Store获取它的最新版本。

理解YooAsset的几个核心概念:

  • 资源包(Package):一个完整的资源集合,对应一个项目或一个大的功能模块。一个项目可以有多个Package。
  • 资源收集器(Collector):定义哪些资源需要被打包,以及打包的策略(如打包粒度、压缩方式、标签等)。
  • 构建管线(BuildPipeline):执行资源打包的流程。YooAsset提供了内置管线(BuiltinBuildPipeline)和可编程管线(ScriptableBuildPipeline),后者功能更强大灵活。
  • 资源系统(Resource System):运行时的资源加载、卸载管理器。它负责与服务器通信、下载、缓存和提供资源。

导入YooAsset后,你需要根据项目情况选择合适的初始化模式。对于热更新项目,通常使用可更新模式(Updatable Mode),它包含了本地缓存和远程更新能力。

2.3 HybridCLR原理浅析与配置

HybridCLR的热更原理可以简单理解为“打补丁”。在传统的IL2CPP构建中,所有C#代码被编译成一个庞大的libil2cpp.a(或类似)的二进制文件,无法动态修改。HybridCLR做了两件事:

  1. 补充元数据:在构建时,它不仅生成最终的IL2CPP代码,还会额外生成一份包含所有类型、方法等信息的“元数据dll”(补充元数据程序集)。
  2. 运行时加载:在游戏运行时,HybridCLR提供了一个虚拟机(基于libil2cpp改造),能够动态加载新的、符合AOT(预先编译)约束的dll,并利用第一步生成的元数据,将这些新dll中的代码“解释”或“适配”到原有的AOT环境中执行。

配置HybridCLR的关键步骤:

  1. 安装:通过Git URL或下载Release包,将HybridCLR插件导入Unity项目。
  2. 设置HybridCLR:在HybridCLR Settings中,指定热更新程序集列表。通常,你会将需要热更的逻辑放在一个或多个独立的程序集(如Game.Hotfix)中,而将框架、第三方库等稳定代码放在主程序集。
  3. 生成补充元数据:这是构建前必须的一步。HybridCLR编辑器工具会为你指定的AOT程序集生成对应的补充元数据dll。
  4. 构建Player:使用HybridCLR提供的构建选项进行打包。它会自动处理补充元数据与主包的集成。

实操心得:在项目初期就规划好程序集划分。一个常见的做法是:Game.Core(核心框架,不热更)、Game.Hotfix(所有热更逻辑)、Game.Lib(第三方库,视情况决定是否热更)。清晰的划分能极大降低后期的维护成本。

3. 框架整合:构建资源与代码的热更管线

现在,我们将YooAsset和HybridCLR串联起来,构建一条自动化的热更新流水线。核心思路是:将HybridCLR需要热更的dll,也视为一种特殊的资源,通过YooAsset进行打包、版本管理和下载。

3.1 项目结构与程序集划分设计

一个良好的项目结构是成功的一半。我推荐如下结构:

Assets/ ├── GameMain/ # 游戏主逻辑(不热更或部分热更) ├── Hotfix/ # 热更新逻辑目录 │ ├── Scripts/ # 热更C#脚本 │ └── Hotfix.asmdef # 热更程序集定义文件 ├── YooAsset/ # YooAsset运行时与编辑器代码 ├── HybridCLR/ # HybridCLR插件 └── Editor/ # 自定义编辑器工具 ├── BuildScripts/ # 构建脚本 └── Configs/ # 打包配置(如YooAsset收集配置)

Hotfix.asmdef中,你需要勾选Override References,并手动添加对UnityEngineUnityEngine.CoreModule以及你项目核心框架(如Game.Core)的引用。确保它不直接或间接引用任何无法热更的、仅在AOT中存在的第三方库(除非该库也被处理为可热更)。

3.2 使用YooAsset打包HybridCLR热更DLL

这是整合的关键一步。我们不能简单地把dll文件扔进Assets目录让YooAsset打包,因为Unity不会将其视为可打包的资源。我们需要一个“转换”步骤。

步骤一:构建热更DLL首先,你需要将Hotfix程序集编译成独立的DLL。这可以通过一个编辑器脚本,调用UnityEditor.Compilation.CompilationPipeline接口,或者更简单——在HybridCLR完成补充元数据生成后,在输出目录(通常是HybridCLRData/AssembliesPostIl2CppStrip)找到对应的Game.Hotfix.dll

步骤二:将DLL转换为Unity可识别的资源Unity可以加载TextAssetBytes文件。我们可以将Game.Hotfix.dll的字节流,保存为一个.bytes文件,或者直接复制到Assets下的某个目录(如Assets/HotfixDlls),并将其后缀改为.bytes。这样,它就成了一个普通的TextAsset资源。

步骤三:配置YooAsset收集规则在YooAsset的收集配置中,将存放热更DLL.bytes文件的目录(如Assets/HotfixDlls)添加为收集路径。你可以为这些DLL资源打上特定的标签,例如dll

步骤四:编写自定义构建脚本我们需要一个一键式的构建脚本,它按顺序执行以下操作:

  1. 清空临时DLL资源目录。
  2. 触发HybridCLR生成补充元数据并编译出热更DLL。
  3. 将热更DLL复制到Assets/HotfixDlls并重命名为.bytes
  4. 调用YooAsset的构建管线,打包资源(此时DLL已被包含在内)。
  5. 生成版本文件(如package.jsonversion.txt),记录本次构建的资源版本和哈希值。
// 伪代码示例,展示核心流程 public static void BuildHotfixPackage() { // 1. 清理旧DLL资源 CleanHotfixDllAssets(); // 2. 调用HybridCLR命令生成热更DLL // 这里需要调用HybridCLR提供的编辑器方法,具体命令参考其文档 HybridCLR.Editor.Commands.PrebuildCommand.GenerateAll(); // 3. 找到生成的DLL,复制到Assets目录并改后缀 string sourceDllPath = Path.Combine(Application.dataPath, "../HybridCLRData/AssembliesPostIl2CppStrip/Game.Hotfix.dll"); string targetAssetPath = Path.Combine(Application.dataPath, "HotfixDlls/Game.Hotfix.bytes"); File.Copy(sourceDllPath, targetAssetPath, true); AssetDatabase.Refresh(); // 4. 调用YooAsset构建资源包 var buildParameters = new ScriptableBuildParameters(); buildParameters.BuildOutputRoot = GetBuildOutputPath(); buildParameters.BuildVersion = "1.0.0"; // ... 其他参数配置 var builder = new ScriptableBuildPipeline(); var buildResult = builder.Run(buildParameters, true); if (!buildResult.Success) throw new Exception($"YooAsset build failed: {buildResult.ErrorInfo}"); // 5. 生成版本信息文件 GeneratePackageVersionFile(buildResult); }

3.3 设计版本管理与更新策略

一个健壮的更新系统离不开清晰的版本管理。我建议采用双版本号策略:

  • 应用版本号(App Version):对应玩家安装的APP版本(如1.0.0)。只有当资源包接口或HybridCLR运行时发生不兼容变更时,才需要强制升级App。
  • 资源版本号(Resource Version):对应通过YooAsset发布的资源包版本(如101)。每次热更新发布,递增此版本号。

在服务器端,你需要维护一个版本配置文件(如version.json),内容可能如下:

{ "latestAppVersion": "1.0.0", "minSupportedAppVersion": "1.0.0", "resourcePackages": { "default": { "version": 105, "size": 15728640, "hash": "a1b2c3d4e5f67890", "downloadUrl": "https://your-cdn.com/packages/default_v105.unity3d" } } }

客户端启动流程:

  1. 检查本地应用版本是否低于minSupportedAppVersion,如果是,提示玩家去商店下载新APP。
  2. 向服务器请求最新的version.json
  3. 对比服务器与本地记录的resourcePackages版本。
  4. 如果版本落后,YooAsset会根据downloadUrl下载新的资源包文件。
  5. 下载完成后,加载新的热更DLL资源(.bytes文件),并通过HybridCLR运行时加载它。

4. 运行时热更新流程实现

框架搭建好后,我们需要在游戏运行时实现一套完整的更新逻辑。这个流程通常放在游戏启动的第一个场景。

4.1 初始化YooAsset资源系统

首先,我们需要初始化YooAsset。这里以可更新模式为例:

using YooAsset; public class GameLauncher : MonoBehaviour { private ResourcePackage _package; IEnumerator Start() { // 1. 初始化资源系统 YooAssets.Initialize(); // 2. 创建资源包 _package = YooAssets.CreatePackage("DefaultPackage"); // 3. 设置该资源包为默认包 YooAssets.SetDefaultPackage(_package); // 4. 初始化资源包 var initParameters = new OfflinePlayModeParameters(); // 联机模式用 HostPlayModeParameters // initParameters.BuildinRootDirectory = ...; // 设置内置资源根目录 // initParameters.RemoteServices = new RemoteServices("https://your-server.com"); // 设置远程服务地址 var initOperation = _package.InitializeAsync(initParameters); yield return initOperation; if(initOperation.Status != EOperationStatus.Succeed) { Debug.LogError($"资源系统初始化失败: {initOperation.Error}"); yield break; } // 初始化成功,进入更新检查流程 yield return CheckForUpdates(); } }

RemoteServices是一个需要自己实现的类,用于告诉YooAsset如何构建资源文件的下载地址。例如:

public class RemoteServices : IRemoteServices { private readonly string _defaultHostServer; private readonly string _fallbackHostServer; public RemoteServices(string defaultHostServer, string fallbackHostServer = "") { _defaultHostServer = defaultHostServer; _fallbackHostServer = fallbackHostServer; } public string GetRemoteMainURL(string fileName) { return $"{_defaultHostServer}/{fileName}"; } public string GetRemoteFallbackURL(string fileName) { if(string.IsNullOrEmpty(_fallbackHostServer)) return $"{_defaultHostServer}/{fileName}"; return $"{_fallbackHostServer}/{fileName}"; } }

4.2 检查、下载与加载热更资源

CheckForUpdates协程中,我们实现更新逻辑:

private IEnumerator CheckForUpdates() { // 1. 从服务器获取最新版本信息 int localResourceVersion = LoadLocalResourceVersion(); ServerVersionInfo serverVersionInfo = yield return FetchServerVersionInfo(); if(serverVersionInfo.resourcePackages["default"].version > localResourceVersion) { // 2. 创建更新器 var packageVersion = serverVersionInfo.resourcePackages["default"].version.ToString(); var updater = _package.CreateResourceUpdater(packageVersion, 30); // 最大同时下载数 if(updater.TotalDownloadCount > 0) { // 3. 开始下载更新 updater.OnDownloadErrorCallback = OnDownloadError; updater.OnDownloadProgressCallback = OnDownloadProgress; updater.BeginDownload(); yield return updater; if(updater.Status != EOperationStatus.Succeed) { // 处理下载失败 yield break; } // 4. 下载完成,更新本地版本记录 SaveLocalResourceVersion(serverVersionInfo.resourcePackages["default"].version); Debug.Log("资源更新完成!"); } } // 5. 更新完成(或无需更新),加载热更DLL yield return LoadAndInitializeHotfixDll(); }

4.3 集成HybridCLR加载热更程序集

这是最后,也是最关键的一步——让新的C#代码运行起来。

private IEnumerator LoadAndInitializeHotfixDll() { // 1. 通过YooAsset加载热更DLL资源(.bytes文件) var handle = _package.LoadRawFileAsync("Game.Hotfix.bytes"); // 使用你打包时设置的地址或标签 yield return handle; if(handle.Status != EOperationStatus.Succeed) { Debug.LogError($"加载热更DLL失败: {handle.Error}"); yield break; } byte[] dllBytes = handle.GetRawFileData(); // 2. 使用HybridCLR加载程序集 // 注意:HybridCLR的加载API可能随版本变化,请以最新文档为准 // 假设我们使用 Assembly.Load(byte[]) System.Reflection.Assembly hotfixAssembly = System.Reflection.Assembly.Load(dllBytes); if(hotfixAssembly == null) { Debug.LogError("HybridCLR加载程序集失败!"); yield break; } Debug.Log($"热更程序集加载成功: {hotfixAssembly.FullName}"); // 3. 寻找入口点并初始化热更逻辑 // 通常,我们会在热更程序集中定义一个静态的“入口类” Type entryType = hotfixAssembly.GetType("Game.Hotfix.Entry"); if(entryType != null) { System.Reflection.MethodInfo initMethod = entryType.GetMethod("Initialize", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); initMethod?.Invoke(null, null); // 调用静态初始化方法 Debug.Log("热更逻辑初始化完成。"); } else { Debug.LogWarning("未在热更程序集中找到入口类 Game.Hotfix.Entry。"); } // 4. 资源句柄可以释放了(但程序集已加载进内存) handle.Release(); }

Game.Hotfix.Entry类中,你可以进行热更模块的初始化,例如注册新的UI系统、替换旧的游戏逻辑管理器等。

// 位于 Game.Hotfix 程序集中 namespace Game.Hotfix { public static class Entry { public static void Initialize() { Debug.Log("[Hotfix] 热更模块启动!"); // 在这里注册新的系统或替换旧逻辑 // 例如:HotfixUIManager.Register(); // 例如:HotfixGameLogic.ReplaceOldLogic(); } } }

5. 实战避坑指南与高级技巧

理论流程走通了,但实战中会遇到各种“坑”。下面是我总结的一些关键问题和解决方案。

5.1 常见问题与排查清单

问题现象可能原因排查步骤与解决方案
HybridCLR加载dll后报错:找不到类或方法1. 热更dll与主包AOT部分不兼容。
2. 热更dll引用了未补充元数据的AOT类型。
3. 程序集强名称不匹配。
1. 检查构建时HybridCLR的“热更新程序集列表”和“AOT程序集列表”是否配置正确。
2. 确保热更代码中使用的所有AOT类型(包括第三方库)都已生成补充元数据。
3. 使用HybridCLR.Editor.Commands.CompileDllCommand编译dll,确保编译环境一致。
YooAsset下载资源失败1. 服务器地址错误或文件不存在。
2. 网络问题。
3. 本地存储空间不足。
1. 检查RemoteServices返回的URL是否正确,用浏览器直接访问测试。
2. 查看YooAsset的日志,确认错误码。网络超时可考虑重试机制。
3. 在初始化参数中设置DownloadFailedTryAgain(下载失败重试次数)。
热更后,旧资源(如图片)未更新1. YooAsset缓存机制导致。
2. 资源包版本未正确更新。
3. 资源依赖关系错误。
1. 在打包时,确保修改过的资源其哈希值已改变(YooAsset默认根据内容计算哈希)。
2. 强制清理YooAsset的沙盒缓存(Package.ClearPackageSandbox()),但需谨慎使用。
3. 使用YooAsset的依赖查询工具,检查资源引用。
打包后游戏体积异常增大1. 热更dll被重复打包进多个AssetBundle。
2. 资源收集规则包含过多不必要文件。
1. 确保dll资源只在唯一的收集规则中,且打包粒度合理。
2. 仔细检查YooAsset的收集配置,使用标签(Tags)精细控制打包范围。
iOS平台审核被拒苹果禁止通过网络下载可执行代码。这是最重要的限制!HybridCLR的热更机制在iOS上无法通过审核。解决方案:
1.区分平台:iOS平台不使用HybridCLR代码热更,仅使用YooAsset进行资源热更。
2. 使用条件编译,在iOS上不执行LoadAndInitializeHotfixDll逻辑。
3. 考虑为iOS准备一套Lua或纯资源热更方案。

5.2 性能优化与内存管理

  • DLL加载时机:不要在游戏关键帧(如战斗场景)中加载大的热更DLL。应在加载界面、登录前等空闲期完成。
  • 资源分包:不要把所有资源打成一个包。利用YooAsset的分包功能,按功能模块(如“角色”、“场景”、“UI”)分包。玩家可以按需下载,减少初始包体和更新流量。
  • 依赖管理:YooAsset能自动处理AssetBundle之间的依赖。但要避免循环依赖。在打包后,使用YooAsset提供的报告工具查看Bundle依赖图。
  • 内存泄漏:HybridCLR加载的程序集无法卸载(除非重启游戏进程)。因此,热更代码中要特别注意静态引用和事件监听,避免导致原生对象无法被垃圾回收。对于需要卸载的热更功能,考虑设计成通过接口动态创建和销毁。

5.3 自动化构建与持续集成

对于团队项目,自动化构建是必须的。你可以编写一个更强大的编辑器脚本或使用命令行参数,将整个流程集成到Jenkins、GitLab CI等CI/CD工具中。

一个典型的CI流水线可能包括:

  1. 拉取代码。
  2. 执行Unity批处理模式构建Player(调用包含HybridCLR处理的构建方法)。
  3. 执行热更DLL编译与资源打包脚本。
  4. 将生成的资源包(.unity3d文件)和版本信息文件自动上传到CDN或测试服务器。
  5. 发送构建完成通知。

这能确保每次提交或每日构建都能产生可热更的包,极大提升测试和发布效率。

6. 框架扩展与未来展望

基础框架搭建完成后,你可以根据项目需求进行深度定制和扩展。

扩展一:差分更新(Patch)YooAsset支持基于文件哈希的差分更新。但对于热更DLL这种二进制文件,简单的哈希变化会导致整个文件被重新下载。可以集成更精细的二进制差分算法(如bsdiff),在服务器端生成补丁包,客户端下载并合并,这对于大型DLL更新能节省大量流量。

扩展二:热更调试与日志在开发阶段,你肯定需要对热更代码进行调试。可以配置HybridCLR和IDE(如Rider、VS),使其支持加载从服务器下载的符号文件(.pdb),从而实现源码级调试。同时,建立一个从热更代码到主工程日志系统的桥梁,确保所有日志都能被统一收集和查看。

扩展三:资源加密与安全为了防止资源被轻易破解,可以对AssetBundle进行加密。YooAsset支持在打包时或运行时进行加密/解密。你可以实现一个IEncryptionServices接口,在打包时加密文件,在运行时通过YooAsset加载前解密。对于热更DLL,也可以考虑进行混淆或加密,但要注意解密后的内存安全问题。

扩展四:回滚机制万一热更版本出现严重Bug,需要有快速回滚到上一个稳定版本的能力。这需要在版本管理上做设计,例如客户端本地保留最近1-2个版本的资源包,并在更新失败或检测到崩溃率飙升时,自动或提示用户回滚。

构建这样一个完整的“YooAsset+HybridCLR”框架需要投入不少前期精力,但一旦搭建完成,它将为你的项目带来巨大的长期收益:近乎实时的Bug修复、灵活的功能迭代、以及优秀的用户体验。记住,框架是为你服务的工具,在理解其原理的基础上,大胆地根据你的项目特点进行裁剪和定制,才能让它发挥最大价值。

http://www.jsqmd.com/news/1328525/

相关文章:

  • 为什么新店开业要3个月?数字化中台实现“开箱即用”能力
  • qmcdump:3分钟掌握QQ音乐加密格式无损转换技术
  • 4步实现跨平台Steam创意工坊模组下载完整方案
  • 实测测评|新媒体营销方案 PPT 怎么做?智在 PPT 真实上手,不吹不黑
  • Unity图集切割实战:基于Texture2D.SetPixel的离线资源处理工具
  • 如何快速掌握绝区零一条龙:新手必备的完整使用教程
  • 数据标注有“三集”:训练集、验证集、测试集到底有什么区别?
  • 2026暑假老师效率助手:跳绳阅读练字打卡,作业收集批改全搞定
  • 无断链三维轨迹重构与战场情报溯源
  • 十万个why:为什么做RAG 最怕的,不是检索失败,是明令禁止瞎答,它还硬编答案
  • 屈原:世人只知屈原投江,却不知他的楚辞,是华夏浪漫文学的起点
  • 【信息科学与工程学】【数据中心】计算机科学与自动化——第三百零五篇 数据中心 Scale-Up、Scale-Out、Scale-Across101 芯片接入数据中心129
  • 杭州专业的蜂窝板材厂家怎么选?看工艺与交付更靠谱 - 热点品牌推荐
  • 2026徐州靠谱家装企业盘点,本土老牌徐州大自然装饰公司 - 招财兔数字员工
  • 网站时光机HTTrack:3分钟掌握离线浏览与数据备份终极指南
  • 英雄联盟Akari助手:免费开源的游戏效率工具,提升你的操作水平300%
  • 掌握谈判主动权,从读懂这本经典谈判书籍开始
  • 基于ARM+FPGA平台在精密测控领域的应用:ZYNQ高精度温振压多参融合采集系统
  • SpringBoot+Vue电影推荐系统开发与协同过滤算法实践
  • 杭州毛坯房地暖选择与安装全攻略
  • GEO技术如何操控大模型推荐系统及防御方案
  • 复杂电磁环境下石油通信训练,基于 HWG1 分析电磁环境模拟技术
  • AI宠物产品客观评测:核心参数与使用体验的实测解析 - 招财兔数字员工
  • 山海万灵 HarmonyOS 文化知识设计续篇(10):地图缩放与区域选中态协同方案
  • 线上竞拍实操技巧,理性参拍不冲动消费
  • 【限时解密】AI景深效果工业级落地手册:从单目深度估计到DOF物理仿真,附NASA开源光学引擎适配补丁
  • 医院药品与样本配送机器人安全性评测:哪家品牌更值得信赖?
  • [实践经验] 指导 AI 写文章时,别只改一篇稿子:把口癖和空话沉淀成 skill 再审一遍
  • tsMuxer视频封装技术深度解析:专业级传输流复用解决方案
  • AI-SEO优化策略:提升内容在ChatGPT中的可见性