在HarmonyOS应用开发中,我们常常面临两个看似独立实则紧密相关的挑战:底层性能的稳定性和上层用户体验的流畅性。本文将通过两个真实案例——会议应用因锁竞争导致的AppFreeze问题,以及AI旅行助手长截图分享的性能优化——深入探讨如何在HarmonyOS 6开发中实现性能与体验的双重提升。

一、会议应用死锁问题:锁竞争的隐蔽陷阱

1.1 问题现象:频繁开关摄像头导致的AppFreeze

某视频会议应用在测试过程中发现一个棘手问题:当用户在会议中频繁开关摄像头时,应用会偶发性地完全卡死(AppFreeze),需要强制重启才能恢复。更令人困惑的是,这个问题并非每次都能复现,给排查带来了极大困难。

通过分析应用日志和代码,开发团队最终锁定了问题根源:锁竞争导致的死锁

1.2 问题代码还原

以下是问题代码的简化版本:

import camera from '@ohos.multimedia.camera';

class CameraManager {
  private lock: Object = new Object(); // 共享锁对象
  
  // 停止捕获图像
  async stopCapture(): Promise<void> {
    synchronized(this.lock) {
      // 停止相机捕获
      await this.cameraDevice?.stopCapture();
      // 释放相机资源
      this.releaseCameraResources();
    }
  }
  
  // 图像数据回调
  onImageReceiverCallback(image: image.Image): void {
    synchronized(this.lock) {
      // 处理图像数据
      this.processImage(image);
      // 渲染并推流
      this.renderAndStream(image);
    }
  }
  
  private processImage(image: image.Image): void {
    // 图像处理逻辑
  }
  
  private renderAndStream(image: image.Image): void {
    // 渲染和推流逻辑
  }
}

1.3 死锁发生机制分析

让我们通过时序图来理解死锁是如何发生的:

sequenceDiagram
    participant User as 用户操作
    participant App as 应用主线程
    participant Camera as 相机服务
    participant Callback as 回调线程
    
    User->>App: 点击"关闭摄像头"
    App->>App: 获取lock锁
    App->>Camera: 调用stopCapture()
    Camera-->>Callback: 触发最后的图像回调
    Callback->>Callback: 尝试获取lock锁(等待中)
    App->>App: 等待stopCapture()完成
    Note over App,Callback: 死锁形成:<br/>主线程持有lock等待回调完成,<br/>回调线程等待lock被释放

关键问题分析

  1. 同步锁滥用stopCapture()onImageReceiverCallback()使用了同一把锁

  2. 执行时序冲突:调用stopCapture()时,相机服务可能仍在发送最后的图像数据

  3. 线程阻塞:主线程持有锁等待回调完成,回调线程等待锁被释放,形成死锁

1.4 解决方案:锁分离策略

根据华为官方文档的指导,解决方案是为不同的操作使用不同的锁

import camera from '@ohos.multimedia.camera';

class SafeCameraManager {
  // 使用两把独立的锁
  private captureLock: Object = new Object();  // 相机操作锁
  private imageLock: Object = new Object();    // 图像处理锁
  
  // 停止捕获图像 - 使用相机操作锁
  async stopCapture(): Promise<void> {
    synchronized(this.captureLock) {
      // 停止相机捕获
      await this.cameraDevice?.stopCapture();
      // 释放相机资源
      this.releaseCameraResources();
    }
  }
  
  // 图像数据回调 - 使用图像处理锁
  onImageReceiverCallback(image: image.Image): void {
    synchronized(this.imageLock) {
      // 处理图像数据
      this.processImage(image);
      // 渲染并推流
      this.renderAndStream(image);
    }
  }
  
  // 安全的图像处理方法
  private processImage(image: image.Image): void {
    // 增加空值检查
    if (!image) {
      console.warn('接收到空图像数据');
      return;
    }
    
    try {
      // 图像处理逻辑
      const processed = this.applyImageFilters(image);
      return processed;
    } catch (error) {
      console.error('图像处理失败:', error);
      // 降级处理:返回原始图像
      return image;
    }
  }
  
  // 带超时机制的渲染推流
  private async renderAndStream(image: image.Image): Promise<void> {
    const timeoutPromise = new Promise((_, reject) => {
      setTimeout(() => reject(new Error('渲染超时')), 5000);
    });
    
    const renderPromise = this.doRenderAndStream(image);
    
    try {
      await Promise.race([renderPromise, timeoutPromise]);
    } catch (error) {
      console.error('渲染推流失败:', error);
      // 触发降级策略
      this.fallbackToAudioOnly();
    }
  }
  
