Android Jetpack 系列(五)Room 本地數據庫實戰詳解

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;準確、快速地檢測孔洞對于保障產品質量至關重要。傳統的…