SharedPreferences源码解读

1,278 阅读9分钟

前言

android中我们可以通过SharedPreferences来对简单的key-value(键-值)对数据进行存取,主要用于轻量级的数据存储。SP采用xml文件格式来保存数据,该文件所在目录位于data/data/packageInfo/shared_prefs

知识点

  1. context.getSharedPreferences()方法的调用源码分析
  2. getXxx调用的源码分析
  3. putXxx的源码分析
  4. apply的源码分析

SharedPreferences对象获取的源码分析

获取SharedPreferences对象的方法,一般有两种方式:

  1. PreferenceManager.getDefaultSharedPreferences()
  2. context.getSharedPreferences()

这里我们以第二种方式来看源码分析,这里context的最终实现类是在ContextImpl中,所以我们在ContextImpl类中查看getSharedPreferences(name, Context.MODE_PRIVATE)的源码:

public SharedPreferences getSharedPreferences(String name, int mode) {
    ....省略无关代码....
    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);
}

public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}

我们看一下getSharedPreferencesPath(name),这里根据传入的name在指定的目录下面创建$name.xml文件,getPreferencesDir()最终获取的是data/data/packageInfo/shared_prefs目录路径(有则获取,没则创建)。然后将其放入mSharedPrefsPaths缓存中。这里的所有操作都是在主线程。
接下来看一下getSharedPreferences(file, mode)的源码。

public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        //这里每次都会先从缓存中获取SharedPreferencesImpl对象,如果存在的话则不用重新创建
        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");
                }
            }
            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;
}

这个方法返回的是SharedPreferencesImpl对象,所以说我们通过getSharedPreferences()方法最终返回的是SharedPreferencesImpl对象,而且第一次调用的时候会将创建的SharedPreferencesImpl对象存在cache缓存中,后面每次调用时都是直接从缓存中获取,并且在获取的时候添加了synchronized关键字,保证了线程的安全。(关于线程安全的内容,我们会从源码中看到,对于读写操作都添加了synchronized关键字以保证线程安全。)

理论上来说到这里我们对getSharedPreferences()方法的源码分析已经结束了,最终返回的是SharedPreferencesImpl对象。但是这里我们还需要分析一下SharedPreferencesImpl的构造方法。

SharedPreferencesImpl的构造函数

我们看一下ShreadPreferencesImpl的构造函数

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

这里我们看一下参数:

  1. mFile代表我们创建的SharedPreferences文件
  2. mBackupFile是一个备份文件,用户写入失败时会进行恢复。

这里我们先看一下makeBackupFile(file)的源码:

static File makeBackupFile(File prefsFile) {
    return new File(prefsFile.getPath() + ".bak");
}

就一行代码,就是创建一个相同文件名的.bak文件(data/data/$packageInfo/shared_prefs/$name.xml.bak)。这个文件会在最终写入成功的时候删除。

然后我们主要看一下startLoadFromDisk()方法,

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

这里是开启了一个线程调用loadFromDisk()方法,

