A hand-drawn doodle illustration on pure white pap

前言

前几天运营找我,说 App 包体积超了应用市场的限制。一看,好家伙,3 个业务模块各引了一份网络库,光重复代码就占了 12MB。

同一个东西装了三遍,这不是浪费是什么?

HSP(Harmony Shared Package)就是来解决这个问题的。跟 HAR 不同,HSP 在运行时只有一份副本,多个模块共享。今天聊聊 HSP 怎么用,什么时候该用。
HarmonyOS7 提供了两种共享包:HAR 是静态的,编译时拷贝到每个使用方;HSP 是动态的,运行时只加载一份。如果你的 App 有多个模块引用同一套代码,HSP 能帮你砍掉重复体积。

我之前一个项目把网络库从 HAR 换成 HSP,包体积直接减少了 8MB。效果就是这么立竿见影。

HSP 解决了什么问题

先看一个真实场景:

App/
├── entry/         → 引用 common_net HAR(4MB)
├── feature_a/     → 引用 common_net HAR(4MB)
└── feature_b/     → 引用 common_net HAR(4MB)
                    总计:12MB 重复代码

HAR 是编译时拷贝,3 个模块各拷一份,同一个库在 App 里存在了 3 次。

换成 HSP 后:

App/
├── entry/         → 引用 common_net HSP
├── feature_a/     → 引用 common_net HSP
├── feature_b/     → 引用 common_net HSP
└── common_net/    → HSP 模块,运行时只加载一份(4MB)
                    总计:4MB,省了 8MB

A hand-drawn doodle illustration on pure white pap

HSP 的核心价值:运行时共享,一份代码多个模块用。

创建 HSP 模块

创建步骤跟 HAR 类似,但选的类型不同。

步骤:

  1. 右键项目根目录 → New → Module
  2. 选择 Shared Shared Library(HSP)
  3. 填写模块名,比如 common_net
  4. 点击 Finish

确认 build-profile.json5

// common_net/build-profile.json5
{
  "apiType": "StageMode",
  "buildOption": {
    "arkOptions": {
      "staticBuild": false   // false 表示动态构建(HSP)
    }
  }
}

A hand-drawn doodle illustration on pure white pap

注意 staticBuild: false,这是 HSP 和 HAR 的关键区别。HAR 是 true,HSP 是 false

HSP 模块的 oh-package.json5

{
  "name": "common_net",
  "version": "1.0.0",
  "description": "网络请求共享库",
  "main": "Index.ets"
}

导出与引用

HSP 的导出机制跟 HAR 一样,通过 Index.ets 控制。

导出 API:

// common_net/src/main/ets/common_net/Index.ets

export { HttpClient } from './HttpClient'
export { ApiResponse } from './model/ApiResponse'
export type { RequestConfig } from './model/RequestConfig'

来看看 HttpClient 的实现:

// common_net/src/main/ets/common_net/HttpClient.ets
import { http } from '@kit.NetworkKit'
import { RequestConfig } from './model/RequestConfig'
import { ApiResponse } from './model/ApiResponse'

export class HttpClient {
  private static instance: HttpClient | null = null

  // 单例模式,确保运行时只有一个实例
  static getInstance(): HttpClient {
    if (!HttpClient.instance) {
      HttpClient.instance = new HttpClient()
    }
    return HttpClient.instance
  }

  async request<T>(config: RequestConfig): Promise<ApiResponse<T>> {
    const httpRequest = http.createHttp()
    try {
      const response = await httpRequest.request(config.url, {
        method: config.method,
        header: config.headers,
        extraData: config.data
      })
      return JSON.parse(response.result as string) as ApiResponse<T>
    } finally {
      httpRequest.destroy()
    }
  }

  async get<T>(url: string): Promise<ApiResponse<T>> {
    return this.request<T>({ url, method: http.RequestMethod.GET })
  }

