-

「Generated by Manus, instructions issued by binbinwang」

本章将深入探讨Android的数据存储与持久化技术,并与iOS的对应方案进行对比。作为一名iOS开发者,了解Android平台的数据存储机制将帮助你更好地设计跨平台应用的数据层。

6.1 SharedPreferences与轻量级存储

SharedPreferences基础

SharedPreferences是Android提供的一种轻量级键值对存储机制,适合存储简单的配置信息和应用设置。

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 获取SharedPreferences实例
val sharedPref = getSharedPreferences("app_settings", Context.MODE_PRIVATE)

// 写入数据
sharedPref.edit().apply {
putString("username", "john_doe")
putInt("age", 25)
putBoolean("notifications_enabled", true)
putFloat("volume_level", 0.8f)
// 两种提交方式
apply() // 异步提交,不返回结果
// 或
// commit() // 同步提交,返回是否成功
}

// 读取数据
val username = sharedPref.getString("username", "") // 第二个参数是默认值
val age = sharedPref.getInt("age", 0)
val notificationsEnabled = sharedPref.getBoolean("notifications_enabled", false)
val volumeLevel = sharedPref.getFloat("volume_level", 0.5f)

// 检查键是否存在
val hasUsername = sharedPref.contains("username")

// 删除数据
sharedPref.edit().apply {
remove("username") // 删除单个键
// clear() // 删除所有键
apply()
}

// 监听数据变化
sharedPref.registerOnSharedPreferenceChangeListener { prefs, key ->
when (key) {
"username" -> {
val newUsername = prefs.getString(key, "")
// 处理用户名变化
}
"notifications_enabled" -> {
val enabled = prefs.getBoolean(key, false)
// 处理通知设置变化
}
}
}

使用Jetpack Preferences库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// build.gradle
dependencies {
implementation "androidx.preference:preference-ktx:1.2.0"
}

// 使用Preferences委托属性
class SettingsManager(context: Context) {
private val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)

var username by StringPreference(prefs, "username", "")
var age by IntPreference(prefs, "age", 0)
var notificationsEnabled by BooleanPreference(prefs, "notifications_enabled", false)
var volumeLevel by FloatPreference(prefs, "volume_level", 0.5f)
}

// 使用
val settings = SettingsManager(context)
settings.username = "jane_doe" // 自动保存
val currentUsername = settings.username // 自动读取

与iOS UserDefaults对比

iOS的UserDefaults

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 获取UserDefaults实例
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

// 写入数据
[defaults setObject:@"john_doe" forKey:@"username"];
[defaults setInteger:25 forKey:@"age"];
[defaults setBool:YES forKey:@"notifications_enabled"];
[defaults setFloat:0.8f forKey:@"volume_level"];
[defaults synchronize]; // iOS 12后不再需要显式调用

// 读取数据
NSString *username = [defaults stringForKey:@"username"];
NSInteger age = [defaults integerForKey:@"age"];
BOOL notificationsEnabled = [defaults boolForKey:@"notifications_enabled"];
float volumeLevel = [defaults floatForKey:@"volume_level"];

// 检查键是否存在
BOOL hasUsername = ([defaults objectForKey:@"username"] != nil);

// 删除数据
[defaults removeObjectForKey:@"username"];
// [defaults removePersistentDomainForName:[[NSBundle mainBundle] bundleIdentifier]]; // 删除所有键

// 监听数据变化
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(defaultsChanged:)
name:NSUserDefaultsDidChangeNotification
object:nil];

- (void)defaultsChanged:(NSNotification *)notification {
NSUserDefaults *defaults = notification.object;
NSString *username = [defaults stringForKey:@"username"];
// 处理变化
}

主要差异

  1. Android的SharedPreferences需要通过edit()获取Editor对象,而iOS的UserDefaults直接设置值
  2. Android提供apply()(异步)和commit()(同步)两种提交方式,而iOS自动处理
  3. Android可以监听特定键的变化,而iOS只能监听整个UserDefaults的变化
  4. Android的SharedPreferences可以创建多个命名实例,而iOS通常使用单例standardUserDefaults
  5. Android需要显式提交更改,而iOS在iOS 12后不再需要显式调用synchronize()

6.2 文件存储

内部存储与外部存储

Android提供了内部存储和外部存储两种文件存储机制:

内部存储

  • 应用私有,其他应用无法访问
  • 随应用卸载自动删除
  • 不需要权限

外部存储

  • 可以是公共的,也可以是应用私有的
  • 应用卸载时,私有外部存储会被删除,公共外部存储不会
  • 访问公共外部存储需要权限

内部存储基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 写入文本文件
fun writeInternalFile(context: Context, filename: String, content: String) {
context.openFileOutput(filename, Context.MODE_PRIVATE).use { fos ->
fos.write(content.toByteArray())
}
}

// 读取文本文件
fun readInternalFile(context: Context, filename: String): String {
return context.openFileInput(filename).bufferedReader().use { it.readText() }
}