  // 降级到纯音频模式
  private fallbackToAudioOnly(): void {
    console.log('切换到纯音频模式');
    // 实现降级逻辑
  }
}

1.5 锁使用的最佳实践

  1. 锁粒度细化:根据操作类型使用不同的锁

  2. 锁超时机制:避免无限期等待

  3. 锁顺序一致:多个锁时按固定顺序获取

  4. 避免在锁内执行耗时操作:减少锁持有时间

二、长截图功能实现:用户体验的性能优化

2.1 业务场景:AI旅行助手的分享难题

在AI旅行助手应用中,用户与AI对话生成的旅行攻略往往内容较长,包含多个景点介绍、美食推荐、交通建议等。用户想要分享完整的攻略给朋友时,面临两个选择:

  1. 传统截图:需要截取多张图片,对方查看不便

  2. 海报生成:动态生成海报图,但消耗大量token且响应慢

我们选择了第三种方案:自动生成长截图

2.2 核心实现原理

长截图的核心原理是:滚动一段距离,截一张图,只保留新增的部分,最后把所有截图按顺序拼接成一张长图

import componentSnapshot from '@ohos.multimedia.image';
import webview from '@ohos.web.webview';
import { BusinessError } from '@ohos.base';

class LongScreenshotManager {
  private webViewController: webview.WebviewController | null = null;
  private isCapturing: boolean = false;
  private screenshotParts: image.PixelMap[] = [];
  
  // 初始化WebView控制器
  async initWebViewController(): Promise<boolean> {
    try {
      this.webViewController = new webview.WebviewController();
      
      // 启用全网页绘制 - 关键步骤!
      if (this.webViewController && 
          typeof this.webViewController.enableWholeWebPageDrawing === 'function') {
        await this.webViewController.enableWholeWebPageDrawing(true);
        return true;
      }
      return false;
    } catch (error) {
      console.error('初始化WebView控制器失败:', error);
      return false;
    }
  }
  
  // 执行长截图
  async captureLongScreenshot(): Promise<image.PixelMap | null> {
    if (this.isCapturing) {
      console.warn('截图操作正在进行中');
      return null;
    }
    
    this.isCapturing = true;
    this.screenshotParts = [];
    
    try {
      // 步骤1:滚动到顶部
      await this.scrollToTop();
      
      // 步骤2:获取页面总高度
      const totalHeight = await this.getPageTotalHeight();
      const viewportHeight = await this.getViewportHeight();
      
      // 步骤3:分段截图
      let currentScroll = 0;
      let lastImageBottom = 0;
      
      while (currentScroll < totalHeight) {
        // 滚动到指定位置
        await this.scrollToPosition(currentScroll);
        
        // 等待滚动动画完成
        await this.sleep(300);
        
        // 截取当前视口
        const currentImage = await this.captureViewport();
        if (!currentImage) {
          throw new Error('截图失败');
        }
        
        // 计算需要保留的部分
        const overlap = this.calculateOverlap(lastImageBottom, viewportHeight);
        const croppedImage = await this.cropImage(currentImage, overlap);
        
        // 保存有效部分
        this.screenshotParts.push(croppedImage);
        lastImageBottom = viewportHeight - overlap;
        
        // 更新滚动位置
        currentScroll += (viewportHeight - overlap);
        
        // 防止无限循环
        if (currentScroll >= totalHeight * 2) {
          console.error('滚动异常,终止截图');
          break;
        }
      }
      
      // 步骤4:拼接所有部分
      const finalImage = await this.mergeScreenshots();
      return finalImage;
      
    } catch (error) {
      console.error('长截图失败:', error);
      return null;
    } finally {
      this.isCapturing = false;
    }
  }
  
  // 计算重叠部分
  private calculateOverlap(lastBottom: number, viewportHeight: number): number {
    // 根据滚动速度和内容类型动态计算重叠
    // 基础重叠为视口高度的20%
    const baseOverlap = Math.floor(viewportHeight * 0.2);
    
    // 根据内容类型调整重叠
    const contentType = this.detectContentType();
    let adjustment = 0;
    
    switch (contentType) {
      case 'text':
        adjustment = 10; // 文本内容需要更多重叠
        break;
      case 'image':
        adjustment = -5; // 图片内容可以减少重叠
        break;
      case 'mixed':
        adjustment = 0;
        break;
    }
    
    return Math.max(10, Math.min(baseOverlap + adjustment, viewportHeight * 0.3));
  }
  
  // 检测内容类型
  private detectContentType(): 'text' | 'image' | 'mixed' {
    // 实现内容类型检测逻辑
    // 可以通过分析DOM或图像特征实现
    return 'mixed';
  }
}