private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        //当备份文件存在时,我们会删除原文件,并把备份文件移到原文件下面  
        //这里的目的是为了后面原文件写入本地时发生错误,下次获取时从备份文件获取
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    Map<String, Object> map = null;
    try {
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                //这里就是将SharedPreferences保存的xml文件写入内存中,这个操作在子线程
                str = new BufferedInputStream(
                    new FileInputStream(mFile), 16 * 1024);
                map = (Map<String, Object>) 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;
    }
    synchronized (mLock) {
        //这个标签会在后面awaitLoadedLocked用到,表示在xml从本地加载到内存完成成功之后置为true
        mLoaded = true;
        try {
            if (thrown == null) {
                if (map != null) {
                    //这里的mMap就是在构造函数中的mMap,用来在内存中缓存我们的配置文件
                    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();
        }
    }
}

SharedPreferences对象获取的小结

这里首先是在data/data/packageInfo/shared_prefs/目录下面创建一个xml文件。而且最终返回的是SharedPreferencesImpl对象。然后在SharedPreferencesImpl对象的构造方法中创建备份文件,并在子线程中将本地创建的xml文件写入到内存中。写入成功后通知其它等待线程。

getXxx调用源码分析

这里我们以getString的源码来进行分析

public String getString(String key, @Nullable String defValue) {
    //对于读写操作要加同步代码块,保证线程安全
    synchronized (mLock) {
        //等待上面的将本地xml读入内存中成功的通知
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

getString()方法的源码很简单,一共就三行代码,后面两行是直接从内存中(mMap)中获取数据,基本没有性能方面的问题。我们主要看一下awaitLoadedLocked()方法。

private void awaitLoadedLocked() {
    //如果没有读取完成,则线程会处于等待状态
    while (!mLoaded) {
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
}

这里要说一下,当我们执行getString()方法时,如果xml文件还没有加载到内存中,那么所有的操作都处于等待状态,由于我们的getString()方法一般是在主线程调用,所以这里可能会造成主线程的卡顿问题。

putXxx的源码流程

相比于getXxx()方法,putXxx()方法比较麻烦一点。这里我们仍然以putString()方法来分析。一般来说putString()方法的格式如下

getSharedPreferences().edit().putString(key, value).apply();或者commit()提交

我们先看一下edit()方法

public Editor edit() {
    synchronized (mLock) {
        awaitLoadedLocked();
    }
    return new EditorImpl();
}

这里源码也很简单,首先是等待xml文件从本地读写到内存中。成功之后会创建一个EditorImpl对象,每次调用edit()方法都会创建新的EditorImpl对象。

接下来我们看一下EditorImpl类中的putString()方法。

public Editor putString(String key, @Nullable String value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}

这里的源码也比较简单,就是将我们传入进来的key-value值存入mModified中。mModified会在后面的apply方法中被遍历。

最后我们看一下apply()方法的源码分析

apply方法的源码分析

apply方法是各种“最佳实践”都推荐的方式,我们就来分析一下apply的源码

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) {
                }
            }
        };
        
    //将awiatCommit添加到LinkedList集合中
    QueuedWork.addFinisher(awaitCommit);
    
    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    
    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) {
        if (mDiskWritesInFlight > 0) {
            mMap = new HashMap<String, Object>(mMap);
        }
        //这是将mMap集合的引用赋值给mapToWriteToDisk,两者指向的是同一块地址
        //所以当后面修改mapToWriteToDisk的值时,mMap对应的值也修改了
        mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;
        boolean hasListeners = mListeners.size() > 0;
        if (hasListeners) {
            keysModified = new ArrayList<String>();
            listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
        }
        synchronized (mEditorLock) {
            boolean changesMade = false;
            //这个表示当调用clear方法时,会清空mapToWriteDisk集合
            if (mClear) {
                if (!mapToWriteToDisk.isEmpty()) {
                    changesMade = true;
                    mapToWriteToDisk.clear();
                }
                mClear = false;
            }
            //这个方法时重点,遍历通过putString添加到mModified集合中的`key-value`
            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;
                        }
                    }
                    //将mModified集合中的 'key-value'存入集合中
                    mapToWriteToDisk.put(k, v);
                }
                changesMade = true;
                if (hasListeners) {
                    keysModified.add(k);
                }
            }
            //清空mModified集合
            mModified.clear();
            if (changesMade) {
                mCurrentMemoryStateGeneration++;
            }
            memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
    }
    return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
            mapToWriteToDisk);
}

这里分析主要过程:

  1. mapToWriteToDisk = mMap这里mMap前面分析过,是将本地xml文件解析到内存的mMap集合中,然后赋值给mapToWriteToDisk集合。
  2. mapToWriteToDisk.put(k, v)表示将mModified集合中的元素存入mapToWriteDisk集合中
  3. mModified.clear()清空mModified中的集合
  4. return new MemoryCommitResult返回MemeoryCommitResult对象

commitToMemory方法的主要过程就是将读取到mMap集合中的元素赋值给mapToWriteToDisk集合中,并将mModified集合中的元素添加到mapToWriteToDisk集合中。并最终返回MemoryCommitResult对象。

这里我们回到apply方法,看一下apply方法的源码,在源码中创建了两个Runnable的实现类:awaitCommitpostWriteRunnalbe。这里最最重要的方法是SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable),我们看一下enqueueDiskWrite()方法的源码:

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                            final Runnable postWriteRunnable) {
    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();
                }
            }
        };
    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    //这里表示当通过commit()方法提交时,会直接在主线程运行run方法
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
    //这里调用apply方法提交时执行的方法,会将writeToDiskRunnable中的run方法最终放在子线程执行
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

