HarmonyOS 6.1 云应用客户端适配实战(五):日志调试与问题排查

前言

在前四篇文章中,我们完成了核心功能的开发。实际适配过程中,遇到了大量的问题需要调试和排查。本文将分享在 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());

// 如果队列持续增长,说明解码跟不上接收速度

优化方案:

  1. 启用多线程解码

    cpp 复制代码
    mCodecCtx->thread_count = 4;
    mCodecCtx->thread_type = FF_THREAD_FRAME;
  2. 调整队列大小

    cpp 复制代码
    common::Queue<cloudapp::Message> mVideoPacketQueue(30);  // 增大缓冲
  3. 丢帧策略

    cpp 复制代码
    if (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 调试

断点调试:

  1. 在 C++ 代码中设置断点
  2. 以 Debug 模式运行应用
  3. 触发断点后查看变量

日志查看:

  • DevEco Studio 底部 "Hilog" 窗口
  • 可以按级别、标签过滤

6.3 远程调试

bash 复制代码
# 转发端口
hdc fport tcp:5555 tcp:5555

# 连接调试器
lldb-server platform --listen "*:5555"

七、总结

本文介绍了 HarmonyOS 平台上的日志调试和问题排查方法:

核心要点:

  1. hilog 与 logcat 的差异: 隐私过滤需要 {public} 标记
  2. 关键路径日志: 追踪完整的数据流
  3. 常见问题排查: 黑屏、卡顿、触摸无响应的系统化排查
  4. 性能分析: CPU、内存、网络的监控
  5. 最佳实践: 合理的日志级别和结构化输出

调试技巧:

  • 使用结构化日志便于过滤和分析
  • 添加防御性检查,提前发现问题
  • 利用 AddressSanitizer 检测内存错误
  • 善用 hdc 命令行工具

下一篇预告:

下一篇(最后一篇)将介绍架构设计与最佳实践,总结整个适配过程中的经验和教训。


作者: Frame Not Work

日期: 2026年6月

系列文章: HarmonyOS 6.1 云应用客户端适配实战

相关推荐
大雷神1 小时前
第40篇|美颜预设:自然、人像、清透如何变成可解释选项
harmonyos
FrameNotWork1 小时前
HarmonyOS 6.1 云应用客户端适配实战(一):环境搭建与编译系统
数码相机·华为·harmonyos
再见6582 小时前
HarmonyOS NEXT 实战:开发一个精美的随机颜色生成器
华为·harmonyos
G_dou_2 小时前
Flutter三方库适配OpenHarmony【color_picker】HSL 调色器项目完整实战
flutter·harmonyos
G_dou_2 小时前
Flutter三方库适配OpenHarmony【random_number】随机数生成器项目完整实战
flutter·harmonyos
FrameNotWork2 小时前
HarmonyOS 6.1 云应用客户端适配实战(三):触摸输入与坐标映射
华为·harmonyos
●VON2 小时前
鸿蒙Flutter实战:日期选择器与截止日期高亮提醒
android·flutter·华为·harmonyos·鸿蒙
●VON3 小时前
鸿蒙Flutter实战:Material 3种子色亮暗双主题系统
android·flutter·harmonyos
慧海灵舟3 小时前
鸿蒙南向开发教程 Day 3:OpenHarmony 线程管理
华为·harmonyos·写文章,赢小鸿ai