美文网首页
SharedPreferences 多进程问题

SharedPreferences 多进程问题

作者: nightkidjj | 来源:发表于2017-05-11 01:58 被阅读466次

    SharedPreferences作为一种数据持久化的方式,是处理简单的key-value类型数据时的首选。但是有时候需要在多进程中共享数据时,如果用SharedPreferences会不会有什么问题?即SharedPreferences是否是进程安全的?

    SharedPreferences如何实现多进程访问?

    谈论进程安全前,先看下SharedPreferences如何实现多进程。我们知道每个SharedPreferences都对应了当前package的data/data/package_name/share_prefs/目录下的一个文件,要让多个进程可访问这个文件,必然需要修改其访问权限,看下SharedPreferences提供了哪些选项, 在Context.java中提供了以下几个mode:

    int MODE_PRIVATE = 0x0000;
    这是默认模式,仅caller uid的进程可访问
    
    int MODE_WORLD_READABLE = 0x0001; 
    所有人可写,也就是任何应用都可修改它,这是极其危险的,因此改选项已被Deprected
    
    int MODE_WORLD_READABLE = 0x0002
    所有人可读,这个参数同样非常危险,可能导致隐私数据泄漏
    
    int MODE_MULTI_PROCESS = 0x0004;
    设置该参数后,每次获取对应的SharedPreferences时都会尝试从磁盘中读取修改过的文件 
    

    多进程访问主要有两种情况:
    1.不同apk(不同packageName,且不具有相同uid)的多进程:
    由于linux文件权限是根据uid设置访问权限,因此必须设置mode为MODE_WORLD_READABLE或MODE_WORLD_WRITABLE,取决于别的应用是否有需要需改该SharedPreferences,由于这种方式需要修改文件权限,且会让所有人都具访问权限,无法只对某个应用授权,所以非常危险,android N上对targetsdk >= 24的应用已明确禁止这两个mode,本文不再做过多解释
    2.同一个packageName或具有相同uid的package里面存在多个进程:
    这种情况下多个进程具有相同的uid,因此不需要修改文件权限,没有权限安全性问题。目前很多apk都支持多进程,例如后台服务与前台页面运行在独立的进程。这种情况是本文重点分析的。

    是否需要设置MODE_MULTI_PROCESS?

    先看下这个mode具体描述

    /**
         * SharedPreference loading flag: when set, the file on disk will
         * be checked for modification even if the shared preferences
         * instance is already loaded in this process.  This behavior is
         * sometimes desired in cases where the application has multiple
         * processes, all writing to the same SharedPreferences file.
         * Generally there are better forms of communication between
         * processes, though.
         *
         * <p>This was the legacy (but undocumented) behavior in and
         * before Gingerbread (Android 2.3) and this flag is implied when
         * targetting such releases.  For applications targetting SDK
         * versions <em>greater than</em> Android 2.3, this flag must be
         * explicitly set if desired.
         *
         * @see #getSharedPreferences
         *
         * @deprecated MODE_MULTI_PROCESS does not work reliably in
         * some versions of Android, and furthermore does not provide any
         * mechanism for reconciling concurrent modifications across
         * processes.  Applications should not attempt to use it.  Instead,
         * they should use an explicit cross-process data management
         * approach such as {@link android.content.ContentProvider ContentProvider}.
         */
    

    当设置这个参数的时候,即使当前进程内已经创建了该SharedPreferences,仍然在每次获取的时候都会尝试从本地文件中刷新。在同一个进程中,同一个文件只有一个实例。MODE_MULTI_PROCESS的作用
    ContextImpl.java

    public SharedPreferences  getSharedPreferences(String name, int mode) {
        //code: new instance if not exists
        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;
    }
    

    这个方法先判断是否已创建SharedPreferences实例,若未创建,则先创建。之后判断mode如果为MODE_MULTI_PROCESS, 则调用startReloadIfChangeUnexpectedly(),看下其实现:
    SharedPreferencesImpl.java

    void startReloadIfChangedUnexpectedly() {
            synchronized (this) {
                // TODO: wait for any pending writes to disk?
                if (!hasFileChangedUnexpectedly()) {
                    return;
                }
                startLoadFromDisk();
            }
        }
    

    最终调用startLoadFromDisk()

    private void startLoadFromDisk() {
            synchronized (this) {
                mLoaded = false;
            }
            new Thread("SharedPreferencesImpl-load") {
                public void run() {
                    synchronized (SharedPreferencesImpl.this) {
                        loadFromDiskLocked();
                    }
                }
            }.start();
        }
    

    可以看出MODE_MULTI_PROCESS的作用就是在每次获取SharedPreferences实例的时候尝试从磁盘中加载修改过的数据,并且读取是在异步线程中,因此一个线程的修改最终会反映到另一个线程,但不能立即反映到另一个进程,所以通过SharedPreferences无法实现多进程同步。
    综合: 如果仅仅让多进程可访问同一个SharedPref文件,不需要设置MODE_MULTI_PROCESS, 如果需要实现多进程同步,必须设置这个参数,但也只能实现最终一致,无法即时同步。

    为什么不推荐使用MODE_MULTI_PROCESS?

    android文档已经Deprected了这个flag,并且说明不应该通过SharedPreference做进程间数据共享?这是为啥呢?从前面但分析可看到当设置这个flag后,每次获取(获取而不是初次创建)SharedPreferences实例的时候,会判断shared_pref文件是否修改过:

    private boolean hasFileChangedUnexpectedly() {
            synchronized (this) {
                if (mDiskWritesInFlight > 0) {
                    // If we know we caused it, it's not unexpected.
                    if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
                    return false;
                }
            }
    
            final StructStat stat;
            try {
                /*
                 * Metadata operations don't usually count as a block guard
                 * violation, but we explicitly want this one.
                 */
                BlockGuard.getThreadPolicy().onReadFromDisk();
                stat = Os.stat(mFile.getPath());
            } catch (ErrnoException e) {
                return true;
            }
    
            synchronized (this) {
                return mStatTimestamp != stat.st_mtime || mStatSize != stat.st_size;
            }
        }
    

    这里先判断mDiskWritesInFlight>0,如果成立,说明是当前进程修改了文件,不需要重新读取。然后通过文件最后修改时间,判断文件是否修改过。如果修改了,则重新读取:

    private void startLoadFromDisk() {
            synchronized (this) {
                mLoaded = false;
            }
            new Thread("SharedPreferencesImpl-load") {
                public void run() {
                    synchronized (SharedPreferencesImpl.this) {
                        loadFromDiskLocked();
                    }
                }
            }.start();
    }
    
    private void loadFromDiskLocked() {
            if (mLoaded) {
                return;
            }
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
            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);
                    } finally {
                        IoUtils.closeQuietly(str);
                    }
                }
            } catch (ErrnoException e) {
            }
            mLoaded = true;
            if (map != null) {
                mMap = map;
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<String, Object>();
            }
            notifyAll();
    }
    

    重点是这段:

    if (mBackupFile.exists()) {
          mFile.delete();
          mBackupFile.renameTo(mFile);
    }
    

    重新读取时,如果发现存在mBackupFile,则将原文件mFile删除,并将mBackupFile重命名为mFile。mBackupFile又是如何创建的呢?答案是在修改SharedPreferences时将内存中的数据写会磁盘时创建的:

    private void writeToFile(MemoryCommitResult mcr) {
            // Rename the current file so it may be used as a backup during the next read
            if (mFile.exists()) {
                if (!mBackupFile.exists()) {
                    if (!mFile.renameTo(mBackupFile)) {
                        mcr.setDiskWriteResult(false);
                        return;
                    }
                } else {
                    mFile.delete();
                }
            }
            FileOutputStream str = createFileOutputStream(mFile);
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
            FileUtils.sync(str);
            str.close();
            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
            final StructStat stat = Os.stat(mFile.getPath());
            synchronized (this) {
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
            }
            // Writing was successful, delete the backup file if there is one.
            mBackupFile.delete();
            mcr.setDiskWriteResult(true);
            return;
        }
    

    这段代码只保留了核心流程,忽略了错误处理流程。可以看到,写文件的步骤大致是:

    1. 将原文件重命名为mBackupFile
    2. 重新创建原文件mFile, 并将内容写入其中
    3. 删除mBackupFile
      所以,只有当一个进程正处于写文件的过程中的时候,如果另一个进程读文件,才会看到mBackupFile, 这时候读进程会将mBackupFile重命名为mFile, 这样读结果是,读进程只能都到修改前的文件,同时,由于mBackupFile重命名为了mFile, 所以写进程写那个文件就没有文件名引用了,因此其写入的内容无法再被任何进程访问到。所以其内容丢失了,可认为写入失败了,而SharedPreferences对这种失败情况没有任何重试机制,所以就可能出现数据丢失的情况。
      回到这段的重点:为什么不推荐用MODE_MULTI_PROCESS?从前面分析可知,这种模式下,每次获取SharedPreferences都会检测文件是否改变,只要读的时候另一进程在写,就会导致写丢失。这样失败概率就会大幅度提高。反之,若不设置这个模式,则只在第一次创建SharedPreferences的时候读取,导致写失败的概率就会大幅度降低,当然,仍然存在失败的可能。

    为什么不做写失败重试?

    为毛android不做写失败重试呢?原因是写进程并不能发现写失败的情况。难道写的过程中,目标文件被删不会抛异常吗?答案是不会。删除文件只是从文件系统中删除了一个节点信息而已,重命名也是新建了一个具有相同名称的节点信息,并把文件地址指向另一个磁盘地址而已,原来,之前的写过程仍然会成功写到原来的磁盘地址。所以目前的实现方案并不能检测到失败。

    有没有办法解决写失败呢?

    个人觉得是可以做到的,读里面读那段关键操作:

    if (mBackupFile.exists()) {
          mFile.delete();
          mBackupFile.renameTo(mFile);
    }
    

    mBackupFile存在,意味着当前正处于写读过程中,这时候是不是可以考虑直接读mBackupFile文件,而不删除mFile呢?这样读话,读取效果一样,都是读的mBackupFile,同时写进程写的mFile也不会被mBacupFile覆盖,写也就能成功了。即使通过这段代码重命名,写进程写完后发现mBackupFile不存在了,其实也能认为发生了读重命名,大可以重试一次。

    读文件过程中,文件被删除会导致读失败吗?

    不会!与重命名一样,文件被删除只是删掉节点信息,磁盘上的文件仍然存在,知道所有打开文件的fd都释放,文件才会真正被删除。

    相关文章

      网友评论

          本文标题:SharedPreferences 多进程问题

          本文链接:https://www.haomeiwen.com/subject/cvlntxtx.html