简介:这套Linux下的海康监控视频Qt示例工程,面向具备C++与Qt基础、正在从事安防系统集成或嵌入式视频应用开发的技术人员。压缩包共220个文件,约1.2MB,主要包含62个头文件、58个C++源码文件及51个界面设计文件,覆盖视频实时预览、本地回放、通道参数配置与远程参数设置等功能模块,同时含有工程配置文件、资源图片及版本管理文件,便于直接导入开发环境进行二次改造。示例重点展示了基于Qt多媒体框架接入并显示海康设备视频流、通过多线程与网络通信保证播放流畅性、以及错误处理与日志记录等实战思路。已有1152人学习浏览,适合用于快速理解Linux下监控客户端的基本架构与关键实现环节,也可作为后续对接其他网络摄像头或扩展设备管理功能的起点工程。
1. 项目定位:Linux 下把海康监控画面接进 Qt 窗口,这套源码能直接落
在 Linux 桌面上实时预览海康摄像头的画面,这个需求听起来简单,落地时却绕不开 SDK 集成这道坎。官方 demo 大多是纯控制台程序,跑起来只能看到日志输出,画面根本不会上屏;想自己封装一遍又得摸清登录、预览、抓图、录像一串接口。这套源码正好补齐了中间那段:用 Qt 做界面,把海康设备网络 SDK 的核心能力封装成可视化 demo,填上设备 IP、端口、用户名密码,点几下按钮就能在 Qt 窗口里看到实时监控画面。它适合两类人:一类是手里有海康摄像头、想在 Linux 下快速出画面的开发,照着代码接 SDK 就能跑通;另一类是想做监控平台或安防业务的工程师,能拿这个 demo 当骨架,换成自己的界面和业务逻辑。下面按源码结构逐层拆解。
2. 系统架构与源码目录:先看清分层和依赖再动手
拿到源码第一件事不是编译,而是把目录结构过一遍。这套 demo 并没有把 SDK 调用直接堆在按钮事件里,而是分了三层:界面层只管用户交互,业务层负责封装海康 SDK 的登录、预览、抓图、录像,底层是 SDK 动态库。这样改界面不会动到 SDK 调用,换设备型号也不用重写 UI。对于一个给二次开发者看的 demo 来说,这种分层比什么都往 mainwindow.cpp 里塞要友好得多。
2.1 整体架构分层
第一层是界面层,由 Qt 的控件组成。主窗口负责布局、按钮事件,预览窗口是一个单独的 QWidget 子类。第二层是业务层,把 SDK 调用封装成一个包装类,里面暴露 Login、StartPreview、CaptureJPEG、StartRecord、StopAll 这类方法,界面层只调用这些方法,不直接与 HCNetSDK 头文件打交道。第三层是 SDK 层,也就是 HCNetSDK 动态库和它配套的播放库,这些库文件放在源码目录的 HCNetSDK/lib 下,运行时通过 rpath 或 LD_LIBRARY_PATH 指定。
这套分层是合理的。我在类似的视频项目里踩过坑,如果直接在按钮事件里到处写 NET_DVR_xxx,后期加一个断线重连就要把整个窗口类翻一遍。业务层隔离之后,可以把登录和重连逻辑收口到一个类里,界面只关心界面状态变化。demo 里把登录失败、预览失败统一转换成错误码信号抛给状态栏,这就是一种很实用的解耦方式。
2.2 目录结构解析
源码目录结构大致是这样的,我从工程里整理出来的:
hcqt-monitor/ ├── HCNetSDK/ │ ├── include/ │ │ ├── HCNetSDK.h │ │ ├── LinuxPlayM4.h │ │ └── comm.h │ └── lib/ │ ├── libHCNetSDK.so │ ├── libPlayCtrl.so │ └── libcrypto.so ├── src/ │ ├── main.cpp │ ├── mainwindow.h / mainwindow.cpp │ ├── previewwidget.h / previewwidget.cpp │ └── hcsdk_wrapper.h / hcsdk_wrapper.cpp ├── ui/ │ ├── mainwindow.ui │ └── resources.qrc ├── examples/ │ └── demo_config.ini ├── HCQtMonitor.pro └── README.md头文件在 HCNetSDK/include 下,动态库在 HCNetSDK/lib 下。src/mainwindow 是主界面,previewwidget 是视频显示控件,hcsdk_wrapper 是 SDK 业务封装。examples 里有一个配置文件示例,记录了设备 IP、端口、通道号这些参数,方便不熟悉 Qt 的人直接改配置而不碰代码。
有一点值得注意:海康 SDK 在 Linux 下还有几个依赖库,比如 libcrypto.so 和 libPlayCtrl.so,拷贝工程时不能只带一个主库,否则运行时会报依赖缺失。我一般把整个 HCNetSDK 目录一起拷走,并在 .pro 里写 RPATH,让二进制在任意目录启动都能找到自己的屏库,这也是工程能直接跑起来的关键。
2.3 依赖与技术选型
这个 demo 的技术栈不算复杂,核心就三样:Qt 负责界面、HCNetSDK 负责设备通信、LinuxPlayM4 负责解码播放。下面把各自职责列出来:
| 模块 | 用途 | 关键点 |
|---|---|---|
| Qt 5 Widgets | 主窗口、按钮、布局 | 需要 qtbase5-dev |
| HCNetSDK | 设备搜索、登录、预览、抓图、录像、PTZ | 官方设备网络 SDK |
| LinuxPlayM4 | 码流解码与 YUV 输出 | 随 SDK 一起发布的播放库 |
| QSettings | 读取 IP、端口、账号配置 | 减少硬编码 |
选型上没有引入 FFmpeg,因为海康 SDK 自带解码链路,先把这条链跑通才是目的。如果后面要兼容 RTSP 协议的第三方摄像头,再考虑用 FFmpeg 拉流替换,那属于二次开发的事。依赖方面,Qt 版本用 5.x 是最稳的,SDK 库本身没有绑死 Qt,Qt 6 也能编,只是 QWidget::winId() 拿到的窗口句柄类型不同,需要显式转换,这个点后续在避坑章节会提到。
2.4 界面层与业务层之间的信号槽设计
demo 里 hcsdk_wrapper 是 QObject 子类,登录结果通过 error(int code) 和 loginSucceeded(LONG userId) 两个信号通知主窗口,预览状态通过 previewStarted() 和 previewStopped() 通知界面。这样 SDK 内部的耗时操作不会阻塞 UI 线程,按钮的 enabled 状态由这些信号驱动,整体结构很干净。
具体到实现,登录按钮点击后先禁用按钮,调用 wrapper 的 Login 方法;登录成功信号到达后再启用预览按钮。如果直接在线程里操作界面控件,Qt 会报警告,所以信号槽传参只传基础类型,不传 SDK 结构体指针,这是这个小项目里值得借鉴的细节。
3. 核心模块拆解:从 SDK 初始化到视频上屏的完整链路
这套 demo 的调用链并不复杂,但每一段都有参数坑。我按执行顺序拆成四步:初始化、登录、预览、数据回调,中途补上抓图和录像。把这四条链走通,整个 SDK 的骨架也就立起来了。
3.1 SDK 初始化与登录
初始化发生在程序启动时,通常在 main.cpp 里调用。看这段代码:
// main.cpp 片段:初始化 SDK 并设置连接参数 NET_DVR_Init(); NET_DVR_SetConnectTime(2000, 1); // 连接超时 2 秒 NET_DVR_SetReconnect(10000, true); // 断线自动重连,10 秒一次NET_DVR_Init 是全局初始化,整个进程只需调用一次。NET_DVR_SetConnectTime 的第一个参数是超时毫秒数,第二个是重试次数;NET_DVR_SetReconnect 设置断线自动重连的间隔和开关。这组参数写好后,后续登录失败时 SDK 会按这个策略自动重试,省去自己维护连接状态。
登录用的是 NET_DVR_Login_V40,它比老版本的 Login_V30 多传一个设备信息结构体,能拿到硬件版本和通道数量,对后续遍历多路通道很有用:
NET_DVR_USER_LOGIN_INFO loginInfo = {0}; snprintf(loginInfo.sDeviceAddress, sizeof(loginInfo.sDeviceAddress), "%s", ip.toStdString().c_str()); loginInfo.wPort = 8000; snprintf(loginInfo.sUserName, sizeof(loginInfo.sUserName), "%s", user.toStdString().c_str()); snprintf(loginInfo.sPassword, sizeof(loginInfo.sPassword), "%s", password.toStdString().c_str()); loginInfo.bUseSSL = false; NET_DVR_DEVICEINFO_V40 devInfo = {0}; LONG lUserID = NET_DVR_Login_V40(&loginInfo, &devInfo); if (lUserID < 0) { qWarning() << "登录失败,错误码:" << NET_DVR_GetLastError(); return; } // 登录成功后,lUserID 是所有后续操作的凭据这里的参数有几个容易错的地方。sDeviceAddress 是设备 IP,wPort 默认 8000,海康设备的 8000 端口是 SDK 通信端口,不是 RTSP 的 554,别填错。bUseSSL 如果开启,库内部会走 TLS 握手,普通局域网环境没必要开。返回的 lUserID 是一个长整型句柄,后面预览、抓图、云台控制都要带上它。错误码通过 NET_DVR_GetLastError 拿,demo 里把它转成 QMessageBox 弹出,排障时非常直观。
3.2 实时预览:把预览窗口句柄交给 SDK
登录之后就是预览。demo 的做法很简单:把 QWidget 的系统窗口句柄传给 SDK,让 SDK 自己去解码绘制:
NET_DVR_PREVIEWINFO previewInfo = {0}; previewInfo.hPlayWnd = (HWND)ui->videoWidget->winId(); // 拿到 Qt 控件的窗口句柄 previewInfo.lChannel = channel; // 通道号,NVR 从 1 开始,IPC 通常也是 1 previewInfo.dwStreamType = 0; // 0 主码流,1 子码流 previewInfo.dwLinkMode = 0; // 0 TCP,1 UDP previewInfo.bBlocked = true; // 阻塞模式,窗口绘制更稳定 LONG lPlayHandle = NET_DVR_RealPlay_V40(lUserID, &previewInfo, NULL, NULL); if (lPlayHandle < 0) { qWarning() << "启动预览失败,错误码:" << NET_DVR_GetLastError(); }关键参数就三个:hPlayWnd 是窗口句柄,在 Linux 下 QWidget::winId() 返回的是 X11 的 Window ID,SDK 直接往这个窗口画;lChannel 选择通道,NVR 多路通道从这里选;dwStreamType 选择主码流还是子码流,主码流分辨率高但带宽占用大,开发调试时建议先用子码流。dwLinkMode 建议保持 TCP,UDP 在某些网络环境下花屏概率高。
这里有个常见的坑:hPlayWnd 传的窗口句柄在 Qt 里必须是已经显示过的窗口,否则 winId() 返回 0。demo 里在构造函数中先 show 了预览控件,再去触发预览,或者在预览按钮事件里先 show 一次,拿到的才是有效句柄。
提示:在 Linux 下通过 QWidget::winId() 拿窗口句柄时,务必在控件 show 之后再调用,否则拿到的可能是 0,预览会黑屏。
3.3 抓图与录像:两个常用接口的落地姿势
抓图在 demo 里用的还是旧接口 NET_DVR_CaptureJPEGPicture,它在海康 IPC 上兼容性不错。调用的前置条件是设备已登录,并且通道号有效:
// 抓图:把 JPEG 数据写入内存缓冲区 QByteArray jpegBuffer(1024 * 1024, 0); // 1MB 缓冲区 NET_DVR_JPEGPICTURE_PARAM jpegParam = {0}; jpegParam.wPicSize = 0; // 0 表示按设备当前分辨率 jpegParam.wPicQuality = 0; // 质量等级 0-6,0 默认 BOOL bRet = NET_DVR_CaptureJPEGPicture(lUserID, channel, &jpegParam, (BYTE*)jpegBuffer.data(), jpegBuffer.size()); if (!bRet) { qWarning() << "抓图失败,错误码:" << NET_DVR_GetLastError(); } else { QFile file(QString("/tmp/capture_%1.jpg").arg(QDateTime::currentMSecsSinceEpoch())); file.open(QIODevice::WriteOnly); file.write(jpegBuffer); file.close(); }这里需要特别说明:NET_DVR_CaptureJPEGPicture 不返回编码后的实际长度,缓冲区大小不够时会直接失败,我一般给 1MB 以上,否则图片稍大就抓不出来。抓下来的 JPEG 直接落盘,不需要再编码。实际项目里还可以用 NET_DVR_SetCapturePictureMode 切换抓图方式,有的设备支持 JPEG 和 BMP 两种输出。
录像相对简单,预览成功后调用 NET_DVR_SaveRealData 就能把码流存下来:
// 开始录像,保存为 mp4 封装 BOOL bStart = NET_DVR_SaveRealData(lPlayHandle, "/home/user/record_20250101.mp4"); // 结束时 NET_DVR_StopSaveRealData(lPlayHandle);这个接口存的是直接从设备拉过来的原始码流,封装格式是海康自己的。回放时一般用 SDK 的播放库去解,或者用 ffmpeg 直接读这个文件。demo 里把录像按钮做成二态的,第一次点开始,第二次点停止,状态用按钮文本切换,实现上就是一个 bool 标记加文件路径参数。
3.4 解码回调与 Qt 显示:数据从哪来、往哪去
上面那种把窗口句柄交给 SDK 的方式最省事,但灵活性差。如果你想在画面上叠加自定义水印、做动侦框或者重新采样,就得走解码回调。demo 里保留了这样一条链路:
// 预览时传入回调函数,SDK 把码流数据回调出来 void CALLBACK OnRealDataCallBack(LONG lPlayHandle, DWORD dwDataType, BYTE* pBuffer, DWORD dwBufSize, void* pUser) { if (dwDataType == NET_DVR_SYSHEAD) { // 系统头,需要先交给播放库解析 PlayM4_SetStreamOpenType(0, 0); PlayM4_OpenStream(0, pBuffer, dwBufSize, 2 * 1024 * 1024); PlayM4_Play(0, (HWND)pUser); // pUser 是窗口句柄 } else if (dwDataType == NET_DVR_STREAMDATA) { // 视音频流数据,送入播放库 PlayM4_InputData(0, pBuffer, dwBufSize); } } // 预览时指定回调 LONG lPlayHandle = NET_DVR_RealPlay_V40(lUserID, &previewInfo, OnRealDataCallBack, (void*)ui->videoWidget->winId());回调方式下,pBuffer 拿到的是 H.264 PS 流,不是裸 YUV。解码工作由 PlayM4 完成,最终视频是直接渲染到 pUser 指向的窗口句柄上。如果后续需要拿到 YUV 自己转 QImage,可以通过 PlayM4_GetYUVData 取出 YUV 帧,再用 QImage 的 Format_RGB888 转换上屏。demo 默认走窗口句柄模式,回调链路作为注释保留,方便二次开发时切换。
3.5 退出时资源释放的固定顺序
调试时最容易翻车的是退出流程。如果先销毁了窗口再停止预览,SDK 还在往已经销毁的句柄上写数据,程序直接崩。demo 的 closeEvent 里做了完整清理:
void MainWindow::closeEvent(QCloseEvent *event) { if (lPlayHandle >= 0) { NET_DVR_StopRealPlay(lPlayHandle); lPlayHandle = -1; } if (lUserID >= 0) { NET_DVR_Logout(lUserID); lUserID = -1; } NET_DVR_Cleanup(); event->accept(); }顺序是有讲究的:先停预览释放播放句柄,再注销登录断开设备连接,最后做全局清理。反过来的话,SDK 内部状态已经被清掉了,再调 StopRealPlay 会拿到无效句柄,行为不可预期。这个习惯我一直保持到现在,任何 SDK 接入都按建立和释放的逆序清理,省掉了很多玄学崩溃。
4. 编译与运行:拿到源码后如何快速跑起来
拿到源码后要做的三件事:装依赖、配置 SDK 库路径、编译运行。命令行和 QtCreator 两条路都走得通,我把命令行的方式写清楚,脚本化部署时更方便。
4.1 准备依赖与 SDK 库文件
系统层面需要安装 Qt 5 开发包。Ubuntu/Debian 下执行:
sudo apt update sudo apt install qtbase5-dev qt5-qmake qtcreatorqtbase5-dev 提供 Qt Widgets 的头文件和库,qt5-qmake 提供 qmake 工具。如果不想用 QtCreator,命令行一样能编。SDK 方面,把 Linux 版设备网络 SDK 解压,将平台对应的 so 文件统一放到工程 HCNetSDK/lib 目录下,include 头文件放到 HCNetSDK/include 下。注意选择 X64 还是 ARM 版本要和本机架构匹配,ARM 板子上编译要换成对应的交叉编译器,我把这个步骤写在 README 里,避免新手在第一步就卡住。
4.2 使用 qmake 构建工程
这个 demo 用的是 qmake 工程文件,HCQtMonitor.pro 写好了源文件和依赖路径:
QT += core gui widgets TARGET = HCQtMonitor TEMPLATE = app CONFIG += c++11 SOURCES += src/main.cpp \ src/mainwindow.cpp \ src/previewwidget.cpp \ src/hcsdk_wrapper.cpp HEADERS += src/mainwindow.h \ src/previewwidget.h \ src/hcsdk_wrapper.h INCLUDEPATH += HCNetSDK/include LIBS += -L$$PWD/HCNetSDK/lib -lHCNetSDK -lPlayCtrl QMAKE_RPATHDIR += $$PWD/HCNetSDK/libINCLUDEPATH 让编译器找到 HCNetSDK.h;LIBS 中 -L 指定库目录,-lHCNetSDK 和 -lPlayCtrl 指定链接库;QMAKE_RPATHDIR 把库目录写进二进制搜索路径,程序运行时就不用每次设置 LD_LIBRARY_PATH。构建命令:
qmake HCQtMonitor.pro make -j$(nproc)编译成功后在当前目录会生成 HCQtMonitor 可执行文件。用 ldd 检查依赖是否齐全:
ldd ./HCQtMonitor | grep HCNetSDK如果显示 not found,说明 RPATH 没生效,或者库目录路径不对。还可以临时加环境变量跑:
export LD_LIBRARY_PATH=$PWD/HCNetSDK/lib:$LD_LIBRARY_PATH ./HCQtMonitor提示:QMAKE_RPATHDIR 只在 qmake 生成 Makefile 时生效,如果修改了 .pro 记得重新 qmake 一次,否则改动不会进入编译链接流程。
4.3 运行与验证
程序启动后是一个登录窗口,界面布局从上到下依次是:设备 IP 编辑框、端口、用户名、密码、通道号,右侧是登录、预览、抓图、录像按钮。首次使用把 IPC 或 NVR 的 IP、用户名密码填进去,点击登录,状态栏显示登录成功;再点预览,视频窗口开始出画面。
验证链路是否正常的顺序我建议这样:先看登录按钮返回的错误码,网络不通一般是 23 号错误(连接超时),账号密码错是 7 号;登录成功后看预览按钮是否返回句柄;最后看录像文件大小是否在增长。如果抓图成功但文件打不开,检查保存路径是否有写权限,/home 下的目录通常没问题,/opt 或 /usr 开头的目录就要注意。配置文件里还可以预设多个设备,用下拉框切换,省去反复输入的重复劳动。
5. 常见问题与避坑记录
调试这套 demo 时我记录的坑不少,挑几条出现频率最高的列在这里,按现象、原因、解决的顺序写,方便对照排查。
5.1 编译期:找不到头文件或链接失败
现象:编译时提示 HCNetSDK.h: No such file or directory;或者链接时报 cannot find -lHCNetSDK。
原因:两种情况原因不同。找不到头文件是 INCLUDEPATH 没生效或者路径写错;链接失败则是 HCNetSDK/lib 目录下没有对应的 so,或者 so 的架构与目标平台不一致。还有一个隐蔽点:海康 SDK 的库名可能带版本后缀,比如 libHCNetSDK.so.6,需要在 lib 目录下建一个软链。
解决:先看目录内容 ls HCNetSDK/lib 和 ls HCNetSDK/include,确认文件存在;再检查 .pro 的 INCLUDEPATH/LIBS 是否使用绝对路径,把 $$PWD 替换成完整路径测试。如果库名带版本号,创建软链:
ln -s libHCNetSDK.so.6 libHCNetSDK.so5.2 运行期:预览黑屏、花屏、崩溃
现象:登录成功但预览窗口黑屏;主码流出现花屏;关闭窗口时程序崩溃。
原因:黑屏一般是窗口句柄无效。QWidget 还没有 show 时调用 winId() 会返回 0,SDK 拿到空句柄自然画不上去。花屏和 dwLinkMode 有关,UDP 链路在网络丢包时会出现马赛克。关闭崩溃常见于退出顺序不对,先销毁了窗口再停止预览,SDK 还在往已经销毁的句柄上写数据。
解决:预览前调用 ui->videoWidget->show(),或者确认控件已显示;链路模式改用 TCP(dwLinkMode=0);退出时严格按 StopRealPlay、Logout、Cleanup 的顺序清理。
5.3 库依赖:运行时提示 so 文件找不到
现象:编译通过,但 ./HCQtMonitor 一运行就报 error while loading shared libraries: libHCNetSDK.so: cannot open shared object file。
原因:运行时动态库搜索路径没有包含 HCNetSDK/lib。qmake 的 QMAKE_RPATHDIR 在某些交叉编译环境下不会写入 ELF,或者库目录是相对路径,程序换目录启动后失效。
解决:直接用 ldd 查看缺失项,然后设置 LD_LIBRARY_PATH;或者把 so 拷到系统库目录 /usr/local/lib 并执行 ldconfig。
5.4 登录返回错误码 7 或 23
现象:设备 IP 填对但一直返回错误码 7(账号密码错)或 23(连接超时)。
原因:错误码 7 有时候不是账号密码本身,而是设备开启非法登录取缔后,短时间内失败次数过多被锁了几分钟;23 则是网络不通,常见于设备 IP 网段和本机不在同一网段,或者填了 554 端口而不是 8000。
解决:用 ping 验证网络连通,用浏览器访问设备 IP 确认网络可达;被锁就等两分钟再试。调试时把 NET_DVR_GetLastError() 的值打出来,对照 SDK 错误码手册,别凭感觉猜。
5.5 抓图返回失败但日志无异常
现象:预览正常,抓图却一直返回 FALSE,错误码是 118。
原因:118 表示通道不支持当前抓图方式。部分 IPC 默认关闭了私有协议抓图,需要开启设备的增强 IP 能力或者在设备侧打开抓图开关。另一个常见原因是缓冲区分太小,图片写不进去。
解决:把缓冲区加大到 2MB,确认 wPicSize 参数是 0;设备侧登录 Web 管理页,在图像设置里开启允许抓图。如果还不行,改用 NET_DVR_SetCapturePictureMode 切换抓图模式再试。
这份问题列表覆盖编译、链接、运行三类,基本是 SDK 开发里最常翻车的几个点。把这些记熟了,再遇到类似报错能省不少排查时间。
6. 二次开发思路:把 demo 改造成自己的监控工具
demo 跑通只是开始,实际项目里很少只预览一路画面。下面三条改造思路优先级最高,按这个顺序做,收益最明显。
第一条是多路预览。把 PreviewWidget 控件放入 QGridLayout,生成 N 个实例,每个实例持有自己的登录句柄和播放句柄。改造时注意 hcsdk_wrapper 不要设计成单例,登录返回的 lUserID 和 lPlayHandle 要按实例管理,否则两路预览会互相覆盖窗口。代码上做一个统一的预览管理类,接收设备列表后逐一创建预览窗口,复用同一批 SDK 库。
第二条是加云台控制。海康 SDK 的云台接口和预览共用登录句柄,按钮按下时调用 NET_DVR_PTZControlWithSpeed(lUserID, channel, PAN_LEFT, 0, 0),松开时再调用一次参数改为 1 停止。上下左右变倍是一组方向常量,操作手感需要在按钮事件里控制好按下和释放的节奏,连点会导致设备响应迟钝,一般加一个 100 毫秒的间隔。
第三条是接入 RTSP 摄像头。工业现场经常混用多品牌设备,海康 SDK 不能直接控制第三方摄像头。进阶做法是用 FFmpeg 拉 RTSP 流,解码成 QImage 后放进同一个 PreviewWidget 渲染,这样保留了同一套 UI 层。改造时保持接口不变,把 hcsdk_wrapper 的 StartPreview 换成打开 RTSP 连接,输出回调给 PreviewWidget,界面代码几乎不用动。
测试新功能时我习惯先跑一条最小链路:先确认登录句柄有效,再调单个方向转动,抓一张图看画面变化;链路通了再做 UI 联动。这套 demo 最大的价值不是界面,而是把 SDK 初始化、登录、句柄生命周期这套顺序固化下来了,换任何设备都绕不开这几个步骤。从那以后我每次拿到新 SDK,都会先画出初始化到清理的时序图再动代码,这个习惯帮我避开不少运行时崩溃。希望帮到你。
本文还有配套的精品资源,点击获取