美文网首页
SharedPreferences源码分析

SharedPreferences源码分析

作者: kksoCoud | 来源:发表于2019-04-25 15:59 被阅读0次

先给出结论,如果不想跟随源码分析的,可以根据结论,对SharedPreferences有个大概的了解。

结论:

1.SharedPreferences 线程是安全的,内部由大量synchronized代码块实现同步
2.SharedPreferences 进程是不安全的(虽然官方提供了MODE_MULTI_PROCESS加载模式,该加载模式在API级别11中添加,在API级别23中被废弃。该SharedPreference loading标志:设置后,即使已在此过程中加载了共享首选项实例,也会检查磁盘上的文件是否已修改。在应用程序具有多个进程的情况下,有时需要此行为,所有进程都写入相同的SharedPreferences文件。但是,通常在进程之间存在更好的通信形式。 ),但官方文档也给出了如下说明:

此常量在API级别23中已弃用
.MODE_MULTI_PROCESS在某些Android版本中无法可靠地工作,并且不提供任何协调跨进程的并发修改的机制。应用程序不应尝试使用它。相反,他们应该使用明确的跨流程数据管理方法,例如ContentProvider

3.首次getSharedPreferences会把文件从磁盘加载到内存(用Map进行保存),之后调用getSharedPreferences获取数据,即从内存中直接获取,也就不会由于大量的synchronized而影响性能
4.apply:同步修改内存中的数据,然后异步回写到磁盘且是将其放入单线程任务队列中执行
commit:同步修改内存中的数据,且同步回写到磁盘,因此会阻塞当前线程

建议:

1.不要使用多进程操作SharedPreferences ,小概率会出现数据丢失(文件被删除)
2.不要存储大量的数据,从磁盘加载SharedPreferences 文件是进行I/O操作的,并且由于文件结构是xml格式的,加载解析都比较耗时,虽然在此过程中是异步的,但如果数据过大,首次调用getSharedPreferences后,然后马上执行getXX()或者putXX()由于需要等待加载文件结束,可能会阻塞线程(如果在UI线程可能会引发ANR)。并且SharedPreferences 中的数据会一次性从磁盘加载到内存,.applycommit都是会将数据写到磁盘的,SharedPreferences 文件不应过大会影响性能
3.尽量使用apply更新数据,因为它是异步写入磁盘的,不会阻塞线程

将您的首选项更改从此编辑器提交回它正在编辑的SharedPreferences对象。这将自动执行请求的修改,替换SharedPreferences中当前的内容。

注意,当两个编辑器同时修改首选项时,最后一个调用apply的编辑器将获胜。

与同步地将其首选项写到持久存储的commit()不同,apply()将其更改立即提交到内存中的SharedPreferences,但是启动到磁盘的异步提交,并且不会通知您任何失败。如果这个SharedPreferences上的另一个编辑器在apply()仍然未完成时执行常规提交(),那么commit()将阻塞,直到所有异步提交和提交本身都完成为止。

由于SharedPreferences实例是流程中的单例实例,如果已经忽略了返回值,那么可以用apply()替换commit()的任何实例。

您不需要担心Android组件的生命周期以及它们与向磁盘写入apply()的交互。该框架确保在切换状态之前完成来自apply()的在途磁盘写操作。

源码分析:

我们通过调用getSharedPreferences获取SharedPreferences对象时,最终会调用ContextImpl.getSharedPreferences(File file, int mode)方法:

    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            //获取缓存中所有SharedPreferences对象集合
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            //缓存中是否已存在我们需要的SharedPreferences
            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);
                //将SharedPreferences对象存入缓存集合中,以便下次直接从内存中获取
                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;
    }

先从缓存中获取,如果未命中则创建SharedPreferencesImpl对象,并保存到缓存中,多次调用getSharedPreferences并不会多次创建对象,由于整个SharedPreferences获取过程中都放在synchronized代码块中,因此在多线程下创建对象是安全的
SharedPreferencesImpl创建过程:

SharedPreferencesImpl(File file, int mode) {
        //需要加载的SharedPreferences文件对象
        mFile = file;
        //备份文件对象,写入失败时会通过其进行数据恢复
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        //数据缓存集合,用于存储SharedPreferences文件中的数据
        mMap = null;
        mThrowable = null;
        //从磁盘加载数据
        startLoadFromDisk();
    }

下面我们看看从磁盘加载数据过程:

 private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                //从磁盘加载数据
                loadFromDisk();
            }
        }.start();
    }

另起一个线程加载SharedPreferences文件

 private void loadFromDisk() {
        synchronized (mLock) {
            if (mLoaded) {
                return;
            }
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }
        ···省略代码···
        str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
        map = (Map<String, Object>) XmlUtils.readMapXml(str);
        ···省略代码···
        synchronized (mLock) {
            mLoaded = true;
            mThrowable = thrown;
        ···省略代码···
            if (map != null) {
                mMap = map;
                mStatTimestamp = stat.st_mtim;
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<>();
            }
           ...     
            mLock.notifyAll();
    
        }
    }