2.3 关键技术点解析

2.3.1 enableWholeWebPageDrawing()的重要性

在Web组件截图时,最常见的坑是调用componentSnapshot.get()只能截取到当前屏幕显示的部分,滚动后截取的内容是空的。这是因为Web组件默认只绘制可见区域。

解决方案:在截图前必须调用enableWholeWebPageDrawing(true)启用全网页绘制。

// 错误示例:直接截图
const screenshot = await componentSnapshot.get(this.webViewController);
// 结果:只能截取到当前视口内容

// 正确示例:先启用全网页绘制
await this.webViewController.enableWholeWebPageDrawing(true);
const screenshot = await componentSnapshot.get(this.webViewController);
// 结果:可以截取完整网页内容

2.3.2 异步处理的时序控制

滚动和截图都是异步操作,需要精确控制时序:

// 错误的时序控制:直接连续调用
await this.scrollToPosition(1000);
const screenshot = await componentSnapshot.get(this.webViewController);
// 问题:滚动动画可能还未完成

// 正确的时序控制:等待动画完成
await this.scrollToPosition(1000);
await this.sleep(300); // 等待滚动动画完成
const screenshot = await componentSnapshot.get(this.webViewController);

2.3.3 内容加载状态检测

对于动态加载的内容,需要确保内容完全加载后再截图:

class WebContentManager {
  private isContentLoaded: boolean = false;
  
  // 监听页面加载完成
  setupWebViewListeners(): void {
    if (!this.webViewController) return;
    
    this.webViewController.onPageEnd(() => {
      this.isContentLoaded = true;
      console.log('页面内容加载完成');
    });
    
    this.webViewController.onPageBegin(() => {
      this.isContentLoaded = false;
    });
  }
  
  // 等待内容加载
  async waitForContentLoad(timeout: number = 10000): Promise<boolean> {
    const startTime = Date.now();
    
    while (!this.isContentLoaded) {
      if (Date.now() - startTime > timeout) {
        console.warn('等待内容加载超时');
        return false;
      }
      await this.sleep(100);
    }
    
    return true;
  }
}

2.4 保存到相册的权限处理

鸿蒙系统要求保存到相册必须使用SaveButton安全控件:

import picker from '@ohos.file.picker';
import photoAccessHelper from '@ohos.file.photoAccessHelper';

class ScreenshotSaver {
  // 保存图片到相册
  async saveToAlbum(pixelMap: image.PixelMap): Promise<boolean> {
    try {
      // 创建照片访问助手
      const phAccessHelper = photoAccessHelper.getPhotoAccessHelper(this.context);
      
      // 创建保存选项
      const createOptions: photoAccessHelper.PhotoCreateOptions = {
        title: `旅行攻略_${new Date().getTime()}.jpg`
      };
      
      // 保存图片
      const uri = await phAccessHelper.createAsset(createOptions);
      await phAccessHelper.writeAsset(uri, pixelMap);
      
      console.log('图片保存成功:', uri);
      return true;
    } catch (error) {
      console.error('保存图片失败:', error);
      return false;
    }
  }
  
  // 使用SaveButton触发保存
  buildSaveButton(): SaveButton {
    const saveButton = new SaveButton();
    
    saveButton.onClick(async () => {
      // 获取要保存的图片
      const screenshot = await this.getScreenshot();
      if (screenshot) {
        const success = await this.saveToAlbum(screenshot);
        if (success) {
          prompt.showToast({ message: '保存成功', duration: 2000 });
        } else {
          prompt.showToast({ message: '保存失败', duration: 2000 });
        }
      }
    });
    
    return saveButton;
  }
}

三、性能优化与用户体验的统一

3.1 两个案例的共同启示

虽然会议应用死锁和长截图功能看似不同,但它们都体现了HarmonyOS开发中的核心原则:

维度

会议应用死锁问题

长截图功能实现

共同原则

资源管理

锁资源竞争导致死锁

内存资源优化(只保留新增部分)

合理分配和管理资源

异步处理

回调与主线程的时序问题

滚动与截图的时序控制

精确控制异步操作时序

错误处理

需要降级到纯音频模式

截图失败时的用户提示

完善的异常处理和降级策略

用户体验

避免应用卡死影响会议

提供流畅的长截图体验

以用户为中心的设计

3.2 性能监控与优化建议

3.2.1 锁竞争监控

class LockMonitor {
  private lockStats: Map<string, { acquireCount: number, waitTime: number }> = new Map();
  