这里我们简单看一下queue方法是如何将wrtieToDiskRunnable放入子线程执行的,这里以前的实现是通过线程池来放入子线程执行的,现在改成通过handlerThread来放入子线程执行run方法。

public static void queue(Runnable work, boolean shouldDelay) {
    Handler handler = getHandler();
    synchronized (sLock) {
        sWork.add(work);
        if (shouldDelay && sCanDelay) {
            handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
        } else {
            handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
        }
    }
}

//看一下getHandler()方法
private static Handler getHandler() {
    synchronized (sLock) {
        if (sHandler == null) {
            HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                    Process.THREAD_PRIORITY_FOREGROUND);
            handlerThread.start();
            sHandler = new QueuedWorkHandler(handlerThread.getLooper());
        }
        return sHandler;
    }
}

//这是QueuedWorkHandler类的handleMessage方法,这个方法在子线程中执行,所以processPendingWork()方法在子线程中执行
public void handleMessage(Message msg) {
    if (msg.what == MSG_RUN) {
        processPendingWork();
    }
}

private static void processPendingWork() {
    synchronized (sProcessingWork) {
        LinkedList<Runnable> work;
        synchronized (sLock) {
            work = (LinkedList<Runnable>) sWork.clone();
            sWork.clear();
            // Remove all msg-s as all work will be processed now
            getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
        }
        if (work.size() > 0) {
            for (Runnable w : work) {
                //这里调用改了传入进来的writeToDiskRunnable的run方法
                w.run();
            }
        }
    }
}

这里在getHandler方法中创建了HandlerThread对象,HandlerThread对象继承自Thread,具体HandlerThread类的用法这里不介绍了,HandlerThread类的源码很简单,主要是在子线程中(run方法中)创建了Looper,所以我们通过sHandler发送的消息最终都是在子线程中进行处理的。然后我们进一步分析可以看到最终是在processPendingWork()方法中执行了传入进来的writeToDiskRunnable的run方法。

这里我们在回到writeToDiskRunnable中的run方法,这里我们主要分析writeToFile()方法,

private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    //判断本地文件是否存在,这里为true
    boolean fileExists = mFile.exists();
    
    // Rename the current file so it may be used as a backup during the next read
    if (fileExists) {
        //判断备份文件是否存在(在前面调用renameTo方法时,mBackupFile文件会被删除),所以这里为false
        boolean backupFileExists = mBackupFile.exists();
        if (!backupFileExists) {
            //这里会将mFile文件修改为备份文件
            if (!mFile.renameTo(mBackupFile)) {
                return;
            }
        } else {
            mFile.delete();
        }
    }
    
    try {
        FileOutputStream str = createFileOutputStream(mFile);
        //通过XmlUtils工具类将mapToWriteToDisk集合中的 'key-value'保存到本地创建的`xml`文件里面。
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        
        FileUtils.sync(str);
        
        // Writing was successful, delete the backup file if there is one.
        mBackupFile.delete();
        
        return;
    }cache(...){
        ...
    }
    
    // Clean up an unsuccessfully written file
    if (mFile.exists()) {
        if (!mFile.delete()) {
            Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
        }
    }
}

writeToFile代码分析:

  1. 先判断mFile(本地xml文件)文件是否存在,存在的话判断mBackupFile(备份文件)是否存在,我们这里是mFile文件存在,mBackupFile文件不存在,所以这里的操作是将mFile文件重命名为备份文件(即添加.bak的后缀),删除mFile文件。
  2. 接着根据mFile创建FileOutputStream,并通过XmlUtils工具将mapToWriteToDisk中的key-value键值对写入到本地xml文件中。
  3. 操作成功的话则删除mBackupFile(备份文件),如果不存在的话则删除mFile

对于writeToFile方法来说,apply方法是在子线程中执行的,而commit方法是在主线程中执行的(具体commit方法比较简单,这里不贴出源码了)。

这里总结一下apply方法:

  1. commitToMemory方法中我们将mMap引用赋值给了mapToWriteToDisk的引用,所以当我们修改mapToWriteToDisk中的值时,mMap中的值也跟着修改了。
  2. 当我们调用getXxx方法来获取值时,直接通过mMap来获取最新的数据。
  3. 我们通过writeToFilemapToWriteToDisk中的配置项异步写入到磁盘。

参考文献