  async post<T>(url: string, data: object): Promise<ApiResponse<T>> {
    return this.request<T>({ url, method: http.RequestMethod.POST, data })
  }
}

HttpClient 设计成单例是有讲究的——HSP 是运行时共享的,如果每个引用方都 new 一个实例,就失去了共享的意义。单例保证了整个 App 只有一个 HTTP 客户端,连接池、拦截器等资源也能统一管理。

在其他模块中引用:

// entry/oh-package.json5
{
  "dependencies": {
    "common_net": "file:../common_net"
  }
}
// entry/src/main/ets/pages/Index.ets
import { HttpClient } from 'common_net'

@Entry
@Component
struct IndexPage {
  async aboutToAppear() {
    const client = HttpClient.getInstance()
    const data = await client.get('/api/home')
    // 处理数据...
  }
}

跟 HAR 的引用方式一模一样,但底层的加载机制完全不同——HAR 是编译时拷贝代码进来,HSP 是运行时加载共享的 .so 文件。

运行时加载

HSP 支持运行时按需加载,这是 HAR 做不到的。

动态导入 HSP:

import { abilityLoader } from '@kit.AbilityKit'

async function loadFeatureModule() {
  try {
    // 动态加载 HSP 模块
    const module = await abilityLoader.loadModule('common_net/Index')

    // 获取导出的类
    const { HttpClient } = module

    // 正常使用
    const client = HttpClient.getInstance()
    const data = await client.get('/api/data')
    console.info('数据加载成功')
  } catch (err) {
    console.error(`模块加载失败: ${err.message}`)
  }
}

这段代码的关键在于 loadModule 它在运行时才去加载 HSP 模块,不是编译时就塞进 App 里。适合这些场景:

  • 功能模块按需加载,首屏只加载核心功能
  • 插件化架构,运行时决定加载哪些模块
  • 减少首屏启动时间,非核心模块延迟加载

敲黑板:动态加载有额外的运行时开销,不要所有模块都用动态加载。核心模块静态引入,非核心模块动态加载,这才是正确姿势。

HSP 与 HAR 选择

到底用 HAR 还是 HSP?别纠结,看这张决策表:

场景推荐方案原因
项目内部模块化HAR简单直接,编译时优化更好
多模块引用同一库HSP避免重复拷贝,减小包体积
跨 App 共享代码HSP多个 App 运行时共享一份
工具函数、小体积库HAR体积不大,没必要用 HSP
网络/图片等大型基础库HSP体积大,共享收益明显
需要运行时按需加载HSPHAR 不支持动态加载
纯 UI 组件库HAR组件库通常不大,HAR 更方便

我的经验: 体积超过 2MB 的共享库用 HSP,小的用 HAR。别为了用 HSP 而用 HSP,HSP 的运行时加载机制会带来一点性能开销。

注意事项

用 HSP 有几个坑得提前知道:

1. HSP 模块必须在 App 内

HSP 不能独立发布和安装,它必须跟 App 一起打包。如果你需要一个独立分发的模块,那不是 HSP 能解决的问题。

2. HSP 不支持多进程共享

HSP 在同一进程内共享,多进程场景下每个进程各加载一份,共享效果就没了。

3. 调试比 HAR 复杂

HSP 是运行时加载的,断点调试和堆栈追踪比 HAR 稍微麻烦一些。DevEco Studio 对 HSP 的调试支持在持续优化,但体验上还是 HAR 更丝滑。

4. 版本一致性

同一 App 内所有模块引用的 HSP 版本必须一致。如果 entry 引了 v1.0,feature_a 引了 v1.1,编译时会报错。

写在最后

HSP 的核心价值就一个词:共享。多个模块用同一套代码,运行时只加载一份,包体积和内存都省了。

但 HSP 不是万能药。小体积的模块用 HAR 更简单,别为了省几百 KB 折腾 HSP。选型的核心标准:体积大、引用多、跨模块共享的库用 HSP,其余用 HAR。

Logo

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

更多推荐