从上面方法可知从磁盘加载SharedPreferences文件分别做了一下几步处理:
1.如果由备份文件,把原文件删除,然后修改备份文件名称,使其成为原文件,达到文件恢复效果(备份文件名以.bak结尾)
2.加载文件并解析成Map
3.标记加载完成
4.将Map赋值给mMap(即数据缓存集合,用于存储SharedPreferences文件中的数据)
5.通过notifyAll唤起所有等待加载的线程
现在SharedPreferences对象已经初始化成功了,下面我们看一下对其数据操作的流程:

getXX()
@Override
@Nullable 
public String getString(String key, @Nullable String defValue) {
      synchronized (mLock) {
          //等待SharedPreferences加载完成
          awaitLoadedLocked();
          String v = (String)mMap.get(key);
          return v != null ? v : defValue;
      } 
}

通过synchronized代码块,可见获取数据是线程安全的,仅仅是从缓存中获取数据,同步不会影响性能。而awaitLoadedLocked()即是等待SharedPreferences文件从磁盘加载到内存完毕后再执行下面的获取数据的方法,因此如果在首次调用getSharedPreferences时,马上调用getXX(),会阻塞线程
接着看看awaitLoadedLocked()里面做了如何处理:

private void awaitLoadedLocked() {
        if (!mLoaded) {
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }

可见,除了首次加载文件时,mLoaded = false会卡在此处等待,当文件加载完毕后会重置加载状态即mLoaded = true,则不会等待直接获取数据

putXX()

修改数据时首先得获取Editor对象(SharedPreferencesImpl.java)

@Override
public Editor edit() {
      synchronized (mLock) {
          awaitLoadedLocked();
      }
      return new EditorImpl();
}
public final class EditorImpl implements Editor {
        private final Object mEditorLock = new Object();

        @GuardedBy("mEditorLock")
        private final Map<String, Object> mModified = new HashMap<>();

        @GuardedBy("mEditorLock")
        private boolean mClear = false;

        @Override
        public Editor putString(String key, @Nullable String value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this;
            }
        }
        ···代码省略···
}

可见我们调用putXX()仅仅是将数据存到mModified中,直到我们调用applycommit时才真正将数据同步到缓存并写入磁盘

apply()
      @Override
      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) {
                        }
                    }
                };
            
            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);
        }

下面看看同步缓存数据流程:

private MemoryCommitResult commitToMemory() {
    ···代码省略···
    synchronized (SharedPreferencesImpl.this.mLock) {
         ···代码省略···
         //缓存数据集合
        mapToWriteToDisk = mMap;
        ···代码省略···
        synchronized (mEditorLock) {
             boolean changesMade = false;
             if (mClear) {
                     if (!mapToWriteToDisk.isEmpty()) {
                            changesMade = true;
                            mapToWriteToDisk.clear();
                      }
                      mClear = false;
             }
            //同步缓存数据
             for (Map.Entry<String, Object> e : mModified.entrySet()) {
                     String k = e.getKey();
                     Object v = e.getValue();
                     if (v == this || v == null) {
                          if (!mapToWriteToDisk.containsKey(k)) {
                              continue;
                          }
                          mapToWriteToDisk.remove(k);
                      } else {
                          if (mapToWriteToDisk.containsKey(k)) {
                               Object existingValue = mapToWriteToDisk.get(k);
                               if (existingValue != null && existingValue.equals(v)) {
                                     continue;
                               }
                           }
                           mapToWriteToDisk.put(k, v);
                      }

                      changesMade = true;
                      if (hasListeners) {
                           keysModified.add(k);
                      }
            }

            mModified.clear();

            if (changesMade) {
                  mCurrentMemoryStateGeneration++;
            }

            memoryStateGeneration = mCurrentMemoryStateGeneration;
       }
     }
     return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,mapToWriteToDisk);
}

然后我们瞅瞅写入磁盘SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
    //如果是commit则postWriteRunnable == null即isFromSyncCommit = true
    //如果是apply则isFromSyncCommit = false
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    final Runnable writeToDiskRunnable = new Runnable() {
        @Override
        public void run() {
            synchronized (mWritingToDiskLock) {
                //写入磁盘
                writeToFile(mcr, isFromSyncCommit);
            }
            synchronized (mLock) {
                mDiskWritesInFlight--;
            }
            if (postWriteRunnable != null) {
                postWriteRunnable.run();
            }
        }
    };

    // commit直接在当前线程直接同步写入磁盘
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
    //apply加入任务队列执行写入磁盘runnable
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();

        synchronized (sLock) {
            sWork.add(work);
            //sCanDelay用于纪录当前线程是否处于写入磁盘任务状态
            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }
private static Handler getHandler() {
        synchronized (sLock) {
            if (sHandler == null) {
                //首次创建一个线程
                HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                        Process.THREAD_PRIORITY_FOREGROUND);
                handlerThread.start();
                //创建一个handler用于接收queue(Runnable work, boolean shouldDelay)发送的消息,然后执行写入磁盘runnable
                sHandler = new QueuedWorkHandler(handlerThread.getLooper());
            }
            return sHandler;
        }
    }
commit()
@Override
public boolean commit() {
    long startTime = 0;
    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;
}

由此可见,不论commit/apply都回调用SharedPreferencesImpl.this.enqueueDiskWrite然后同步/异步执行写入磁盘操作

相关文章

网友评论

      本文标题:SharedPreferences源码分析

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