ArkTS数据存储机制、使用方法及最佳实践

在HarmonyOS应用开发中,ArkTS作为核心的声明式开发范式,提供了多层次、多类型的数据存储与状态管理方案,以满足从简单配置到复杂结构化数据、从临时状态到持久化存储的多样化需求。其核心设计思想是根据数据的不同生命周期、访问范围和结构特性,选用最合适的存储方案,以确保应用性能、数据安全与开发效率。

为快速理解各方案的定位与选择依据,可参考下表对比:

方案类别 核心机制 数据生命周期 主要适用场景 关键特性
本地持久化存储 Preferences(键值对) 应用卸载前 用户偏好、简单配置 轻量、异步、键值对
  关系型数据库(SQLite) 应用卸载前 复杂结构化数据、需查询排序 完整SQL支持、事务、加密
  文件系统API 应用卸载前 图片、文档、缓存文件等非结构化数据 灵活、流式操作
应用状态管理 AppStorage(应用级) 应用运行时 跨页面/组件的全局状态(如用户登录信息) 应用级单例、响应式同步
  LocalStorage(页面级) 页面实例运行时 同一页面内组件间共享状态(如表单数据) 页面级实例、响应式同步
  PersistentStorage(持久化状态桥) 持久化 + 运行时 需持久化且全局访问的状态(如主题、令牌) 连接AppStorage与持久化存储

一、本地持久化存储方案详解与代码实践

本地持久化存储用于将数据永久保存在设备上,确保应用重启后数据不丢失,是存储用户生成内容或配置的核心手段。

1. Preferences(首选项)
这是一种轻量级的键值对存储,适用于保存用户偏好设置、简单的应用配置或标志位。其操作基于异步Promise,支持创建多个数据库实例以实现数据按业务模块隔离。

// 导入preferences模块
import preferences from '@ohos.data.preferences';

async function demoPreferences() {
  // 1. 获取UIAbility的上下文,通常在EntryAbility中获取
  let context = ...; // 例如:globalThis.abilityContext
  // 创建或打开名为`user_settings`的Preferences实例
  let pref: preferences.Preferences = await preferences.getPreferences(context, 'user_settings');

  // 2. 存储数据(支持string、number、boolean等基本类型)
  await pref.put('username', '李华');
  await pref.put('notifications_enabled', true);
  await pref.put('last_login_time', Date.now());
  await pref.flush(); // 将内存中的数据异步写入持久化文件

  // 3. 读取数据,需提供默认值
  let username: string = await pref.get('username', 'Guest');
  let notificationsOn: boolean = await pref.get('notifications_enabled', false);
  console.log(`用户名: ${username}, 通知开启: ${notificationsOn}`);

  // 4. 数据管理
  await pref.delete('last_login_time'); // 删除单个键
  // await pref.clear(); // 清空当前数据库所有数据(慎用)
}

2. 关系型数据库(SQLite)
对于需要复杂查询、事务支持、数据关联和索引的结构化数据,应使用内嵌的SQLite关系型数据库。ArkTS通过@ohos.data.relationalStore模块提供了完整的ORM和SQL操作能力。

// 导入relationalStore模块
import relationalStore from '@ohos.data.relationalStore';
import common from '@ohos.app.ability.common';

async function demoRdb() {
  // 1. 定义SQL语句(建议使用常量管理)
  const SQL_CREATE_TABLE = `
    CREATE TABLE IF NOT EXISTS task (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT NOT NULL,
        description TEXT,
        priority INTEGER DEFAULT 0,
        created_date INTEGER,
        is_done INTEGER DEFAULT 0 CHECK(is_done IN (0, 1))
    )
  `;

  // 2. 配置并获取RdbStore连接
  let context: common.UIAbilityContext = ...; // 获取UIAbilityContext
  const STORE_CONFIG: relationalStore.StoreConfig = {
    name: 'task.db', // 数据库文件名称
    securityLevel: relationalStore.SecurityLevel.S1 // 设置数据库安全级别,S1为基础加密
  };
  
  let rdbStore: relationalStore.RdbStore;
  try {
    rdbStore = await relationalStore.getRdbStore(context, STORE_CONFIG);
    console.info('RDB Store created successfully.');
  } catch (err) {
    console.error(`Failed to get RdbStore. Code: ${err.code}, message: ${err.message}`);
    return;
  }

  // 3. 执行建表语句
  await rdbStore.executeSql(SQL_CREATE_TABLE);

  // 4. 插入数据(使用ValuesBucket封装数据)
  const taskToInsert: relationalStore.ValuesBucket = {
    'title': '学习ArkTS数据存储',
    'description': '掌握Preferences和RDB的使用',
    'priority': 1,
    'created_date': Date.now()
  };
  let insertId = await rdbStore.insert('task', taskToInsert);
  console.info(`Inserted task with ID: ${insertId}`);

  // 5. 查询数据(使用RdbPredicates构建查询条件)
  let predicates = new relationalStore.RdbPredicates('task');
  predicates.equalTo('is_done', 0) // 查询未完成的任务
          .orderByDesc('priority') // 按优先级降序
          .limit(10); // 限制返回10条
  let columns: Array<string> = ['id', 'title', 'priority'];
  let resultSet: relationalStore.ResultSet = await rdbStore.query(predicates, columns);
  
  // 6. 遍历结果集
  while (resultSet.goToNextRow()) {
    let id = resultSet.getLong(resultSet.getColumnIndex('id'));
    let title = resultSet.getString(resultSet.getColumnIndex('title'));
    console.info(`Task [${id}]: ${title}`);
  }
  resultSet.close(); // 必须关闭结果集释放资源

  // 7. 更新数据
  let updateValues: relationalStore.ValuesBucket = { 'is_done': 1 };
  let updatePredicates = new relationalStore.RdbPredicates('task');
  updatePredicates.equalTo('id', insertId);
  let rowsUpdated = await rdbStore.update(updateValues, updatePredicates);
  console.info(`Updated ${rowsUpdated} row(s).`);

  // 8. 使用事务确保数据一致性(例如:标记完成并记录完成时间)
  await rdbStore.beginTransaction(); // 开始事务
  try {
    await rdbStore.update(updateValues, updatePredicates);
    // 可以在此执行其他相关更新操作...
    await rdbStore.commit(); // 提交事务
  } catch (e) {
    await rdbStore.rollback(); // 发生错误时回滚
    console.error('Transaction failed:', e);
  }
}

3. 文件系统API
通过@ohos.file.fs等模块,可以直接操作应用沙箱内的目录和文件,适合存储图片、音频、视频、文档或任何自定义格式的缓存文件。这是最灵活但也最底层的存储方式。

import fs from '@ohos.file.fs';
import common from '@ohos.app.ability.common';

