别一上来就 Preferences 一把梭

做 HarmonyOS 应用,只要涉及数据展示,早晚要碰本地缓存。很多同学第一反应就是 Preferences:简单、key-value、几行代码就能存。于是把用户信息、列表数据、甚至整个 JSON 都往里塞。

结果呢?列表数据稍微大一点,页面就开始卡。点一下卡半秒,滑动掉帧,用户以为你写了个假 App。

问题不在 Preferences 本身,而在你把它用错了地方。Preferences 适合存轻量配置,比如主题、语言、登录 token。一旦数据上了几百条、需要按条件查询、需要分页,就该换关系型数据库 RDB 了。

这篇就讲清楚:Preferences 和 RDB 各自适合什么场景,怎么封装一个统一的缓存层,以及实际开发中那些容易踩的坑。

https://placeholder-01.jpg/

一、Preferences:轻量配置的优选

Preferences 的 API 很简单,核心就几个方法:

typescript

import preferences from '@ohos.data.preferences';

// 获取 Preferences 实例
let pref = await preferences.getPreferences(context, 'myStore');

// 存
await pref.put('username', '张三');
await pref.flush();

// 取
let username = await pref.get('username', '默认值') as string;

注意几个点:

第一,getPreferences 是异步的。 别在 build() 里调用,最好在 aboutToAppear 或应用启动时初始化一次,把实例存起来复用。

第二,put 之后必须 flush。 不调 flush,数据只存在内存里,应用一杀就没了。很多同学存完没生效,就是漏了这行。

第三,Preferences 有大小限制。 官方建议单个 value 不要超过 8KB,整个 store 不要超过 1MB。你存个长列表 JSON,很容易就超了。

所以 Preferences 的正确用法是:存配置、存标记、存小对象。比如:

typescript

// 用户登录态
await pref.put('token', token);
await pref.put('userId', userId);
await pref.put('theme', 'dark');

// 首次启动标记
await pref.put('hasLaunched', true);

这些数据量小、读写频繁、不需要复杂查询,用 Preferences 最合适。

二、RDB:结构化数据的归宿

当数据变成列表、需要按条件筛选、需要分页、需要排序,就该上 RDB 了。

RDB 基于 SQLite,用法和 Android 的 Room 有点像,但更原始一些。先建表:

typescript

import relationalStore from '@ohos.data.relationalStore';

const STORE_CONFIG: relationalStore.StoreConfig = {
  name: 'MyApp.db',
  securityLevel: relationalStore.SecurityLevel.S1
};

const SQL_CREATE_TABLE = `
  CREATE TABLE IF NOT EXISTS user (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    age INTEGER,
    email TEXT
  )
`;

let rdbStore: relationalStore.RdbStore;

async function initRdb(context: Context) {
  rdbStore = await relationalStore.getRdbStore(context, STORE_CONFIG);
  await rdbStore.executeSql(SQL_CREATE_TABLE);
}

插入数据:

typescript

const valueBucket: relationalStore.ValuesBucket = {
  name: '张三',
  age: 28,
  email: 'zhangsan@example.com'
};

await rdbStore.insert('user', valueBucket);

查询数据:

typescript

const predicates = new relationalStore.RdbPredicates('user');
predicates.equalTo('age', 28);
predicates.orderByDesc('id');

const resultSet = await rdbStore.query(predicates, ['id', 'name', 'age', 'email']);

while (resultSet.goToNextRow()) {
  const id = resultSet.getLong(resultSet.getColumnIndex('id'));
  const name = resultSet.getString(resultSet.getColumnIndex('name'));
  console.info(`${id} - ${name}`);
}
resultSet.close();

RDB 的优势很明显:支持 SQL、支持索引、支持事务、数据量大也不虚。但缺点是写起来比 Preferences 麻烦,而且需要手动管理结果集关闭,否则会内存泄漏。

https://placeholder-02.png/

三、封装一个统一的缓存层

