1. 简介

在需要轻量级本地持久化的场景中,DataStore 是一个理想的选择(详见《Android Jetpack 系列(四)DataStore 全面解析与实践》)。但当你面临如下需求时,本地数据库便显得尤为重要:

  1. 复杂的数据模型管理;
  2. 数据之间存在关联关系;
  3. 支持部分字段更新;
  4. 实现离线缓存与增量更新。

为此,Google 在 Jetpack 架构组件中推出了新一代本地数据库解决方案:Room。它对原生 SQLite 进行了封装,提供类型安全、编译期校验的数据库访问方式,极大简化了样板代码的编写。

Room 的典型使用场景包括:

  1. 缓存网络请求结果,实现离线访问;
  2. 管理本地复杂结构化数据;
  3. 实现高效的增量数据更新及多表关系维护。

主要优势包括:

  1. SQL 编译期校验:提前发现错误,提升稳定性;
  2. 注解驱动开发:减少冗余代码;
  3. 简化数据迁移:支持版本演进;
  4. 与 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")// 可选 Test helperstestImplementation("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(实体类)

  1. 对应数据库中的一张表;
  2. 使用 @Entity(tableName = "xxx") 注解声明;
  3. 类的每个字段就是表中的一列;
  4. 至少要有一个主键(@PrimaryKey);
  5. 支持复合主键、忽略字段、嵌套对象。

3.2 DAO(数据访问对象)

  1. 使用 @Dao 注解声明;
  2. 用于定义 SQL 操作(@Query)或封装常用方法(如 @Insert、@Update、@Delete);
  3. 支持挂起函数(suspend)、LiveData、Flow 等多种返回类型;
  4. 可以组合多个操作为一个事务(使用 @Transaction 注解);
  5. 支持多表查询与动态SQL构建。

3.3 Database(数据库类)

  1. 使用 @Database 注解声明,并指定 entities、version;
  2. 必须继承自 RoomDatabase;
  3. 是 Room 的核心入口,提供 DAO 实例访问;
  4. 建议通过单例或其他线程安全方式创建实现。

三者关系

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 参数可设置多个列的组合对实体实例进行唯一标识,也就是通过多个列定义一个复合主键。例如:
@Entity(primaryKeys = ["firstName", "lastName"])

data class User(

    val firstName: String?,

    val lastName: String?

)

ignoredColumns 参数用于指定某字段是忽略字段,例如实体继承了父实体的字段,则通过该参数进行指定。例如:
open class User {

    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 }}}}
}

数据库类必须满足以下条件:

  1. 该类必须带有 @Database 注解,该注解包含列出所有与数据库关联的数据实体的 entities 数组。
  1. 该类必须是一个抽象类,用于扩展 RoomDatabase。
  2. 对于与数据库关联的每个 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 支持的字段类型是有限的,比如:

  1. 不支持直接存储 Date、List、Map、Enum 等类型;
  2. 如果你的实体类中包含这些类型的字段,就会报错。

此时,可以使用 @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()

注意:

  1. ALTER TABLE 仅支持新增字段,但不支持删除或修改字段类型。
  2. 如需修改列或结构,需要手动新建临时表转存数据,关于 SQL 语句的相关知识,这里就不作过多演示。
  3. 一般情况下已发布版本的数据库,尽量不进行修改结构和删除字段。

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
)

说明:

  1. @Relation 会自动根据外键字段将 User 和 Product 加载进来;
  2. @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()

注意:

  1. 密码必须妥善保管,否则数据无法恢复。
  2. 性能相较未加密数据库略有下降,但一般可接受。

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 的优势:

  1. 密钥保护:密钥被存储在专用的硬件或系统区域中(如 TEE 或 StrongBox),不暴露给应用层。
  2. 受限使用:密钥只能通过特定操作使用,不能直接导出,避免明文或反编译暴露。
  3. 生命周期控制:可以设置使用限制,比如只允许解锁设备时使用、绑定到生物识别认证、设定失效时间等。
  4. 绑定安全环境:即使 APK 被反编译,也无法还原出 Keystore 中的密钥。因为 Keystore 里的密钥是绑定到包名 + 签名 + 用户空间的,如上述示例,就算通过ROOT手机后导出 SharedPreferences,获取到 encrypted 和 iv并且反编译后看到KeystoreHelper.decrypt() 的代码逻辑,在别的设备上也会解密失败。或者就算同样的设备同样的APK反编译后重打包,因为签名不一样,无会解失败。

