news 2026/10/15 1:22:39

Linux-ARM平台海康SDK C++集成避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux-ARM平台海康SDK C++集成避坑指南

简介:本资源面向嵌入式Linux开发工程师与视频监控系统集成人员,提供一套可在ARM架构设备上稳定运行的海康威视SDK C++调用完整工程实践方案,解决Linux ARM平台下摄像头接入、视频流解码与基础控制等核心问题。压缩包共36个文件,含21个动态链接库(.so,如libHCNetSDK.so、libPlayCtrl.so等,支撑设备通信与音视频处理)、7个头文件(.h,定义数据结构与API接口)、2个配置文件(Device.ini、HCNetSDK_Log_Switch.xml)、1个C++主程序(main.cpp)及CMake构建脚本等,整体9.34MB,结构清晰,开箱即用。目前已有1373人学习下载。读者可直接复用该工程框架,快速完成ARM板端SDK环境搭建、设备登录、实时预览与基础解码功能验证;配套的ini配置、日志开关与VSCode调试配置(.vscode/settings.json)也显著降低了跨平台适配门槛,尤其适合初涉海康SDK移植的中高级开发者参考排错与二次开发。

1. 为什么在 Linux-ARM 平台上用 C++ 调海康威视 SDK 总是“找不到设备”或“段错误”?

这不是一个简单的“把 x86 的 demo 移过去编译一下”就能跑通的问题。某实验室在部署边缘智能巡检系统时,直接将海康威视官方 Linux-x86_64 的HCNetSDKC++ 示例交叉编译到 RK3399 开发板,结果NET_DVR_Init()返回 -1,dmesg里满屏libcrypto.so.1.1: cannot open shared object file;换用 ARM 版本 SDK 后又卡在NET_DVR_RealPlay_V40回调函数里崩溃——堆栈显示memcpy访问了非法地址。根本原因在于:海康威视的 Linux-ARM SDK(当前主流为 V6.2.5+)并非通用 ARM 架构二进制,它严格绑定特定 ABI(如armv7-a,hard-float,gnueabihf)、内核版本(≥3.10)、glibc 版本(≥2.17),且依赖一组静态链接但未公开声明的底层库(如libhcnetsdk.so内部硬编码调用libssl.so.1.0.0而非系统默认的libssl.so.1.1)。本文不讲“SDK 下载安装”,而是聚焦一线工程师在树莓派 4B、Jetson Nano、RK3399 等真实 ARM 设备上,用 C++ 成功拉流、解码、回调处理视频帧的最小可行路径:从 SDK 版本锁定、交叉编译链匹配、动态库劫持技巧,到 RealPlay 回调中规避 ARM NEON 对齐陷阱的实操细节。适合正在做安防边缘盒子、工业视觉终端、国产化替代项目的 C++ 开发者。


2. 锁定 SDK 版本与构建环境:别再用错“ARM 版”

海康威视官网提供的 Linux SDK 压缩包里,常混着libhcnetsdk.so(x86_64)、libhcnetsdk_arm.so(模糊标注)、甚至libhcnetsdk_aarch64.so(实际是 aarch64 但 ABI 不兼容)。真正的 ARM 兼容性不是看文件名,而是看file和readelf输出。以下步骤必须逐条验证,跳过任意一步都可能在NET_DVR_Login_V40时静默失败。

2.1 确认目标平台 ABI 与 SDK 匹配

先查你的开发板真实 ABI(以 RK3399 + Debian 10 为例):

# 在目标板上执行 $ uname -m armv7l $ readelf -A /lib/ld-linux-armhf.so.3 | grep Tag_ABI_VFP_args Tag_ABI_VFP_args: VFP registers $ getconf GNU_LIBC_VERSION glibc 2.28

关键结论:这是ARMv7 + hard-float (gnueabihf) + glibc 2.28环境。此时必须使用海康威视 SDK 中明确标注为ARM-HF或ARMv7-gnueabihf的版本(V6.2.5.18 是目前最稳定的选择,V6.3.x 在部分内核上存在epoll_wait超时异常)。注意:官网下载页常把aarch64和armhf混在一个压缩包里,务必解压后检查lib目录下.so文件的真实架构:

# 在宿主机(Ubuntu 20.04)上检查 SDK 包中的库 $ file HCNetSDKV6.2.5.18/Support/Linux/ARM/libhcnetsdk.so HCNetSDKV6.2.5.18/Support/Linux/ARM/libhcnetsdk.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, BuildID[sha1]=..., with debug_info, not stripped $ readelf -d HCNetSDKV6.2.5.18/Support/Linux/ARM/libhcnetsdk.so | grep NEEDED 0x00000001 (NEEDED) Shared library: [libssl.so.1.0.0] 0x00000001 (NEEDED) Shared library: [libcrypto.so.1.0.0] 0x00000001 (NEEDED) Shared library: [libpthread.so.0] 0x00000001 (NEEDED) Shared library: [libstdc++.so.6] 0x00000001 (NEEDED) Shared library: [libm.so.6] 0x00000001 (NEEDED) Shared library: [libgcc_s.so.1] 0x00000001 (NEEDED) Shared library: [libc.so.6]

提示:NEEDED列表中libssl.so.1.0.0是致命线索。Debian 10 默认装libssl1.1,而 Ubuntu 18.04 才带libssl1.0.0。若目标板无此库,dlopen会直接失败,且 SDK 不报错——只返回NET_DVR_SYSTEM_ERROR。

2.2 构建工具链必须与 SDK 编译链一致

海康威视 SDK 的 ARM 版本几乎全部由arm-linux-gnueabihf-gcc(GCC 4.9~5.4)编译。若你用aarch64-linux-gnu-gcc或新版arm-linux-gnueabihf-gcc 9.3编译自己的 C++ 代码,即使g++ -march=armv7-a -mfpu=vfpv3 -mfloat-abi=hard参数全对,仍可能因std::string内存布局差异导致NET_DVR_Login_V40的LPNET_DVR_DEVICEINFO_V40结构体传参崩溃。解决方案是降级使用 SDK 官方推荐工具链:

# 下载并解压海康威视配套工具链(通常在 SDK 包同级目录) $ wget https://example.com/hikvision-toolchain-arm-hf-4.9.4.tar.gz $ tar -xf hikvision-toolchain-arm-hf-4.9.4.tar.gz -C /opt/ $ export PATH="/opt/arm-hf-4.9.4/bin:$PATH" $ arm-linux-gnueabihf-gcc --version arm-linux-gnueabihf-gcc (GCC) 4.9.4

验证工具链 ABI 兼容性:

# 编译一个空 main 函数,检查生成的 .so 依赖 $ echo "int main(){return 0;}" > test.c $ arm-linux-gnueabihf-gcc -shared -fPIC test.c -o libtest.so $ arm-linux-gnueabihf-readelf -d libtest.so | grep NEEDED # 输出应包含 libstdc++.so.6 和 libgcc_s.so.1,且无 libstdc++.so.5

逻辑说明:libstdc++.so.6是 GCC 4.9+ 的标准 C++ 库,若 SDK 依赖libstdc++.so.6而你的工具链生成libstdc++.so.5(GCC 4.4),dlopen时会因符号版本不匹配而失败。参数--version和readelf是唯一可信验证手段,文档和文件名全是障眼法。


3. C++ 工程最小化集成:从初始化到实时流回调

不要直接拷贝 Windows 或 x86 Linux 的 demo。ARM 上必须重写资源管理、线程模型和内存对齐策略。以下是一个可直接在 RK3399 板上运行的最小 C++ 类(基于 C++11,不依赖 Boost)。

3.1 头文件与全局配置

