资讯详情

资讯详情

Linux下工业相机图像采集:从UVC到SDK的完整实践指南

1. 拿到相机别急着写代码先确认它的“身份”再谈调用方案我最早在 Linux 上调迈德威视相机时吃过一次亏拿到的是同一批 USB 接口的工业相机按供应商文档装好 SDK写了一个标准的 OpenCVVideoCapture(0)采集程序结果有的相机能出图有的相机打开后全黑还有一台干脆在/dev/video0下都找不到设备节点。后来才发现问题根本不在于代码而在于我一开始就没分清这台相机到底是以 UVC 标准设备暴露给系统还是必须走厂商私有协议。如果你现在正在做类似的项目先别打开编辑器花十分钟把相机的“身份”摸清楚。这个动作能给你省下后面一整天的排错时间。1.1 接口类型决定了你要走哪条技术路线迈德威视的相机覆盖了 USB 2.0、USB 3.0、GigE 网口等几种常见工业相机形态。接口类型直接决定了操作系统能不能把它当成“标准摄像头”来识别走 UVC 协议的 USB 相机Linux 内核会通过uvcvideo驱动自动识别系统里会出现/dev/videoX设备节点OpenCV 可以直接通过 V4L2 后端访问。走私有协议或 USB3 Vision 协议的相机内核不一定能识别成标准摄像头必须安装厂商 SDK通过 SDK 里的库去枚举和取流。GigE 接口的相机本质上完全不依赖/dev/videoX它是在网卡上跑的 GigE Vision 协议OpenCV 原生不支持必须借助 SDK 或第三方 GigE Vision 库。很多第一次接触工业相机的人会把“USB 接口 标准摄像头”画等号这是最容易翻车的认知偏差。1.2 用三条命令快速判断设备类型在 Linux 下判断相机是不是被内核识别为标准摄像头最直接的办法就是看设备节点枚举情况。下面这三条命令是我每次接手新相机时的固定起手式lsusb dmesg | grep -i usb | tail -30 v4l2-ctl --list-deviceslsusb能看到 USB 总线上的厂商 ID 和产品 ID。迈德威视的 USB 相机通常会出现自己的 VID/PID。如果这条命令里能看到设备但v4l2-ctl --list-devices里没有对应名字基本可以判断相机不是标准 UVC 设备或者当前固件没有开启 UVC 兼容模式。dmesg是用来确认内核驱动有没有成功匹配。如果日志里出现uvcvideo: Found UVC 1.00 device这类信息说明系统已经把它当 UVC 设备处理了。如果只看到New USB device found但没有 uvcvideo 的加载记录那大概率要走 SDK。v4l2-ctl需要额外安装Ubuntu/Debian 下执行sudo apt install v4l-utils。这个工具不仅能列设备还能直接查询相机的分辨率、像素格式、帧率范围非常关键。对于已经在跑 OpenCV 项目的老手这套命令也照样常用。1.3 网口相机先解决 IP 规划再谈调用GigE 迈德威视相机是纯网口相机没有/dev/videoX节点。它靠网卡通信第一步不是写代码而是把网络层打通。一般 GigE 相机出厂 IP 可能是192.168.1.x段你的电脑网卡需要配置成同一网段才能发现它。工业相机通常推荐用静态 IP不要依赖 DHCP。我个人的习惯是给相机单独用一块网卡或者至少单独一个网段避免跟办公网络混在一起。常用命令如下sudo ip addr add 192.168.1.100/24 dev eth0 sudo ip link set eth0 up ping 192.168.1.2假如相机 IP 是192.168.1.2能 ping 通则说明二层三层都通了后面才能进入 SDK 枚举环节。这里要注意部分 GigE 相机的默认 IP 并不是192.168.1.2具体要看说明书或 SDK 自带的搜索工具。实在不确定的时候可以用arp-scan扫一下网段内的设备找到相机 MAC 对应的 IP。1.4 同一台相机的“双模式”问题更隐蔽的情况是同型号的迈德威视 USB 相机可能同时支持 UVC 模式和私有 SDK 模式通过厂商工具或相机内部配置切换。比如默认固件是私有协议OpenCV 打不开但用厂商配置工具切到 UVC 模式后video0就出现了。所以如果你在/dev/videoX里没看到设备不要急着定义“必须用 SDK”先到厂商工具里翻一遍相机设置看看有没有 UVC 开关。反过来也一样如果 OpenCV 能打开但很多高级特性比如硬件触发、精准曝光控制拿不到看看是不是因为你正跑在 UVC 兼容模式下需要切回 SDK 原生模式才能解锁更完整的寄存器控制。2. 环境准备把 OpenCV、SDK 和权限一次配齐调用工业相机本质上是在跟硬件打交道。Linux 下的硬件访问权限、库依赖、编译链接这些环节任何一个没弄好后面都会变成玄学问题。我见过太多人卡在Could not open video device上结果只是当前用户没有/dev/videoX的访问权限。2.1 OpenCVapt 安装还是源码编译对于大多数调用相机的项目我建议直接用发行版自带的 OpenCV。Ubuntu/Debian 系统上执行sudo apt update sudo apt install build-essential cmake git libopencv-dev v4l-utils这样装完C 工程里find_package(OpenCV REQUIRED)就能直接用Python 环境则用pip install opencv-python即可。那什么时候需要源码编译 OpenCV如果你的项目要改到 OpenCV 内部代码或者需要 OpenCV 的contrib模块和特定第三方库深度绑定又或者你用的 Ubuntu 版本太老导致 apt 仓库里的 OpenCV 版本过低——这些情况才值得自己编。否则源码编译费时费力对“调用相机”这个目标来说收益很低。给一个版本判断命令pkg-config --modversion opencv42.2 迈德威视 Linux SDK 的典型目录结构迈德威视官方会提供 Linux 版 SDK解压之后一般会看到这些目录include/ // 头文件比如 camera_api.h lib/ // 动态库比如 libmv_camera.so bin/ // 演示程序和工具 doc/ // 开发文档和示例拿到 SDK 后先把include和lib放到项目里或者设置环境变量。我个人建议不用系统全局安装直接在项目里引用好处是换电脑、升级 SDK 时不会污染系统目录。如果 SDK 里的动态库没有自动进入系统链接器搜索路径运行程序时可能会报cannot open shared object file。这时有两种解法sudo ldconfig /path/to/sdk/lib或者在运行程序前设置export LD_LIBRARY_PATH/path/to/sdk/lib:$LD_LIBRARY_PATH这两招都处理不了再检查库文件本身是不是被strip过或者跟当前系统 glibc 版本不匹配。2.3 用 udev 规则绕开烦人的权限问题Linux 下访问摄像头设备经常遇到Permission denied。你当然可以每次都用sudo运行程序但这在真实项目里非常不优雅尤其是程序要开机自启、对接服务端时总不能把整个服务都跑在 root 下。正确做法是写 udev 规则把相机设备权限放开给指定用户或用户组。先查看相机的 VID/PIDlsusb假设输出里有ID 2bdf:0302这样的内容其中2bdf是厂商 ID0302是产品 ID。然后新建 udev 规则文件sudo nano /etc/udev/rules.d/99-mindvision.rules内容如下SUBSYSTEMusb, ATTR{idVendor}2bdf, MODE0666, GROUPvideo保存后重载规则sudo udevadm control --reload-rules sudo udevadm trigger注意2bdf是示例值实际以你lsusb看到的厂商 ID 为准。如果 VID 有多种可以写多行或者去掉ATTR{idVendor}限定改为按产品名匹配。还可以把当前用户加入video组sudo usermod -aG video $USER改完记得重新登录一下组权限才会生效。2.4 CMake 工程模板项目的CMakeLists.txt我推荐写成这样兼容 UVC 路线和 SDK 路线cmake_minimum_required(VERSION 3.10) project(mindvision_capture LANGUAGES CXX) find_package(OpenCV REQUIRED) include_directories( ${CMAKE_SOURCE_DIR}/include ) link_directories( ${CMAKE_SOURCE_DIR}/lib ) add_executable(capture src/main.cpp) target_link_libraries(capture ${OpenCV_LIBS} mv_camera pthread )pthread是必须加的。工业相机 SDK 在 Linux 下几乎都会用多线程做图像回调不加 pthread 链接 90% 会报undefined reference to pthread_*而这类报错往往要到链接阶段才出现新手经常摸不着头脑。3. UVC 模式用 OpenCV VideoCapture 直接取图的完整路径如果你的相机确认走 UVC 模式那最舒服的调用方式就是把 OpenCV 当主力。不需要处理 SDK 初始化、回调概念、驱动兼容性几十行代码就能出图。3.1 先用 v4l2-ctl 把相机的“底细”摸清写代码前先花两分钟看设备支持什么格式。这个动作能避免很多运行时踩坑。v4l2-ctl --list-devices v4l2-ctl -d /dev/video0 --list-formats-ext假设输出里显示支持YUYV和MJPG并且列出了对应分辨率下的帧率那你心里就有底了。要注意的是MJPG是硬件编码的 MJPEG 流OpenCV 解码后开销比 YUYV 小但同样分辨率下帧率上限可能不同。再看当前格式v4l2-ctl -d /dev/video0 --get-fmt-video如果格式不是你想要的可以先在代码里手动设置不依赖默认值。3.2 最小可用的 OpenCV 采集代码下面这段就是我认为“能用且不该再简化”的版本#include opencv2/opencv.hpp #include iostream int main() { cv::VideoCapture cap(0, cv::CAP_V4L2); if (!cap.isOpened()) { std::cerr failed to open camera std::endl; return -1; } cap.set(cv::CAP_PROP_FOURCC, cv::VideoWriter::fourcc(M, J, P, G)); cap.set(cv::CAP_PROP_FRAME_WIDTH, 1280); cap.set(cv::CAP_PROP_FRAME_HEIGHT, 1024); cap.set(cv::CAP_PROP_FPS, 30); cv::Mat frame; while (true) { cap frame; if (frame.empty()) continue; cv::imshow(mindvision uvc, frame); if (cv::waitKey(1) q) break; } cap.release(); return 0; }第一行里我显式传了cv::CAP_V4L2后端。OpenCV 的VideoCapture(0)虽然会自动选择后端但多摄像头环境里默认后端可能会按-1的模糊逻辑选错设备。显式指定后端能减少不确定性。cap.set(cv::CAP_PROP_FOURCC, ...)要放在设置分辨率之前这一点很多人不知道。某些相机在换像素格式时会重置宽高和帧率所以正确的设置顺序是“像素格式优先分辨率其次帧率最后”。3.3 像素格式、分辨率、帧率之间如何匹配工业相机不像消费级网络摄像头那样“随便给一组参数就一定能跑”。UVC 模式下像素格式、分辨率、帧率三者之间存在一个类似能力矩阵的关系。比如 1280x1024 下支持 30fps但切到 1920x1200 后帧率可能掉到 15fps或者 1280x1024 只支持 YUYV不支持 MJPG。遇到帧率上不去或被强制降低时先问自己三个问题当前分辨率是否超出了 USB 带宽承载范围当前像素格式在目标帧率下是否被v4l2-ctl确认支持是否被应用的自动白平衡、自动曝光拖慢了排查时直接用v4l2-ctl -d /dev/video0 --list-formats-ext对照查询这种“查表”比猜代码靠谱得多。3.4 为什么cap.open(0)能通但画面全黑这是 UVC 相机调用里最典型的现象isOpened()返回true但frame.empty()偶尔为真或者图像整个是黑的。最常见的原因是自动曝光和自动增益没有启动。很多工业相机上电后默认处于手动曝光且曝光值很小或者默认增益为零在暗光环境下画面就是纯黑的。解决办法是打开自动曝光或者手动设置一个合理曝光值cap.set(cv::CAP_PROP_AUTO_EXPOSURE, 0.75); // V4L2 中 0.75 表示开启自动曝光 cap.set(cv::CAP_PROP_EXPOSURE, 100); // 若关闭自动则设置具体曝光值注意这里0.75是 V4L2 驱动约定里的数值不是百分比。而且不同驱动实现可能不完全一致如果这个方式不生效就用v4l2-ctl --set-ctrl exposure_auto1或v4l2-ctl --set-ctrl exposure_absolute100来确认控制项是否存在。另一个容易被忽略的原因是触发模式没有关闭。有些工业相机固件默认处于“等待硬触发”状态导致不发送连续图像。你在 UVC 模式下虽然能打开设备但没有外部触发信号自然拿不到帧。这种情况下去厂商配置工具里把触发模式切回连续采集模式问题立刻消失。4. 非 UVC 与高性能场景官方 SDK 调用与 OpenCV Mat 零拷贝封装UVC 模式胜在简单但它的能力上限也很明显无法精确控制曝光时间到微秒级、无法使用硬件触发、多相机同时取流时带宽调度不受控、部分非标准像素格式拿不到。当项目要求进入更专业的机器视觉流程时切换到官方 SDK 是绕不开的一步。4.1 为什么 UVC 模式不是万能的先看几个真实场景视觉定位要求相机帧率稳定在 60fps且每帧曝光时间必须精确锁定不能有自动调整。产线上用 PLC 发送硬件触发信号相机只有在触发到达时才采集一帧。同时接 4 台相机要求同步曝光UVC 模式很难保证时间对齐。这些场景下厂商 SDK 的价值不在于“能取图”而在于把相机的底层能力完整暴露出来触发模式、GPIO 输入输出、精确像素时钟、多相机管理、帧元数据里附带时间戳等。这才是工业相机和普通摄像头拉开差距的地方。4.2 SDK 初始化到抓帧的完整流程下面以迈德威视常见的 SDK 风格为例给出一个最小流程。真正写代码时函数名以你下载的头文件为准但流程是高度一致的#include camera_api.h #include opencv2/opencv.hpp int main() { // 1. 初始化 SDK MV_CC_Initialize(); // 2. 枚举设备 MV_CC_DEVICE_INFO_LIST deviceList; int ret MV_CC_EnumDevices(MV_USB_DEVICE | MV_GIGE_DEVICE, deviceList); if (ret ! 0 || deviceList.nDeviceNum 0) { std::cerr no device found std::endl; return -1; } // 3. 创建句柄并打开设备 MV_CC_HANDLE handle nullptr; MV_CC_CreateHandle(handle, deviceList.pDeviceInfo[0]); MV_CC_OpenDevice(handle); // 4. 设置像素格式和采集参数 MV_CC_SetPixelFormat(handle, BGR8); // 5. 开始取流 MV_CC_StartGrabbing(handle); // 6. 获取一帧 MV_FRAME_OUT frameInfo {0}; ret MV_CC_GetImageBuffer(handle, frameInfo, 1000); if (ret 0) { std::cout width frameInfo.stFrameInfo.nWidth height frameInfo.stFrameInfo.nHeight std::endl; MV_CC_FreeImageBuffer(handle, frameInfo); } // 7. 停止并释放 MV_CC_StopGrabbing(handle); MV_CC_CloseDevice(handle); MV_CC_DestroyHandle(handle); MV_CC_Finalize(); return 0; }这套流程里最容易踩坑的是“帧缓存没释放”。GetImageBuffer拿到的缓冲区是属于 SDK 内部的用完之后必须调FreeImageBuffer归还否则内存池很快被耗尽程序跑几分钟后就会开始 “No memory left for frame”。4.3 把 SDK 帧缓冲封装成 OpenCV Mat避免不必要的内存拷贝工业相机 SDK 回传的图像数据直接算内存里的字节流。OpenCV 的 Mat 可以通过预分配内存包装外部数据做到零拷贝。只要相机像素格式是 BGR8就可以这样MV_FRAME_OUT frameInfo {0}; MV_CC_GetImageBuffer(handle, frameInfo, 1000); cv::Mat raw( frameInfo.stFrameInfo.nHeight, frameInfo.stFrameInfo.nWidth, CV_8UC3, frameInfo.pBufAddr ); // 此时 raw 和 SDK 内部缓冲共享同一块内存不要 resize raw cv::Mat img raw.clone(); // 如果需要长期保存则 clone MV_CC_FreeImageBuffer(handle, frameInfo);整段代码的精髓在于创建 Mat 时传frameInfo.pBufAddr不会发生多余复制。用clone()保存长期使用的帧是为了避免 SDK 缓冲区被释放后raw变成悬垂指针。如果只是做实时显示直接使用raw即可但要保证在当前循环迭代内用掉。不同像素格式对应的 Mat 类型不同要特别小心相机输出格式OpenCV Mat 类型备注Mono8CV_8UC1可直接显示为灰度图BGR8CV_8UC3可直接送入 OpenCV 彩色算法RGB8CV_8UC3需要cvtColor转成 BGRBayerRG8CV_8UC1需要cvtColor用 Bayer 转换YUV422需要驱动/SDK转换不建议手写转换容易出格式问题很多新手在 BayerRG8 下直接当成彩图用结果画面呈紫色、绿色条纹或棋盘格状。这类问题不是“相机坏了”而是“像素格式解释错了”。4.4 GigE 相机的发现、连接与多相机管理GigE 相机通过网口通信SDK 的枚举过程会扫描网段内的 GigE Vision 设备。第一步还是网络层通畅前面已经提到过静态 IP 的配置。之后在代码里枚举设备时过滤MV_GIGE_DEVICE然后打开对应索引即可。多台 GigE 相机同时工作时不仅要配置好各自 IP还要注意网卡缓冲区和巨型帧设置。可以把网卡 MTU 提高到 9000如果相机和交换机都支持 jumbo frame能明显减少同一帧被拆成多个 UDP 包的频率降低丢包概率。另外多个相机最好分别绑定不同的网卡或者走交换机的不同端口避免一台相机占满带宽导致另一台掉帧。5. 图像卡顿、花屏、CPU 拉满的排错链路调用相机只是第一步真正让项目上线跑稳才是难点。下面这几个问题是工业相机项目里出现频率最高的我按“现象 → 原因 → 排查 → 解决”的路径写出来你可以直接拿去对照。5.1 花屏和颜色错乱先怀疑像素格式再怀疑传输丢包花屏最常见的原因是像素格式设置与数据解释不一致。比如相机实际输出 BayerRG8你却在代码里把它当成 BGR8 处理或者相机输出 RGGB 顺序但代码按 BGGR 顺序做 Bayer 转换。排查顺序是这样# 1. 确认相机当前输出格式 v4l2-ctl -d /dev/video0 --get-fmt-video # 2. 查看 SDK 枚举到的像素格式列表 # 在代码里遍历并打印 nSupportedPixelFormatsGigE 相机花屏还有一种更隐蔽的可能丢包。GigE Vision 的 UDP 传输若交换机缓冲不够或网卡未开启巨型帧某些帧数据不完整图像上就会呈现条纹状花屏。用 SDK 自带的丢包统计函数看nFrameLost和丢包率是否持续增长。如果是丢包问题调大接收缓冲区大小同时确认网线是千兆及以上。5.2 USB 带宽与帧缓冲设置卡顿和掉帧的根源USB 3.0 相机在理论上带宽很大但实际可用带宽会受主板 USB 控制器、线缆长度、屏蔽质量影响。出现持续掉帧时第一步不是改代码而是先降分辨率或降帧率看问题是否缓解。如果项目对帧率有硬性要求优化顺序是把像素格式从 YUYV 换成 MJPG减少传输数据量。把 USB 相机插在机箱背板的原生 USB 3.0 口上避免前置面板转接线。换一根质量过关、长度不超过 3 米的 USB 3.0 线。OpenCV 里把缓冲区压缩到最小降低延迟cap.set(cv::CAP_PROP_BUFFERSIZE, 1);缓冲区大小的设置也很讲究。工业场景里缓冲区太大反而增加延迟缓冲区太小则容易出现帧丢失。BUFFERSIZE1可以保证拿到的是最新帧适合实时定位类项目如果设备本身帧率远高于处理速度用较大的缓冲会平滑一些但延迟会上去。5.3 CPU 持续拉满显示和编码是主要开销有些项目跑起来后 CPU 占用率居高不下你看代码里也就cap frame加一个imshow似乎没什么负载。但实际上 OpenCV 的imshow和waitKey在高帧率下会反复进行窗口刷新CPU 开销很大。更离谱的是有人每帧都做cv::resize、cv::imwrite然后这种代码直接丢到生产环境跑。缓解策略不需要实时预览时把imshow去掉只做采集和处理。需要预览时降低预览频率比如每 5 帧只显示 1 帧。保存视频时不要一张张imwrite使用VideoWriter输出 H264 或 H265压缩和编码在 CPU 上做也比逐帧写磁盘高效得多。如果仍然不够考虑把VideoCapture放到独立线程用双缓冲队列传给处理线程避免imshow阻塞采集线程。说实在的很多“相机掉帧”问题其实不是相机掉帧而是主线程处理不过来导致采集循环被卡住。5.4 用连续帧监控做压力测试检查相机是否稳定我习惯写一个小工具连续采集 1000 帧统计耗时和空帧数并打印帧号和时间戳。这样可以快速暴露大概率问题。int count 0; cv::Mat frame; auto start std::chrono::steady_clock::now(); while (count 1000) { cap frame; if (frame.empty()) { std::cout empty frame at count count std::endl; continue; } count; } auto end std::chrono::steady_clock::now(); double elapsed std::chrono::durationdouble(end - start).count(); std::cout fps count / elapsed std::endl;运行后如果毫无输出说明帧率和稳定性都正常。如果频繁输出empty frame说明相机没有稳定出帧如果 fps 明显低于设定帧率则需要去检查带宽、像素格式、CPU 占用这些局部问题。6. 项目比“能取图”更值钱的细节代码能跑通只是开始。真实项目里设备随时可能被拔掉、程序要跑一整天甚至一个月、多个相机的触发要保持同步。这些边缘问题才是决定项目交付质量的关键。6.1 热插拔和设备节点漂移的应对USB 相机最麻烦的问题之一就是/dev/videoX的编号不固定。今天插上去是video0明天可能变成video2。硬件上顺序一变程序里的固定编号就失效了。解决办法有几个层次最简单的是用v4l2-ctl --list-devices手动核对但这不是自动化方案。按设备路径或序列号绑定设备节点写 udev 规则时用ATTR{serial}区分。程序启动时遍历所有/dev/video*逐个cap.open()再通过读取相机信息判断是否是目标设备。用厂商 SDK 的序列号访问接口这是最稳的做法因为序列号是硬件唯一标识不随插入顺序变化。代码里的健壮性也很重要。cap frame失败时不要立刻崩溃而应该尝试重新打开设备并给出明确的错误提示。长时间无人值守运行时我一般会在isOpened()为假时做 3 次重连间隔 2 秒三次都失败才退出。6.2 长时间运行的内存增长与句柄泄漏工业视觉项目经常是 7x24 小时运行。如果程序存在内存泄漏两三天后内存占用就会从 200MB 涨到 2GB最后被 OOM Killer 干掉。最常见的泄漏点SDK 获取帧后忘记FreeImageBuffer。OpenCV Mat 被clone()后存入长期容器但容器不断增长且没有清理策略。每次循环里用VideoWriter打开文件但没及时release。相机对象重连时没有正确释放旧句柄。我的做法是每跑一万帧打印一次/proc/self/status里的 VmRSS 数值记录是否持续增长。一旦发现涨优先检查最近改动的代码里有没有“获取资源但未释放”的分支。另外不要用cv::imwrite在循环里存图进行长期运行测试那会占用巨量磁盘 IO并且掩盖真正的性能问题。6.3 触发模式下的帧同步注意事项多相机视觉系统里“同步”是一个非常核心的需求。如果每个相机各自自由运行那么同一时刻拍到的画面在时间上会有偏差这对运动物体的三维重建、尺寸测量都是致命的。工业相机支持两种常用同步思路硬件触发同步一个外部信号源输出脉冲同时接到所有相机的触发输入端。每来一个脉冲所有相机同时曝光。这种方式精度最高适合高速运动的场景。软件触发同步通过 SDK 向每台相机发送软件触发命令虽然时间上不如硬件触发精准但胜在不需要额外接线。用 SDK 做软件触发时流程大概是// 设置触发模式为 software MV_CC_SetEnumValueByString(handle, TriggerMode, On); MV_CC_SetEnumValueByString(handle, TriggerSource, Software); // 每次需要图像时发送一次触发信号 MV_CC_SetCommandValue(handle, TriggerSoftware); // 等待图像回调 MV_CC_GetImageBuffer(handle, frameInfo, 1000);需要注意触发模式下GetImageBuffer的超时时间要设得比触发间隔大一些否则程序会误判为“采集超时”。如果在实际项目里发现图像偶尔偏暗或偏亮可以优先检查触发信号是否稳定其次检查曝光时间是否和触发周期匹配。6.4 最后一点个人经验说实话我在 Linux 下调工业相机觉得最值钱的不是某个神奇函数而是一套稳定的检查习惯拿到相机先确认设备类型跑通前先用工具验证参数出问题先从硬件链路查再怀疑代码逻辑。这个顺序能帮你过滤掉 80% 的无关变量。每次新接手一台相机我都会把上面这些检查流程完整跑一遍确认没问题后才开始写业务代码这比任何提速技巧都管用。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →