简介:面向C#与VB.NET开发者的OpenCvSharp计算机视觉示例代码包,覆盖.NET Core、.NET Framework及UWP平台,集成了通用示例代码库、VB.NET专用模块和基础功能封装库,适合进行跨平台图像处理、摄像头应用开发或OpenCV二次学习的初中级程序员。压缩包共256个文件,约24.23MB,以127个cs源码文件为主体,配合xaml界面、config/xml配置、jpg与bmp样例图像及UWP工程文件,按语言和平台分模块组织,便于快速定位。目前已有78人参与学习下载。包内除了常规图像读写与滤波示例,还包含UWP摄像头采集实例、VB.NET调用案例以及机器学习/深度学习相关的prototxt模型描述文件,可直接参考图像预处理、算法封装和跨平台部署的完整代码结构,有助于减少重复造轮子的时间,用于真实项目起步或课程设计均较合适。
1. 这个代码库是什么:C#与VB.NET开发者的跨平台图像处理方案
先讲一个真实场景:某开发者接手一个桌面质检项目,C# WinForms 写的,要求在截图里定位 ROI 并做灰度比对。他第一次接触 OpenCV 时先去翻官方绑定,结果发现官方对 .NET 的支持一直不温不火,部署时还经常遇到原生依赖纠缠不清的问题。后来换到 OpenCvSharp,从读图到灰度化再到二值化,半小时跑通,他才意识到:在 .NET 生态里做图像处理,真正好用的路径是 OpenCvSharp 这样的封装绑定,而不是去折腾官方库。标题里这个资源包,本质就是围绕 OpenCvSharp 整理出来的一套代码集合:既覆盖 C# 和 VB.NET 两套语言,又兼容 .NET Core 与 .NET Framework 两种运行时,还单独塞了一个 UWP 摄像头采集的实测示例。换句话说,你拿到手的不只是几个 Demo 文件,而是一条从环境搭建、基础能力封装到摄像头实时采集的完整落地链路。适合谁用?如果你在 Windows 上做桌面工具、质检脚本、WPF/WinForms 项目,或者正准备把 OpenCV 能力接进 UWP 应用,这个方向值得你往下读。已经用 Python 版 OpenCV 写过代码的开发者,也能在半小时内把手感迁移过来——坑比想象中少,但坑的位置几乎完全不一样。
2. OpenCvSharp 环境搭建:.NET Core 与 .NET Framework 的配置差异
2.1 包选择:OpenCvSharp4 与运行时包的关系
先从 NuGet 说起。你搜 OpenCvSharp 会看到好几个名字相近的包,很容易只装一个就跑,然后被 DllNotFoundException 卡住半天。常见且可靠的做法是装两个包:OpenCvSharp4 提供托管层,也就是所有 C# 类型、Mat 封装、函数入口;OpenCvSharp4.runtime.win 提供 Windows 下的原生 DLL,也就是 OpenCvSharpExtern.dll 以及它背后依赖的 OpenCV 原生库。托管层负责 P/Invoke 调用,原生层负责真正干活,两层缺一不可。
dotnet add package OpenCvSharp4 dotnet add package OpenCvSharp4.runtime.win如果你用的是 Visual Studio 的包管理器,也可以直接在 NuGet 界面里依次搜索并安装这两个包。逻辑说明:OpenCvSharp4 本身不含任何原生代码,编译时不会报错,运行时才暴露问题,所以很多人误以为只装这个就够了。参数说明:runtime.win 这个包名带 win 后缀,意味着它只覆盖 Windows;如果你后面要部署到 Linux 容器或边缘设备,需要额外装系统级 OpenCV,或者找对应的 runtime.linux 包。
.NET Framework 项目与 .NET Core 项目在引用上略有区别。Framework 4.6.1 及以上的项目,安装 OpenCvSharp4 后通常会自动把原生 DLL 复制到输出目录;.NET Core 3.1 及以上的项目同样如此。但老项目如果目标框架定在 4.5,大概率装不上或者运行时报错,建议先确认目标框架。
2.2 最小 Demo:读图、灰度化、保存
环境装好后,第一件事不是看封装库,而是先写一个最小 Demo 验证底层调用链是否通。新建控制台项目,贴下面这段代码。
using OpenCvSharp; class Program { static void Main() { using (var src = Cv2.ImRead("sample.jpg", ImreadModes.Color)) { using (var gray = new Mat()) { Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY); Cv2.ImWrite("output.jpg", gray); } } System.Console.WriteLine("done"); } }逻辑说明:ImRead 把图片读成 Mat,CvtColor 做颜色空间转换,ImWrite 写回磁盘。三个都是 OpenCvSharp 对原生 OpenCV 函数的直接封装,能跑通就说明托管层和原生层的链接是完整的。参数说明:ImreadModes.Color 对应 CV_8UC3 的三通道彩色图;如果读灰度原图,可以改用 ImreadModes.Grayscale,省掉一次 CvtColor。代码里的 using 不能省,Mat 内部持有非托管内存,不释放的话,跑几次循环内存就上去了。
这一步如果报错,多数是原生 DLL 没被复制到输出目录。打开 bin 目录看有没有 OpenCvSharpExtern.dll,没有就去检查 runtime 包有没有安装,再不行就手动把 DLL 的 CopyLocal 属性改为 True。
2.3 PlatformTarget 与原生 DLL 架构匹配
接下来是这块最烧人的地方:位数匹配。OpenCV 原生库是区分 x64 和 x86 的,而 .NET 项目默认的 AnyCPU 在运行时既可能跑 64 位也可能跑 32 位,取决于宿主进程。如果你的程序集被一个 32 位宿主加载,原生 DLL 却是 64 位,运行时直接给你抛 BadImageFormatException,这属于典型的玄学报错——代码没毛病,环境在打架。
| 项目平台 | 原生 DLL | 结果 |
|---|---|---|
| x64 | x64 | 正常 |
| x86 | x86 | 正常 |
| AnyCPU | x64 | 在 64 位进程下正常,32 位宿主下 Crash |
| AnyCPU | x86 | 在 64 位进程下 Crash |
我一般会在项目文件里显式钉死目标平台,而不是依赖默认值。修改 csproj 文件:
<PropertyGroup> <PlatformTarget>x64</PlatformTarget> </PropertyGroup>逻辑说明:PlatformTarget 决定程序集以什么位数 JIT 编译和加载,钉死 x64 后就别再改,除非你的部署环境明确是 32 位。参数说明:如果有原生插件或第三方 COM 组件是 32 位,你被迫用 x86,那就把 runtime 包也换成 x86 对应的原生文件,两个保持一致。这个检查清单值得记下来:程序集位数、原生 DLL 位数、宿主进程位数三者一致,缺一个都会在运行时翻车。跨平台方向上还要注意,runtime.win 只解决 Windows 部署,Linux 下需要另行处理。
3. 通用基础功能封装库:从 Mat 到 Bitmap 的常用方法实现
3.1 封装形态:静态工具类与资源释放
拿到这类代码库之后,大多数人会先看示例模块,再把通用的读图、转灰度、缩放、保存这些操作抽到一个类里。常见做法是写一个静态工具类 ImageHelper,而不是实例类。原因很简单:图像处理函数是无状态的,输入 Mat 输出 Mat,不需要保存成员变量,静态方法调用最短、最直接。另一个原因是释放逻辑可以集中在同一个地方处理,降低调用方忘记释放的概率。
using OpenCvSharp; using System.Drawing; using System.Drawing.Imaging; public static class ImageHelper { public static Mat Load(string path, ImreadModes mode = ImreadModes.Color) { return Cv2.ImRead(path, mode); } public static Mat ToGray(Mat src) { var gray = new Mat(); Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY); return gray; } public static Mat Resize(Mat src, int width, int height) { var dst = new Mat(); Cv2.Resize(src, dst, new Size(width, height)); return dst; } public static void Save(Mat src, string path) { Cv2.ImWrite(path, src); } public static Bitmap ToBitmap(Mat src) { return BitmapConverter.ToBitmap(src); } public static Mat FromBitmap(Bitmap bmp) { return BitmapConverter.ToMat(bmp); } }逻辑说明:每个方法都返回新的 Mat 或 Bitmap,调用方负责 Dispose;方法内部不持有任何静态缓存,所以线程安全压力小。ToGray 和 Resize 内部 new 出来的 Mat,是给调用方的新对象,不能在这里释放,释放责任在调用方。参数说明:ImreadModes 默认走彩色模式;Resize 没指定插值算法时 OpenCV 默认用双线性插值,日常够用,但做缩略图或 OCR 预处理时建议改成 InterpolationFlags.Area 或 Cubic,效果差别明显。这个方法清单基本覆盖了桌面项目里八成以上的需求,再往上加就是画框、直方图、模板匹配这类场景函数,不建议全部塞进同一个类,按业务拆模块更好维护。
3.2 核心方法:从 Bitmap 到 Mat 的互转
Windows 桌面开发绕不开 System.Drawing.Bitmap,但 OpenCvSharp 的 Mat 才是算法核心。两者互转是出现频率最高的操作之一。上面代码里的 BitmapConverter 是 OpenCvSharp 自带的互转工具,它兼容 24 位和 32 位位图,但有几个边界要注意。
public static Bitmap MatToBitmap(Mat src) { if (src.Type() == MatType.CV_8UC1) { var bgr = new Mat(); Cv2.CvtColor(src, bgr, ColorConversionCodes.GRAY2BGR); var bmp = BitmapConverter.ToBitmap(bgr); bgr.Dispose(); return bmp; } return BitmapConverter.ToBitmap(src); }逻辑说明:灰度图是单通道 CV_8UC1,直接转 Bitmap 在某些显示控件里会变成奇怪的伪彩色或直接花屏,先转成三通道 BGR 再转能规避这个问题。参数说明:这里临时生成的 bgr 用完立刻 Dispose,避免每次调用泄漏 3 通道内存;如果你的场景里灰度图转 Bitmap 是高频操作,建议缓存转换结果而不是每次新建。
3.3 C# 与 VB.NET 调用封装库的语法对照
这个代码库里专门给了 VB.NET 专用示例模块,说明 VB 开发者群体确实存在,且语法坑和 C# 不太一样。同一个封装库,两种语言调用时差异集中在 Imports、Using 语句和对象初始化上。
using OpenCvSharp; var src = ImageHelper.Load("sample.jpg"); using (var gray = ImageHelper.ToGray(src)) { ImageHelper.Save(gray, "output.jpg"); } src.Dispose();Imports OpenCvSharp Dim src As Mat = ImageHelper.Load("sample.jpg") Using gray As Mat = ImageHelper.ToGray(src) ImageHelper.Save(gray, "output.jpg") End Using src.Dispose()逻辑说明:C# 的 using 关键字和 VB 的 Using...End Using 在语义上完全一致,都是保证 IDisposable 对象在作用域结束时被释放。VB 对 Imports 的敏感度比 C# 更高,漏一条常常等到运行时报错才意识到。参数说明:VB 的 Dim 语句配合 As Mat 显式声明类型,和 C# 的 var 不同,VB 也支持类型推断写法 Dim src = ImageHelper.Load(...) ,但建议保留 As 声明,方便维护。
VB.NET 有个容易踩的差异:Option Strict On 开启后,整数类型之间的隐式转换会被禁止,Mat 构造函数的边界参数如果从 Double 传给 Int32,编译会直接报错。解决方式是用 CInt 或 CType 显式转换。这个代码库专门区分 VB.NET 示例模块,大概率也是因为这些问题在 C# 里根本不出现,C# 开发者很难理解为什么要额外准备一套 VB 示例。
4. UWP 摄像头应用开发:从 MediaCapture 到 OpenCvSharp 处理管线
4.1 MediaCapture 初始化与预览启动
UWP 摄像头采集和传统的 WinForms AForge 套路完全不同:UWP 强制走 MediaCapture 管线,不能直接操作摄像头句柄。先把基本采集能力跑起来,再谈图像处理。
using Windows.Media.Capture; using Windows.Media.Capture.Frames; using Windows.Media; using Windows.Graphics.Imaging; using System.Linq; public sealed partial class MainPage : Page { private MediaCapture _mediaCapture; private MediaFrameReader _frameReader; private async Task InitCameraAsync() { _mediaCapture = new MediaCapture(); await _mediaCapture.InitializeAsync(new MediaCaptureInitializationSettings { StreamingCaptureMode = StreamingCaptureMode.Video }); var source = _mediaCapture.FrameSources.Values .FirstOrDefault(s => s.Info.MediaStreamType == MediaStreamType.VideoPreview); _frameReader = await _mediaCapture.CreateFrameReaderAsync(source, MediaEncodingSubtypes.Bgra8); _frameReader.FrameArrived += OnFrameArrived; await _frameReader.StartAsync(); } }逻辑说明:CreateFrameReaderAsync 的第二个参数是输出像素格式,这里指定 Bgra8 是为了后续转 Mat 时直接映射到 CV_8UC4,省掉一次像素格式转换。MediaEncodingSubtypes 在 Windows.Media 命名空间下,项目引用里必须包含。参数说明:MediaStreamType.VideoPreview 对应预览流,和 Photo 流不同;如果你要拍高清照片,应改用 Photo 流,分辨率完全是两套参数。
别忘了在 Package.appxmanifest 里声明摄像头权限,否则运行时直接抛 Access Denied。声明顺序:功能标签里勾选 Webcam 和 Microphone,前者管视频,后者管录音。如果你只做图像分析,可以单独用视频源初始化,StreamingCaptureMode 设为 Video 而不是 AudioAndVideo,这样省掉麦克风权限的合规负担。
4.2 SoftwareBitmap 转 Mat:像素格式与步长
MediaFrameReader 回传的帧包装在 VideoFrame 里,内部是 SoftwareBitmap。OpenCvSharp 不认 SoftwareBitmap,必须转成 Mat 才能调用算法。这一步看起来简单,坑却不小。
private void OnFrameArrived(MediaFrameReader sender, MediaFrameArrivedEventArgs args) { using (var frame = sender.TryAcquireLatestFrame()) { if (frame == null) return; var softwareBitmap = frame.VideoMediaFrame.SoftwareBitmap; if (softwareBitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8) softwareBitmap = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8); byte[] buffer = new byte[softwareBitmap.PixelWidth * softwareBitmap.PixelHeight * 4]; softwareBitmap.CopyToBuffer(buffer.AsBuffer()); using (var mat = new Mat(softwareBitmap.PixelHeight, softwareBitmap.PixelWidth, MatType.CV_8UC4, buffer)) { using (var cloned = mat.Clone()) { ProcessFrame(cloned); } } } }逻辑说明:CopyToBuffer 会把整帧像素按 BGRA 顺序连续拷贝到 byte[],此时 byte[] 的布局正好等于一个 CV_8UC4 的 Mat。new Mat(rows, cols, type, buffer) 把字节数组直接包进 Mat,但 Mat 内部只持有数组引用的固定句柄,不复制数据,所以必须 Clone 一次,让 Mat 拥有独立内存,否则 buffer 一旦被 GC 移动或释放,Mat 就成了野指针。参数说明:PixelWidth 和 PixelHeight 的顺序不能写反,Mat 构造函数先 rows 后 cols,对应的是高和宽,写反之后图像直接转置。想省这一步的人也可以把 buffer 生命周期跟着 Mat 走,但没人保证 GC 在这期间不做压缩,Clone 是最稳妥的做法。
4.3 实时帧处理与界面绑定
拿到 Mat 之后就是 OpenCvSharp 的地盘了。处理完的结果要显示到 XAML 上,Mat 不能直接绑定,需要再转回 SoftwareBitmap 或 WriteableBitmap。这里涉及一个性能取舍:直接操作 UI 线程会在高帧率时卡界面,帧率一高 UI 线程被阻塞,画面就变成幻灯片。
private void ProcessFrame(Mat frame) { using (var gray = ImageHelper.ToGray(frame)) { var bgra = new Mat(); Cv2.CvtColor(gray, bgra, ColorConversionCodes.GRAY2BGRA); var softwareBitmap = new SoftwareBitmap(BitmapPixelFormat.Bgra8, bgra.Cols, bgra.Rows, BitmapAlphaMode.Ignore); byte[] buffer = new byte[bgra.Cols * bgra.Rows * 4]; System.Runtime.InteropServices.Marshal.Copy(bgra.Data, buffer, 0, buffer.Length); using (var stream = buffer.AsBuffer().AsStream()) { softwareBitmap.CopyFromBuffer(stream); } var source = new SoftwareBitmapSource(); await source.SetBitmapAsync(softwareBitmap); PreviewImage.Source = source; } }逻辑说明:SoftwareBitmap 创建时需要指定宽高和像素格式,从 Mat 的 Cols 和 Rows 取;Marshal.Copy 把 Mat 的 Data 指针内容拷到托管数组,再通过 Buffer 的 Stream 塞进 SoftwareBitmap。参数说明:BitmapAlphaMode.Ignore 表示忽略 alpha 通道,BGRA 的第 4 字节会被原样拷贝但不会被 UI 层解释,这个模式下性能最好。每帧都 new SoftwareBitmapSource 是低频做法,高频场景应该复用控件并调用 SetBitmapAsync 更新帧内容。
从底层角度看,UWP 这套管线真正的瓶颈不在算法,而在传输:SoftwareBitmap 到 byte[] 再到 Mat,每个环节都在拷贝内存。想优化的话,可以把 buffer 数组池化,用 ArrayPool 复用内存,减少 GC 压力。这个优化能明显拉高帧率,尤其是做实时标记、实时滤镜这类对延迟敏感的场景。至于摄像头分辨率,别一上来就开 4K,OpenCV 处理 1080p 都嫌重,保持在 720p 上下做开发调试,画面流畅度和算法耗时都能接受。
5. OpenCvSharp 常见问题避坑:DLL 缺失、架构匹配与内存泄漏
5.1 DllNotFoundException:OpenCvSharpExtern.dll 找不到
现象:代码编译通过,一运行就抛 DllNotFoundException,提示找不到 OpenCvSharpExtern.dll。有时候在开发机正常,换一台机器部署就炸。
原因:OpenCvSharpExtern.dll 是托管层和原生层之间的桥,它由 runtime 包提供并复制到输出目录。只装了 OpenCvSharp4 托管包,或者部署时手动精简了文件,这个 DLL 就缺位。还有种情况是杀毒软件或系统策略把 DLL 拦了,但概率较低,先排查有没有复制到位。
解决:确认两个包都装上;打开输出目录检查 OpenCvSharpExtern.dll 是否存在;如果存在但还报错,右键 DLL 属性确认它没有被系统标记为受保护或阻止。再补一个排查动作:用 Dependency Walker 或类似工具看 OpenCvSharpExtern.dll 依赖的 VC++ 运行库是否齐全,很多服务器环境缺 VC++ Redistributable,也是同样的报错表现。
5.2 BadImageFormatException:位数不匹配
现象:程序启动时报 BadImageFormatException,提示程序集格式或依赖的格式不正确,有时还带着一句“试图加载格式不正确的程序”。
原因:进程位数和原生 DLL 位数不一致。AnyCPU 编译的程序在 64 位系统上默认跑 64 位,但如果你引用的某个第三方组件强制进程转成 32 位,OpenCV 的 64 位原生 DLL 就加载不了。反过来说,钉死 x86 的平台跑在 64 位系统上也可能遇到同类问题,前提是原生 DLL 拿到的是 x64 版本。
解决:统一位数。csproj 里钉死 PlatformTarget,重启 Visual Studio 清掉 obj/bin 缓存重新编译。注意修改 PlatformTarget 后要把输出目录清空再试,残留的同名 DLL 会干扰排查。32 位和 64 位混用,同一份代码在本地正常、到 CI 机器就崩,大概率也是这个原因。
5.3 Mat 内存泄漏:预览跑几分钟就卡顿
现象:UWP 摄像头预览或桌面端批量处理图片时,程序内存不断上涨,帧率越来越低,运行 10 分钟以后明显卡顿。
原因:Mat 是托管外壳包非托管内存,外壳会被 GC 回收,但内部的原生内存必须显式释放。只要有一处忘记 Dispose,那部分内存就直接漏掉。UWP 帧处理是高频率路径,一帧漏一点,几分钟就堆成天文数字。
解决:所有临时 Mat 都套 using 或显式调用 Dispose。这属于基本纪律,再补充一个隐性排查点:注意 Cv2.CvtColor 的第一参数和输出参数如果同一个对象,某些重载会原地覆盖,这不算泄漏,但要注意别在释放后继续引用。调试阶段可以用 OpenCvSharp 自带的环境变量开启内存诊断,或者用性能监视器盯句柄数和内存曲线。养成一个习惯:每个返回 Mat 的方法,要么交给 using,要么文档里写明谁调用谁负责释放。
5.4 VB.NET 里 Using 语句与类型转换的坑
现象:VB.NET 项目里,Mat 释放不彻底,或者开启 Option Strict 后代码编译不过。最常见的是 Mat 的整数参数从 Double 隐式转换失败,或者忘记把 Using 和 Dim 写在同一行导致对象在作用域外释放。
原因:VB.NET 的语法允许很多隐式转换,但 Option Strict On 会把这些口子全部封死。很多 C# 转 VB 的开发者习惯把 C# 的 using var 直译成 Dim xxx As Mat,后半句没有套 Using 块,对象生命周期无限延长。
解决:在 VB 里统一按“Using mat As Mat = xxx”的结构写,保证作用域结束即释放。类型转换用 CType 或 CInt 显式写清楚。代码库里如果保留了 VB.NET 专用示例模块,建议直接参照它来写,不要去猜 C# 的等价写法能不能原样搬过去。VB 这块的坑其实就一个核心词:显式。
5.5 Linux 部署时原生库缺失
现象:Windows 开发机上一切正常,部署到 Linux 容器后启动报 DllNotFoundException,错误里指向 libopencv 相关名称。
原因:runtime.win 这个包只带 Windows 原生 DLL,Linux 环境没有。你要是只把发布目录拷过去,缺的是整个 OpenCV 原生层。
解决:在 Linux 部署机上安装系统级 OpenCV 运行库,或者自行编译原生 OpenCV 并设置 LD_LIBRARY_PATH 指向 DLL 所在目录。.NET Core/.NET 5 以上的项目还可以考虑容器镜像里预装 libopencv-dev,把依赖在构建阶段解决掉。部署环节再记一条:Windows 上跑通不等于跨平台跑通,原生依赖才是跨平台移植的真成本。
6. 进阶验证:用帧率统计与单元测试检验你的图像处理管线
封装库写完了,摄像头管线也通了,怎么确认它真的能扛住业务压力?两个手段:帧率统计和单元测试。帧率统计是最直观的性能验证,别用感觉判断卡不卡,直接量化。
var sw = System.Diagnostics.Stopwatch.StartNew(); int frames = 0; while (sw.Elapsed.TotalSeconds < 10) { ProcessFrame(frame); frames++; } sw.Stop(); double fps = frames / sw.Elapsed.TotalSeconds; System.Console.WriteLine($"FPS: {fps:F2}");逻辑说明:统计 10 秒内的处理帧数,除以耗时得到平均帧率。注意 ProcessFrame 里如果包含 UI 更新或文件写入,帧率会被拖低,这个统计反映的是端到端能力。参数说明:测试时间窗口 10 秒够用,太短波动大,太长浪费时间;把窗口设成参数,方便在低配机器上快速摸底。
单元测试是验证算法正确性的直接手段。灰度化之后,一个像素点三通道变单通道,数值上应该等于原图对应点按 OpenCV 权重公式计算的结果。写一个简单断言就能发现封装层有没有写错:
[TestMethod] public void ToGray_ShouldMatchOpenCVFormula() { using var src = ImageHelper.Load("test.bmp", ImreadModes.Color); using var gray = ImageHelper.ToGray(src); var b = src.At<Vec3b>(10, 10)[0]; var g = src.At<Vec3b>(10, 10)[1]; var r = src.At<Vec3b>(10, 10)[2]; var expected = (int)(0.114 * b + 0.587 * g + 0.299 * r); Assert.AreEqual(expected, gray.At<byte>(10, 10), 1); }逻辑说明:OpenCV 的灰度默认用 ITU-R BT.601 权重,三个通道的权重和是 1,整数误差控制在 1 个灰度级内算通过。参数说明:断言容差设为 1,避免浮点实现差异导致测试闪红;测试图片固定用纯色块,能更快定位转换逻辑的问题。
最后交代一个习惯:我拿到任何 OpenCvSharp 相关代码库,第一件事永远是跑最小 Demo,第二件事是检查所有 Mat 有没有配套释放,第三件事才是读业务代码。这个顺序帮我在无数个项目里省下过半天起步调试时间。你按标题里的结构去整理自己的代码库时,也建议保持这个顺序——环境先通,资源释放先合规,再谈算法和性能。希望帮到你。
本文还有配套的精品资源,点击获取