前言
在前四篇文章中,我们完成了核心功能的开发。实际适配过程中,遇到了大量的问题需要调试和排查。本文将分享在 HarmonyOS 平台上的日志调试技巧和常见问题的排查方法。
本文内容:
- HarmonyOS hilog 日志系统的使用
- 绕过隐私过滤的技巧
- 关键路径日志设计
- 常见问题排查方法(黑屏、卡顿、触摸无响应)
- 性能分析与优化
一、HarmonyOS 日志系统
1.1 hilog vs Android logcat
| 特性 | Android logcat | HarmonyOS hilog |
|---|---|---|
| 日志级别 | VERBOSE/DEBUG/INFO/WARN/ERROR | DEBUG/INFO/WARN/ERROR/FATAL |
| 日志 API | __android_log_print | OH_LOG_Print / OH_LOG_INFO 等宏 |
| 隐私过滤 | 无 | 默认开启,需要 {public} 标记 |
| 日志查看 | adb logcat | hdc shell hilog |
| 日志持久化 | logcat -f | hilog -w |
1.2 hilog 基本使用
头文件引入:
cpp
#include <hilog/log.h>
日志宏定义:
cpp
// Logger.h
#ifdef OHOS
#define LOG_DOMAIN 0x0000
#define LOG_TAG "dlca"
#define LOGI(fmt, ...) \
OH_LOG_INFO(LOG_APP, "[%{public}s:%{public}d] " fmt, \
__func__, __LINE__, ##__VA_ARGS__)
#define LOGW(fmt, ...) \
OH_LOG_WARN(LOG_APP, "[%{public}s:%{public}d] " fmt, \
__func__, __LINE__, ##__VA_ARGS__)
#define LOGE(fmt, ...) \
OH_LOG_ERROR(LOG_APP, "[%{public}s:%{public}d] " fmt, \
__func__, __LINE__, ##__VA_ARGS__)
#else
// Android / Windows 实现
#endif
使用示例:
cpp
LOGI("Video decoder initialized: %{public}s", codec->name);
LOGW("Packet queue is full, size: %{public}d", queue.size());
LOGE("Failed to create EGL context: 0x%{public}x", eglGetError());
1.3 隐私过滤机制
问题表现:
cpp
// 代码
LOGI("x=%d y=%d", x, y);
// 输出
x=<private> y=<private> // ❌ 被过滤了
原因:
HarmonyOS 为了保护用户隐私,默认过滤所有日志中的参数值。
解决方案:使用 {public} 标记
cpp
// 正确写法
LOGI("x=%{public}d y=%{public}d", x, y);
// 输出
x=857 y=433 // ✅ 正常显示
支持的格式符:
cpp
LOGI("int: %{public}d", 123);
LOGI("string: %{public}s", "hello");
LOGI("hex: 0x%{public}x", 0xABCD);
LOGI("float: %{public}.2f", 3.14);
LOGI("pointer: %{public}p", ptr);
注意事项:
- 不要在生产环境打印真正的隐私数据
- {public} 只用于调试信息(坐标、尺寸、状态码等)
- 敏感数据(密码、token)不应该打印
二、关键路径日志设计
2.1 视频渲染路径
目标: 追踪从接收视频包到屏幕显示的完整流程
cpp
// 1. 接收视频包
void CloudClient::OnFrame(const std::shared_ptr<cloudapp::Message>& frameMessage) {
auto frame = &frameMessage->frame();
LOGI("RX video packet: idx=%{public}llu size=%{public}d type=%{public}d",
frame->frame_index(), frame->data().size(), frame->type());
mVideoPacketQueue.Push(frameMessage);
}
// 2. 解码
int FFmpegDecoder::Decode(std::shared_ptr<cloudapp::Message> msg) {
LOGI("Decode start: pts=%{public}lld", msg->frame().frame_index());
// 解码...
LOGI("Decode done: %{public}dx%{public}d", avFrame->width, avFrame->height);
return 0;
}
// 3. 渲染
void GLViewOHOS::HandleNewFrame(EventOneVideoFrameDecoded* evData) {
LOGI("Render frame: %{public}dx%{public}d fmt=%{public}d",
evData->Width, evData->Height, (int)evData->MCPixfmt);
// 上传纹理...
// 绘制...
eglSwapBuffers(mEglDisplay, mEglSurface);
LOGI("Render done");
}
分析方法:
bash
# 过滤视频相关日志
hdc shell hilog | grep "RX video\|Decode\|Render"
# 检查是否有日志丢失
# 应该看到:RX → Decode → Render 的完整链路
2.2 触摸输入路径
cpp
// 1. ArkTS 层
console.log('[Touch] DOWN local:', localX, localY, '→ remote:', x, y)
// 2. N-API 层
LOGI("N-API: sendPointerEvent x=%{public}d y=%{public}d mask=0x%{public}x",
x, y, buttonMask);
// 3. C++ 归一化
LOGI("Before normalize: x=%{public}d y=%{public}d screen=%{public}dx%{public}d",
x, y, mCompressedPacketVideoWidth, mCompressedPacketVideoHeight);
LOGI("After normalize: (%{public}d,%{public}d) -> (%{public}d,%{public}d)",
x, y, finalX, finalY);
// 4. WebSocket 发送
LOGI("WS sent: type=%{public}d size=%{public}d",
(int)message->type(), (int)bin.size());
时序分析:
bash
# 查看触摸事件的端到端延迟
hdc shell hilog | grep "\[Touch\]\|sendPointerEvent\|WS sent"
# 正常情况下,每个触摸事件应该在几毫秒内完成整个流程
2.3 连接建立路径
cpp
// 1. WebSocket 连接
LOGI("WS connecting: %{public}s", url.c_str());
websocket_client_->on_connected.connect([](asio2::error_code ec) {
if (ec) {
LOGE("WS connect failed: %{public}s", ec.message().c_str());
} else {
LOGI("WS connected");
}
});
// 2. 协议握手
void CloudClient::OnControlConnected() {
LOGI("Control connected, sending config request");
mControl->SendGetConfigEvent();
}
// 3. 接收配置
void CloudClient::HandleRequestConfigData(const cloudapp::RequestConfigData& configData) {
LOGI("Received config: resolution=%{public}dx%{public}d codec=%{public}d",
configData.width(), configData.height(), configData.video_codec());
}
三、常见问题排查
3.1 黑屏问题
现象: XComponent 显示黑色,没有视频画面
排查步骤:
Step 1: 检查 Native Window 创建
typescript
// ArkTS
const surfaceId = this.xComponentController.getXComponentSurfaceId()
console.log('[ControlPage] Surface ID:', surfaceId) // 应该非 0
this.nativeWindowPtr = dlcaPlayer.createNativeWindowFromSurfaceId(Number(surfaceId))
console.log('[ControlPage] Native window:', this.nativeWindowPtr) // 应该非 0
cpp
// C++
OHNativeWindow* window = OH_NativeWindow_CreateNativeWindowFromSurfaceId(surfaceId);
LOGI("Native window created: %{public}p", window); // 应该非 null
Step 2: 检查 EGL 初始化
cpp
LOGI("EGL display: %{public}p", mEglDisplay);
LOGI("EGL surface: %{public}p", mEglSurface);
LOGI("EGL context: %{public}p", mEglContext);
GLint viewport[4];
glGetIntegerv(GL_VIEWPORT, viewport);
LOGI("Viewport: %{public}d,%{public}d %{public}dx%{public}d",
viewport[0], viewport[1], viewport[2], viewport[3]);
// 宽高应该与屏幕匹配,不是 1x1
Step 3: 检查视频数据接收
cpp
void HandleNewFrame(EventOneVideoFrameDecoded* evData) {
LOGI("Frame received: %{public}dx%{public}d size=%{public}d",
evData->Width, evData->Height, evData->DataSize);
}
// 应该持续打印,帧率约 30-60 FPS
Step 4: 检查 OpenGL 错误
cpp
void CheckGLError(const char* op) {
GLint error = glGetError();
if (error != GL_NO_ERROR) {
LOGE("OpenGL error after %{public}s: 0x%{public}x", op, error);
}
}
// 在关键操作后调用
glTexImage2D(...);
CheckGLError("glTexImage2D");
常见原因总结:
| 原因 | 检查点 | 解决方案 |
|---|---|---|
| Viewport 错误 | 1x1 或未设置 | eglQuerySurface 查询尺寸 |
| 纹理格式不对 | 格式未设置或错误 | 设置 MCPixfmt = kYUV420P |
| 没有解码帧 | 无 HandleNewFrame 日志 | 检查解码器初始化和队列 |
| EGL 初始化失败 | eglGetError 返回错误 | 检查配置参数 |
3.2 卡顿问题
现象: 视频播放不流畅,时卡时不卡
排查步骤:
Step 1: 检查帧率
cpp
// 统计帧率
static int frameCount = 0;
static auto lastTime = std::chrono::steady_clock::now();
void HandleNewFrame(EventOneVideoFrameDecoded* evData) {
frameCount++;
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - lastTime).count();
if (elapsed >= 1) {
LOGI("FPS: %{public}d", frameCount / elapsed);
frameCount = 0;
lastTime = now;
}
}
Step 2: 检查解码延迟
cpp
int FFmpegDecoder::Decode(std::shared_ptr<cloudapp::Message> msg) {
auto startTime = std::chrono::steady_clock::now();
// 解码...
auto endTime = std::chrono::steady_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
endTime - startTime).count();
if (duration > 33) { // 超过一帧时间(30fps)
LOGW("Decode slow: %{public}lld ms", duration);
}
}
Step 3: 检查队列积压
cpp
LOGI("Video queue size: %{public}d", mVideoPacketQueue.Size());
// 如果队列持续增长,说明解码跟不上接收速度
优化方案:
-
启用多线程解码
cppmCodecCtx->thread_count = 4; mCodecCtx->thread_type = FF_THREAD_FRAME; -
调整队列大小
cppcommon::Queue<cloudapp::Message> mVideoPacketQueue(30); // 增大缓冲 -
丢帧策略
cppif (mVideoPacketQueue.Size() > 20) { // 丢弃非关键帧 if (!frame->key()) { LOGW("Drop non-key frame"); continue; } }
3.3 触摸无响应
现象: 触摸屏幕没有反应,服务端没有收到事件
排查步骤:
Step 1: 检查 ArkTS 事件
typescript
.onTouch((event: TouchEvent) => {
console.log('[Touch] Event:', event.type, event.touches?.length)
})
// 应该打印 Down/Move/Up 事件
Step 2: 检查坐标转换
typescript
const x = Math.floor(localX * remoteW / localW)
console.log('[Touch] Local:', localX, '→ Remote:', x, 'Scale:', remoteW, '/', localW)
// 检查 remoteW 和 localW 是否正确
Step 3: 检查 N-API 调用
cpp
static napi_value SendPointerEvent(napi_env env, napi_callback_info info) {
LOGI("N-API called: sendPointerEvent");
// 应该打印
}
Step 4: 检查 C++ 层处理
cpp
bool CloudClient::SendPointerEvent(int x, int y, int buttonMask, ...) {
LOGI("SendPointerEvent: x=%{public}d y=%{public}d mask=0x%{public}x",
x, y, buttonMask);
if (!enable_mouse_event_send_) {
LOGW("Mouse event disabled");
return false;
}
if (mCurrentControlPrivilege != cloudapp::kMain) {
LOGW("Not main controller");
return false;
}
// ...
}
Step 5: 检查网络发送
cpp
LOGI("WS sent: type=%{public}d size=%{public}d",
(int)message->type(), (int)bin.size());
// 应该打印 type=80(kMouseEvent2)
常见原因:
- 控制权限不是主控(kMain)
- WebSocket 未连接
- 屏幕分辨率未设置(0x0)
- 坐标转换错误
3.4 崩溃问题
现象: 应用突然崩溃,Signal 11 (SIGSEGV)
排查步骤:
Step 1: 查看崩溃日志
bash
hdc shell hilog | grep "SIGSEGV\|backtrace"
Step 2: 定位崩溃点
Backtrace:
#0 pc 00000000001a2b40 libdlca_cloudapp.so (dl::ControlClient::HandleMessage+0x40)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
函数名
Step 3: 添加防御性检查
cpp
void ControlClient::HandleMessage(const MessagePtr& message) {
if (!message) {
LOGE("Message is null");
return;
}
LOGI("HandleMessage: type=%{public}d", message->type());
// 处理消息...
}
Step 4: 使用 AddressSanitizer
cmake
add_compile_options(-fsanitize=address)
add_link_options(-fsanitize=address)
四、性能分析
4.1 CPU 使用率分析
查看进程 CPU 占用:
bash
hdc shell top | grep cn.dolit
代码中统计 CPU 时间:
cpp
#include <chrono>
auto start = std::chrono::high_resolution_clock::now();
// 执行操作...
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
LOGI("Operation took: %{public}lld us", duration.count());
4.2 内存使用分析
查看进程内存:
bash
hdc shell dumpsys meminfo cn.dolit.cloudapp.hmos.app
检测内存泄漏:
cpp
// 在析构函数中添加日志
~CloudClient() {
LOGI("CloudClient destroyed: %{public}p", this);
}
// 如果对象一直没有析构,说明有内存泄漏
4.3 网络流量统计
cpp
class ControlClient {
std::atomic<uint64_t> mSendBytes{0};
std::atomic<uint64_t> mRecvBytes{0};
public:
void AsyncPostMessage(const MessagePtr& message) {
std::string bin = message->SerializeAsString();
mSendBytes += bin.size();
websocket_client_->AsyncSendBin(bin);
}
void OnMessage(std::string_view buffer) {
mRecvBytes += buffer.size();
// 处理消息...
}
void PrintStats() {
LOGI("Network stats: TX=%{public}llu RX=%{public}llu",
mSendBytes.load(), mRecvBytes.load());
}
};
五、日志最佳实践
5.1 日志级别使用
cpp
// DEBUG: 详细的调试信息(生产环境关闭)
LOGD("Packet detail: seq=%d size=%d flags=0x%x", seq, size, flags);
// INFO: 关键流程信息
LOGI("Video decoder started: codec=%s resolution=%dx%d", codec, w, h);
// WARN: 警告信息(可恢复的错误)
LOGW("Packet queue full, dropping frame");
// ERROR: 错误信息(功能受影响)
LOGE("Failed to decode frame: error=%d", error);
// FATAL: 致命错误(应用无法继续)
// HarmonyOS 会自动触发崩溃报告
5.2 结构化日志
cpp
// 使用统一的格式
LOGI("[%{public}s] %{public}s: %{public}s",
"Module", "Operation", "Result");
// 示例
LOGI("[Video] Decode: Success (1920x1080, H264)");
LOGI("[Network] Connect: Failed (timeout)");
LOGI("[Touch] SendEvent: OK (x=857, y=433)");
5.3 避免日志泛滥
cpp
// ❌ 错误:高频循环中打印
for (int i = 0; i < 1000000; i++) {
LOGI("Processing: %d", i); // 日志太多
}
// ✅ 正确:采样打印
for (int i = 0; i < 1000000; i++) {
if (i % 10000 == 0) {
LOGI("Processing: %{public}d / 1000000", i);
}
}
// ✅ 正确:只在异常情况打印
for (int i = 0; i < 1000000; i++) {
if (process(i) != 0) {
LOGE("Process failed at: %{public}d", i);
}
}
5.4 条件编译日志
cpp
#ifdef DEBUG
#define LOGV(fmt, ...) LOGI(fmt, ##__VA_ARGS__)
#else
#define LOGV(fmt, ...) // 生产环境不打印
#endif
// 使用
LOGV("Verbose debug info: value=%d", value);
六、调试工具
6.1 hdc (HarmonyOS Device Connector)
bash
# 查看设备
hdc list targets
# 查看日志
hdc shell hilog
# 过滤日志
hdc shell hilog | grep "dlca"
# 保存日志
hdc shell hilog -w /data/log/hilog.txt
# 安装应用
hdc install app.hap
# 清除应用数据
hdc shell bm clean -n cn.dolit.cloudapp.hmos.app
6.2 DevEco Studio 调试
断点调试:
- 在 C++ 代码中设置断点
- 以 Debug 模式运行应用
- 触发断点后查看变量
日志查看:
- DevEco Studio 底部 "Hilog" 窗口
- 可以按级别、标签过滤
6.3 远程调试
bash
# 转发端口
hdc fport tcp:5555 tcp:5555
# 连接调试器
lldb-server platform --listen "*:5555"
七、总结
本文介绍了 HarmonyOS 平台上的日志调试和问题排查方法:
核心要点:
- hilog 与 logcat 的差异: 隐私过滤需要 {public} 标记
- 关键路径日志: 追踪完整的数据流
- 常见问题排查: 黑屏、卡顿、触摸无响应的系统化排查
- 性能分析: CPU、内存、网络的监控
- 最佳实践: 合理的日志级别和结构化输出
调试技巧:
- 使用结构化日志便于过滤和分析
- 添加防御性检查,提前发现问题
- 利用 AddressSanitizer 检测内存错误
- 善用 hdc 命令行工具
下一篇预告:
下一篇(最后一篇)将介绍架构设计与最佳实践,总结整个适配过程中的经验和教训。
作者: Frame Not Work
日期: 2026年6月
系列文章: HarmonyOS 6.1 云应用客户端适配实战