前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Jetpack:DataStore必知的几个优点

Jetpack:DataStore必知的几个优点

作者头像
Rouse
发布2020-12-14 10:08:57
1.2K0
发布2020-12-14 10:08:57
举报
文章被收录于专栏:Android补给站Android补给站

最近Jetpack又增加了新成员,提出了一个关于小型数据存储相关的DataStore组件。

根据官网的描述,DataStore完全是对标现有的SharedPreferences

SharedPreferences相信大家都有用过,既然在现有的基础上提出DataStore那自然是为了解决SharedPreferences的缺点的。

如果你还不知道SharedPreferences有什么缺点?没关系,我们正好来复习一遍。你可以对标一下在使用SharedPreferences的过程中是否也遇到过这些问题。

SharedPreferences的糟心事

为了精简语言,下面都将SharedPreferences简称sp

一次性读取阻塞主线程

代码语言:javascript
复制
sp = getSharedPreferences("settings_preference", Context.MODE_PRIVATE)

在使用sp的过程中,会通过getSharedPreferences来初始化sp

上面这段代码最终会进入SharedPreferencesImplloadFromDisk方法。

具体调用就不带大家走一遍了,如果都贴出来文章就变成代码粘贴板了,我们只关注核心逻辑,其它感兴趣的可以自行查看源码

代码语言:javascript
复制
private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }
 
    // Debugging
    if (mFile.exists() && !mFile.canRead()) {
        Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
    }
 
    Map<String, Object> map = null;
    StructStat stat = null;
    Throwable thrown = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                map = (Map<String, Object>) XmlUtils.readMapXml(str);
            } catch (Exception e) {
                Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        // An errno exception means the stat failed. Treat as empty/non-existing by
        // ignoring.
    } catch (Throwable t) {
        thrown = t;
    }
 
    synchronized (mLock) {
        mLoaded = true;
        mThrowable = thrown;
 
        // It's important that we always signal waiters, even if we'll make
        // them fail with an exception. The try-finally is pretty wide, but
        // better safe than sorry.
        try {
            if (thrown == null) {
                if (map != null) {
                    mMap = map;
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                } else {
                    mMap = new HashMap<>();
                }
            }
            // In case of a thrown exception, we retain the old map. That allows
            // any open editors to commit and store updates.
        } catch (Throwable t) {
            mThrowable = t;
        } finally {
            mLock.notifyAll();
        }
    }
}

在这里通过对象锁mLock机制来对其进行加锁操作。只有当sp文件中的数据全部读取完毕之后才会调用mLock.notifyAll()来释放锁。

而另一边对应的获取数据的get方法,例如getString方法

代码语言:javascript
复制
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}
 
private void awaitLoadedLocked() {
    if (!mLoaded) {
        // Raise an explicit StrictMode onReadFromDisk for this
        // thread, since the real read will be in a different
        // thread and otherwise ignored by StrictMode.
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }
    while (!mLoaded) {
        try {
            mLock.wait(); // 等待sp文件读取完毕
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}

这里会在awaitLoadedLocked方法中调用mLock.wait()来等待sp的初始化完成。

所以如果sp文件过大,初始化所花的时间过多,会导致后面sp数据获取时的阻塞。

类型不安全

在我们使用sp过程中,用的最多的应该是它的putget方法。现在我们用这两个方法来写一段代码

代码语言:javascript
复制
sp = getSharedPreferences("settings_preference", Context.MODE_PRIVATE)
 
// 某一个地方的逻辑
sp.edit().putString("key_name_from_sp", "from sp").apply()
 
// 另一个地方的逻辑
sp.edit().putInt("key_name_from_sp", "from sp").apply()
 
// 获取key_name_from_sp值
sp.getString("key_name_from_sp", "")

如果你运行上面的代码你可以发现程序运行异常,本质问题是对同一个key赋值了不同类型的值。将原来String类型的值转变成Int类型。由于sp内部是通过Map来保存对于的key-value,所以它并不能保证key-value的类型固定,也进一步导致通过get方法来获取对应key的值的类型也是不安全的。这就造成了所谓的类型不安全。

代码语言:javascript
复制
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

getString的源码中,会进行类型强制转换,如果类型不对就会导致程序崩溃。由于sp不会在代码编译时进行提醒,只能在代码运行之后才能发现,所以就避免不掉可能发生的异常,从而导致sp类型不安全。

apply异步没有回调

为了防止sp写入时阻塞线程,一般都会使用apply方法来将数据异步提交到磁盘,即写入到文件中。

虽然apply是异步,但它并没有返回值,同样也没有对应的结果回调。

代码语言:javascript
复制
public void apply() {
 ...
}

导致ANR

apply异步提交解决了线程的阻塞问题,但如果apply任务过多数据量过大,可能会导致ANR的产生。

ANR的产生是主线程长时间未响应导致的。apply不是异步的吗?它怎么又会产生ANR呢?

来看下apply的源码

代码语言:javascript
复制
public void apply() {
    final long startTime = System.currentTimeMillis();
 
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }

                if (DEBUG && mcr.wasWritten) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " applied after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
        };
 
    // 注意:将awaitCommit添加到队列中
    QueuedWork.addFinisher(awaitCommit);
 
    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                // 成功写入磁盘之后才将awaitCommit移除
                QueuedWork.removeFinisher(awaitCommit);
            }
        };
 
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
 
    // Okay to notify the listeners before it's hit disk
    // because the listeners should always get the same
    // SharedPreferences instance back, which has the
    // changes reflected in memory.
    notifyListeners(mcr);
}

