// base/core/java/android/app/ContextImpl.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"; } }
@Override public SharedPreferences getSharedPreferences(File file, int mode){ SharedPreferencesImpl sp; synchronized (ContextImpl.class) { final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); 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())) { thrownew IllegalStateException("SharedPreferences in credential encrypted " + "storage are not available until after user is unlocked"); } } 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; }
/** * Map from package name, to preference name, to cached preferences. */ @GuardedBy("ContextImpl.class") privatestatic ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
@GuardedBy("ContextImpl.class") 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); }
// 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(); } } }
@GuardedBy("mLock") privatevoidawaitLoadedLocked(){ 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(); } catch (InterruptedException unused) { } } if (mThrowable != null) { thrownew IllegalStateException(mThrowable); } }
@Override public Editor edit(){ // TODO: remove the need to call awaitLoadedLocked() when // requesting an editor. will require some work on the // Editor, but then we should be able to do: // // context.getSharedPreferences(..).edit().putString(..).apply() // // ... all without blocking. synchronized (mLock) { awaitLoadedLocked(); }
returnnew EditorImpl(); }
publicfinalclassEditorImplimplementsEditor{ privatefinal Object mEditorLock = new Object();
@GuardedBy("mEditorLock") privatefinal Map<String, Object> mModified = new HashMap<>();
// 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(){ long memoryStateGeneration; List<String> keysModified = null; Set<OnSharedPreferenceChangeListener> listeners = null; Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) { // 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); } mapToWriteToDisk = mMap; mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0; if (hasListeners) { keysModified = new ArrayList<String>(); listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); }
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(); // "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 (!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++; }
// Return value from EditorImpl#commitToMemory() privatestaticclassMemoryCommitResult{ finallong memoryStateGeneration; @Nullablefinal List<String> keysModified; @Nullablefinal Set<OnSharedPreferenceChangeListener> listeners; final Map<String, Object> mapToWriteToDisk; final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
// 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); }
/** * Enqueue an already-committed-to-memory result to be written * to disk. * * They will be written to disk one-at-a-time in the order * that they're enqueued. * * @param postWriteRunnable if non-null, we're being called * from apply() and this is the runnable to run after * the write proceeds. if null (from a regular commit()), * then we're allowed to do this disk write on the main * thread (which in addition to reducing allocations and * creating a background thread, this has the advantage that * we catch them in userdebug StrictMode reports to convert * them where possible to apply() ...) */ privatevoidenqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable){ finalboolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() { @Override publicvoidrun(){ synchronized (mWritingToDiskLock) { writeToFile(mcr, isFromSyncCommit); } synchronized (mLock) { mDiskWritesInFlight--; } if (postWriteRunnable != null) { postWriteRunnable.run(); } } };
// Typical #commit() path with fewer allocations, doing a write on // the current thread. if (isFromSyncCommit) { boolean wasEmpty = false; synchronized (mLock) { wasEmpty = mDiskWritesInFlight == 1; } if (wasEmpty) { writeToDiskRunnable.run(); return; } }
publicclassQueuedWork{ /** * Queue a work-runnable for processing asynchronously. * * @param work The new runnable to process * @param shouldDelay If the message should be delayed */ publicstaticvoidqueue(Runnable work, boolean shouldDelay){ Handler handler = getHandler();
@GuardedBy("mWritingToDiskLock") privatevoidwriteToFile(MemoryCommitResult mcr, boolean isFromSyncCommit){ long startTime = 0; long existsTime = 0; long backupExistsTime = 0; long outputStreamCreateTime = 0; long writeTime = 0; long fsyncTime = 0; long setPermTime = 0; long fstatTime = 0; long deleteTime = 0;
if (DEBUG) { startTime = System.currentTimeMillis(); }
boolean fileExists = mFile.exists();
// Rename the current file so it may be used as a backup during the next read if (fileExists) { boolean needsWrite = false;
// Only need to write if the disk state is older than this commit if (mDiskStateGeneration < mcr.memoryStateGeneration) { if (isFromSyncCommit) { needsWrite = true; } else { synchronized (mLock) { // No need to persist intermediate states. Just wait for the latest state to // be persisted. if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) { needsWrite = true; } } } }
if (!needsWrite) { mcr.setDiskWriteResult(false, true); return; }
boolean backupFileExists = mBackupFile.exists();
if (DEBUG) { backupExistsTime = System.currentTimeMillis(); }
// Attempt to write the file, delete the backup and return true as atomically as // possible. If any exception occurs, delete the new file; next time we will restore // from the backup. try { FileOutputStream str = createFileOutputStream(mFile);
if (DEBUG) { outputStreamCreateTime = System.currentTimeMillis(); }
// Clean up an unsuccessfully written file if (mFile.exists()) { if (!mFile.delete()) { Log.e(TAG, "Couldn't clean up partially-written file " + mFile); } } mcr.setDiskWriteResult(false, false); }