// 检查文件是否存在
fun fileExists(context: Context, filename: String): Boolean {
val file = File(context.filesDir, filename)
return file.exists()
}

// 删除文件
fun deleteFile(context: Context, filename: String): Boolean {
return context.deleteFile(filename)
}

// 列出所有文件
fun listFiles(context: Context): Array<String> {
return context.fileList()
}

// 获取缓存目录
val cacheFile = File(context.cacheDir, "temp_data.txt")

外部存储基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 检查外部存储状态
fun isExternalStorageWritable(): Boolean {
return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
}

fun isExternalStorageReadable(): Boolean {
return Environment.getExternalStorageState() in
setOf(Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED_READ_ONLY)
}

// 获取应用私有外部存储目录
val privateExternalDir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)

// 写入文件
fun writeExternalFile(file: File, content: String) {
FileOutputStream(file).use { fos ->
fos.write(content.toByteArray())
}
}

// 读取文件
fun readExternalFile(file: File): String {
return FileInputStream(file).bufferedReader().use { it.readText() }
}

// Android 10+的分区存储
// 使用MediaStore访问公共媒体
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "image.jpg")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
}

val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
uri?.let { imageUri ->
contentResolver.openOutputStream(imageUri).use { outputStream ->
// 写入图像数据
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
}
}

与iOS文件系统对比

iOS的文件系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 获取Documents目录
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *filePath = [documentsPath stringByAppendingPathComponent:@"file.txt"];

// 写入文本文件
NSString *content = @"Hello, iOS!";
NSError *error;
[content writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (error) {
NSLog(@"Error writing file: %@", error);
}

// 读取文本文件
NSError *readError;
NSString *fileContent = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:&readError];
if (readError) {
NSLog(@"Error reading file: %@", readError);
}

// 检查文件是否存在
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:filePath];

// 删除文件
NSError *deleteError;
[[NSFileManager defaultManager] removeItemAtPath:filePath error:&deleteError];
if (deleteError) {
NSLog(@"Error deleting file: %@", deleteError);
}

// 列出目录内容
NSError *listError;
NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:documentsPath error:&listError];
if (listError) {
NSLog(@"Error listing directory: %@", listError);
}

// 获取缓存目录
NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];

主要差异

  1. Android区分内部存储和外部存储,而iOS主要使用沙盒目录(Documents、Library、tmp)
  2. Android的外部存储可以是公共的,而iOS应用只能访问自己的沙盒和特定共享区域
  3. Android 10+引入了分区存储,限制了对公共存储的直接访问,而iOS一直采用沙盒模型
  4. Android需要权限才能访问公共外部存储,而iOS通过文件选择器或照片库API访问用户文件
  5. Android应用卸载时会清理内部存储和私有外部存储,而iOS清理整个应用沙盒

6.3 SQLite数据库

SQLite基础操作

SQLite是Android内置的轻量级关系型数据库,适合存储结构化数据:

创建数据库帮助类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class DatabaseHelper(context: Context) : SQLiteOpenHelper(
context, DATABASE_NAME, null, DATABASE_VERSION
) {
companion object {
private const val DATABASE_NAME = "app_database.db"
private const val DATABASE_VERSION = 1

// 表定义
private const val SQL_CREATE_USERS = """
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE,
age INTEGER,
created_at INTEGER
)
"""

private const val SQL_DELETE_USERS = "DROP TABLE IF EXISTS users"
}

override fun onCreate(db: SQLiteDatabase) {
// 创建表
db.execSQL(SQL_CREATE_USERS)
}

override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
// 升级数据库
if (oldVersion < 2) {
// 版本1到版本2的升级逻辑
db.execSQL("ALTER TABLE users ADD COLUMN phone_number TEXT")
}

if (oldVersion < 3) {
// 版本2到版本3的升级逻辑
}
}
}