这里关键点是会将awaitCommit加入到QueuedWork队列中,只有当awaitCommit执行完之后才会进行移除。

这是一方面,我们再来看另一方面。

ActivityonPauseonStopServiceonDestory中会等待QueuedWork中的任务全部完成,一旦QueuedWork中的任务非常耗时,例如sp的写入磁盘数据量过多,就会导致主线程长时间未响应,从而产生ANR

具体调用分别在ActivityThread中的handlePauseActivityhandlePauseActivityhandleStopService方法中。

代码语言:javascript
复制
public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,
        int configChanges, PendingTransactionActions pendingActions, String reason) {
    ActivityClientRecord r = mActivities.get(token);
    if (r != null) {
        if (userLeaving) {
            performUserLeavingActivity(r);
        }
 
        r.activity.mConfigChangeFlags |= configChanges;
        performPauseActivity(r, finished, reason, pendingActions);

        // Make sure any pending writes are now committed.
        if (r.isPreHoneycomb()) {
            //等待任务完成
            QueuedWork.waitToFinish();
        }
        mSomeActivitiesChanged = true;
    }
}

那如何解决呢?首先使用sp不要存储过大的key-value数据,本身sp就是轻量的存储,对于大数据还是使用room来存储。

此类ANR都是经由QueuedWork.waitToFinish()触发的,如果在调用此函数之前,将其中保存的队列手动清空,那么是不是能解决问题呢,答案是肯定的。

另外在今日头条的一篇文章中已经提出解决ANR的方法,具体解决可以自行查看

https://mp.weixin.qq.com/s?__biz=MzI1MzYzMjE0MQ==&mid=2247484387&idx=1&sn=e3c8d6ef52520c51b5e07306d9750e70&scene=21#wechat_redirect

不能跨进程通信

sp是不能跨进程通信的,虽然在获取sp的时候提供了MODE_MULTI_PROCESS,但内部并不是用来跨进程的。

代码语言:javascript
复制
public SharedPreferences getSharedPreferences(File file, int mode) {
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
        getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        // 重新读取SP文件内容
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}

在这里使用MODE_MULTI_PROCESS只是重新读取一遍文件而已,并不能保证跨进程通信。

上面的sp问题不知道你在使用的过程中是否有遇到过,或者说有幸中标几条,大家可以留言来对比一下,说出你的故事(此处应该有酒)。

DataStore

针对sp那几个问题,DataStore都够能规避。为了精简语言,下面都将DataStore简称ds

  1. ds内部使用kotlin协程通过挂起的方式来避免阻塞线程,同时也避免产生ANR
  2. ds不仅支持sp同时还支持protocol buffers类型的存储,而protocol buffers可以保证数据类型安全。
  3. ds能够在编译阶段提醒sp类型错误,保证sp类型的类型不安全问题。
  4. ds使用Flow来获取数据,每次保存数据之后都会通知最近的Flow
  5. ds完美支持sp数据的迁移,你可以无成本过渡到ds

所以ds将会是Android后续轻量数据存储的首选组件。我们也是时候来了解ds的使用。

引入DataStore

首先我们要引入ds,方式很简单直接在build中添加依赖即可。唯一需要注意的是ds支持spprotocol buffers两种类型,所以对应的也有两种依赖。

代码语言:javascript
复制
// Preferences DataStore
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"
 
// Proto DataStore
implementation  "androidx.datastore:datastore-core:1.0.0-alpha01"

下面针对这两种类型分别做介绍。

创建DataStore

针对sp类型的数据,ds只需通过createDataStore方法来获取对应的ds对象

代码语言:javascript
复制
private val dataStore = createDataStore("settings")

其中settings为对应的文件名,存储方式为datastore/ + name + .preferences_pb

protocol buffers类型需要额外实现Serializer接口,提供读写的入口。

代码语言:javascript
复制
object SettingsSerializer : Serializer<Settings> {
 
    override fun readFrom(input: InputStream): Settings {
        return Settings.parseFrom(input)
    }
 
    override fun writeTo(t: Settings, output: OutputStream) {
        t.writeTo(output)
    }
 
}
 
private val dataStoreProto = createDataStore("settings.pb", SettingsSerializer)

其中的Settings类是通过protocol buffers脚本自动生成的。要生成Settings类,你需要做两件事

  1. 配置protocol buffers环境
  2. 编写.proto文件

所以你可能需要懂一点protocol buffers相关的语法。

如果后续有空,可能会单独开文章介绍一下protocol buffers相关的内容,大厂用的基本上都是protocol buffers

代码语言:javascript
复制
syntax = "proto3";

option java_multiple_files = true;

message Settings {
    string key_name = 1;
}