项目里如果每个页面都直接调 Preferences 或 RDB,代码会散得到处都是。更好的做法是封装一个 CacheManager,对外暴露统一接口,内部根据数据类型选择存储方式。

先定义接口:

typescript

export interface CacheOptions {
  type: 'preferences' | 'rdb';
  key: string;
  value: any;
}

然后实现:

typescript

export class CacheManager {
  private static instance: CacheManager;
  private pref: preferences.Preferences | null = null;
  private rdb: relationalStore.RdbStore | null = null;

  static getInstance(): CacheManager {
    if (!CacheManager.instance) {
      CacheManager.instance = new CacheManager();
    }
    return CacheManager.instance;
  }

  async init(context: Context) {
    this.pref = await preferences.getPreferences(context, 'app_cache');
    this.rdb = await relationalStore.getRdbStore(context, {
      name: 'app_cache.db',
      securityLevel: relationalStore.SecurityLevel.S1
    });
    await this.rdb.executeSql(`
      CREATE TABLE IF NOT EXISTS cache (
        key TEXT PRIMARY KEY,
        value TEXT,
        updateTime INTEGER
      )
    `);
  }

  async set(key: string, value: any, useRdb: boolean = false) {
    if (useRdb) {
      const bucket: relationalStore.ValuesBucket = {
        key,
        value: JSON.stringify(value),
        updateTime: Date.now()
      };
      await this.rdb!.insert('cache', bucket);
    } else {
      await this.pref!.put(key, JSON.stringify(value));
      await this.pref!.flush();
    }
  }

  async get<T>(key: string, useRdb: boolean = false): Promise<T | null> {
    if (useRdb) {
      const predicates = new relationalStore.RdbPredicates('cache');
      predicates.equalTo('key', key);
      const resultSet = await this.rdb!.query(predicates, ['value']);
      if (resultSet.goToFirstRow()) {
        const value = resultSet.getString(resultSet.getColumnIndex('value'));
        resultSet.close();
        return JSON.parse(value) as T;
      }
      resultSet.close();
      return null;
    } else {
      const value = await this.pref!.get(key, '') as string;
      return value ? JSON.parse(value) as T : null;
    }
  }
}

这样调用方只需要关心 key 和 value,不用管底层用的是 Preferences 还是 RDB。小数据走 Preferences,大数据走 RDB,切换成本很低。

四、踩坑合集

坑一:Preferences 存大 JSON,主线程卡顿。

Preferences 的 put 和 get 虽然是异步的,但底层序列化和反序列化是在主线程做的。你存一个 500KB 的 JSON,序列化就要几十毫秒,页面直接掉帧。所以大列表千万别用 Preferences。

坑二:RDB 查询没关 ResultSet。

上面代码里每次 query 后面都跟了 resultSet.close()。不关的话,游标会一直占着资源,查几次之后应用就崩了。这个坑非常常见,一定要养成习惯。

坑三:RDB 升级没写迁移逻辑。

应用版本更新时,如果数据库表结构变了,需要实现 onUpgrade。不处理的话,老用户升级后直接闪退。

typescript

const STORE_CONFIG: relationalStore.StoreConfig = {
  name: 'MyApp.db',
  securityLevel: relationalStore.SecurityLevel.S1
};

rdbStore = await relationalStore.getRdbStore(context, STORE_CONFIG);
rdbStore.version = 2; // 设置新版本号,触发 onUpgrade

然后在 getRdbStore 的回调里处理升级。别偷懒,这一步迟早要补。

https://placeholder-03.png/

五、总结

这篇讲了 Preferences 和 RDB 的适用场景、基本用法和封装思路。核心记住三点:

  1. 小数据用 Preferences,大数据用 RDB,别混着用。

  2. Preferences 的 put 后要 flush,RDB 的 ResultSet 用完要 close。

  3. 封装统一缓存层,别让业务代码直接依赖具体存储 API。

Logo

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

更多推荐