前言

做了这么多安全相关的功能,今天换个方向——聊聊网络。智能生活助手做到现在,有个问题越来越明显:App 冷启动后第一次加载数据,用户能明显感觉到"转圈"。尤其是弱网环境下,简直让人想摔手机。HarmonyOS 7 在网络层做了几个大动作,QUIC 协议支持和冷启动预建链,今天把它们用起来。

QUIC 为什么比 TCP 快

TCP 建立连接要三次握手,TLS 还要再来一趟,加起来至少 2-3 个 RTT 才能开始传数据。QUIC 基于 UDP,把传输层握手和加密握手合到了一起,1 个 RTT 就能开始传东西。如果之前连过,甚至能做到 0-RTT 恢复。

具体到用户体感:TCP + TLS 的首次请求可能要 200-300ms 在握手上,QUIC 首次 100ms 左右,恢复连接几乎无感。

另一个好处是 QUIC 的多路复用没有队头阻塞。TCP 上一个包丢了,后面所有数据都得等。QUIC 每个流独立,一个流丢包不影响其他流。

Notion-style clean technical illustration comparin

还有一个很多人不知道的点:QUIC 的连接迁移能力。TCP 连接是靠四元组(源IP、源端口、目标IP、目标端口)标识的,用户从 Wi-Fi 切到 4G,IP 变了,连接就断了。QUIC 用 Connection ID 标识连接,网络切换后不需要重新握手,直接在新网络上继续传数据。这对移动场景下的用户体验提升非常大。

冷启动预建链是什么

App 启动的时候,从 UIAbility 的 onCreate 到第一个页面渲染完成,中间至少有几百毫秒。预建链的思路是:在这段"空窗期"里,提前把网络连接建好。等页面真正开始请求数据时,连接已经是 ready 状态了。

HarmonyOS 7 把预建链做成了系统级能力,通过 NetworkKit 的预连接 API 就能用。

给智能助手接入 QUIC

配置网络能力

先在项目里引入 NetworkKit 并配置 QUIC:

// oh-package.json5
{
  "dependencies": {
    "@kit.NetworkKit": "^7.0.0"
  }
}

Notion-style architectural diagram illustrating 'C

然后在 module.json5 里声明网络权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      },
      {
        "name": "ohos.permission.GET_NETWORK_INFO"
      }
    ]
  }
}

Notion-style process flowchart for 'Cold Start Pre

QUIC 连接配置

创建一个网络管理器来管理 QUIC 连接:

import { network } from '@kit.NetworkKit';
import { http } from '@kit.NetworkKit';

class NetworkManager {
  private quicSession: network.QUICSession | null = null;
  private baseUrl: string = 'https://api.smartlife.example.com';

  // 初始化 QUIC 会话
  async initQuicSession(): Promise<void> {
    const config: network.QUICConfig = {
      // 服务器地址
      serverAddress: this.baseUrl,
      // 连接超时
      connectTimeout: 5000,
      // 启用 0-RTT(如果之前连过)
      enableZeroRTT: true,
      // 连接保活间隔(毫秒)
      keepAliveInterval: 30000,
      // 最大并发流数
      maxConcurrentStreams: 100,
      // 拥塞控制算法
      congestionControl: network.CongestionControl.BBR,
    };

    try {
      this.quicSession = await network.createQUICSession(config);

      // 监听连接状态
      this.quicSession.on('stateChange', (state: network.QUICState) => {
        console.info(`QUIC 连接状态: ${state}`);
        if (state === network.QUICState.DISCONNECTED) {
          // 断线自动重连
          this.reconnect();
        }
      });

      console.info('QUIC 会话创建成功');
    } catch (err) {
      console.error('QUIC 会话创建失败:', JSON.stringify(err));
      // QUIC 不可用时降级到 HTTP/2
      console.info('降级到 HTTP/2');
    }
  }

  // 通过 QUIC 发送请求
  async request<T>(path: string, options?: RequestOptions): Promise<T> {
    if (this.quicSession?.state === network.QUICState.CONNECTED) {
      return this.quicRequest<T>(path, options);
    }
    // 降级到普通 HTTP
    return this.httpRequest<T>(path, options);
  }

  private async quicRequest<T>(path: string, options?: RequestOptions): Promise<T> {
    const req: network.QUICRequest = {
      method: options?.method ?? 'GET',
      url: `${this.baseUrl}${path}`,
      headers: options?.headers ?? {},
      body: options?.body,
      // 请求优先级(0-255,越小越高)
      priority: options?.priority ?? 128,
    };

    const response = await this.quicSession!.sendRequest(req);
    return JSON.parse(response.body) as T;
  }

  private async httpRequest<T>(path: string, options?: RequestOptions): Promise<T> {
    const httpRequest = http.createHttp();
    const response = await httpRequest.request(`${this.baseUrl}${path}`, {
      method: options?.method === 'POST' ? http.RequestMethod.POST : http.RequestMethod.GET,
      header: options?.headers,
      extraData: options?.body,
    });
    httpRequest.destroy();
    return JSON.parse(response.result as string) as T;
  }