使用protocol buffers运行上面的代码就能自动帮我们生成对应的Settings类。其中它里面的一个变量就是keyName_,它是String类型。通过创建类与对应变量的方式来约定类型的安全。

spprotocol buffers类型的读操作使用方式都一样,首先都要创建Preferences.Key类型的key

代码语言:javascript
复制
val DATA_KEY = preferencesKey<String>("key_name")

对应的preferencesKey如下:

代码语言:javascript
复制
inline fun <reified T : Any> preferencesKey(name: String): Preferences.Key<T> {
    return when (T::class) {
        Int::class -> {
            Preferences.Key<T>(name)
        }
        String::class -> {
            Preferences.Key<T>(name)
        }
        Boolean::class -> {
            Preferences.Key<T>(name)
        }
        Float::class -> {
            Preferences.Key<T>(name)
        }
        Long::class -> {
            Preferences.Key<T>(name)
        }
        Set::class -> {
            throw IllegalArgumentException("Use `preferencesSetKey` to create keys for Sets.")
        }
        else -> {
            throw IllegalArgumentException("Type not supported: ${T::class.java}")
        }
    }
}

它支持IntStringBooleanFloatLong类型的数据,另外还有一个preferencesSetKey,用来支持set类型的数据。

调用preferencesKey每次都创建一个Preferences.Key对象,那它这样如何保证是同一个key呢?

如果你去看源码就会一目了然。

代码语言:javascript
复制
internal constructor(val name: String) {
    override fun equals(other: Any?) =
        if (other is Key<*>) {
            name == other.name
        } else {
            false
        }
 
    override fun hashCode(): Int {
        return name.hashCode()
    }
}

原来是它重写了equals方法,内部实现对name的比较。那么只要创建preferencesKey时传入的name相同,就能保证获取到的是同一个key的数据。

有了key,再来通过dataStore.data.map来获取Flow,同时暴露出对应的Preferences

代码语言:javascript
复制
private suspend fun read() {
    dataStore.data.map {
        // unSafe type
        if (it[DATA_KEY] is String) {
            it[DATA_KEY] ?: ""
        } else {
            "type is String: ${it[DATA_KEY] is String}"
        }
    }.collect {
        Toast.makeText(this@DataStoreActivity, "read result: $it", Toast.LENGTH_LONG).show()
    }
}

同时在read中写了一个验证SharedPreference类型不安全的示例。如果在别的地方赋值了DATA_KEYString类型的数据时,将会弹出else中的语句。

下面是protocol buffers的读取

代码语言:javascript
复制
private suspend fun protoRead() {
    dataStoreProto.data.map {
        // safe type
        it.keyName
    }.collect {
        Toast.makeText(this, "read result success form proto: $it", Toast.LENGTH_LONG).show()
    }
}

需要注意的是这里获取的数据就是类型安全的。这里的it对应的就是在ds创建时产生的Settings

spprotocol buffers有所不同。

对于sp直接使用dataStore.edit来写入数据

代码语言:javascript
复制
private suspend fun write(value: String) {
    dataStore.edit {
        it[DATA_KEY] = value
        LogUtils.d("dataStore write: $value")
    }
}

protocol buffers使用的是updateData方法

代码语言:javascript
复制
private suspend fun protoWrite(value: String) {
    dataStoreProto.updateData {
        it.toBuilder().setKeyName(value).build()
    }
}

迁移SharedPreferences

迁移也分为两种,一种是迁移到dssp中;另一种是迁移到protocol buffers中。

具体来看,如果迁移到dssp中,只需在之前创建ds基础上额外再加一个migrations参数。

代码语言:javascript
复制
private val dataStore = createDataStore("settings", migrations = listOf(SharedPreferencesMigration(this, "settings_preference")))

通过创建SharedPreferencesMigration来迁移对应的sp数据。

下面是迁移到protocol buffers

代码语言:javascript
复制
val settingsDataStore: DataStore<Settings> = context.createDataStore(
    produceFile = { File(context.filesDir, "settings.preferences_pb") },
    serializer = SettingsSerializer,
    migrations = listOf(
        SharedPreferencesMigration(
            context,
            "settings_preferences"            
        ) { sharedPrefs: SharedPreferencesView, currentData: UserPreferences ->
            // Map your sharedPrefs to your type here
          }
    )
)

迁移完之后需要执行一次代码,同时应该停止再次使用sp。如果迁移成功将会删除之前sp.xml类型的文件,生成对应ds文件。

最后附上一张Google分析的SharedPreferencesDataStore的区别图

目前可以看到DataStore还处在alpha版本,非常期待它之后的正式版本。

另外,针对DataStore的使用,我写了一个demo,大家可以在android-api-analysis中获取。

https://github.com/idisfkj/android-api-analysis

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-12-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Android补给站 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • SharedPreferences的糟心事
    • 一次性读取阻塞主线程
      • 类型不安全
        • apply异步没有回调
          • 导致ANR
            • 不能跨进程通信
            • DataStore
              • 引入DataStore
                • 创建DataStore
                      • 迁移SharedPreferences
                      相关产品与服务
                      对象存储
                      对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档