前言
在前三篇文章中,我们完成了视频渲染和触摸输入的功能开发。本文将聚焦于一个容易被忽视但至关重要的问题:内存管理和崩溃修复。
本文背景:
在适配过程中,我们遇到了一个棘手的崩溃问题:应用退出时必现 SIGSEGV 段错误。经过深入分析,发现是 C++ 成员析构顺序导致的典型问题。
本文涉及的技术点:
- C++ 成员变量析构顺序
- ASIO 异步 I/O 的生命周期管理
- WebSocket 连接安全清理
- 智能指针的正确使用
- 崩溃调试技巧
一、崩溃现象与分析
1.1 崩溃表现
问题描述:
- 视频播放正常,触摸输入正常
- 点击"退出"按钮后应用崩溃
- 崩溃信号:SIGSEGV(段错误)
- 崩溃位置:WebSocket 回调函数中
崩溃日志:
Signal 11 (SIGSEGV), code 1 (SEGV_MAPERR) Fault addr: 0x0000000000000010 Backtrace: #0 pc 00000000001a2b40 libdlca_cloudapp.so (dl::ControlClient::on_message+0x40) #1 pc 00000000001a3c50 libdlca_cloudapp.so (asio2::ws_client::on_recv+0x120) #2 pc 00000000001a5d80 libdlca_cloudapp.so (asio::detail::completion_handler+0x80)1.2 问题定位
通过添加日志和分析调用栈,发现:
析构顺序问题:
classCloudClient{asio2::iopool io_ctx_;// 先声明std::shared_ptr<ControlClient>mControl;// 后声明};C++ 成员变量的析构顺序与声明顺序相反:
mControl先析构(停止 WebSocket)io_ctx_后析构(停止 ASIO)
问题:
mControl析构时触发了 WebSocket 的关闭回调,但此时io_ctx_还在运行,回调函数尝试访问已析构的ControlClient对象,导致段错误。WebSocket 生命周期问题:
websocket_client_->on_message.connect([weak_thiz=weak_from_this()](std::string_view buffer){autothiz=weak_thiz.lock();// ← 这里返回 nullptrif(!thiz)return;// ← 但可能已经跳过检查thiz->HandleMessage(...);// ← 访问已释放的内存});
二、C++ 成员析构顺序详解
2.1 析构顺序规则
核心规则:成员变量的析构顺序与声明顺序严格相反
classExample{public:A a_;// ① 第一个声明B b_;// ② 第二个声明C c_;// ③ 第三个声明~Example(){// 析构顺序:c_ → b_ → a_(与声明相反)}};为什么这样设计?
- 后声明的成员可能依赖于先声明的成员
- 析构时应该先释放依赖项,再释放被依赖项
- 例如:
B的构造函数可能使用了A,所以B应该先析构
2.2 CloudClient 的原始声明
// cloud_client.h(错误的顺序)classCloudClient{// 其他成员...asio2::iopool io_ctx_;// ① 先声明asio2::timer asio_timer_;// ②std::shared_ptr<ControlClient>mControl;// ③ 后声明std::shared_ptr<StreamClient>mStreamClient;// ④};析构顺序(错误):
④ mStreamClient 析构 ↓ 触发 WebSocket 关闭 ③ mControl 析构 ↓ 触发 WebSocket 关闭,可能有回调正在执行 ② asio_timer_ 析构 ① io_ctx_ 析构 ↓ 停止事件循环,但回调已经在访问已释放的内存2.3 修复方案
核心思路:让io_ctx_最后析构,确保所有依赖它的对象都已经清理完毕。
// cloud_client.h(正确的顺序)classCloudClient{// 先声明依赖项std::shared_ptr<View>view_;std::shared_ptr<Statistics>mStatistics;std::shared_ptr<ControlClient>mControl;std::shared_ptr<StreamClient>mStreamClient;// ... 其他依赖 io_ctx_ 的成员// 最后声明基础设施(最后析构)asio2::iopool io_ctx_;asio2::timer asio_timer_;};析构顺序(正确):
① io_ctx_ 和 asio_timer_ 最后析构 ↑ 此时所有回调都已经清理完毕 ② mControl、mStreamClient 先析构 ↓ 干净地关闭连接 ③ 其他成员继续析构代码修改:
// cloud_client.hclassCloudClient{public:// ... 公共接口 ...// 基础设施:必须先声明,最后析构asio2::iopool io_ctx_;// ← 移到这里asio2::timer asio_timer_;// ← 移到这里private:// 依赖基础设施的成员std::shared_ptr<View>view_;std::shared_ptr<ControlClient>mControl;std::shared_ptr<StreamClient>mStreamClient;// ...};关键注释:
// 基础设施:必须先声明,最后析构asio2::iopool io_ctx_;asio2::timer asio_timer_;三、ASIO 异步回调的安全模式
3.1 使用 weak_ptr 避免循环引用
classControlClient:publicstd::enable_shared_from_this<ControlClient>{public:voidInit(){websocket_client_->on_connected.connect([weak_thiz=weak_from_this()](asio2::error_code ec){// ① 尝试提升为 shared_ptrautothiz=weak_thiz.lock();if(!thiz){// 对象已析构,直接返回return;}// ② 安全地访问成员if(ec){++thiz->reconnect_failed_count_;// ...}else{thiz->reconnect_failed_count_=0;thiz->mOnConnectedDelegate.Broadcast();}});}};为什么使用 weak_ptr?
- 避免循环引用:
ControlClient持有websocket_client_,回调又捕获ControlClient - 安全检查:析构时
weak_ptr::lock()返回nullptr - 自动清理:不需要手动断开连接
3.2 回调中的错误处理
websocket_client_->on_message.connect([weak_thiz=weak_from_this()](std::string_view buffer){autothiz=weak_thiz.lock();if(!thiz)return;// ← 关键:提前返回// 解析 Protobuf 消息std::shared_ptr<cloudapp::Message>message=std::make_shared<cloudapp::Message>();if(!message->ParseFromArray(buffer.data(),buffer.size())){LogE(kLogCommon,"Failed to parse protobuf");return;// ← 解析失败,安全返回}// 处理消息thiz->HandleMessage(message);});最佳实践:
- 所有回调开头都检查
weak_ptr::lock() - 捕获异常,避免回调中的崩溃传播到 ASIO
- 使用
std::string_view避免不必要的拷贝
3.3 io_ctx 的停止时机
CloudClient::~CloudClient(){Stop();// ← 显式停止// mControl 等成员会自动析构// io_ctx_ 最后析构(已经停止)}voidCloudClient::Stop(){if(mStop.exchange(true)){return;// 已经停止过了}// 1. 停止所有子系统if(mControl){mControl->Stop();}if(mStreamClient){mStreamClient->Stop();}// 2. 等待事件循环结束io_ctx_.stop();// 3. 等待所有线程退出if(mRenderThread.joinable()){mRenderThread.join();}if(mVideoDecodeThread.joinable()){mVideoDecodeThread.join();}}关键点:
Stop()在析构函数中显式调用- 先停止业务逻辑,再停止事件循环
- 等待所有线程退出后再析构
四、WebSocket 安全清理
4.1 ControlClient 的 Stop 实现
voidControlClient::Stop(){// 设置标志,防止重复停止already_closed_=true;// 在 io_ctx 线程中关闭(避免跨线程访问)ctx_->iopool().post([websocket_client_weak=std::weak_ptr(websocket_client_)](){if(autowebsocket_client=websocket_client_weak.lock()){websocket_client->Close();}});}为什么使用 post?
- WebSocket 的关闭必须在 io_ctx 线程中进行
- 使用
weak_ptr避免循环引用 - 即使
websocket_client_已经析构,weak_ptr::lock()也会安全返回nullptr
4.2 WebSocketClient 的 Close 实现
classWebSocketClient{public:voidClose(){if(is_closed_)return;is_closed_=true;// 断开所有信号连接on_connected.disconnect_all_slots();on_closed.disconnect_all_slots();on_message.disconnect_all_slots();// 关闭底层连接if(ws_session_){ws_session_->stop();}}private:boolis_closed_=false;signal<void(asio2::error_code)>on_connected;signal<void()>on_closed;signal<void(std::string_view)>on_message;};关键操作:
- 设置
is_closed_标志 - 断开所有信号连接(防止回调)
- 停止底层 session
五、智能指针的正确使用
5.1 shared_ptr vs unique_ptr
选择原则:
// ✅ 独占所有权:使用 unique_ptrstd::unique_ptr<FFmpegDecoder>mVideoDecoder;// ✅ 共享所有权:使用 shared_ptrstd::shared_ptr<ControlClient>mControl;std::shared_ptr<Statistics>mStatistics;// ❌ 避免:裸指针CloudClient*mClient;// 容易内存泄漏转换方法:
// unique_ptr → shared_ptrautoshared=std::move(unique);// 不能:shared_ptr → unique_ptr// (除非使用自定义删除器)5.2 enable_shared_from_this 的使用
classControlClient:publicstd::enable_shared_from_this<ControlClient>{public:voidRegisterCallbacks(){// ✅ 正确:使用 weak_from_this()websocket_client_->on_message.connect([weak_thiz=weak_from_this()](std::string_view buffer){autothiz=weak_thiz.lock();if(!thiz)return;thiz->HandleMessage(buffer);});// ❌ 错误:捕获 thiswebsocket_client_->on_message.connect([this](std::string_view buffer){// 危险!this->HandleMessage(buffer);// 可能访问已释放的内存});}};注意事项:
- 只能在
shared_ptr管理的对象中使用 - 必须继承
std::enable_shared_from_this<T> - 构造函数中不能调用
shared_from_this()
5.3 循环引用的检测与避免
问题示例:
classA{std::shared_ptr<B>b_;};classB{std::shared_ptr<A>a_;// ❌ 循环引用};// A 和 B 永远不会被释放解决方案:
classA{std::shared_ptr<B>b_;// 强引用};classB{std::weak_ptr<A>a_;// ✅ 弱引用};检测方法:
// 在析构函数中添加日志~ControlClient(){LOGI("ControlClient destroyed");// 如果没有打印,说明有泄漏}六、崩溃调试技巧
6.1 使用 AddressSanitizer(ASan)
编译选项:
if(CMAKE_BUILD_TYPE STREQUAL "Debug") add_compile_options(-fsanitize=address) add_link_options(-fsanitize=address) endif()ASan 输出示例:
==12345==ERROR: AddressSanitizer: heap-use-after-free READ of size 8 at 0x60300000eff0 thread T0 #0 0x7f8b9c in ControlClient::HandleMessage #1 0x7f8ba0 in lambda::operator()6.2 添加防御性日志
voidControlClient::HandleMessage(constMessagePtr&message){// 入口日志LOGI("HandleMessage: type=%d",message->type());// 关键路径日志switch(message->type()){casecloudapp::kTickEvent:LOGI("Handling tick event");HandleTickMessage();break;// ...}// 出口日志LOGI("HandleMessage: done");}技巧:
- 使用唯一的 ID 标识对象:
[Client-%p] - 记录进入和退出:
BEGIN/END - 记录关键状态变化
6.3 使用断点调试
GDB/LLDB 命令:
# 运行到崩溃点(gdb)run# 查看调用栈(gdb)backtrace# 查看变量(gdb)print this(gdb)print mControl# 查看内存(gdb)x/16x 0x60300000eff0七、其他内存安全实践
7.1 RAII(资源获取即初始化)
classScopedLock{public:explicitScopedLock(std::mutex&mtx):mtx_(mtx){mtx_.lock();}~ScopedLock(){mtx_.unlock();}private:std::mutex&mtx_;};// 使用{ScopedLocklock(mMutex);// 临界区}// 自动解锁C++11 标准版本:
std::lock_guard<std::mutex>lock(mMutex);7.2 避免悬空引用
// ❌ 危险:返回局部变量的引用conststd::string&GetName(){std::string name="test";returnname;// 返回后 name 被销毁}// ✅ 正确:返回值std::stringGetName(){return"test";}// ✅ 正确:返回成员的引用conststd::string&GetName(){returnmName;// mName 是成员变量}7.3 谨慎使用 std::move
std::string str="hello";std::string moved=std::move(str);// ❌ 错误:继续使用 moved-from 对象std::cout<<str;// 未定义行为// ✅ 正确:重新赋值后才能使用str="world";std::cout<<str;// OK八、总结
本文深入分析了一个典型的崩溃问题,并介绍了内存管理的最佳实践:
核心要点:
- 析构顺序很重要:基础设施(io_ctx)应该最后析构
- 异步回调使用 weak_ptr:避免访问已释放的内存
- 显式停止:在析构前显式停止所有异步操作
- 智能指针:优先使用 shared_ptr/unique_ptr,避免裸指针
- RAII:资源管理应该绑定到对象生命周期
调试技巧:
- AddressSanitizer 检测内存错误
- 防御性日志记录关键路径
- GDB/LLDB 调试崩溃现场
下一篇预告:
下一篇将介绍日志调试与问题排查的技巧,包括 HarmonyOS hilog 的使用、黑屏/卡顿等常见问题的排查方法。
作者:[Frame Not Work]
日期:2026年6月
系列文章:HarmonyOS 6.1 云应用客户端适配实战