手记

SharePreference原理及跨进程数据共享的问题

SharePreference原理及跨进程数据共享的问题


    SharedPreferences是Android提供的数据持久化的一种手段,适合单进程、小批量的数据存储与访问。为什么这么说呢?因为SharedPreferences的实现是基于单个xml文件实现的,并且,所有持久化数据都是一次性加载到内存,如果数据过大,是不合适采用SharedPreferences存放的。而适用的场景是单进程的原因,由于Android原生的文件访问并不支持多进程互斥,所以SharePreferences也不支持,如果多个进程更新同一个xml文件,就可能存在同步互斥问题,后面会详细分析这几个问题。本文源码基于SKD-25版本分析。

SharedPreferences的实现原理之:持久化数据的加载

首先,从基本使用简单看下SharedPreferences的实现原理:

[代码]java代码:

?


<span style="font-size: 9pt;">    //创建    mSharedPreferences = context.getSharedPreferences("test", Context.MODE_PRIVATE);</span>        //存取    SharedPreferences.Editor editor = mSharedPreferences.edit();    editor.putInt(key1, value1);    editor.putString(key2,value2);    editor.putBoolean(key3,value3);    editor.apply();<br>    //获取    editor.getInt(key,default);

context.getSharedPreferences其实就是简单的调用ContextImpl的getSharedPreferences,具体实现如下

[代码]java代码:

?