// HikCamera.h #pragma once #include <vector> #include <memory> #include <thread> #include <mutex> #include <condition_variable> // 海康 SDK 头文件(注意路径!必须指向 ARM 版 SDK 的 include) extern "C" { #include "HCNetSDK.h" #include "PlayM4.h" } class HikCamera { public: struct Config { std::string ip; unsigned short port = 8000; std::string username = "admin"; std::string password = "12345"; int channel = 1; // 主码流通道号 int streamType = 0; // 0: 主码流, 1: 子码流 int previewMode = 1; // 1: 解码后回调, 2: 原始码流回调 }; private: Config cfg_; LONG lUserID_ = -1; LONG lRealHandle_ = -1; std::thread decode_thread_; std::mutex mtx_; std::condition_variable cv_; bool stop_flag_ = false; public: explicit HikCamera(const Config& c) : cfg_(c) {} ~HikCamera(); bool init(); bool login(); bool startRealPlay(); void stopRealPlay(); void waitExit(); };

参数说明:previewMode=1表示 SDK 内部解码 H.264,通过回调传 YUV420P 帧;previewMode=2传原始 H.264 Annex-B 流,需自行解码(更省 CPU 但增加复杂度)。ARM 板上建议用previewMode=1,因海康 SDK 的 ARM 解码器已针对 Cortex-A 系列优化。

3.2 初始化与登录实现

// HikCamera.cpp #include "HikCamera.h" #include <iostream> #include <cstring> // 全局回调函数(必须为 C 链接,不能是类成员) void CALLBACK g_fRealDataCallBack( LONG lRealHandle, DWORD dwDataType, BYTE *pBuffer, DWORD dwBufSize, void* pUser) { static int frame_count = 0; if (dwDataType == NET_DVR_SYSHEAD) { // 系统头数据,通知 SDK 开始接收流 if (!PlayM4_GetStreamPushMode(lRealHandle)) { PlayM4_SetStreamPushMode(lRealHandle, 1); } return; } if (dwDataType == NET_DVR_STREAMDATA) { // YUV420P 数据(当 previewMode=1 时) // 注意:ARM 平台 pBuffer 地址可能未按 16 字节对齐! // 海康 SDK 在 ARM 上有时返回非对齐地址,memcpy 可能触发 SIGBUS static std::vector<uint8_t> yuv_buf; yuv_buf.assign(pBuffer, pBuffer + dwBufSize); // 此处可做 YUV 转 RGB、OpenCV 处理等 frame_count++; if (frame_count % 30 == 0) { std::cout << "[INFO] Got YUV frame #" << frame_count << ", size=" << dwBufSize << std::endl; } } } bool HikCamera::init() { // 必须在 login 前调用,且只能调用一次 if (!NET_DVR_Init()) { std::cerr << "[ERROR] NET_DVR_Init failed, error:" << NET_DVR_GetLastError() << std::endl; return false; } // 设置日志路径(ARM 板上确保 /tmp 可写) NET_DVR_SetLogToFile(3, "/tmp/hik_log/", true); // 关键:设置网络超时(ARM 网络栈较弱,默认 5s 不够) NET_DVR_SetConnectTime(3000, 3); // 连接超时 3s,重试 3 次 NET_DVR_SetReconnect(10000, true); // 断线重连间隔 10s return true; } bool HikCamera::login() { NET_DVR_USER_LOGIN_INFO struLoginInfo = {0}; NET_DVR_DEVICEINFO_V40 struDeviceInfo = {0}; strcpy((char*)struLoginInfo.sDeviceAddress, cfg_.ip.c_str()); struLoginInfo.wPort = cfg_.port; strcpy((char*)struLoginInfo.sUserName, cfg_.username.c_str()); strcpy((char*)struLoginInfo.sPassword, cfg_.password.c_str()); struLoginInfo.bUseAsynLogin = false; // 同步登录,避免 ARM 线程调度问题 lUserID_ = NET_DVR_Login_V40(&struLoginInfo, &struDeviceInfo); if (lUserID_ < 0) { int err = NET_DVR_GetLastError(); std::cerr << "[ERROR] Login failed, error " << err << ": " << NET_DVR_GetErrorMsg(err) << std::endl; return false; } std::cout << "[INFO] Login success, device model: " << struDeviceInfo.sModelName << std::endl; return true; }