function demoFileSystem() {
  // 获取应用的文件目录路径(沙箱内)
  let context = getContext(this) as common.UIAbilityContext;
  let filesDir = context.filesDir; // 类似 `/data/app/.../files`
  let cacheDir = context.cacheDir; // 缓存目录,系统可能在存储不足时清理

  // 场景1:写入文本配置文件
  let configPath = `${filesDir}/app_config.json`;
  let configContent = JSON.stringify({ theme: 'dark', language: 'zh-CN' });
  
  // 使用同步API示例(也有异步版本)
  let file = fs.openSync(configPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
  fs.writeSync(file.fd, configContent);
  fs.closeSync(file.fd);
  console.info(`Config saved to: ${configPath}`);

  // 场景2:读取文件
  file = fs.openSync(configPath, fs.OpenMode.READ_ONLY);
  let stat = fs.statSync(file.fd);
  let buffer = new ArrayBuffer(stat.size);
  let readLen = fs.readSync(file.fd, buffer);
  let readContent = String.fromCharCode.apply(null, new Uint8Array(buffer.slice(0, readLen)));
  fs.closeSync(file.fd);
  let config = JSON.parse(readContent);
  console.info(`Read config theme: ${config.theme}`);

  // 场景3:管理文件与目录
  let imagesDir = `${filesDir}/images`;
  if (!fs.accessSync(imagesDir)) {
    fs.mkdirSync(imagesDir); // 创建目录
  }
  // 列出目录内容
  let dir = fs.opendirSync(imagesDir);
  let entry = fs.readSync(dir);
  while (entry !== undefined) {
    console.info(`Found entry: ${entry.name}`);
    entry = fs.readSync(dir);
  }
  fs.closeSync(dir);
}

二、应用状态管理方案详解与代码实践

状态管理用于在应用运行时高效地共享和同步可变数据,其核心是响应式机制。AppStorageLocalStorage通过与UI组件装饰器(如@StorageLink)绑定,实现数据变化自动触发UI更新的效果。

1. AppStorage:应用级全局状态单例
AppStorage是应用范围内唯一的可变状态管理中心,类似于“全局状态仓库”。任何组件都可以通过装饰器与之建立连接,读取或修改其中的状态,修改会实时同步到所有绑定了该状态的组件。

// 在任意ets文件中操作AppStorage
import { AppStorage } from '@kit.ArkUI';

// --- 在应用逻辑层(如ViewModel或Service)中管理状态 ---
function initAppGlobalState() {
  // SetOrCreate: 如果key不存在则创建并设置初始值,存在则更新
  AppStorage.SetOrCreate<number>('globalCounter', 0);
  AppStorage.SetOrCreate<string>('currentUser', '');
  AppStorage.SetOrCreate<boolean>('isDarkMode', false);
}

function updateUserProfile(name: string) {
  // Set: 直接更新已有key的值,会触发所有相关UI更新
  AppStorage.Set<string>('currentUser', name);
}

// --- 在UI组件中使用 ---
@Entry
@Component
struct HomePage {
  // 使用@StorageLink建立双向绑定:组件内修改会写回AppStorage,并触发其他绑定组件更新
  @StorageLink('globalCounter') counter: number = 0;
  // 使用@StorageProp建立单向绑定:组件内修改是局部的,不会影响AppStorage中的源数据
  @StorageProp('currentUser') userName: string = '';

  build() {
    Column({ space: 10 }) {
      Text(`全局计数器: ${this.counter}`).fontSize(30)
      Button('计数器+1')
        .onClick(() => {
          this.counter += 1; // 修改会同步到AppStorage,触发其他页面的Text更新
        })

      Text(`欢迎您, ${this.userName}`).fontSize(20)
      Button('模拟登录')
        .onClick(() => {
          // 通过AppStorage的API更新,同样会触发UI更新
          AppStorage.Set<string>('currentUser', '张三');
          // 由于userName是@StorageProp单向绑定,此处this.userName不会立即改变,需等下次渲染
        })

      // 另一个组件也绑定同一个全局状态
      CounterDisplayComponent()
    }
  }
}

// 另一个独立的组件
@Component
struct CounterDisplayComponent {
  @StorageLink('globalCounter') displayCounter: number = 0; // 绑定同一个key

  build() {
    Text(`另一个组件看到的计数: ${this.displayCounter}`).fontSize(18)
      .fontColor(Color.Red)
  }
}

2. LocalStorage:页面级状态共享
LocalStorage是页面级别的状态存储,每个页面实例可以拥有自己的LocalStorage对象。它用于在同一个页面(或同一个UIAbility)内的组件树中共享状态,其生命周期与页面实例绑定,页面销毁则状态释放。

// 通常在页面入口处创建并注入LocalStorage实例
// 假设这是某个页面的ets文件
let pageLocalStorage = new LocalStorage();
// 初始化页面级状态
pageLocalStorage.setOrCreate('formData', { name: '', age: 0 });
pageLocalStorage.setOrCreate('pageTitle', '编辑个人信息');

@Entry(pageLocalStorage) // 将storage实例注入页面根组件
@Component
struct PersonalInfoPage {
  // 使用@LocalStorageLink建立双向绑定
  @LocalStorageLink('formData') formData: Object = {};
  @LocalStorageLink('pageTitle') title: string = '';

  build() {
    Column() {
      Text(this.title).fontSize(24)
      
      // 表单输入组件
      TextInput({ placeholder: '请输入姓名' })
        .value(this.formData.name)
        .onChange((value: string) => {
          this.formData = { ...this.formData, name: value }; // 需要整体赋值以触发UI更新
        })
      
      // 子组件,接收同一个LocalStorage实例
      ChildComponent({ storage: pageLocalStorage })
    }
  }
}

// 子组件
@Component
struct ChildComponent {
  // 通过构造参数接收父组件传递的LocalStorage实例
  @Consume storage: LocalStorage;
  
  // 在子组件内部使用装饰器绑定
  @LocalStorageLink('formData') linkedFormData: Object = {};

  build() {
    Column() {
      Text(`子组件接收到的姓名: ${this.linkedFormData.name}`)
      Button('清空姓名')
        .onClick(() => {
          // 修改会同步到共享的LocalStorage,从而更新父组件中的显示
          this.linkedFormData = { ...this.linkedFormData, name: '' };
        })
    }
  }
}

3. PersistentStorage:持久化状态管理桥
PersistentStorage是一个工具类,用于在AppStorage中的特定属性和持久化存储(底层是Preferences)之间建立双向同步链路。它解决了需要将全局状态持久化的常见需求。

import { PersistentStorage, AppStorage } from '@kit.ArkUI';

// 应用启动时(如EntryAbility的onCreate中)进行初始化关联
export function initPersistentState() {
  // 将AppStorage中的`userToken`属性持久化到本地,初始值为''
  PersistentStorage.PersistProp<string>('userToken', '');
  
  // 可以关联多个属性
  PersistentStorage.PersistProp<boolean>('isFirstLaunch', true);
  PersistentStorage.PersistProp<string>('appTheme', 'light');
}

// 之后,在应用的任何地方操作AppStorage,变化都会自动保存
function performLogin(token: string) {
  // 此设置操作会:1. 更新AppStorage内存中的值;2. 自动将新值写入持久化文件
  AppStorage.Set<string>('userToken', token);
}

function logout() {
  AppStorage.Set<string>('userToken', ''); // 清空token也会同步持久化
}

// 在UI组件中,可以像使用普通AppStorage属性一样使用它
@Entry
@Component
struct MainPage {
  @StorageLink('userToken') token: string = '';
  @StorageLink('appTheme') theme: string = 'light';

  aboutToAppear() {
    // 应用启动后,PersistentStorage会自动将本地保存的值读回AppStorage
    // 因此这里可以直接使用可能已存在的持久化值
    if (this.token) {
      console.info('检测到已保存的登录令牌,自动跳转...');
    }
  }

  build() {
    Column() {
      Text(`当前主题: ${this.theme}`)
      Button('切换主题')
        .onClick(() => {
          let newTheme = this.theme === 'light' ? 'dark' : 'light';
          AppStorage.Set<string>('appTheme', newTheme); // 切换并自动保存偏好
        })
    }
  }
}

三、最佳实践与选型建议

  1. 清晰的数据分层策略

    • 临时/页面状态:仅在单个页面内使用的交互状态(如输入框内容、弹窗显示状态)应使用 LocalStorage
    • 全局应用状态:需要跨多个页面访问和修改的状态(如用户登录信息、全局主题、购物车)应使用 AppStorage
    • 持久化全局配置:需要持久化且全局访问的轻量级数据(如用户设置、登录令牌)应使用 PersistentStorage + AppStorage 组合。
    • 复杂业务数据:用户创建的内容、订单、消息等结构化数据,应使用 关系型数据库 进行增删改查。
    • 简单配置与标志:是否首次启动、简单的开关设置等,使用 Preferences 更为轻便。
    • 非结构化媒体或文件:用户头像、下载的文档、离线缓存等,使用 文件系统API 存储。
  2. 性能与内存优化

    • 避免大对象存入响应式存储:不要将大型数组或复杂嵌套对象直接存入AppStorageLocalStorage,因为每次修改都会触发深度比较和UI更新,可能导致性能问题。应存储引用或ID,详细数据通过其他方式管理。
    • 合理使用装饰器:对于只读的全局状态,使用@StorageProp替代@StorageLink,避免不必要的双向同步开销。仅在组件确实需要修改并同步回全局状态时才使用@StorageLink
    • 及时清理资源:数据库ResultSet、文件描述符等使用后必须及时关闭。
  3. 数据安全与隐私保护

    • 敏感信息加密:存储密码、身份令牌等敏感信息时,务必利用存储方案提供的加密功能。例如,RDB配置securityLevel,Preferences数据也会被系统自动加密保护。
    • 最小权限原则:文件系统操作应严格限定在应用沙箱内,避免请求不必要的文件访问权限。
    • 网络数据安全:与远程服务器同步数据时,必须使用HTTPS等安全协议,并对用户隐私数据实施严格的访问控制和脱敏处理。
  4. 典型场景组合示例(用户系统)

    // 1. 持久化令牌 (PersistentStorage + AppStorage)
    PersistentStorage.PersistProp<string>('authToken', '');
    
    // 2. 用户偏好设置 (Preferences)
    // 在SettingService中管理
    async function saveUserPreference(key: string, value: any) {
      let pref = await preferences.getPreferences(context, 'user_prefs');
      await pref.put(key, value);
      await pref.flush();
    }
    
    // 3. 用户个人资料 (关系型数据库)
    // 在UserRepository中管理
    async function saveUserProfile(user: User) {
      let valueBucket: relationalStore.ValuesBucket = {
        'userId': user.id,
        'name': user.name,
        'avatarPath': user.avatarLocalPath // 头像保存为文件
      };
      await rdbStore.insert('user_profile', valueBucket);
    }
    
    // 4. 头像文件 (文件系统)
    async function saveAvatarImage(userId: string, imageData: ArrayBuffer) {
      let avatarPath = `${context.filesDir}/avatars/${userId}.jpg`;
      // ... 写入文件操作
    }
    
    // 5. 在UI组件中组合使用
    @Entry
    @Component
    struct MyApp {
      @StorageLink('authToken') token: string;
      
      build() {
        Column() {
          if (this.token) {
            // 已登录,显示主界面,主界面从数据库和AppStorage读取数据
            MainPage()
          } else {
            // 未登录,显示登录页
            LoginPage()
          }
        }
      }
    }
    

通过深入理解并合理运用ArkTS提供的这套从内存到持久化、从局部到全局的完整数据存储与状态管理方案,开发者可以构建出数据流清晰、响应迅速、体验流畅且稳定可靠的HarmonyOS应用。


参考来源

 

Logo

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

更多推荐