【底层】关于析构函数、IDisposable 、托管资源、非托管资源
背景
最近在做后台快捷键的功能,当应用不被聚焦时,也需要响应快捷键的输入。于是就用到了Win32 API里的系统钩子,系统钩子会用到IntPtr去记录hookID,这些资源都是非托管资源,绕过了C#的内存管理系统。在网上搜索了腾讯云社区里关于GlobalKeyBoardHook的文章[1]后,比较好奇里面的IntPtr是在哪里被释放的,会不会是在UnhookWindowsHookEx里已经被定义的。
另外我在析构函数(~GlobalKeyBoardHook)里调用了UnhookWindowsHookEx,认为会在游戏结束之后,释放这些系统监听资源。但却在多次游戏退出运行回到Editor状态时,遇到了卡顿,甚至让Unity闪退,造成Fetal Error的情况。而我之后在OnDestroy和OnApplicationQuit里添加了UnhookKeyboard之后,就又恢复正常了。遂开始探究这个问题。
析构函数
析构函数[6]在C#中已经很少使用了,它主要用于释放非托管资源,执行清理任务。它的调用时机是不确定的,当它不被任何对象引用之后,会由.NET的垃圾回收器决定何时进行回收执行(虽然也可以通过GC.Collect去强制执行垃圾回收,它会调用所有未被引用着的对象的析构函数,但是这样做比较耗费性能)。博文[2]里提到垃圾收集器是懒惰的,它并不会在变量出了作用域和生命期后立即回收,而是在它认为适当的时候才回收,一般是内存紧张。甚至文章的实验里,关闭窗体后,进程终止了,才不情愿地执行了垃圾回收。
针对于正在做的系统钩子的功能,增加Debug观察执行时机。
~GlobalKeyboardHook()
{
// 增加Debug,观察调用时机
UnityEngine.Debug.Log("~GlobalKeyboardHook , hook ID:" + _hookID);
// 执行解除,释放非托管资源
this.UnhookKeyboard();
}
发现虽然在析构函数中调用了UnHook,但是实际上在Unity运行停止时,并没有调用析构函数,而是下一次运行才执行。它始终在进行键盘监听和执行回调,回调中有资源的生成,但是我的逻辑里只有运行时中才对这些回调生成的资源进行了一些移除管理,而现在没在运行中,所以造成停止时候卡顿的情况。
IDisposable
析构函数由GC执行,而GC执行的时机是不确定或者会滞后的。所以就有了IDisposable,它是一个接口,用于在使用完资源后进行释放和清理。它定义了一个Dispose()方法,该方法用于释放由对象持有的非托管资源,如文件句柄、数据库连接、网络连接等,且由用户主动去调用(或者使用using关键字自动调用)。另外如果在Dispose()里加上 GC.SuppressFinalize(this);
这个对象就不会被GC再处理了,防止GC再去重复做一些工作,导致错误。所以一般代码都会写成这样:
using UnityEngine;
using System;
public class MyResource : IDisposable
{
private bool disposed = false;
private Texture2D managedTexture; // 托管资源
private IntPtr unmanagedResource; // 非托管资源
public MyResource()
{
managedTexture = new Texture2D(256, 256);
unmanagedResource = SomeNativeLibrary.CreateResource();
}
public void Dispose()
{
// 手动执行托管资源和非托管资源的释放
Dispose(true);
// 表明如果用户手动调用了这个Dispose函数,那就不让GC再执行一遍析构函数了
GC.SuppressFinalize(this);
}
/// disposeManagedResources= true 表示释放非托管资源的同时,也把托管资源释放。
/// disposeManagedResources= false 表示只释放非托管资源
protected virtual void Dispose(bool disposeManagedResources)
{
if (!disposed)
{
if (disposeManagedResources)
{
// 释放托管资源
if (managedTexture != null)
{
UnityEngine.Object.Destroy(managedTexture);
managedTexture = null;
}
}
// 释放非托管资源
if (unmanagedResource != IntPtr.Zero)
{
SomeNativeLibrary.ReleaseResource(unmanagedResource);
unmanagedResource = IntPtr.Zero;
}
disposed = true;
}
}
/// 这里还保留了析构函数,是因为万一用户忘了手动去调用Dispose,那就由GC之后把它释放
~MyResource()
{
Dispose(false);
}
}
标准做法总结[3]
1)继承IDisposable接口;
2)实现Dispose()方法,在其中释放托管资源和非托管资源,并将对象本身从垃圾回收器中移除(让垃圾回收器不在回收此资源);
3)实现类析构函数,在其中释放非托管资源。(这里是为了防止用户忘记调用(2)中实现的Dispose)
关于using关键字
Dispose方法除了手动调用的方式外,通过using关键字创建的对象,在片段结尾也会自动调用Dispose方法,如下:
using(MyResource resource = new MyResource()
{
// ...
}
// 结束后resource.Dispose已经被调用执行。
托管资源/非托管资源区分
托管资源(Managed resources)
托管资源[5]是由托管代码(managed code)创建和管理的资源。它们是由编程语言和运行时环境(.NET CLR 的GC 机制)自动进行内存管理和垃圾回收的,无需显式释放的资源。托管资源包括但不限于以下内容:
- 托管对象:由编程语言创建的对象,如类的实例、数组、委托、接口等。
- 托管堆上的内存:由运行时环境分配和管理的内存。
- .NET提供的文件类:由运行时环境分配和管理的文件句柄(如.NET类库中的FileStream类,继承自Stream类,就是一个托管类,会被CLR托管)
- 反射:C#中的反射机制也是托管资源,通过反射可以动态地获取和操作程序集、类型、成员等。
- 事件:C#中的事件(event)订阅触发都是CLR进行的。
- 异常处理:C#中的异常处理机制也是由CLR进行管理的,CLR会自动捕获并处理异常。
关于FileStream、SqlConnection、Graphic之类的类到底是不是托管类,这个问题可以参考[4]。我认为它们是托管资源,只是它们管理着自己的非托管资源(文件、数据库连接、图像等)。这些对象在被GC回收时或者被用户主动调用Close时候负责去清除它们所占有的非托管资源。
非托管资源(Unmanaged resources)
非托管资源是由非托管代码(unmanaged code)创建和管理的资源。它们不受编程语言和运行时环境的自动内存管理和垃圾回收机制控制,需要手动释放和清理。非托管资源包括但不限于以下内容:
- 非托管对象:由使用非托管语言(如C++)创建的对象。
- 文件资源
- 网络连接
- 数据库资源
- Win32 API:Win32 API是一组用于访问Windows操作系统的函数和数据结构。这些函数和数据结构是以非托管的C/C++语言编写的,可以使用DllImport属性在C#中访问它们。举个例子:
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
参考
[1] https://cloud.tencent.com/developer/information/C%23应用程序中的全局键盘捕获-article
[2] https://blog.csdn.net/q414620221/article/details/52763764
[3] https://www.jianshu.com/p/1e2e897ac5e2
[4] https://www.cnblogs.com/ziyeyimeng/articles/2528749.html
[5] https://www.cnblogs.com/buzheng11/p/17661667.html
[6] https://learn.microsoft.com/zh-cn/previous-versions/66x5fx1b(v%3Dvs.120)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)