  // 监控锁获取
  monitorLockAcquire(lockName: string): () => void {
    const startTime = Date.now();
    
    return () => {
      const endTime = Date.now();
      const waitTime = endTime - startTime;
      
      const stats = this.lockStats.get(lockName) || { acquireCount: 0, waitTime: 0 };
      stats.acquireCount++;
      stats.waitTime += waitTime;
      this.lockStats.set(lockName, stats);
      
      // 预警:等待时间过长
      if (waitTime > 100) { // 100ms
        console.warn(`锁 ${lockName} 等待时间过长: ${waitTime}ms`);
        this.reportLockContention(lockName, waitTime);
      }
    };
  }
  
  // 报告锁竞争
  private reportLockContention(lockName: string, waitTime: number): void {
    // 实现锁竞争报告逻辑
    // 可以发送到监控系统或记录到本地日志
  }
}

3.2.2 截图性能优化

class ScreenshotOptimizer {
  // 自适应截图质量
  getOptimalQuality(contentType: string): image.ImageFormat {
    switch (contentType) {
      case 'text':
        return {
          format: 'image/jpeg',
          quality: 70 // 文本内容可以降低质量
        };
      case 'image':
        return {
          format: 'image/png',
          quality: 90 // 图片内容需要较高质量
        };
      default:
        return {
          format: 'image/jpeg',
          quality: 80
        };
    }
  }
  
  // 内存使用监控
  monitorMemoryUsage(): void {
    const memoryInfo = process.getMemoryInfo();
    const usagePercentage = (memoryInfo.used / memoryInfo.total) * 100;
    
    if (usagePercentage > 80) {
      console.warn('内存使用率过高,清理截图缓存');
      this.clearScreenshotCache();
    }
  }
  
  // 清理截图缓存
  private clearScreenshotCache(): void {
    // 清理临时截图数据
  }
}

3.3 完整的长截图分享流程

graph TD
    A[用户点击分享] --> B[检查WebView状态]
    B --> C{WebView已初始化?}
    C -->|否| D[初始化WebView控制器]
    C -->|是| E[启用全网页绘制]
    D --> E
    E --> F[滚动到页面顶部]
    F --> G[获取页面总高度]
    G --> H[开始分段截图]
    H --> I[滚动到当前位置]
    I --> J[等待动画完成]
    J --> K[截取当前视口]
    K --> L[计算并裁剪重叠部分]
    L --> M[保存有效部分]
    M --> N{是否到达页面底部?}
    N -->|否| O[更新滚动位置]
    O --> I
    N -->|是| P[拼接所有截图]
    P --> Q[生成最终长图]
    Q --> R[显示预览界面]
    R --> S[用户确认保存]
    S --> T[使用SaveButton保存到相册]
    T --> U[分享到其他应用]
    
    style A fill:#e1f5fe
    style T fill:#c8e6c9
    style U fill:#fff3e0

四、总结与最佳实践

通过以上两个案例的深入分析,我们可以总结出HarmonyOS 6开发中的关键最佳实践:

4.1 多线程编程的黄金法则

  1. 锁分离原则:不同功能使用不同的锁,避免不必要的竞争

  2. 锁粒度控制:锁的范围尽可能小,持有时间尽可能短

  3. 超时机制:所有可能阻塞的操作都应该有超时控制

  4. 资源有序释放:按照申请顺序的逆序释放资源

4.2 复杂功能实现的系统思维

  1. 分而治之:将复杂功能分解为独立的子任务

  2. 时序控制:精确控制异步操作的执行顺序

  3. 状态管理:明确记录和管理功能的各种状态

  4. 错误隔离:确保局部错误不会导致整体功能失败

4.3 性能与体验的平衡艺术

  1. 渐进增强:先保证核心功能,再优化体验

  2. 优雅降级:在资源不足时提供可用的替代方案

  3. 用户感知:通过加载状态、进度提示等改善用户感知

  4. 资源回收:及时释放不再需要的资源

4.4 持续优化与监控

  1. 性能基线:建立关键性能指标的基线数据

  2. 监控告警:设置合理的监控阈值和告警机制

  3. 用户反馈:建立便捷的用户反馈渠道

  4. 迭代优化:基于数据和反馈持续优化

在HarmonyOS 6开发中,无论是底层的多线程安全,还是上层的用户体验优化,都需要开发者具备系统性的思维和细致入微的实践。通过本文的两个案例,我们希望您能掌握:

  • 如何识别和解决隐蔽的多线程问题

  • 如何实现复杂但流畅的用户功能

  • 如何在性能与体验之间找到最佳平衡点

记住,优秀的应用不仅要有强大的功能,更要有稳定的性能和优秀的体验。这需要我们在开发过程中始终保持对细节的关注和对用户需求的深刻理解。

 

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