Android Jetpack 系列(五)Room 本地数据库实战详解
1. 简介
在需要轻量级本地持久化的场景中,DataStore 是一个理想的选择(详见《Android Jetpack 系列(四)DataStore 全面解析与实践》)。但当你面临如下需求时,本地数据库便显得尤为重要:
- 复杂的数据模型管理;
- 数据之间存在关联关系;
- 支持部分字段更新;
- 实现离线缓存与增量更新。
为此,Google 在 Jetpack 架构组件中推出了新一代本地数据库解决方案:Room。它对原生 SQLite 进行了封装,提供类型安全、编译期校验的数据库访问方式,极大简化了样板代码的编写。
Room 的典型使用场景包括:
- 缓存网络请求结果,实现离线访问;
- 管理本地复杂结构化数据;
- 实现高效的增量数据更新及多表关系维护。
主要优势包括:
- SQL 编译期校验:提前发现错误,提升稳定性;
- 注解驱动开发:减少冗余代码;
- 简化数据迁移:支持版本演进;
- 与 LiveData / Flow 集成:实现响应式 UI 更新。
2. 添加依赖
2.1 添加 Room 依赖项
要在项目中使用 Room,需要在模块的 build.gradle.kts 或 build.gradle 文件中添加如下依赖项:
dependencies {val room_version = "2.7.2"implementation("androidx.room:room-runtime:$room_version")ksp("androidx.room:room-compiler:$room_version")// Kotlin 扩展与协程支持implementation("androidx.room:room-ktx:$room_version")// 可选:RxJava / Paging / TestingtestImplementation("androidx.room:room-testing:$room_version")
}
2.2启用 KSP(Kotlin Symbol Processing)
Room 使用 KSP 编译时执行注解处理,KSP类似于 kpat,但更高效,特别适合 Kotlin 项目。ksp允许库开发者创建编译时代码生成器,比如自动生成依赖注入代码、路由代码、数据库操作代码等。
ksp和 kapt 的区别:
项目 | ksp | kapt |
性能 | 更快、更轻量 | 较慢,尤其在大型项目中 |
对 Kotlin 支持 | 原生支持 Kotlin | 本质是处理 Java 注解,Kotlin 兼容性一般 |
编译时间 | 更短 | 更长 |
错误提示 | 更接近源码,容易定位问题 | 有时提示不准确 |
下面是启用 KSP 的方式:
工程级 build.gradle.kts 中添加:
plugins {alias(libs.plugins.android.application) apply falsealias(libs.plugins.kotlin.android) apply falsealias(libs.plugins.kotlin.compose) apply falseid("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
}
注意 :KSP 的前缀版本需与 Kotlin 版本保持一致。如 Kotlin 版本为 2.0.21,KSP 的版本必须以 2.0.21 开头。
模块级 build.gradle.kts 中启用:
plugins {alias(libs.plugins.android.application)alias(libs.plugins.kotlin.android)alias(libs.plugins.kotlin.compose)id("com.google.devtools.ksp")
}
3. 了解 Room
Room 的架构由三大核心组件构成:
3.1 Entity(实体类)
- 对应数据库中的一张表;
- 使用 @Entity(tableName = "xxx") 注解声明;
- 类的每个字段就是表中的一列;
- 至少要有一个主键(@PrimaryKey);
- 支持复合主键、忽略字段、嵌套对象。
3.2 DAO(数据访问对象)
- 使用 @Dao 注解声明;
- 用于定义 SQL 操作(@Query)或封装常用方法(如 @Insert、@Update、@Delete);
- 支持挂起函数(suspend)、LiveData、Flow 等多种返回类型;
- 可以组合多个操作为一个事务(使用 @Transaction 注解);
- 支持多表查询与动态SQL构建。
3.3 Database(数据库类)
- 使用 @Database 注解声明,并指定 entities、version;
- 必须继承自 RoomDatabase;
- 是 Room 的核心入口,提供 DAO 实例访问;
- 建议通过单例或其他线程安全方式创建实现。
三者关系
Room 中,Database 是数据库的核心入口,负责提供 DAO 实例。而 DAO 提供访问 Entity 数据的能力。通过 DAO,可以实现对 Entity 对应表的增删改查操作。如下图说明了 Room 的不同组件之间的关系。
4 实战示例
本节将构建一个完整的 Room 使用案例,涉及三个典型数据表:用户表(User)、商品表(Product)、订单表(Order)。内容涵盖了 Entity 注解的多种用法、DAO 接口的定义,以及数据库的创建与使用。
4.1 数据实体(Entity)
Room 中的实体类(Entity)用于定义数据库中的数据结构。每个实体类对应数据库中的一张表,其字段对应表中的列。通过注解即可完成表结构定义,无需手写 SQL 语句。
用户表:User
@Entity(tableName = "user",indices = [Index(value = ["username"], unique = true)]
)
data class User(@PrimaryKey(autoGenerate = true)@ColumnInfo(name = "user_id")val userId: Long = 0,val username: String,val password: String,@ColumnInfo(name = "full_name")val fullName: String,@Embedded(prefix = "addr_")val address: Address
) {@Ignoreval isOnline: Boolean = false
}
地址嵌套类:Address
data class Address(val province:String,val city: String,val zipCode: String
)
商品表:Product
@Entity(tableName = "products",indices = [Index(value = ["name"], unique = true)]
)
data class Product(@PrimaryKey(autoGenerate = true)@ColumnInfo(name = "product_id")val productId: Long = 0L,val name: String,val price: Double
)
订单表:Order
@Entity(tableName = "orders",foreignKeys = [ForeignKey(entity = User::class,parentColumns = ["user_id"],childColumns = ["user_owner_id"],onDelete = ForeignKey.CASCADE),ForeignKey(entity = Product::class,parentColumns = ["product_id"],childColumns = ["product_id"],onDelete = ForeignKey.NO_ACTION)],indices = [Index(value = ["user_owner_id"]),Index(value = ["product_id"])]
)
data class Order(@PrimaryKey(autoGenerate = true)@ColumnInfo(name = "order_id")val orderId: Long = 0L,@ColumnInfo(name = "user_owner_id")val userOwnerId: Long, @ColumnInfo(name = "product_id")val productId: Long, val quantity: Int
)
注意:SQLite 表名和列名称不区分大小写。
常用注解说明:
注解 | 说明 |
@Entity | 声明表结构,可设置表名、索引、外键、主键等。 |
tableName 参数表示表名,为空则 Room 将类名称用作数据库表名称 | |
indices 参数表示为某一个表字段加上索引,其中value表示字段名;unique表示是否唯一。 | |
ForeignKeys 参数用于声明多个外键。ForeignKey内的参数: entity 参数表示外键指向的实体类; parentColumns 参数表示指向实体类的表字段名; childColumns 表示当前表中的字段名; onDelete 表示主表删除后是否会自动删除本表的数据,CASCADE 会删除;NO_ACTION不删除。 | |
primaryKeys 参数可设置多个列的组合对实体实例进行唯一标识,也就是通过多个列定义一个复合主键。例如: data class User( val firstName: String?, val lastName: String? ) | |
ignoredColumns 参数用于指定某字段是忽略字段,例如实体继承了父实体的字段,则通过该参数进行指定。例如: var picture: Bitmap? = null } @Entity(ignoredColumns = ["picture"]) data class RemoteUser( @PrimaryKey val id: Int, val hasVpn: Boolean ) : User() | |
@ColumnInfo | 映射字段名称到数据库列名,name参数为空或者省略该注解,Room 默认使用字段名称作为数据库中的列名称。 |
@PrimaryKey | 声明列字段为表主键,若autoGenerate参数为true,则表示可自动递增生成。 |
@Embedded | 声明字段为嵌套字段,字段类型对应类内的字段同样会存储在数据库。prefix参数表示为内部字段加上前缀。例如上面User的address字段,它在数据库实际上是对应三个字段:addr_province、addr_city 和 addr_zipCode。 |
@Ignore | 声明字段仅用于类本身,但不会创建数据库列。 |
4.2 数据访问对象(DAO)
DAO(Data Access Object)是访问数据库的核心接口。Room 会在编译时自动生成其实现代码,保障类型安全。
推荐将 DAO 定义为接口(也可为抽象类),并始终使用 @Dao 注解标记。
用户Dao:UserDao
@Dao
interface UserDao {// 增@Insertfun insertUser(user: User): Long// 增@Insertfun insertUsers(vararg users: User): List<Long>// 增@Insertfun insertBothUsers(user1: User, user2: User)// 增@Insertfun insertUsersAndFriends(user: User, friends: List<User>)// 删@Deletefun deleteUser(user: User): Int// 删@Deletefun deleteUsers(vararg users: User) : Int// 改@Updatefun updateUser(user: User): Int// 改@Updatefun updateUsers(vararg users: User) : Int// 更改用户密码@Query("UPDATE user SET password = :newPassword WHERE username = :username")fun updatePassword(username: String, newPassword: String): Int// 根据用户ID查询用户@Query("SELECT * FROM user WHERE user_id = :userId")fun getUserByUserId(userId: Long): User?// 无条件查询所有用户@Query("SELECT * FROM user")fun getAllUserList(): List<User>// 支持LiveData@Query("SELECT * FROM user")fun getAllUsersLiveData(): LiveData<List<User>>// 支持响应式流@Query("SELECT * FROM user")fun getAllUsersFlow(): Flow<List<User>>// 输入用户数组查询包含的结果@Query("SELECT * FROM user WHERE user_id IN (:userIds)")fun getUsersByUserIds(userIds: IntArray): List<User>// 根据用户名查询用户@Query("SELECT * FROM user WHERE username LIKE :username LIMIT 1")fun getUserByUsername(username: String): User?// 根据全名模糊查找用户@Query("SELECT * FROM user WHERE full_name LIKE '%' || :fullName || '%'")fun findUsersByFullName(fullName: String): List<User>// 强制插入用户,通过事务方式先删除再插入新的@Transactionfun forceInstallUser(user: User): Long {val newUser = getUserByUsername(user.username)newUser?.let {val result = deleteUser(it)if (result > 0) {val userId = insertUser(user)return userId}}return 0}
}
商品Dao:ProductDao
@Dao
interface ProductDao {// 若已存在则替换@Insert(onConflict = OnConflictStrategy.REPLACE)fun insert(product: Product): Long@Query("SELECT * FROM products WHERE name LIKE :name LIMIT 1")fun getProduct(name: String): Product@Query("SELECT * FROM products")fun getAllProducts(): List<Product>
}
订单Dao:OrderDao
@Dao
interface OrderDao {@Insertfun insert(order: Order): Long@Query("SELECT * FROM orders WHERE user_owner_id = :userId")fun getOrdersByUser(userId: Long): List<Order>@Query("SELECT * FROM user JOIN orders ON user.user_id = orders.user_owner_id")fun loadUserAndOrders(): Map<User, List<Order>>
}
Dao注解说明:
注解 | 说明 |
@Dao | 声明针于数据表的增删改查操作。 |
@Insert | 插入数据方法,onConflict 参数可设置若新增数据主键或索引存在冲突时的处理方法:REPLACE 替换;ABORT 中止;IGNORE 忽略。 |
@Delete | 删除数据方法 |
@Update | 更新数据方法 |
@Query | 根据SQL语句执行操作的方法,大多情况下用于查询单表或联表数据,也可以用于更复杂的删除、更新或插入数据操作。 |
用于查询返回多个结果时,还可以配合LiveData或Flow 返回。 | |
注意:Room 会在编译时验证 SQL 查询。这意味着,如果查询出现问题,则会出现编译错误,而不是运行时失败。 | |
@Transaction | 标记方法在数据库中作为一个事务执行,方法中所有的数据库操作要么全部成功执行并提交,要么在中途出错时全部回滚,以保障数据一致性。 |
4.3 数据库类:AppDatabase
数据库类需继承自 RoomDatabase,并使用 @Database 注解指定实体类与版本。
@Database(entities = [User::class, Product::class, Order::class], version = 1)
abstract class AppDatabase : RoomDatabase() {abstract fun userDao(): UserDaoabstract fun productDao(): ProductDaoabstract fun orderDao(): OrderDaocompanion object {@Volatileprivate var INSTANCE: AppDatabase? = nullfun getInstance(context: Context): AppDatabase {return INSTANCE ?: synchronized(this) {INSTANCE ?: Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database").build().also { INSTANCE = it }}}}
}
数据库类必须满足以下条件:
- 该类必须带有 @Database 注解,该注解包含列出所有与数据库关联的数据实体的 entities 数组。
- 该类必须是一个抽象类,用于扩展 RoomDatabase。
- 对于与数据库关联的每个 DAO 类,数据库类必须定义返回其实例的抽象方法。
注解说明:
注解 | 说明 |
@Database | 声明数据库。 |
entities 参数用于声明数据库中的表。 | |
version 参数用于声明数据库版本。 |
注意:
Room 实例化成本较高,建议采用单例模式避免重复创建。
多进程支持:
如需支持多进程访问数据库,请调用 .enableMultiInstanceInvalidation()。这样每个进程中都有一个 AppDatabase 实例,可以在一个进程中使共享数据库文件失效,并且这种失效会自动传播到其他进程中 AppDatabase 的实例。
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database").enableMultiInstanceInvalidation().build()
数据库文件说明:
databaseBuilder方法中name 参数指定数据库文件名,数据库文件默认保存在 app 私有目录的 databases/ 子目录下,完整路径如下:
/data/data/<应用包名>/databases/<数据库名>.db
如果你想将数据库放在自定义路径,可以使用:
Room.databaseBuilder(context,AppDatabase::class.java,File(context.filesDir, "custom/path/app_database").absolutePath
)
可以通过以下方式获取数据库文件的绝对路径:
val dbFile: File = context.getDatabasePath("app_database")
Log.d(TAG, "数据库路径为:${dbFile.absolutePath}")
如果你在测试或调试中想确认数据库是否存在,可以这样判断:
if (dbFile.exists()) {Log.d(TAG, "数据库已存在")
} else {Log.d(TAG, "数据库尚未创建")
}
注意:
只有在你第一次访问数据库(如调用 db.userDao().getAll())或显式触发数据库操作后,数据库文件才会真正创建。
4.4 调用示例
suspend fun test(context: Context) = withContext(Dispatchers.IO) {val db = AppDatabase.getInstance(context)val dbFile: File = context.getDatabasePath("app_database")Log.d(TAG, "数据库路径为:${dbFile.absolutePath}")if (dbFile.exists()) {Log.d(TAG, "数据库已存在")} else {Log.d(TAG, "数据库尚未创建")}// 插入用户val user = User(username = "zyx",password = "123456",fullName = "子云心",address = Address("广东省", "广州市", "510000"))val userId = db.userDao().insertUser(user)Log.d(TAG, "insert result, userId: $userId")if (dbFile.exists()) {Log.d("DB_PATH", "数据库已存在")} else {Log.d("DB_PATH", "数据库尚未创建")}// 更新全名val updatedUser = user.copy(userId = userId, fullName = "马户")val updateUserNumber = db.userDao().updateUser(updatedUser)Log.d(TAG, "update user result, number: $updateUserNumber")// 更改用户密码val updatePasswordResult = db.userDao().updatePassword("zyx", "9527")Log.d(TAG, "update password result, number: $updatePasswordResult")// 根据用户名查询用户val userResult = db.userDao().getUserByUsername("zyx")Log.d(TAG, "getUserByUsername result: $userResult")// 根据全名模糊查找用户val usersResult = db.userDao().findUsersByFullName("马")Log.d(TAG, "getUsersByFullName result: $usersResult")// 无条件查询所有用户val usersList = db.userDao().getAllUserList()Log.d(TAG, "getAllUserList result: $usersList")
}
注意:
Room 所有数据库操作必须在主线程之外执行,例如使用 Dispatchers.IO。
5. Room 的进阶使用
5.1 类型转换(TypeConverter)
Room 支持的字段类型是有限的,比如:
- 不支持直接存储 Date、List、Map、Enum 等类型;
- 如果你的实体类中包含这些类型的字段,就会报错。
此时,可以使用 @TypeConverter 注解来自定义转换逻辑,让 Room 知道如何将这些类型转换为数据库支持的类型进行存储和读取。
以 User 实体为例,我们为其新增一个 Date 类型的 birthday 字段:
@Entity(tableName = "user",indices = [Index(value = ["username"], unique = true)]
)
data class User(@PrimaryKey(autoGenerate = true)@ColumnInfo(name = "user_id")val userId: Long = 0,val username: String,val password: String,@ColumnInfo(name = "full_name")val fullName: String,@Embedded(prefix = "addr_")val address: Address,val birthday: Date
) {@Ignoreval isOnline: Boolean = false
}
创建类型转换器:
class Converters {@TypeConverterfun fromTimestamp(value: Long?): Date? {return value?.let { Date(it) }}@TypeConverterfun dateToTimestamp(date: Date?): Long? {return date?.time}
}
然后在数据库类中注册该转换器:
@TypeConverters(Converters::class)
@Database(entities = [User::class, Product::class, Order::class], version = 1)
abstract class AppDatabase : RoomDatabase() {abstract fun userDao(): UserDaoabstract fun productDao(): ProductDaoabstract fun orderDao(): OrderDaocompanion object {@Volatileprivate var INSTANCE: AppDatabase? = nullfun getInstance(context: Context): AppDatabase {return INSTANCE ?: synchronized(this) {INSTANCE ?: Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database").build().also { INSTANCE = it }}}}
}
这样,Room 会自动将 Date 类型转换为 Long 存储到数据库中,再反向转换回来,整个过程无需手动干预。
同理:支持 List<String> 类型
Room 同样不支持直接存储集合类型,如 List<String>。可以将其序列化为逗号拼接的字符串:
@TypeConverter
fun fromString(value: String): List<String> {return if (value.isEmpty()) emptyList() else value.split(",")
}@TypeConverter
fun fromList(list: List<String>): String {return list.joinToString(",")
}
5.2 数据库版本升级(Migration)
在上一节中我们为 User 表新增了 birthday 字段,运行时会遇到如下异常:
java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number. Expected identity hash: xxx, found: xxx
这是因为修改了数据库结构但未更新版本号。更新版本后如果未提供迁移逻辑,又会出现:
java.lang.IllegalStateException: A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(...) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* functions.
这个错误说明 Room 发现版本变化但无法找到迁移路径。
5.2.1 推荐方案:添加 Migration(数据保留)
定义版本 1 → 2 的迁移逻辑:
val MIGRATION_1_2 = object : Migration(1, 2) {override fun migrate(db: SupportSQLiteDatabase) {db.execSQL("ALTER TABLE user ADD COLUMN birthday INTEGER DEFAULT 0 NOT NULL")}
}
使用addMigrations注册迁移:
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database").addMigrations(MIGRATION_1_2).build()
注意:
- ALTER TABLE 仅支持新增字段,但不支持删除或修改字段类型。
- 如需修改列或结构,需要手动新建临时表转存数据,关于 SQL 语句的相关知识,这里就不作过多演示。
- 一般情况下已发布版本的数据库,尽量不进行修改结构和删除字段。
5.2.2 替换方案:破坏性迁移(开发期可用,数据会被清除)
使用 fallbackToDestructiveMigration 设置破坏迁移:
Room.databaseBuilder(context, AppDatabase::class.java, "app_database").fallbackToDestructiveMigration(true) // 每次版本变动就清空旧库重建.build()
注意:
此方式会清空旧数据并重建数据库,不推荐在正式环境使用。
5.3 多表联查
5.3.1 使用 SQL JOIN(灵活强大)
定义结果数据类:
data class OrderInfo(val orderId: Long,val quantity: Int,val username: String,val fullName: String,val productName: String,val productPrice: Double
)
在 OrderDao 中书写 JOIN 查询语句:
@Dao
interface OrderDao {// ……@Query("""SELECT o.order_id AS orderId,o.quantity AS quantity,u.username AS username,u.full_name AS fullName,p.name AS productName,p.price AS productPriceFROM orders oINNER JOIN user u ON o.user_owner_id = u.user_idINNER JOIN products p ON o.product_id = p.product_id""")fun getOrderInfoList(): List<OrderInfo>
}
优点:灵活、可控、支持复杂筛选。
缺点:字段映射需手动维护,代码稍显冗长。
5.3.2 使用 @Relation(结构清晰)
定义嵌套数据类:
data class OrderDetail(@Embedded val order: Order,@Relation(parentColumn = "user_owner_id",entityColumn = "user_id")val user: User,@Relation(parentColumn = "product_id",entityColumn = "product_id")val product: Product
)
说明:
- @Relation 会自动根据外键字段将 User 和 Product 加载进来;
- @Embedded val order: Order 是基础订单表本身。
在 OrderDao 中添加查询方法:
@Dao
interface OrderDao {//……@Query("SELECT * FROM orders")fun getOrderDetailList(): List<OrderDetail>
}
优点:类型安全、自动加载。
缺点:不支持复杂过滤或自定义字段,性能略低于原生 JOIN。
5.4数据库加密
5.4.1 使用SQLCipher
如果你存储在本地的数据较为敏感,不希望数据库文件被导出后被 SQLite工具直接打开,可以集成 SQLCipher 版本的 Room,实现数据库加密。
添加依赖:
dependencies {// ……implementation ("net.zetetic:android-database-sqlcipher:4.5.4")
}
创建加密数据库:
import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SupportFactoryval passphrase: ByteArray = SQLiteDatabase.getBytes("your-secure-password".toCharArray())
val factory = SupportFactory(passphrase)Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database").openHelperFactory(factory) // 使用加密支持工厂.build()
注意:
- 密码必须妥善保管,否则数据无法恢复。
- 性能相较未加密数据库略有下降,但一般可接受。
5.4.2 使用 Android Keystore 管理密码
在上述示例中,虽然使用了SQLCipher对数据库文件进行了加密,但是密码明文定义在代码中,这样会被攻击者通过反编译代码从而获取密码,加密的动作就变得形同虚设。一般希望对本地数据加密保护时,会将密码通过 Android Keystore动态生成和存储。
Keystore 是 Android 系统提供的一套安全机制,用于在设备上安全地生成、存储和使用加密密钥,而不让应用本身直接接触密钥的原始内容。这有助于防止密钥被反编译、提取或泄露。
创建 Keystore 加密解密辅助类:
object KeystoreHelper {private const val KEY_ALIAS = "my_db_key_alias"private const val ANDROID_KEYSTORE = "AndroidKeyStore"private const val TRANSFORMATION = "AES/GCM/NoPadding"fun encrypt(plainText: String): Pair<String, String> {generateSecretKeyIfNeeded()val cipher = Cipher.getInstance(TRANSFORMATION)cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())val iv = cipher.ivval encrypted = cipher.doFinal(plainText.toByteArray())return Base64.encodeToString(encrypted, Base64.NO_WRAP) to Base64.encodeToString(iv, Base64.NO_WRAP)}fun decrypt(encryptedBase64: String, ivBase64: String): String {val encrypted = Base64.decode(encryptedBase64, Base64.NO_WRAP)val iv = Base64.decode(ivBase64, Base64.NO_WRAP)val cipher = Cipher.getInstance(TRANSFORMATION)val spec = GCMParameterSpec(128, iv)cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec)return String(cipher.doFinal(encrypted))}private fun generateSecretKeyIfNeeded() {val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }if (!keyStore.containsAlias(KEY_ALIAS)) {val keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE)val parameterSpec = KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT).setBlockModes(KeyProperties.BLOCK_MODE_GCM).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE).build()keyGen.init(parameterSpec)keyGen.generateKey()}}private fun getSecretKey(): SecretKey {val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }return keyStore.getKey(KEY_ALIAS, null) as SecretKey}
}
定义获取密码方法:
fun getDatabasePassword(context: Context): String {val prefs: SharedPreferences = context.getSharedPreferences("secure_prefs", Context.MODE_PRIVATE)// 获取上次生成的 encrypted 和 ivval encrypted = prefs.getString("encrypted", null)val iv = prefs.getString("iv", null)if (encrypted != null && iv != null) {// 解密出密码return KeystoreHelper.decrypt(encrypted, iv)}// 生成随机密码val password = UUID.randomUUID().toString()// 加密密码获取 encrypted 和 ivval (newEncrypted, newIv) = KeystoreHelper.encrypt(password)// 保存 encrypted 和 ivprefs.edit { putString("encrypted", newEncrypted).putString("iv", newIv) }return password
}
替换明文密码创建加密数据库:
import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SupportFactoryval password = getDatabasePassword(context)
val passphrase: ByteArray = SQLiteDatabase.getBytes(password.toCharArray())
val factory = SupportFactory(passphrase)Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database").openHelperFactory(factory) // 使用加密支持工厂.build()
Keystore 的优势:
- 密钥保护:密钥被存储在专用的硬件或系统区域中(如 TEE 或 StrongBox),不暴露给应用层。
- 受限使用:密钥只能通过特定操作使用,不能直接导出,避免明文或反编译暴露。
- 生命周期控制:可以设置使用限制,比如只允许解锁设备时使用、绑定到生物识别认证、设定失效时间等。
- 绑定安全环境:即使 APK 被反编译,也无法还原出 Keystore 中的密钥。因为 Keystore 里的密钥是绑定到包名 + 签名 + 用户空间的,如上述示例,就算通过ROOT手机后导出 SharedPreferences,获取到 encrypted 和 iv并且反编译后看到KeystoreHelper.decrypt() 的代码逻辑,在别的设备上也会解密失败。或者就算同样的设备同样的APK反编译后重打包,因为签名不一样,无会解失败。
6. 总结
Room 作为 Jetpack 架构组件中本地数据库解决方案,拥有良好的类型安全、编译时校验与响应式扩展能力。
- 推荐搭配 Paging、WorkManager 等组件构建现代 Android 应用架构;
- 对于需要结构化缓存、本地持久化、关系数据管理等场景尤其适合;
- 实际开发中应重视数据库升级策略与数据安全保护。
更多详细的 Room 介绍,请访问 Android 开发者官网。