博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
SharePreference源码学习和多进程的场景
阅读量:5989 次
发布时间:2019-06-20

本文共 9879 字,大约阅读时间需要 32 分钟。

复习了下SharePreference的使用,以及了解下SharePreference的源码实现,解决多进程情况下的SharePreference问题,做下笔记。

参考文章:

源码分析:

SharePreference的多进程解决方案:

SharePreference

Android平台中一个轻量级的存储库,用来保存应用程序的各种配置信息。本质是一个以“key-value”键值对的方式保存数据的xml文件。

文件保存地址:在/data/data/package name/shared_prefs目录下就可以查看到这个文件了

简单使用例子

//获取得到SharePreference,第一个参数是文件名称,第二个参数是操作模式        //一般是MODE_PRIVATE模式,指定该SharedPreferences数据只能被本应用程序读、写        SharedPreferences sharedPreferences = getSharedPreferences("test", MODE_PRIVATE);        //创建Editor对象        SharedPreferences.Editor editor=sharedPreferences.edit();        //保存数据        editor.putString("name","donggua");        //editor.commit();        editor.apply();        //读取数据        String result=sharedPreferences.getString("name","默认值");复制代码

commit和apply的区别

当使用commit去提交数据的时候,发现IDE提示让我们使用apply方法。

  • commit:同步提交,commit将同步的把数据写入磁盘和内存缓存,并且有返回值。
  • apply:异步提交,会把数据同步写入内存缓存,然后异步保存到磁盘,可能会失败,失败不会收到错误回调。

两者的区别:

  • commit的效率会比apply慢一点。在一个进程中,如果在不关心提交结果是否成功的情况下,优先考虑apply方法。
  • 都是原子性操作,但是原子的操作不同。commit的从数据提交到保存到内存后再保存到磁盘中,中间不可打断。而apply方法是将数据保存到内存后就可以返回了,异步执行保存到磁盘的操作,

源码分析

获取SharePreference对象

利用Context获取到SharePreference实例,ContextImpl是Context的实现类,实现了getSharedPreferences方法。

  • 因为SharedPreferences是支持自定义文件名的,所以这里利用了ArrayMap<File, SharedPreferencesImpl>来缓存不同文件对应的SharedPreferencesImpl对象。一个File文件对应一个SharePreference对象。
  • getSharedPreferencesCacheLocked(),获取缓存的ArrayMap<File, SharedPreferencesImpl>对象,没有则创建一个。
@Overridepublic SharedPreferences getSharedPreferences(File file, int mode) {    SharedPreferencesImpl sp;    synchronized (ContextImpl.class) {        //获取缓存的map        final ArrayMap
cache = getSharedPreferencesCacheLocked(); //拿到对应的文件的SharePreference sp = cache.get(file); if (sp == null) { checkMode(mode); if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) { if (isCredentialProtectedStorage() && !getSystemService(UserManager.class) .isUserUnlockingOrUnlocked(UserHandle.myUserId())) { throw new IllegalStateException("SharedPreferences in credential encrypted " + "storage are not available until after user is unlocked"); } } //没有缓存,创建SharedPreferencesImpl对象,并保存到缓存中 sp = new SharedPreferencesImpl(file, mode); cache.put(file, sp); return sp; } } if ((mode & Context.MODE_MULTI_PROCESS) != 0 || getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) { // If somebody else (some other process) changed the prefs // file behind our back, we reload it. This has been the // historical (if undocumented) behavior. sp.startReloadIfChangedUnexpectedly(); } return sp;}@GuardedBy("ContextImpl.class")private ArrayMap
getSharedPreferencesCacheLocked() { if (sSharedPrefsCache == null) { sSharedPrefsCache = new ArrayMap<>(); } final String packageName = getPackageName(); ArrayMap
packagePrefs = sSharedPrefsCache.get(packageName); if (packagePrefs == null) { packagePrefs = new ArrayMap<>(); sSharedPrefsCache.put(packageName, packagePrefs); } return packagePrefs;}复制代码

SharedPreferencesImpl是SharedPreferences接口的实现类,实现了commit和apply方法。先看看SharedPreferencesImpl的构造方法。会异步调用一个startLoadFromDisk的方法,作用是从磁盘中把SharePreference文件里面保存的xml信息读取到内存中,并保存到Map里面。

SharedPreferencesImpl(File file, int mode) {    mFile = file;    mBackupFile = makeBackupFile(file);    mMode = mode;    mLoaded = false;    mMap = null;    mThrowable = null;    startLoadFromDisk();}private void startLoadFromDisk() {    synchronized (mLock) {        mLoaded = false;    }    new Thread("SharedPreferencesImpl-load") {        public void run() {            loadFromDisk();        }    }.start();}private void loadFromDisk(){//省略部分代码try {    stat = Os.stat(mFile.getPath());    if (mFile.canRead()) {        BufferedInputStream str = null;        try {            str = new BufferedInputStream(                    new FileInputStream(mFile), 16 * 1024);            //进行xml解析            map = (Map
) 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;}}//省略部分代码。//将解析结果保存的map进行赋值if (map != null) { mMap = map; mStatTimestamp = stat.st_mtim; mStatSize = stat.st_size;}复制代码