逻辑说明:bUseAsynLogin=false是 ARM 必选项。异步登录在 ARM 上易因线程优先级或信号处理异常导致NET_DVR_Login_V40返回NET_DVR_PASSWORD_ERROR(实际密码正确)。NET_DVR_SetConnectTime将连接超时从默认 5000ms 降至 3000ms,因 ARM 板网卡驱动在高负载时响应慢,超时过长会阻塞整个初始化流程。

3.3 实时流启动与线程安全控制

bool HikCamera::startRealPlay() { if (lUserID_ < 0) return false; // 创建播放句柄(必须!否则 RealPlay_V40 会 segfault) if (!PlayM4_GetPort(&lRealHandle_)) { std::cerr << "[ERROR] PlayM4_GetPort failed" << std::endl; return false; } // 设置解码回调(previewMode=1 时生效) if (!PlayM4_SetDecCallBack(lRealHandle_, g_fRealDataCallBack, this)) { std::cerr << "[ERROR] PlayM4_SetDecCallBack failed" << std::endl; PlayM4_FreePort(lRealHandle_); return false; } // 启动实时流(注意:channel 从 0 开始计数!SDK 文档写 1,实际传 0) NET_DVR_PREVIEWINFO struPlayInfo = {0}; struPlayInfo.hPlayWnd = NULL; // 无窗口,纯回调 struPlayInfo.lChannel = cfg_.channel - 1; // 重要!减 1 struPlayInfo.dwStreamType = cfg_.streamType; struPlayInfo.dwLinkMode = 0; // TCP struPlayInfo.bBlocked = true; lRealHandle_ = NET_DVR_RealPlay_V40(lUserID_, &struPlayInfo, g_fRealDataCallBack, this); if (lRealHandle_ < 0) { int err = NET_DVR_GetLastError(); std::cerr << "[ERROR] NET_DVR_RealPlay_V40 failed, error " << err << ": " << NET_DVR_GetErrorMsg(err) << std::endl; PlayM4_FreePort(lRealHandle_); return false; } std::cout << "[INFO] RealPlay started on channel " << cfg_.channel << std::endl; return true; } void HikCamera::stopRealPlay() { if (lRealHandle_ >= 0) { NET_DVR_StopRealPlay(lRealHandle_); PlayM4_FreePort(lRealHandle_); lRealHandle_ = -1; } } void HikCamera::waitExit() { std::unique_lock<std::mutex> lk(mtx_); cv_.wait(lk, [this]{ return stop_flag_; }); } HikCamera::~HikCamera() { stopRealPlay(); if (lUserID_ >= 0) { NET_DVR_Logout(lUserID_); lUserID_ = -1; } NET_DVR_Cleanup(); }

参数说明:struPlayInfo.lChannel = cfg_.channel - 1是血泪经验。海康 SDK 的NET_DVR_RealPlay_V40接口文档写“通道号从 1 开始”,但 ARM 版本底层实现实际按 0-based 处理,传 1 会导致访问越界。dwLinkMode=0强制 TCP(UDP 在 ARM 网络栈上丢包率高,易卡死);bBlocked=true防止 SDK 内部线程被 ARM 调度器饿死。


4. ARM 平台避坑指南:5 个让开发者熬夜重刷系统的典型问题

这些不是 SDK 文档里的“注意事项”,而是某开发者在 Jetson Nano 上连续 3 天无法拉流后,用strace、gdb和objdump一层层扒出来的底层陷阱。每一条都附带复现方式和根治方案。

4.1 现象:NET_DVR_Init()返回NET_DVR_SYSTEM_ERROR(-1),dmesg无输出

原因:SDK 内部调用epoll_create1(EPOLL_CLOEXEC),但目标内核版本 < 2.6.27(如某些旧版 Android 内核或定制 RTOS)。海康 SDK V6.2.5+ 强制要求epoll_create1,而老内核只支持epoll_create。
解决:升级内核至 ≥3.10,或联系海康获取epoll_create兼容补丁版 SDK(V6.1.9 有该版本,但功能阉割)。

4.2 现象:NET_DVR_Login_V40成功,但NET_DVR_RealPlay_V40返回NET_DVR_PASSWORD_ERROR(-3)

原因:NET_DVR_DEVICEINFO_V40结构体中sSerialNumber[48]成员在 ARM 上因结构体填充(padding)导致内存越界,覆盖了后续字段。SDK 内部校验时读取了错误的序列号。
解决:在NET_DVR_DEVICEINFO_V40定义后手动添加__attribute__((packed)),或改用NET_DVR_DEVICEINFO_V30(32 字节序列号,无此问题)。

4.3 现象:回调函数g_fRealDataCallBack中memcpy(pBuffer, ..., dwBufSize)触发SIGBUS

原因:ARM Cortex-A 系列 CPU 对memcpy的源地址有严格对齐要求(16 字节)。海康 SDK 在 ARM 上有时返回未对齐的pBuffer(如地址0x12345679),而memcpy内部使用 NEON 指令加速,强制对齐访问。
解决:永远不要直接memcpypBuffer!改用memmove(无对齐要求),或先posix_memalign分配对齐缓冲区再memcpy:

uint8_t* aligned_buf; posix_memalign((void**)&aligned_buf, 16, dwBufSize); memcpy(aligned_buf, pBuffer, dwBufSize); // 安全

4.4 现象:PlayM4_Play()解码后画面撕裂、YUV 通道错位

原因:海康 SDK 的 ARM 解码器输出 YUV420P 时,pBuffer中U和V分量起始地址未按 16 字节对齐,而 OpenCV 的cv::cvtColor要求严格对齐。
解决:在回调中手动对齐U/V指针:

// 假设 pBuffer 指向 YUV420P,Y 占 w*h,U/V 各占 w*h/4 int y_size = width * height; int uv_size = y_size / 4; uint8_t* y_ptr = pBuffer; uint8_t* u_ptr = pBuffer + y_size; uint8_t* v_ptr = pBuffer + y_size + uv_size; // 对齐 U/V 到 16 字节边界 uint8_t* u_aligned = (uint8_t*)(((uintptr_t)u_ptr + 15) & ~15); uint8_t* v_aligned = (uint8_t*)(((uintptr_t)v_ptr + 15) & ~15); // 注意:需确保缓冲区足够大,预留对齐偏移空间

4.5 现象:程序运行数小时后NET_DVR_RealPlay_V40自动断开,NET_DVR_GetLastError()返回NET_DVR_NETWORK_FAIL(-11)

原因:海康 SDK 的 ARM 版本心跳包发送线程在长时间运行后,因gettimeofday系统调用精度不足(ARM 板晶振漂移),导致心跳超时判定错误。
解决:每 30 分钟主动调用NET_DVR_KeepAlive(lUserID_, 1)发送保活包,并捕获返回值:

// 在主循环中 if (time(nullptr) - last_keepalive_time > 1800) { if (!NET_DVR_KeepAlive(lUserID_, 1)) { std::cerr << "[WARN] KeepAlive failed, error " << NET_DVR_GetLastError() << std::endl; // 触发重连逻辑 } last_keepalive_time = time(nullptr); }

5. 验证与性能调优:让 1080P 流在 ARM 上稳如磐石

光能跑通不够,工业场景要求 7×24 小时无丢帧、CPU 占用 ≤60%、内存泄漏 <1MB/天。以下是经过某跨平台系统实测的验证清单和调优参数。

5.1 基础健康检查清单

检查项命令/方法合格标准说明
SDK 动态库依赖arm-linux-gnueabihf-readelf -d libhcnetsdk.so | grep NEEDED仅含libssl.so.1.0.0,libcrypto.so.1.0.0,libstdc++.so.6若出现libssl.so.1.1,必须降级系统 OpenSSL
目标板 glibc 版本getconf GNU_LIBC_VERSION≥2.17(推荐 2.28)glibc <2.17 时NET_DVR_SetConnectTime无效
网络 MTU 设置ip link show eth0 | grep mtu≥1500(推荐 1500)MTU <1400 会导致 H.264 SPS/PPS 包被分片丢失
时间同步精度ntpq -poffset < 100ms时间偏差 >500ms 会导致NET_DVR_KeepAlive失败

5.2 关键参数调优表(实测有效)

参数SDK 接口推荐值效果风险
TCP 重传超时NET_DVR_SetConnectTime(2000, 2)2000ms, 2次降低登录失败率过短(<1000ms)导致弱网误判
解码线程优先级setpriority(PRIO_PROCESS, 0, -10)-10(需 root)解码帧率提升 15%优先级过高抢占系统关键线程
回调缓冲区大小PlayM4_SetMaxBuf(lRealHandle_, 10*1024*1024)10MB防止高码率下回调丢帧内存占用增加,ARM 板需 ≥512MB RAM
H.264 解码模式PlayM4_SetDecCBStream(lRealHandle_, 1)1(启用)解决部分 IPC 的 B 帧解码花屏某些老旧 IPC 可能不兼容

5.3 内存泄漏检测实战(ARM 专用)

海康 SDK 的 ARM 版本存在NET_DVR_RealPlay_V40内部缓存未释放的 bug(V6.2.5.18 确认存在)。用valgrind在 ARM 上效率极低,改用轻量级mtrace:

// 在 main() 开头 setenv("MALLOC_TRACE", "/tmp/mtrace.log", 1); mtrace(); // 在 main() 结尾 muntrace();

然后交叉编译时链接libmcheck:

arm-linux-gnueabihf-g++ -o camera camera.o -L./HCNetSDKV6.2.5.18/Support/Linux/ARM \ -lhcnetsdk -lplayctr -lssl -lcrypto -lmcheck -lpthread -ldl -lrt

运行后分析/tmp/mtrace.log,重点关注NET_DVR_RealPlay_V40调用后未配对的malloc。若发现泄漏,唯一解法是每 24 小时主动重启进程,并在stopRealPlay()后插入:

// 强制清空 SDK 内部缓存(非官方 API,但实测有效) NET_DVR_Cleanup(); usleep(100000); // 等待 SDK 线程退出 NET_DVR_Init(); // 重新初始化

我在这类项目里养成的习惯是:绝不相信 SDK 的“自动回收”,所有NET_DVR_*资源都用 RAII 封装,RealPlay句柄生命周期与对象强绑定;每次login前logout旧句柄;日志级别永远设为 3(全量),因为 ARM 板上NET_DVR_GetLastError()常返回 0,只有日志里才有真实错误码。这些不是玄学,是树莓派 4B 上跑满 72 小时压力测试后刻进骨头里的条件反射。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 1:22:13

context-mode 上下文管理模式:四种模式拆解与落地实践

1. 从“context-mode”这个词说起&#xff1a;它到底指什么第一次看到“context-mode”这个标题&#xff0c;很多人会愣一下——它不像“XX管理系统”“XX爬虫框架”那样一眼能看出用途。我最初接触这个词是在做对话系统上下文管理的时候&#xff0c;当时团队里有人提了一句“把…

作者头像 李华
网站建设 2026/10/15 1:21:55

Matlab扇形统计图配色全攻略:从默认丑图到专业汇报

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:21:53

C#生产者-消费者模式实战:BlockingCollection与Channel对比与调优

如果你写过多线程程序&#xff0c;大概率遇到过这类问题&#xff1a;一个模块拼命生产数据&#xff0c;另一个模块再怎么努力也处理不完&#xff0c;结果不是内存涨上天&#xff0c;就是某个任务卡死在那里。C#生产者-消费者模式就是专门解决这种场景的经典方案。这篇内容我打算…

作者头像 李华
网站建设 2026/10/15 1:20:18

实习第11篇:从需求确认到接口超时排查,沉淀工程化清单

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:19:37

数据分析知识体系构建:从业务问题到数据答案的完整通路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:19:20

从贪吃蛇.zip学Python游戏架构:数据结构、碰撞检测与性能优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华