安卓数据存储

本文详细探讨了安卓应用常见的数据存储方式,包括文件系统、SharedPreferences、SQLite数据库,以及它们的优缺点、权限管理和适用场景。特别关注了SharedPreferences的异步提交机制和SQLite的事务处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

安卓数据存储方式主要有:文件、SharePreferences、Database、网络、ContentProvider

文件

内容类型访问方式需要的权限其他应用是否可以访问应用卸载时是否删除文件
仅供应用自身使用

内部存储

getFilesDir

getCacheDir

访问内部存储永远不需要申请权限

不可访问

外部存储

getExternalFilesDir

getExternalCacheDir

在 Android 4.4 或更高版本上访问外部存储时,不需要申请权限
可共享的媒体文件文件 API

READ_EXTERNAL_STORAGE

WRITE_EXTERNAL_STORAGE

可以访问,需要申请权限READ_EXTERNAL_STORAGE
  • getFilesDir : 返回文件系统上目录的绝对路径,它用于永久存储
  • getCacheDir : Cache 目录,在某些情况下系统可以删除该目录

SharedPreferences

  • getXXX 方法是同步的,该方法必须等待 SP 加载完成(getSharePreferences时加载),在主线程调用时可能会导致ANR
  • SP 不能保证类型安全,get 和 put 方法可以传递不同类型的数据,运行时可能存在类转换异常
  • SP 会将数据存储在静态的成员变量中,因此加载的数据会一直留在内存中
  • apply 方法是异步的,但是当生命周期处于 handleStopService、 handlePauseActivity、 handleStopActivity 时会一直等待 apply 方法将数据保存成功,从而阻塞主线程造成 ANR
    • 在 8.0 之后 QueuedWork.waitToFinish 方法做了很大的优化,当生命周期切换的时候,会主动触发任务的执行,而不是一直在等着
    • //xref: /frameworks/base/core/java/android/app/SharedPreferencesImpl.java
      public void apply() {
          final MemoryCommitResult mcr = commitToMemory();
          final Runnable awaitCommit = new Runnable () {
              public void run() {
                  try {
                      mcr.writtenToDiskLatch.await();
                  } catch (InterruptedException ignored) {
                  }
              }
          };
          QueuedWork.add(awaitCommit);
          Runnable postWriteRunnable = new Runnable() {
              public void run() {
                  awaitCommit.run();
                  QueuedWork.remove(awaitCommit);
              }
          };
          SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
      }
      
      //xref: /frameworks/base/core/java/android/app/QueuedWork.java
      public static void waitToFinish() {
           Runnable toFinish;
           while ((toFinish = sPendingWorkFinishers.poll()) != null) {
               toFinish.run();
           }
      }
    • 当调用 apply 方法的时候,执行磁盘写入,都是全量写入,在 8.0 之前,调用 N 次 apply 方法,就会执行 N 次磁盘写入,在 8.0 之后,apply 方法调用了多次,只会执行最后一次写入
  • apply 方法无法获取到操作结果,只能通过 Listener 感知变化
  • public interface OnSharedPreferenceChangeListener {
        /**
         * Called when a shared preference is changed, added, or removed. This
         * may be called even if a preference is set to its existing value.
         *
         * <p>This callback will be run on your main thread.
         *
         * @param sharedPreferences The {@link SharedPreferences} that received
         *            the change.
         * @param key The key of the preference that was changed, added, or
         *            removed.
         */
        void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
    }
  • 设置了 MODE_MULTI_PROCESS 时,会重新读取 SP 文件内容,但 SP 不能用于跨进程通信

SharedPreferences两种提交方式的区别:​​

  • commit返回boolean值验证是否提交成功 ;apply第二种没有返回值
  • commit同步提交硬盘,容易造成线程堵塞;apply先提交到内存,然后异步提交到磁盘

DataStore

  • SharedPreferences has a synchronous API that can appear safe to call on the UI thread, but which actually does disk I/O operations. Furthermore, apply blocks the UI thread on fsync. Pending fsync calls are triggered every time any service starts or stops, and every time an activity starts or stops anywhere in your application. The UI thread is blocked on pending fsync calls scheduled by apply, often becoming a source of ANRs.
  • SharedPreferences throws parsing errors as runtime exceptions.

