22、数据存储:SharedPreferences、Room数据库在TV应用中的实践
TV应用的数据存储,说实话,跟手机端有挺大区别的。你想想看,用户拿着遥控器,坐在三米外看电视,操作频率低,但数据持久化的需求一点不少——用户偏好设置、观看历史、收藏列表,这些都得存下来。
我这些年做TV开发,踩过不少存储相关的坑。今天咱们就聊聊SharedPreferences和Room数据库在TV场景下的正确用法。
22.1 SharedPreferences:轻量级配置存储
SharedPreferences适合存什么?说白了就是键值对,比如用户设置的音量、字幕开关、语言偏好这些。数据量小,结构简单,用SP最合适。
核心要点:TV应用中,SP主要用于存储用户配置,而非业务数据。
22.1.1 基本用法
// 获取SP实例,建议用应用级别的Context
SharedPreferences sp = context.getSharedPreferences("tv_config", Context.MODE_PRIVATE);
// 写入数据
sp.edit()
.putBoolean("subtitle_enabled", true)
.putString("language", "zh-CN")
.putInt("volume_level", 70)
.apply(); // 注意用apply()而非commit()
// 读取数据
boolean subtitleEnabled = sp.getBoolean("subtitle_enabled", false);
String language = sp.getString("language", "en");
int volume = sp.getInt("volume_level", 50);
这里有个细节:apply()是异步写入,commit()是同步写入。TV应用里,我建议一律用apply()。为什么?因为TV的UI线程很宝贵,同步写入可能卡顿,用户按遥控器时能明显感觉到延迟。
我的习惯:在TV应用中,所有SP写入操作都使用apply()。如果确实需要知道写入结果,可以用apply()配合SharedPreferences.OnSharedPreferenceChangeListener监听。
22.1.2 TV场景下的特殊处理
TV应用有个特点:用户可能长时间不操作,然后突然连续操作。比如调音量,用户可能连按十几次。这时候如果每次按键都写SP,性能开销不小。
我建议的做法是:
// 防抖写入:500ms内只写一次
private Handler mHandler = new Handler();
private Runnable mSaveRunnable;
public void saveVolumeWithDebounce(int volume) {
if (mSaveRunnable != null) {
mHandler.removeCallbacks(mSaveRunnable);
}
mSaveRunnable = () -> {
sp.edit().putInt("volume_level", volume).apply();
};
mHandler.postDelayed(mSaveRunnable, 500);
}
嗯,这个模式我在多个TV项目里都用过,效果不错。用户连按遥控器时,只有最后一次操作会被保存。
22.2 Room数据库:结构化数据持久化
当数据量变大、结构变复杂时,SP就不够用了。比如用户的观看历史、收藏的频道列表、播放进度记录——这些需要增删改查,用Room才是正道。
Room是Google官方推荐的ORM框架,基于SQLite封装。我刚开始用的时候觉得有点重,但用习惯了发现,它确实能减少很多样板代码。
22.2.1 实体定义
@Entity(tableName = "watch_history")
public class WatchHistory {
@PrimaryKey(autoGenerate = true)
private long id;
@ColumnInfo(name = "content_id")
private String contentId;
@ColumnInfo(name = "content_title")
private String contentTitle;
@ColumnInfo(name = "progress")
private long progress; // 播放进度,单位毫秒
@ColumnInfo(name = "duration")
private long duration; // 总时长
@ColumnInfo(name = "last_watch_time")
private long lastWatchTime; // 最后观看时间戳
// getter/setter 省略
}
注意:TV应用中,contentId建议用String类型而非int。因为不同内容源(如优酷、爱奇艺)的ID格式可能不同,String兼容性更好。我曾经因为这个踩过坑,后来统一改成String了。
22.2.2 DAO接口
@Dao
public interface WatchHistoryDao {
@Query("SELECT * FROM watch_history ORDER BY last_watch_time DESC LIMIT 50")
LiveData<List<WatchHistory>> getRecentHistory();
@Query("SELECT * FROM watch_history WHERE content_id = :contentId")
WatchHistory getByContentId(String contentId);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(WatchHistory history);
@Query("DELETE FROM watch_history WHERE last_watch_time < :threshold")
void deleteOldRecords(long threshold);
@Query("DELETE FROM watch_history")
void clearAll();
}
这里我用了LiveData包装查询结果。TV应用里,数据变化后UI需要自动更新,LiveData配合ViewModel用起来很顺手。
22.2.3 数据库实例
@Database(entities = {WatchHistory.class, Favorite.class}, version = 1, exportSchema = false)
public abstract class TvDatabase extends RoomDatabase {
private static volatile TvDatabase INSTANCE;
public abstract WatchHistoryDao watchHistoryDao();
public abstract FavoriteDao favoriteDao();
public static TvDatabase getInstance(Context context) {
if (INSTANCE == null) {
synchronized (TvDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context.getApplicationContext(),
TvDatabase.class,
"tv_app.db"
)
.fallbackToDestructiveMigration()
.build();
}
}
}
return INSTANCE;
}
}
避坑指南:TV应用升级时,数据库迁移是个麻烦事。我建议开发阶段用fallbackToDestructiveMigration(),发布前再写正式的Migration。另外,数据库版本号一定要谨慎递增,我曾经手滑改错过,导致用户数据全丢了……
22.3 TV场景下的存储策略对比
| 特性 | SharedPreferences | Room数据库 |
|---|---|---|
| 适用场景 | 用户配置、开关状态 | 结构化业务数据 |
| 数据量 | 小(KB级别) | 大(MB级别) |
| 查询能力 | 仅键值对 | SQL查询、排序、过滤 |
| 线程安全 | 需自行处理 | 内置线程安全 |
| 类型安全 | 弱(需类型转换) | 强(编译时检查) |
| 性能 | 读写快,但大量写入卡顿 | 批量操作高效 |
22.4 知识体系结构图
22.5 实战建议
我个人习惯是:
- 用户设置类(音量、字幕、语言)→ SharedPreferences
- 业务数据类(观看历史、收藏、播放进度)→ Room
- 临时缓存类(网络请求结果、图片缓存)→ 内存缓存或磁盘缓存
另外,TV应用有个特点:用户可能长时间不关机。这意味着应用进程可能运行好几天。如果数据写入不及时,突然断电或系统重启,数据就丢了。所以,关键数据(比如播放进度)建议在onPause时立即写入,不要等。
我曾经犯过的错:有个项目里,我把播放进度放在onStop里保存。结果用户按Home键回到桌面,系统可能很快杀掉TV应用进程,onStop不一定能执行完。后来改成onPause里保存,问题就解决了。
好了,关于TV应用的数据存储,核心就是这些。记住一句话:SP管配置,Room管数据,各司其职,别混着用。