6. 总结

Room 作为 Jetpack 架构组件中本地数据库解决方案,拥有良好的类型安全、编译时校验与响应式扩展能力。

  1. 推荐搭配 Paging、WorkManager 等组件构建现代 Android 应用架构;
  2. 对于需要结构化缓存、本地持久化、关系数据管理等场景尤其适合;
  3. 实际开发中应重视数据库升级策略与数据安全保护。

更多详细的 Room 介绍,请访问 Android 开发者官网。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/918888.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/918888.shtml
英文地址,请注明出处:http://en.pswp.cn/news/918888.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C语言实现类似C#的格式化输出

在C#中&#xff0c;格式化输出可以使用索引占位符以及复合格式的占位符&#xff0c;可以不用关心后面参数是什么类型&#xff0c;使用起来非常方便&#xff0c;如下简单的示例&#xff1a; Console.WriteLine("{2} {1} {0} {{{2}}}", "Hello, World!", 1,…

一人公司方法论

** 一人公司方法论 ** 那什么是一人公司&#xff1f; 字面的理解就是一个人运营的公司&#xff0c;但实际上它指代的是比较少的人运营的小公司&#xff0c;一般来说 1 ~ 3 个人运营的公司&#xff0c;也可以把它放到一人公司的范围以内。其他一些形式&#xff0c;比如说一个人再…

Ceph CSI 镜像删除流程与 Trash 机制失效问题分析文档

#作者&#xff1a;闫乾苓 文章目录一、问题背景二、实际行为三、源码分析四、分析与推论五、期望行为与建议优化六、结论一、问题背景 在生产环境中&#xff0c;为避免因误操作导致的永久数据丢失&#xff0c;Ceph RBD 提供了 Trash 功能&#xff0c;允许将镜像“软删除”至回…

.NET Framework 3.5 不原生支持PreApplicationStartMethod特性

.NET Framework 3.5 不原生支持PreApplicationStartMethod特性。这个特性是在 .NET Framework 4.0 中引入的&#xff0c;用于在应用程序启动早期执行初始化逻辑。 在.NET 3.5 中&#xff0c;如果你需要实现类似的 “应用启动时自动注册模块” 功能&#xff0c;需要通过手动配置…

智能巡检技术浅析

从机载智能硬件到深度学习算法&#xff0c;从实时边缘计算到数字孪生平台&#xff0c;无人机AI智能巡检通过多模态感知、自主决策和持续进化&#xff0c;实现从"被动检查"到"主动预防"的跨越式发展。机载智能硬件边缘计算与机载AI芯片当代先进巡检无人机已…

【图像算法 - 11】基于深度学习 YOLO 与 ByteTrack 的目标检测与多目标跟踪系统(系统设计 + 算法实现 + 代码详解 + 扩展调优)

前言 详细视频介绍 【图像算法 - 11】基于深度学习 YOLO 与 ByteTrack 的目标检测与多目标跟踪系统&#xff08;系统设计 算法实现 代码详解 扩展调优&#xff09;在计算机视觉应用中&#xff0c;目标检测与多目标跟踪的结合是实现智能视频分析的关键。本文基于 YOLO 检测模…

AI加持下的智能路由监控:Amazon VPC Direct Connect实战指南

> 一次流量突增引发的生产事故,如何催生出融合流日志、机器学习与自动化告警的智能监控体系 深夜2点,电商平台运维负责人李明的手机疯狂报警——北美用户下单量断崖式下跌。他紧急登录系统,发现跨境专线延迟飙升至2000ms。**经过3小时的排查**,罪魁祸首竟是新部署的CDN…

具身智能竞速时刻,百度百舸提供全栈加速方案

2025年&#xff0c;全球具身智能赛道迎来快速发展期&#xff0c;技术方向日益清晰。每一家企业都面临着同样的核心命题&#xff1a;如何将前沿的模型能力&#xff0c;转化为在真实世界各类场景中可规模化应用落地的机器人产品&#xff1f;这背后&#xff0c;是研发团队对模型迭…

JavaScript 压缩与混淆实战:Terser 命令行详解

使用 Terser 压缩 JavaScript 文件&#xff08;基础 现代语法问题解决&#xff09; 在前端开发中&#xff0c;随着业务复杂度增加&#xff0c;JavaScript 文件体积越来越大。 文件大带来的问题&#xff1a; 加载慢&#xff1a;文件越大&#xff0c;浏览器下载和解析时间越长…

【数据结构初阶】--排序(三):冒泡排序、快速排序

&#x1f618;个人主页&#xff1a;Cx330❀ &#x1f440;个人简介&#xff1a;一个正在努力奋斗逆天改命的二本觉悟生 &#x1f4d6;个人专栏&#xff1a;《C语言》《LeetCode刷题集》《数据结构-初阶》 前言&#xff1a;在上篇博客的学习中&#xff0c;我们掌握了直接选择排序…

名词概念:什么是尾部误差?

“尾部误差”就是指误差分布在两端的那一小撮、但数值特别大的误差——也就是离中心&#xff08;均值/中位数&#xff09;很远的“极端样本”的误差。对应统计学里的“分布尾部”&#xff08;tails&#xff09;。通俗点&#xff1a;大多数样本误差都很小&#xff0c;但总会有少…

记对外国某服务器的内网渗透

本专栏是笔者的网络安全学习笔记&#xff0c;一面分享&#xff0c;同时作为笔记 文章目录前文链接前言上线CS上线rdp后渗透信息收集SMB Pth攻击权限维持魔幻上线提权关Windows Defenderend前文链接 WAMP/DVWA/sqli-labs 搭建burpsuite工具抓包及Intruder暴力破解的使用目录扫描…

速卖通平台关键字搜索商品列表列表接口实现指南:从接口分析到代码落地

在跨境电商开发中&#xff0c;速卖通平台的商品数据获取是许多开发者关注的焦点。本文将详细介绍如何实现速卖通关键字搜索商品列表接口&#xff0c;涵盖接口请求参数分析、签名机制、分页处理及完整代码实现&#xff0c;帮助开发者快速对接速卖通开放平台。一、接口基本信息速…

UE UDP通信

1.确保工程为C工程&#xff0c;在项目工程的xx.Build.cs中加入Networking和Sockets模块。PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "Networking", "Socke…

JavaScript 逻辑运算符与实战案例:从原理到落地

JavaScript 中的逻辑运算符不仅是条件判断的核心&#xff0c;还能通过“短路特性”简化代码&#xff1b;结合 DOM 操作的实战案例&#xff0c;更能体现其灵活性。本文整理了逻辑运算符的个人理解、优先级规则&#xff0c;以及 4 个高频实战需求的实现方案&#xff0c;附个人思路…

Android RxJava 过滤与条件操作详解

RxJava 是一个基于观察者模式的响应式编程库&#xff0c;在 Android 开发中被广泛使用。其中&#xff0c;过滤和条件操作是 RxJava 中非常重要的一部分&#xff0c;它们允许我们对数据流进行精细控制。本文将详细介绍 RxJava 中常用的过滤与条件操作符及其使用场景。一、过滤操…

云手机都具有哪些特点?

云手机拥有着便捷的远程操作功能&#xff0c;让用户无论身处何地&#xff0c;只要能连接网络&#xff0c;就能通过手机、电脑等终端设备远程操控云手机&#xff0c;无需受限于物理位置&#xff0c;大大提升了工作的灵活性与便捷性。云手机主要是依赖于云计算技术&#xff0c;能…

Sparse-ICP—(4) 加权稀疏迭代最近点算法(matlab版)

目录 一、算法原理 1、原理概述 2、参考文献 二、代码实现 三、结果展示 一、算法原理 1、原理概述 见:Sparse-ICP—(1)稀疏迭代最近点算法 2、参考文献 二、代码实现 SparseWeightedDistance.m function [move_points,T] =

统信UOS安装NFS共享文件夹

在 UOS ARM 架构系统上安装和配置 NFS 服务&#xff0c;实现与局域网中其他服务器共享文件夹的步骤如下&#xff1a;1. 安装 NFS 服务首先更新系统并安装 NFS 服务器组件&#xff1a;bash# 更新软件包列表 sudo apt update# 安装NFS服务器 sudo apt install nfs-kernel-server …

【完整源码+数据集+部署教程】孔洞检测系统源码和数据集:改进yolo11-RetBlock

背景意义 研究背景与意义 随着工业自动化和智能制造的快速发展&#xff0c;孔洞检测作为关键的质量控制环节&#xff0c;受到了广泛关注。孔洞的存在可能会影响产品的强度、密封性和整体性能&#xff0c;因此&#xff0c;准确、快速地检测孔洞对于保障产品质量至关重要。传统的…