读取数据

例如SharedPreferencesImpl的实现getString()方法,是直接从内存中的mMap直接就把数据读取出来,并没有涉及到磁盘操作。(恍然大悟,以前以为读取数据也要去读取file文件)

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

保存数据

EditorImpl类实现了Editor接口。apply和commit都会调用commitToMemory方法,将数据保存到内存中,后面调用enqueueDiskWrite将数据保存到磁盘中。

//临时缓存多个key的数据,后面提交数据的时候,就遍历这个map就行private final Map
mModified = new HashMap<>();//CountDownLatch,等待直到保存到磁盘的操作完成。final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);//在当前线程直接写文件,调用await,同步等待,最后返回操作的结果result@Overridepublic boolean commit() { long startTime = 0; if (DEBUG) { startTime = System.currentTimeMillis(); } //保存到内存中 MemoryCommitResult mcr = commitToMemory(); //保存到磁盘 SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null /* sync write on this thread okay */); try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException e) { return false; } finally { if (DEBUG) { Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration + " committed after " + (System.currentTimeMillis() - startTime) + " ms"); } } notifyListeners(mcr); return mcr.writeToDiskResult;}//异步等待保存操作,无法获取操作的结果@Overridepublic 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"); } } }; QueuedWork.addFinisher(awaitCommit); Runnable postWriteRunnable = new Runnable() { @Override public void run() { awaitCommit.run(); 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);}复制代码

多进程中的SharePreference

上面讲的默认是单进程中的SharePreference,读取操作是直接从内存中的Map读取的,不涉及IO操作。如果是在多进程中的话,不同进程之间的内存并不是共享的,这个时候读写同一个SharePreference就会出现问题了。比如多个进程对同一个sharedpreference进行修改,总会有一个进程获取到的结果不是实时修改后的结果。

解决方法:推荐使用ContentProvider来处理多进程间的文件共享。

ContentProvider的特点:

  • ContentProvider内部的同步机制会防止多个进程同时访问,避免数据冲突。
  • ContentProvider的数据源,并不是只能选择数据库,其实核心操作就在update()和query()这两个操作,里面操作存取的数据源其实可以根据我们需要,替换成文件,也可以换成SharedPreferences。

所以我们可以使用ContentProvider做了一下中间媒介,让它帮我们实现多进程同步机制,里面操作的数据改成SharedPreferences来实现。这样的话就可以实现了跨进程访问SharePreference。

下面简单地写一个demo,读取的时候只需要传进相应的uri就行了。比如下面的代码,path字段的第二个是fileName,第三个是key值。