基本CRUD操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
class UserDao(private val dbHelper: DatabaseHelper) {

// 创建用户
fun insertUser(user: User): Long {
val db = dbHelper.writableDatabase

val values = ContentValues().apply {
put("name", user.name)
put("email", user.email)
put("age", user.age)
put("created_at", System.currentTimeMillis())
}

return db.insert("users", null, values)
}

// 查询所有用户
fun getAllUsers(): List<User> {
val db = dbHelper.readableDatabase
val users = mutableListOf<User>()

val cursor = db.query(
"users",
arrayOf("id", "name", "email", "age", "created_at"),
null,
null,
null,
null,
"name ASC"
)

with(cursor) {
while (moveToNext()) {
val user = User(
id = getLong(getColumnIndexOrThrow("id")),
name = getString(getColumnIndexOrThrow("name")),
email = getString(getColumnIndexOrThrow("email")),
age = getInt(getColumnIndexOrThrow("age")),
createdAt = getLong(getColumnIndexOrThrow("created_at"))
)
users.add(user)
}
}
cursor.close()

return users
}

// 按ID查询用户
fun getUserById(userId: Long): User? {
val db = dbHelper.readableDatabase

val cursor = db.query(
"users",
arrayOf("id", "name", "email", "age", "created_at"),
"id = ?",
arrayOf(userId.toString()),
null,
null,
null
)

var user: User? = null

with(cursor) {
if (moveToFirst()) {
user = User(
id = getLong(getColumnIndexOrThrow("id")),
name = getString(getColumnIndexOrThrow("name")),
email = getString(getColumnIndexOrThrow("email")),
age = getInt(getColumnIndexOrThrow("age")),
createdAt = getLong(getColumnIndexOrThrow("created_at"))
)
}
}
cursor.close()

return user
}

// 更新用户
fun updateUser(user: User): Int {
val db = dbHelper.writableDatabase

val values = ContentValues().apply {
put("name", user.name)
put("email", user.email)
put("age", user.age)
}

return db.update(
"users",
values,
"id = ?",
arrayOf(user.id.toString())
)
}

// 删除用户
fun deleteUser(userId: Long): Int {
val db = dbHelper.writableDatabase

return db.delete(
"users",
"id = ?",
arrayOf(userId.toString())
)
}

// 使用原始SQL查询
fun getUsersOlderThan(age: Int): List<User> {
val db = dbHelper.readableDatabase
val users = mutableListOf<User>()

val cursor = db.rawQuery(
"SELECT * FROM users WHERE age > ? ORDER BY age DESC",
arrayOf(age.toString())
)

with(cursor) {
while (moveToNext()) {
val user = User(
id = getLong(getColumnIndexOrThrow("id")),
name = getString(getColumnIndexOrThrow("name")),
email = getString(getColumnIndexOrThrow("email")),
age = getInt(getColumnIndexOrThrow("age")),
createdAt = getLong(getColumnIndexOrThrow("created_at"))
)
users.add(user)
}
}
cursor.close()

return users
}

// 事务示例
fun insertUsersInTransaction(users: List<User>): Boolean {
val db = dbHelper.writableDatabase

return try {
db.beginTransaction()

for (user in users) {
val values = ContentValues().apply {
put("name", user.name)
put("email", user.email)
put("age", user.age)
put("created_at", System.currentTimeMillis())
}

db.insert("users", null, values)
}

db.setTransactionSuccessful()
true
} catch (e: Exception) {
false
} finally {
db.endTransaction()
}
}
}

// 数据类
data class User(
val id: Long = 0,
val name: String,
val email: String,
val age: Int,
val createdAt: Long = 0
)

Room持久化库

Room是Android Jetpack提供的SQLite抽象层,简化了数据库操作:

```
// 实体定义
@Entity(tableName = “users”)
data class User(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = “name”) val name: String,
@ColumnInfo(name = “email”) val email: String,
@ColumnInfo(name = “age”) val age: Int,
@ColumnInfo(name = “created_at”) val createdAt: Long = System.currentTimeMillis()
)

// DAO接口
@Dao
interface UserDao {
@Insert
fun insert(user: User): Long

@Insert
fun insertAll(users: List<User>)

@Update
fun update(user: User): Int

@Delete
fun delete(user: User): Int

@Query("SELECT * FROM users")
fun getAllUsers(): List<User>

@Query("SELECT * FROM users WHERE id = :userId")
fun getUserById(userId: Long): User?

@Query("SELECT * FROM users WHERE age > :minAge ORDER BY age DESC")
fun getUsersOlderThan(minAge: Int): List<User>

// 返回LiveData(自动观察数据变化)
@Query("SELECT * FROM users ORDER BY name ASC")
fun observeAllUsers(): LiveData<List<User>>

// 使用Flow
@Query("SELECT * FROM users WHERE age > :minAge")
fun getUsersOlderThanFlow(minAge: Int): Flow<List<User>>

// 使用挂起函数
@Query("DELETE FROM users WHERE age < :maxAge")
suspend fun deleteYoungerThan(maxAge: Int): Int

}

// 数据库定义
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao

companion object {
    @Volatile
    private var INSTANCE: AppDatabase? = null
    
    fun getDatabase(context: Context): AppDatabase {
        return INSTANCE ?: synchronized(this) {
            val instance = Room.databaseBuilder(
                context.applicationContext,
                AppDatabase::class.java,
                "app_database"
            )
            .fallbackToDestructiveMigration()  // 升级失败时重建数据库
            //.addMigrations(MIGRATION_1_2)  // 添加迁移策略
            .build()
            INSTANCE = instance
            instance
        }
    }
    
    private val MIGRATION_1_2 = object : Migration(1, 2) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("ALTER TABLE users ADD COLUMN phone_number TEXT")
        }
    }
}

}

// 使用Room
class UseTo save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with grep -n in order to find the line numbers of what you are looking for.