@Override   public SharedPreferences getSharedPreferences(String name, int mode) {       // At least one application in the world actually passes in a null       // name.  This happened to work because when we generated the file name       // we would stringify it to "null.xml".  Nice.       if (mPackageInfo.getApplicationInfo().targetSdkVersion <               Build.VERSION_CODES.KITKAT) {           if (name == null) {               name = "null";           }       }        File file;       synchronized (ContextImpl.class) {           if (mSharedPrefsPaths == null) {               mSharedPrefsPaths = new ArrayMap<>();           }           file = mSharedPrefsPaths.get(name);           if (file == null) {               file = getSharedPreferencesPath(name);               mSharedPrefsPaths.put(name, file);           }       }       return getSharedPreferences(file, mode);   }

    校验与SharePreferences对应的xml文件是否存在。若xml文件都不存在,那么还需要创建xml文件。最后调用了 getSharedPreferences(file, mode)。(与SharePreferences对应的xml文件位置一般都在/data/data/包名/shared_prefs目录下,后缀一定是.xml)

[代码]java代码:

?


@Override    public SharedPreferences getSharedPreferences(File file, int mode) {        checkMode(mode);        SharedPreferencesImpl sp;        synchronized (ContextImpl.class) {            final ArrayMap<file, sharedpreferencesimpl=""> cache = getSharedPreferencesCacheLocked();            sp = cache.get(file);            if (sp == null) {                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;    }</file,>

[代码]java代码:

?


private ArrayMap<file, sharedpreferencesimpl=""> getSharedPreferencesCacheLocked() {        if (sSharedPrefsCache == null) {            sSharedPrefsCache = new ArrayMap<>();        }         final String packageName = getPackageName();        ArrayMap<file, sharedpreferencesimpl=""> packagePrefs = sSharedPrefsCache.get(packageName);        if (packagePrefs == null) {            packagePrefs = new ArrayMap<>();            sSharedPrefsCache.put(packageName, packagePrefs);        }         return packagePrefs;    }</file,></file,>

    getSharedPreferences()方法中调用getSharedPreferencesCacheLocked()方法获取ArrayMap<File, SharedPreferencesImpl>对象。首次创建时sp==null,执行sp = new SharedPreferencesImpl(file, mode);

[代码]java代码:

?


SharedPreferencesImpl(File file, int mode) {       mFile = file;       mBackupFile = makeBackupFile(file);       mMode = mode;       mLoaded = false;       mMap = null;       startLoadFromDisk();   }

   代码很简单,继续调用startLoadFromDisk()方法。

[代码]java代码:

?

    代码中我们发现启动了一个新线程加载数据。

[代码]java代码:

?

private void loadFromDisk() {       synchronized (SharedPreferencesImpl.this) {           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 map = null;       StructStat stat = null;       try {           stat = Os.stat(mFile.getPath());           if (mFile.canRead()) {               BufferedInputStream str = null;               try {                   str = new BufferedInputStream(                           new FileInputStream(mFile), 16*1024);                   map = XmlUtils.readMapXml(str);               } catch (XmlPullParserException | IOException e) {                   Log.w(TAG, "getSharedPreferences", e);               } finally {                   IoUtils.closeQuietly(str);               }           }       } catch (ErrnoException e) {           /* ignore */       }        synchronized (SharedPreferencesImpl.this) {           mLoaded = true;           if (map != null) {               mMap = map;               mStatTimestamp = stat.st_mtime;               mStatSize = stat.st_size;           } else {               mMap = new HashMap<>();           }           notifyAll();       }   }

    可以看到其实就是直接使用xml解析工具XmlUtils,读取xml文件,读取完成之后,xml中的配置项都会被加载到内存,再次访问的时候,其实访问的是内存缓存。同时加载完成后回调用notifyAll()方法,通知其他等待线程。

SharedPreferences的实现原理之:持久化数据的更新

    通常更新SharedPreferences的时候是首先获取一个SharedPreferences.Editor,利用它缓存一批操作,之后当做事务提交,有点类似于数据库的批量更新。Editor是一个接口,这里的实现是一个EditorImpl对象,它首先批量预处理更新操作,之后再提交更新,在提交事务的时候有两种方式,一种是apply,另一种commit,两者的区别在于:何时将数据持久化到xml文件,前者是异步的,后者是同步的。Google推荐使用前一种,因为,就单进程而言,只要保证内存缓存正确就能保证运行时数据的正确性,而持久化,不必太及时,这种手段在Android中使用还是很常见的,比如权限的更新也是这样,况且,Google并不希望SharePreferences用于多进程,因为不安全。看一下apply与commit的区别

[代码]java代码:

?


public final class EditorImpl implements Editor {        private final Map<string, object=""> mModified = Maps.newHashMap();        private boolean mClear = false;<string><string>        .......        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);                    }                };<span style="white-space:pre">  </span>    //延迟写入到XML文件中            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);        }         // Returns true if any changes were made        private MemoryCommitResult commitToMemory() {            MemoryCommitResult mcr = new MemoryCommitResult();            synchronized (SharedPreferencesImpl.this) {                // We optimistically don't make a deep copy until                // a memory commit comes in when we're already                // writing to disk.                if (mDiskWritesInFlight > 0) {                    // We can't modify our mMap as a currently                    // in-flight write owns it.  Clone it before                    // modifying it.                    // noinspection unchecked                    mMap = new HashMap<string, object="">(mMap);                }                mcr.mapToWriteToDisk = mMap;                mDiskWritesInFlight++;                 boolean hasListeners = mListeners.size() > 0;                if (hasListeners) {                    mcr.keysModified = new ArrayList<string>();                    mcr.listeners =                            new HashSet<onsharedpreferencechangelistener>(mListeners.keySet());                }                 synchronized (this) {                    if (mClear) {                        if (!mMap.isEmpty()) {                            mcr.changesMade = true;                            mMap.clear();                        }                        mClear = false;                    }                     for (Map.Entry<string, object=""> e : mModified.entrySet()) {                        String k = e.getKey();                        Object v = e.getValue();                        // "this" is the magic value for a removal mutation. In addition,                        // setting a value to "null" for a given key is specified to be                        // equivalent to calling remove on that key.                        if (v == this || v == null) {                            if (!mMap.containsKey(k)) {                                continue;                            }                            mMap.remove(k);                        } else {                            if (mMap.containsKey(k)) {                                Object existingValue = mMap.get(k);                                if (existingValue != null && existingValue.equals(v)) {                                    continue;                                }                            }                            mMap.put(k, v);                        }                         mcr.changesMade = true;                        if (hasListeners) {                            mcr.keysModified.add(k);                        }                    }                     mModified.clear();                }            }            return mcr;        }         public boolean commit() {            MemoryCommitResult mcr = commitToMemory();            SharedPreferencesImpl.this.enqueueDiskWrite(                mcr, null /* sync write on this thread okay */);            try {                mcr.writtenToDiskLatch.await();            } catch (InterruptedException e) {                return false;            }            notifyListeners(mcr);            return mcr.writeToDiskResult;        }         private void notifyListeners(final MemoryCommitResult mcr) {            if (mcr.listeners == null || mcr.keysModified == null ||                mcr.keysModified.size() == 0) {                return;            }            if (Looper.myLooper() == Looper.getMainLooper()) {                for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {                    final String key = mcr.keysModified.get(i);                    for (OnSharedPreferenceChangeListener listener : mcr.listeners) {                        if (listener != null) {                            listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key);                        }                    }                }            } else {                // Run this function on the main thread.                ActivityThread.sMainThreadHandler.post(new Runnable() {                        public void run() {                            notifyListeners(mcr);                        }                    });            }        }    }</string,></onsharedpreferencechangelistener></string></string,></string></string></string,>

    从上面可以看出两者最后都是先调用commitToMemory,将更改提交到内存,在这一点上两者是一致的,之后又都调用了enqueueDiskWrite进行数据持久化任务,不过commit函数一般会在当前线程直接写文件。而apply则提交一个事务到线程池中。

[代码]java代码:

?


private void enqueueDiskWrite(final MemoryCommitResult mcr,                                  final Runnable postWriteRunnable) {        final Runnable writeToDiskRunnable = new Runnable() {                public void run() {                    synchronized (mWritingToDiskLock) {                        writeToFile(mcr);                    }                    synchronized (SharedPreferencesImpl.this) {                        mDiskWritesInFlight--;                    }                    if (postWriteRunnable != null) {                        postWriteRunnable.run();                    }                }            };         final boolean isFromSyncCommit = (postWriteRunnable == null);         // Typical #commit() path with fewer allocations, doing a write on        // the current thread.        if (isFromSyncCommit) {            boolean wasEmpty = false;            synchronized (SharedPreferencesImpl.this) {                wasEmpty = mDiskWritesInFlight == 1;            }            if (wasEmpty) {                writeToDiskRunnable.run();                return;            }        }         QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);    }

    不过如果有线程在写文件,那么就不能直接写,这个时候就跟apply函数一致了,但是,如果直观说两者的区别的话,直接说commit同步,而apply异步应该也是没有多大问题的。

SharePreferences多进程使用问题

    SharePreferences在新建的有个mode参数,可以指定它的加载模式,MODE_MULTI_PROCESS是Google提供的一个多进程模式,但是这种模式并不是我们说的支持多进程同步更新等,它的作用只会在getSharedPreferences的时候,才会重新从xml重加载,如果我们在一个进程中更新xml,但是没有通知另一个进程,那么另一个进程的SharePreferences是不会自动更新的。

[代码]java代码:

?


@Override    public SharedPreferences getSharedPreferences(File file, int mode) {      <span style="white-space:pre">    </span>......<file, sharedpreferencesimpl="">        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();        }        ......    }</file,>

    也就是说MODE_MULTI_PROCESS只是个鸡肋Flag,对于多进程的支持几乎为0。

总结

  • SharePreferences是Android基于xml实现的一种数据持久化手段

  • SharePreferences不支持多进程

  • SharePreferences的commit与apply一个是同步一个是异步(大部分场景下)

  • 不要使用SharePreferences存储太大的数据

原文链接:http://www.apkbus.com/blog-664680-77882.html

0人推荐
随时随地看视频
慕课网APP

热门评论

很厉害很厉害很厉害的样子哦

查看全部评论