public class MultiProcessSharePreference extends ContentProvider{@Nullable@Overridepublic Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {    //获取xml的文件名,默认取path字段的第一个    Log.d(TAG, "query: uri:" + uri);    String tableName = uri.getPathSegments().get(0);    String name = uri.getPathSegments().get(1);    String key = uri.getPathSegments().get(2);    Log.d(TAG, "query: tableName:" + tableName);    Log.d(TAG, "query: fileName:" + name);    Log.d(TAG, "query: key:" + key);    //创建sharedPreferences对象    SharedPreferences sharedPreferences = getContext().getSharedPreferences(name, Context.MODE_PRIVATE);    //创建一个cursor对象    MatrixCursor cursor = null;    switch (uriMatcher.match(uri)) {        case CODE_PREFERENCE_STRING:            String value = sharedPreferences.getString(key, "默认值");            cursor = new MatrixCursor(PREFERENCE_COLUMNS, 1);            MatrixCursor.RowBuilder rowBuilder = cursor.newRow();            rowBuilder.add(value);            break;        default:            Log.d(TAG, "query: Uri No Match");    }    return cursor;}}复制代码
  • MatrixCursor: 如果需要一个cursor而没有一个现成的cursor的话,那么可以使用MatrixCursor实现一个虚拟的表。MatrixCursor.RowBuilder是用来添加Row数据的,通过rowBuilder的add方法,就可以把数值添加到行里面了。使用场景:比如ContentProvider的query方法是返回一个cursor类型的数据,而数据源用的是SharePreference,这个时候就可以利用MatrixCursor。MartixCursor本质上是用一个一位数据来模拟一个二维数据,根据行值和列值就可以找到对应的数据了。

MatrixCursor的源码解析:

//定义每一列的字段名字public static final String COLUMN_VALUE = "value";//创建一个字符数组,字符数组的值对应着表的字段private static String[] PREFERENCE_COLUMNS = {COLUMN_VALUE};//构造一个MatrixCursor对象MatrixCursor  cursor = new MatrixCursor(PREFERENCE_COLUMNS, 1);//通过matrixCursor的addRow方法添加一行值MatrixCursor.RowBuilder rowBuilder = cursor.newRow();rowBuilder.add(value);复制代码
  • 优化一下的思路:
    • 在ContentProvider里面加一个HashMap<String,SharePreference>进行一下缓存,key值是文件名,value是对应的SharePreference对象,这样的话,就不用每次都去加载SharePreference对象了。
    • 在ContentProvider里面实现回调listener,在key值有变化的时候,进行通知订阅者。

转载地址:http://ziilx.baihongyu.com/

你可能感兴趣的文章
从手忙脚乱到袖手旁观:RPA对财务流程的颠覆
查看>>
python大佬养成计划----HTML网页设计(序列)
查看>>
EOS Asia & 思否区块链达成战略合作
查看>>
文本分析——分词、统计词频、词云
查看>>
网站安全:你面临2个至关重要的挑战!
查看>>
ES6学习笔记2---对象的扩展
查看>>
零 bug 策略
查看>>
阿里数据中台七年演化史——行在口述干货
查看>>
中国学术力量不容小觑,首届华人带头的国际分布式人工智能学术会议将于北京召开 ...
查看>>
东南亚科技公司BLUE Mobile完成C轮融资,蚂蚁金服领投、愉悦资本跟投 ...
查看>>
传统网站维护难,智能化建站平台帮助国际学校一站解决 ...
查看>>
在巴塞罗那,华为挥别昨日 | MWC 2019
查看>>
我们3666人的【阿里Java技术进阶】钉钉大群,又增添新 “成员” 啦~ ...
查看>>
同盾科技与中科实数共建互联网反黑产联合实验室 ...
查看>>
读懂微信:从1.0到7.0版本,一个主流IM社交工具的进化史 ...
查看>>
Flask 教程 第十章:邮件支持
查看>>
C++雾中风景13:volatile解惑
查看>>
绿色版Mysql自动建立my.ini和命令行启动并动态指定datadir路径
查看>>
华为5G手机发布!5G和AI给世界带来什么?
查看>>
SQL Server使用sp_spaceused查看表记录存在不准确的情况
查看>>