  private async reconnect(): Promise<void> {
    try {
      await this.quicSession?.reconnect();
    } catch (err) {
      console.warn('QUIC 重连失败,将使用 HTTP 降级');
    }
  }
}

interface RequestOptions {
  method?: string;
  headers?: Record<string, string>;
  body?: string;
  priority?: number;
}

export const networkManager = new NetworkManager();

冷启动预建链

在 UIAbility 的 onCreate 里触发预建链,跟页面渲染并行执行:

import { network } from '@kit.NetworkKit';
import { common } from '@kit.AbilityKit';

export default class EntryAbility extends UIAbility {
  async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
    // 第一时间发起预建链,不要等页面加载
    this.preconnectNetwork();

    // 初始化其他模块...
    await this.initModules();
  }

  private async preconnectNetwork(): Promise<void> {
    // 系统级预建链:在应用启动阶段预先建立网络连接
    const preconnectConfig: network.PreconnectConfig = {
      // 需要预连接的地址列表
      urls: [
        'https://api.smartlife.example.com',
        'https://cdn.smartlife.example.com',
      ],
      // 预连接策略
      strategy: network.PreconnectStrategy.ON_APP_LAUNCH,
      // DNS 预解析
      enableDnsPrefetch: true,
      // TLS 预握手
      enableTlsPreHandshake: true,
      // QUIC 预连接(如果支持)
      enableQuicPreconnect: true,
      // 预连接超时
      timeout: 3000,
    };

    try {
      await network.preconnect(preconnectConfig);
      console.info('预建链完成');
    } catch (err) {
      // 预建链失败不影响主流程,只是后续请求走正常连接
      console.warn('预建链部分失败:', JSON.stringify(err));
    }
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    // 页面开始加载时,预建链大概率已经完成
    windowStage.loadContent('pages/Index');
  }
}

预建链的关键点:要早。在 onCreate 的第一行就触发,别等到 onWindowStageCreate 再做。这两者之间通常有 200-500ms 的间隔,足够完成一次 DNS 解析 + TLS 握手了。

弱网降级方案

智能助手里有个视频监控的功能,在弱网下需要自动降级。利用 HarmonyOS 7 的网络质量感知能力:

import { network } from '@kit.NetworkKit';

class AdaptiveStreaming {
  private qualityLevel: number = 3; // 1-5,5 最高
  private networkMonitor: network.NetworkMonitor | null = null;

  // 启动网络质量监控
  startMonitoring(): void {
    this.networkMonitor = network.createNetworkMonitor({
      // 采样间隔
      sampleInterval: 2000,
      // 关注的指标
      metrics: [
        network.Metric.RTT,
        network.Metric.DOWNLINK_BANDWIDTH,
        network.Metric.PACKET_LOSS_RATE,
      ],
    });

    this.networkMonitor.on('qualityChange', (quality: network.NetworkQuality) => {
      this.adaptStreamQuality(quality);
    });
  }

  private adaptStreamQuality(quality: network.NetworkQuality): void {
    const { rtt, downlinkBandwidth, packetLossRate } = quality;

    let newLevel: number;

    if (rtt < 50 && packetLossRate < 0.01 && downlinkBandwidth > 5000) {
      newLevel = 5; // 极好:4K
    } else if (rtt < 100 && packetLossRate < 0.03 && downlinkBandwidth > 2000) {
      newLevel = 4; // 好:1080p
    } else if (rtt < 200 && packetLossRate < 0.05 && downlinkBandwidth > 800) {
      newLevel = 3; // 一般:720p
    } else if (rtt < 500 && packetLossRate < 0.1 && downlinkBandwidth > 300) {
      newLevel = 2; // 差:480p
    } else {
      newLevel = 1; // 极差:360p 或纯音频
    }

    if (newLevel !== this.qualityLevel) {
      this.qualityLevel = newLevel;
      this.switchStream(this.qualityLevel);
    }
  }

  private switchStream(level: number): void {
    const streamConfig: Record<number, StreamConfig> = {
      5: { resolution: '3840x2160', bitrate: 15000, codec: 'H265' },
      4: { resolution: '1920x1080', bitrate: 6000, codec: 'H265' },
      3: { resolution: '1280x720', bitrate: 2500, codec: 'H264' },
      2: { resolution: '854x480', bitrate: 1000, codec: 'H264' },
      1: { resolution: '640x360', bitrate: 400, codec: 'H264', audioOnly: false },
    };

    const config = streamConfig[level];
    console.info(`切换画质到 Level ${level}: ${config.resolution}`);

    // 通知播放器切换流
    AppStorage.setOrCreate('streamConfig', config);
  }
}

interface StreamConfig {
  resolution: string;
  bitrate: number;
  codec: string;
  audioOnly?: boolean;
}

QUIC 连接池管理

实际项目中,你的应用可能同时跟多个后端服务通信。这时候需要一个连接池来管理多个 QUIC 会话,避免为每个请求都创建新连接:

import { network } from '@kit.NetworkKit';

class QUICConnectionPool {
  private sessions: Map<string, network.QUICSession> = new Map();
  private maxSessions: number = 5;

  // 获取或创建指定 host 的 QUIC 会话
  async getSession(host: string): Promise<network.QUICSession> {
    const existing = this.sessions.get(host);
    if (existing && existing.state === network.QUICState.CONNECTED) {
      return existing;
    }

    // 超过最大会话数,关闭最久没用的
    if (this.sessions.size >= this.maxSessions) {
      this.evictOldestSession();
    }

    const session = await network.createQUICSession({
      serverAddress: host,
      enableZeroRTT: true,
      keepAliveInterval: 30000,
      congestionControl: network.CongestionControl.BBR,
    });

    session.on('stateChange', (state: network.QUICState) => {
      if (state === network.QUICState.DISCONNECTED) {
        this.sessions.delete(host);
      }
    });

    this.sessions.set(host, session);
    return session;
  }

  private evictOldestSession(): void {
    // 关闭最早创建的会话(简单 FIFO,生产环境建议用 LRU)
    const firstKey = this.sessions.keys().next().value;
    if (firstKey) {
      this.sessions.get(firstKey)?.close();
      this.sessions.delete(firstKey);
    }
  }

  // 应用退出时清理所有连接
  destroyAll(): void {
    for (const [_, session] of this.sessions) {
      session.close();
    }
    this.sessions.clear();
  }
}

连接池的核心思路是复用。QUIC 的 0-RTT 恢复只在同一个会话内有效,频繁创建销毁会丧失这个优势。建议把连接池做成全局单例,在 EntryAbilityonDestroy 里统一清理。

弱网下的直播优化

HarmonyOS 7 还针对弱网直播场景做了优化,可以通过 LiveStreamKit 开启:

import { liveStream } from '@kit.LiveStreamKit';

async function createOptimizedStream(): Promise<liveStream.StreamSession> {
  const session = await liveStream.createSession({
    // 启用弱网优化
    weakNetworkOptimization: true,
    // 自适应码率
    adaptiveBitrate: true,
    // 前向纠错(丢包时尝试恢复)
    fec: true,
    // SVC 分层编码(弱网时只传基础层)
    svcEncoding: true,
    // 关键帧间隔缩短(弱网时加快恢复)
    keyFrameInterval: 1000,
    // Jitter Buffer 自适应
    jitterBuffer: {
      mode: liveStream.JitterBufferMode.ADAPTIVE,
      minDelay: 50,
      maxDelay: 500,
    },
  });

  // 监听网络状态和降级事件
  session.on('degradation', (event: liveStream.DegradationEvent) => {
    console.warn(`直播降级: ${event.reason}, 当前码率: ${event.currentBitrate}kbps`);
  });

  return session;
}

踩坑经验

预建链不是万能的。如果你的 App 启动后用户不一定马上发请求(比如先看开屏广告),预建链建好的连接可能已经超时断开了。建议根据用户行为预判:如果大概率会在 5 秒内发请求,预建链收益很大;否则可能白费。

QUIC 需要服务端支持。这个听起来像废话,但真有人忘了。你的服务端得支持 QUIC 协议(HTTP/3),客户端的 QUIC 配置才有意义。建议服务端先配好,再做客户端接入。

弱网判断别只看信号格数。信号满格也可能拥塞严重。用 NetworkMonitor 的 RTT 和丢包率做判断比信号强度靠谱得多。

网络调试小技巧

接入 QUIC 和预建链后,怎么验证它们真的生效了?几个调试方法分享给你们。

抓包看握手过程。用 HarmonyOS 自带的网络抓包工具,可以清楚看到 QUIC 的握手只有一轮 Initial 包交换,而 TCP 要三轮。如果你的服务端也支持 HTTP/3,对比一下两种协议的完整请求耗时,差距一目了然。

利用 DevEco Studio 的 Network Inspector。新版 IDE 里有个 Network Inspector 面板,能实时查看所有网络请求的连接类型(QUIC/TCP/HTTP2)、握手耗时、首字节时间(TTFB)。不用加任何埋点代码就能看到这些数据,特别方便。

模拟弱网测试。DevEco Studio 6.0 新增了网络模拟功能,可以设置延迟、丢包率、带宽限制。我一般用 RTT 200ms + 丢包 3% 来模拟地铁场景,RTT 500ms + 丢包 10% 来模拟电梯场景。在这些条件下跑一遍完整流程,确认降级逻辑是否正常工作。

实际收益

我们在智能助手里接入这套方案后,实测数据:

  • 冷启动首次 API 请求耗时从 380ms 降到 120ms(预建链 + QUIC 0-RTT)
  • 弱网环境(RTT 300ms+,丢包 5%)下视频卡顿率降低了约 60%
  • 整体网络请求成功率从 96.2% 提升到 99.1%

数据说话,优化效果是实打实的。下篇聊聊怎么把整个项目从 HarmonyOS 6 迁移到 7,顺便把发布流程走一遍。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