In both implementations, DataStore saves the preferences in a file and performs all data operations on Dispatchers.IO unless specified otherwise.

While both Preferences DataStore and Proto DataStore allow saving data, they do this in different ways

  • Preference DataStore, like SharedPreferences, has no way to define a schema or to ensure that keys are accessed with the correct type.
  • val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "test")
    
    object DataStoreTest {
    
        private val STRING_KEY = stringPreferencesKey("example_counter")
        private val INT_KEY = intPreferencesKey("example_counter")
    
        fun readPreferencesDataStore(context: Context): Flow<String> {
            return context.dataStore.data
                .map { preferences ->
                    preferences[STRING_KEY] ?: ""
                }
                .flowOn(Dispatchers.IO)
        }
    
        suspend fun writePreferencesDataStore(context: Context): Boolean {
            return context.dataStore.edit { settings ->
                val currentCounterValue = settings[STRING_KEY] ?: "sparrow"
                settings[STRING_KEY] = currentCounterValue + "1"
            }.contains(STRING_KEY)
        }
    }
  • Proto DataStore lets you define a schema using Protocol buffers. Using Protobufs allows persisting strongly typed data. They are faster, smaller, simpler, and less ambiguous than XML and other similar data formats. While Proto DataStore requires you to learn a new serialization mechanism, we believe that the strongly typed schema advantage brought by Proto DataStore is worth it.
  • // Preferences保存的是 key/value,因此没办法保证类型安全,而 PB 中会保存具体的类型
    syntax = "proto3";
    option java_package = "com.example.application";
    message Settings {
        int32 example_counter = 1;
    }
    
    object SettingsSerializer : Serializer<Settings> {
        override val defaultValue: Settings = Settings.getDefaultInstance()
        override suspend fun readFrom(input: InputStream): Settings {
            return Settings.parseFrom(input)
        }
        override suspend fun writeTo(t: Settings, output: OutputStream) { 
            t.writeTo(output)
        }
    }
    
    val Context.settingsDataStore: DataStore<Settings> by dataStore(
        fileName = "settings.pb",
        serializer = SettingsSerializer
    )
    
    val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
        .map { settings ->
            settings.exampleCounter
        }
    
    suspend fun incrementCounter() {
        context.settingsDataStore.updateData { currentSettings ->
            currentSettings.toBuilder()
                .setExampleCounter(currentSettings.exampleCounter + 1)
                .build()
        }
    }

Database

SQLiteOpenHelper : A helper class to manage database creation and version management.

class DatabaseHelper(context: Context?, name: String?, factory: CursorFactory?, version: Int) :
    SQLiteOpenHelper(context, name, factory, version) {

    companion object {
        const val LABEL = "scrutiny"
        const val CREATE_BOOK_TABLE = "create table book (id integer primary key autoincrement," +
                " author text, price real, pages integer, name text)"
        const val DROP_BOOK_TABLE = "drop table if exists book"
    }

    override fun onCreate(db: SQLiteDatabase?) {
        log("database create")
        db?.execSQL(CREATE_BOOK_TABLE)
    }

    //当打开数据库时传入的版本号与当前的版本号不同时会调用该方法
    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        log("database update")
        handleUpgrade(db, oldVersion, newVersion)
    }

    private fun handleUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        db?.execSQL(DROP_BOOK_TABLE)
        onCreate(db)
    }

    private fun log(s: String) {
        Log.e(LABEL, s)
    }
}

SQLiteDatabase : Exposes methods to manage a SQLite database.

class DatabaseActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //只需将version改为大于原来版本号即可触发onUpdate方法
        val helper = DatabaseHelper(this, "BookStore.db", null, 4)

        findViewById<Button>(R.id.button1).setOnClickListener {
            val db: SQLiteDatabase = helper.writableDatabase
            val values = ContentValues()
            values.put("name", "sparrow")
            values.put("author", "chen")
            values.put("price", 89.9)
            db.insert("Book", null, values)
        }

        findViewById<Button>(R.id.button2).setOnClickListener {
            val db = helper.writableDatabase
            val values = ContentValues()
            values.put("price", 109.9)
            db.update("Book", values, "name = ?", arrayOf("sparrow"))
        }

        findViewById<Button>(R.id.button3).setOnClickListener {
            val db = helper.writableDatabase
            db.delete("Book", "price > ?", arrayOf("100"))
        }

        findViewById<Button>(R.id.button4).setOnClickListener {
            val db = helper.writableDatabase
            val cursor = db.query("Book", null, null, null, null, null, null)
            if (cursor.moveToFirst()) {
                do {
                    val name = cursor.getString(cursor.getColumnIndex("name"))
                    val author = cursor.getString(cursor.getColumnIndex("author"))
                    val price = cursor.getString(cursor.getColumnIndex("price"))
                    Log.e("scrutiny", "$name $author $price")
                } while (cursor.moveToNext())
            }
            cursor.close()
        }
    }
}

ROOM

@Entity、@Dao、@Database

Do you need to add default data to your database, right after it was created or when the database is opened?

Use RoomDatabase#Callback! Call the addCallback method when building your RoomDatabase and override either onCreate or onOpen. 

Do you have multiple tables in your database and find yourself copying the same Insert, Update and Delete methods?

DAOs support inheritance, so create a BaseDao<T> class, and define your generic @Insert, @Update and @Delete methods there

interface BaseDao<T> {
    @Insert
    fun insert(vararg obj: T)
}
@Dao
abstract class DataDao : BaseDao<Data>() {
    @Query("SELECT * FROM Data")
    abstract fun getData(): List<Data>
}

Execute queries in transactions with minimal boilerplate code Annotating a method with @Transaction makes sure that all database operations you’re executing in that method will be run inside one transaction. The transaction will fail when an exception is thrown in the method body. 

@Dao
abstract class UserDao {
    
    @Transaction
    open fun updateData(users: List<User>) {
        deleteAllUsers()
        insertAll(users)
    }
    @Insert
    abstract fun insertAll(users: List<User>)
    @Query("DELETE FROM Users")
    abstract fun deleteAllUsers()
}

When you’re querying the database, do you use all the fields you return in your query?

Take care of the amount of memory used by your app and load only the subset of fields you will end up using. This will also improve the speed of your queries by reducing the IO cost. 

In the previousUser-Pet example, we can say that we have a one-to-many relation: a user can have multiple pets. Let’s say that we want to get a list of users with their pets: List<UserAndAllPets>

@Query(“SELECT * FROM Users”)
public List<User> getUsers();
@Query(“SELECT * FROM Pets where owner = :userId”)
public List<Pet> getPetsForUser(String userId);

To make this simpler, Room’s @Relation annotation automatically fetches related entities. @Relation can only be applied to a List or Set of objects. The UserAndAllPets class has to be updated:

class UserAndAllPets {
   @Embedded
   var user: User? = null
   @Relation(parentColumn = “userId”, entityColumn = “owner”)
   var pets: List<Pet> = ArrayList()
}

@Transaction
@Query(“SELECT * FROM Users”)
List<UserAndAllPets> getUsers();

Avoid false positive notifications for observable queries

@Query(“SELECT * FROM Users WHERE userId = :id)
fun getUserById(id: String): LiveData<User>
@Query(“SELECT * FROM Users WHERE userId = :id)
fun getUserById(id: String): Flowable<User>

You’ll get a new emission of the User object whenever that user updates. But you will also get the same object when other changes (deletes, updates or inserts) occur on the Users table that have nothing to do with the User you’re interested in, resulting in false positive notifications. Even more, if your query involves multiple tables, you’ll get a new emission whenever something changed in any of them.

Room only knows that the table has been modified but doesn’t know why and what has changed. Therefore, after the re-query, the result of the query is emitted by the LiveData or Flowable. Since Room doesn’t hold any data in memory and can’t assume that objects have equals(), it can’t tell whether this is the same data or not. You need to make sure that your DAO filters emissions and only reacts to distinct objects.

@Dao
abstract class UserDao : BaseDao<User>() {
@Query(“SELECT * FROM Users WHERE userid = :id”)
protected abstract fun getUserById(id: String): Flowable<User>

fun getDistinctUserById(id: String): 
   Flowable<User> = getUserById(id).distinctUntilChanged()
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

little-sparrow

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值