SharedPreferences原理浅析

SharedPreferences原理浅析

1.概述

SharedPreferences是用来访问和修改偏好preference数据的接口,可以通过Context.getSharedPreferences()方法返回SharedPreferences。

对于任意一组偏好设置数据,只有一个共享的SharedPreferences实例。

修改preferences必须通过一个Editor对象来确保存储数据的一致性以及控制数据何时存储。

SharedPreferences是一个接口,里面定义了很多数据存储与获取的接口。

public interface SharedPreferences {
    /*
    * 定义了一个回调接口,当SharedPreference被修改时,触发该方法
    */
    public interface OnSharedPreferenceChangeListener {
         void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
    }

    /*
    * 用来修改SharedPreference里面values的接口
    * 所有在editor中的修改都是批量修改,只有调用了commit()或者apply()方法之后,修改才生效
    */
    public interface Editor {
        Editor putString(String key, @Nullable String value);
        Editor putInt(String key, int value);
        Editor remove(String key);
        Editor clear();
        boolean commit();
        void apply();
    }

    String getString(String key, @Nullable String defValue);
    int getInt(String key, int defValue);
    Editor edit();
    void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
    void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
}

我们可以通过Context.getSharedPreferences()方法返回一个SharedPreferences实现,该实现是SharedPreferencesImpl类。接下来将来分析SharedPreferencesImpl实现类。

2.源码分析

2.1 ContextImpl.getSharedPreferences()

public SharedPreferences getSharedPreferences(String name, int mode) {
    // 在Android 4.4以前,如果name为null的话,则会把它当成null.xml来处理
    if (mPackageInfo.getApplicationInfo().targetSdkVersion <
            Build.VERSION_CODES.KITKAT) {
        if (name == null) {
            name = "null";
        }
    }

    File file;
    synchronized (ContextImpl.class) {
        if (mSharedPrefsPaths == null) {
            mSharedPrefsPaths = new ArrayMap<>();
        }
        // 根据name查找对应的File文件是否存在,不存在,则根据name创建一个File文件,并把该File文件保存到一个Map集合中,以备后续使用
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            file = getSharedPreferencesPath(name);
            mSharedPrefsPaths.put(name, file);
        }
    }
    return getSharedPreferences(file, mode);//见2.2
}

在ContextImpl中定义了两个与SharedPreference相关的ArrayMap,它们分别缓存<preference name,File>和<package name,<File,SharedPreferencesImpl>>。它们的定义如下:

 /**
 * Map from package name, to preference name, to cached preferences.
 */
@GuardedBy("ContextImpl.class")
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

/**
 * Map from preference name to generated path.
 */
@GuardedBy("ContextImpl.class")
private ArrayMap<String, File> mSharedPrefsPaths;

我们在查找一个SharedPreferences时,首先需要根据SharedPreferences的name在mSharedPrefsPaths中查找到对应的File文件,然后根据当前应用包名package name,在sSharedPrefsCache中查找当前应用包名对应的ArrayMap<File, SharedPreferencesImpl>,最后根据File文件,查找对应的SharedPreferencesImpl类。查找过程大致如下:

preference name ——> File

package name ——> ArrayMap<File,SharedPreferencesImpl>———> SharedPreferencesImpl

2.2 ContextImpl.getSharedPreferences()

public SharedPreferences getSharedPreferences(File file, int mode) {
    checkMode(mode);// 检查文件模式
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();//根据当前应用包名,获取对应的ArrayMap<File, SharedPreferencesImpl>
        sp = cache.get(file);//根据文件,获取对应的SharedPreferencesImpl
        if (sp == null) {
            sp = new SharedPreferencesImpl(file, mode);//创建一个新的ShardPreferenceImpl对象,见2.3
            cache.put(file, sp);//存入缓存中
            return sp;
        }
    }
    // 如果是多进程模式,或者是Android 3.0以前,则检测是否有其他进程在后台修改SharedPreferences
    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;
}

SharedPreference的创建模式mode有以下几种:

  • MODE_PRIVATE:默认模式,该模式下创建的文件只能被当前应用或者与该应用具有相同SharedUserID的应用访问。
  • MODE_WORLD_READABLE:允许其他应用读取这个模式创建的文件。在Android N上使用该模式将抛出SecurityException异常。
  • MODE_WORLD_WRITEABLE:允许其他应用写入这个模式创建的文件。在Android N上使用该模式将抛出SecurityException异常。
  • MODE_APPEND:如果文件已经存在了,则在文件的尾部添加数据。
  • MODE_MULTI_PROCESS:SharedPreference加载标志,当设置了该标志,则在磁盘上的文件将会被检查是否修改了,尽管SharedPreference实例已经在该进程中被加载了。

在checkMode()方法中,主要是检查是否在Android N上使用了MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE模式,如果使用了,则抛出异常。

在获取SharedPreference时,不是每次都会重新创建一个新的SharedPreference实例,而是先从缓存中,查找是否存在对应的SharedPreference实例,如果有相应的实例,则直接返回。如果不存在,则创建一个新的SharedPreference实例,并把它保存在缓存中,以备下次使用。

private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }

    final String packageName = getPackageName();//获取当前应用的包名
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);//获取当前应用包名对应的ArrayMap<File, SharedPreferencesImpl>
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);//保存到缓存中
    }

    return packagePrefs;
}

由于ContextImpl是应用的执行环境,每一个应用里面可以包含有多个SharedPreference文件。因此,为了更好的定位SharedPreference文件,首先根据应用包名进行筛选,得到ArrayMap<File, SharedPreferencesImpl>,然后再通过SharedPreference文件名进行筛选,得到SharedPreferencesImpl。

可以看到,SharedPreferencesImpl只会被创建一次,之后会被保存在缓存中,后续的获取操作都是从缓存中获取SharedPreferencesImpl实例对象。

2.3 SharedPreferencesImpl()

SharedPreferencesImpl(File file, int mode) {
    mFile = file;//保存SharedPreference对应的xml文件
    mBackupFile = makeBackupFile(file);//构建备份文件,以.bak后缀结尾
    mMode = mode;//保存创建模式
    mLoaded = false;//初始化mLoaded为false
    mMap = null;//初始化Map<String, Object>为空
    startLoadFromDisk();//开始从磁盘中加载数据,见2.4
}

在SharedPreferencesImpl的构造函数中,主要是初始化了一些重要成员变量,并开始从磁盘中加载数据到内存中。

2.4 SharedPreferencesImpl.startLoadFromDisk()

private void startLoadFromDisk() {
    synchronized (this) {
        mLoaded = false;
    }
    //创建一个线程来执行从磁盘中加载数据
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();// 见2.5
        }
    }.start();
}

该方法主要是创建一个子线程,然后在子线程中执行具体的加载操作。

2.5 SharedPreferencesImpl.loadFromDisk()

private void loadFromDisk() {
    synchronized (SharedPreferencesImpl.this) {
        // 如果已经被加载了,则直接返回
        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);//读取文件内容到Map集合中,Map集合是一个Map<String, Object>的集合
            } catch (XmlPullParserException | IOException e) {
                Log.w(TAG, "getSharedPreferences", e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        /* ignore */
    }

    synchronized (SharedPreferencesImpl.this) {
        mLoaded = true;//将mLoaded置为空,让等待加载完成的线程被唤醒
        if (map != null) {
            mMap = map;//保存加载内容到mMap中
            mStatTimestamp = stat.st_mtime;//记录时间戳
            mStatSize = stat.st_size;//记录文件大小
        } else {
            mMap = new HashMap<>();
        }
        notifyAll();//唤醒被阻塞的线程
    }
}

可以看到,在loadFromDisk()方法中,主要的工作是将磁盘中的文件内容读取到内存中,然后再唤醒阻塞等待的线程,告诉他们数据已经读取到内存中了。这里用到了典型的wait()/notifyAll()机制,来同步线程之间的交互。那在什么时候会调用wait()呢?在我们获取SharedPreferencesImpl来存储数据和获取数据时,都会调用到wait()。

SharedPreferences数据的存储需要借助Editor来实现,通过Editor操作后,再通过commit()或apply()方法将修改同步到磁盘中。commit()方法是有返回结果的,来表示修改是否成功了。而apply()方法是没有返回结果,它只是提交一个写入磁盘的请求,然后由子线程去执行。

Editor是通过SharedPreferencesImpl的edit()方法来创建的。

2.6 SharedPreferencesImpl.edit()

public Editor edit() {
    synchronized (this) {//同步方法,保证每次只有一个线程执行加载操作
        awaitLoadedLocked();//等待SharedPreferencesImpl加载到内存中,见2.7
    }

    return new EditorImpl();//创建一个新的Editor实现。
}

在创建Editor之前,需要等待SharedPreferencesImpl加载到内存中,然后才会创建一个Editor实现类EditorImpl。

2.7 SharedPreferencesImpl.awaitLoadedLocked()

private void awaitLoadedLocked() {
    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();
    }
    // 如果mLoad为false,则一直循环等待下去
    while (!mLoaded) {
        try {
            wait();//阻塞等待
        } catch (InterruptedException unused) {
        }
    }
}

在awaitLoadedLocked()方法中,主要是通过mLoaded变量来控制循环阻塞等待,该变量是在SharedPreferencesImpl的loadFromDisk()方法中,被置为true了,并通过notifyAll()方法唤醒等待的线程。

因为从磁盘中加载SharedPreference数据到内存中是一个耗时操作,因此需要在子线程中执行加载操作,当子线程加载完成后,需要给主线程发送一个通知,唤醒被阻塞等待的操作。

在开始创建SharedPreferencesImpl时,就会从磁盘中加载xml文件到内存中,加载完成后,将mLoaded置为true,并唤醒正在等待的线程。因为在通过Context.getSharedPreferences()获取到SharedPreferencesImpl时,此时有可能数据并未全部都从磁盘加载到内存中,因此需要在操作SharedPreferencesImpl之前,等待数据从磁盘加载到内存中。awaitLoadedLocked()操作就是用来完成等待数据从磁盘加载到内存中,该方法返回后,可以确保所有的数据都加载到内存中了,后续的所有操作都是针对内存中数据进行操作了。

EditorImpl实现了Editor接口,获取到EditorImpl之后,就可以通过Editor对SharedPreference中的数据进行修改了。Editor的putXXX方法对数据的修改都只是在内存中对数据进行修改,只有调用了commit()或apply()方法之后,才会真正同步修改到磁盘中。

2.8 EditorImpl.putString()

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

EditorImpl的putXXX方法,主要是将数据保存在一个Map中,这些数据是存储在内存中,只有调用了commit()或apply()方法之后,才会同步到磁盘中。

private final Map<String, Object> mModified = Maps.newHashMap();//暂时保存需要写入磁盘的数据

2.9 EditorImpl.commit()

public boolean commit() {
    MemoryCommitResult mcr = commitToMemory();//构建需要写入磁盘的数据,见2.10
    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay*/);//见2.11
    try {
        mcr.writtenToDiskLatch.await();//等待写入完成
    } catch (InterruptedException e) {
        return false;
    }
    notifyListeners(mcr);//通知等待者,写入已经完成了
    return mcr.writeToDiskResult;//返回写入是否成功
}

在将内存中保存的数据写入到磁盘时,需要借助MemoryCommitResult类,构建写入磁盘的数据。然后将这个写入操作由子线程来执行,并等待子线执行完成。当子线程写入完成后或者发生了异常,通知等待者写入完成了,并把写入结果返回给调用者。

2.10 EditorImpl.commitToMemory()

private MemoryCommitResult commitToMemory() {
    MemoryCommitResult mcr = new MemoryCommitResult();//创建MemoryCommitResult实例
    synchronized (SharedPreferencesImpl.this) {
        // 在准备将内存中的数据写入到磁盘时,如果已经正在执行写入操作,则先采用深拷贝,复制mMap中的数据
        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);//深拷贝mMap中的数据
        }
        mcr.mapToWriteToDisk = mMap;//将该Map赋值给需要写入磁盘的Map。
        mDiskWritesInFlight++;//更新正在执行写入磁盘的操作次数

        boolean hasListeners = mListeners.size() > 0;//返回已经注册的OnSharedPreferenceChangeListener数量
        if (hasListeners) {
            mcr.keysModified = new ArrayList<String>();
            mcr.listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
        }

        synchronized (this) {
            boolean changesMade = false;
            //是否需要清除内容
            if (mClear) {
                if (!mMap.isEmpty()) {
                    changesMade = true;
                    mMap.clear();
                }
                mClear = false;
            }

            // 循环将mModified集合中的数据拷贝到mMap中
            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                // 如果需要修改的value为null,则移除该key对应的entry
                if (v == this || v == null) {
                    if (!mMap.containsKey(k)) {
                        continue;
                    }
                    mMap.remove(k);
                } else {
                    // 如果已经存在相同的值,则直接返回,否则将该<k,v>添加到mMap集合中
                    if (mMap.containsKey(k)) {
                        Object existingValue = mMap.get(k);
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    mMap.put(k, v);//添加该<k,v>集合到Map中
                }

                changesMade = true;//发生了改变
                if (hasListeners) {
                    mcr.keysModified.add(k);//更新被修改的key集合
                }
            }

            mModified.clear();//清空mModified Map集合

            if (changesMade) {
                mCurrentMemoryStateGeneration++;
            }

            mcr.memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
    }
    return mcr;
}

可以看到commitToMemory()方法主要完成以下几件事情:

  1. 创建一个MemoryCommitResult对象,该对象封装了一些写入磁盘的状态;
  2. 对mMap集合做一个深拷贝,并把它保存在MemoryCommitResult的mapToWriteToDisk变量中;
  3. 如果注册了OnSharedPreferenceChangeListener监听者,则创建一个ArrayList列表,来保存被修改的key列表;
  4. 如果设置了清除标志位mClear,则先清空mMap集合;
  5. 将EditorImpl中mModified集合中的数据拷贝到mMap集合中,如果key对应value已经存在了,则跳过拷贝。如果key对应的value为null,则删除该key对应的Entry。
  6. 清空mModified集合,并返回创建的MemoryCommitResult对象。

将mModified集合中的数据拷贝到mMap集合中,具有缓存的作用,如果应用再次马上查询SharedPreference时,则可以先从mMap集合中直接返回结果,而不用从磁盘中读取。

2.11 EditorImpl.enqueueDiskWrite()

private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
    final boolean isFromSyncCommit = (postWriteRunnable == null);//是否同步提交

    final Runnable writeToDiskRunnable = new Runnable() {
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr, isFromSyncCommit);//写入到磁盘文件中,见2.12
                }
                synchronized (SharedPreferencesImpl.this) {
                    mDiskWritesInFlight--;//写入完成后,更新正在执行写入操作数
                }
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };

    if (isFromSyncCommit) {//如果是同步写入操作
        boolean wasEmpty = false;
        synchronized (SharedPreferencesImpl.this) {
            wasEmpty = mDiskWritesInFlight == 1;//在创建MemoryCommitResult时,如果之前的写入操作都完成了的话,则mDiskWritesInFlight的值为1
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();//执行run()方法,同步调用
            return;
        }
    }

    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);//如果之前还有写入操作,则将该写入操作放入工作队列中,等待执行。
}

在enqueueDiskWrite()方法中,首先根据参数postWriteRunnable是否为null来判断是否同步写入操作。接着创建一个写入磁盘的Runnable,在该Runnable中执行具体的写入磁盘文件的操作。如果是同步写入操作,并且当前没有写入操作,则直接调用writeToDiskRunnable的run()方法,在当前线程中执行磁盘写入操作。如果是同步写入操作,并且当前有正在执行的写入操作,则将该writeToDiskRunnable放入工作队列中,等待线程随后执行。

QueuedWork.singleThreadExecutor()方法返回的是一个单个线程的执行器Executor,里面有一个无界的队列来保存执行任务。这样的话,可以保证任务是顺序的执行,并且保证每次只有一个任务执行。

public static ExecutorService singleThreadExecutor() {
    synchronized (QueuedWork.class) {
        if (sSingleThreadExecutor == null) {
            // TODO: can we give this single thread a thread name?
            sSingleThreadExecutor = Executors.newSingleThreadExecutor();
        }
        return sSingleThreadExecutor;
    }
}

2.12 SharedPreferencesImpl.writeToFile()

 private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    
    if (mFile.exists()) {// 如果文件已经存在了
        boolean needsWrite = false;

        // 只有磁盘上文件状态比当前文件更旧时,才执行更新操作
        if (mDiskStateGeneration < mcr.memoryStateGeneration) {
            if (isFromSyncCommit) {//同步写入操作
                needsWrite = true;
            } else {
                synchronized (this) {
                    // No need to persist intermediate states. Just wait for the latest state to
                    // be persisted.
                    if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                        needsWrite = true;
                    }
                }
            }
        }
        
        // 不需要立即写入,则在MemoryCommitResult中记录该结果,然后直接返回
        if (!needsWrite) {
            mcr.setDiskWriteResult(true);//记录写入成功了,唤醒等待写入结果的线程
            return;
        }

        if (!mBackupFile.exists()) {//如果备份文件不存在
            if (!mFile.renameTo(mBackupFile)) {//将新文件重命名为备份文件
                mcr.setDiskWriteResult(false);
                return;
            }
        } else {
            mFile.delete();//如果备份文件已经存在了,则删除mFile文件
        }
    }

    // 当尝试写入文件时,删除备份文件,并返回true。如果在写入过程中发生了异常,则删除新的文件,下一次从备份文件中恢复。
    try {
        FileOutputStream str = createFileOutputStream(mFile);//从File中获取FileOutputStream
        if (str == null) {//获取失败,则取消写入操作
            mcr.setDiskWriteResult(false);
            return;
        }
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);//借助XmlUtils工具,将MemoryCommitResult中保存的Map数据,写入到FileOutputStream中。
        FileUtils.sync(str);//执行FileOutputStream的flush操作,同步到磁盘中

        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);//设置文件的访问模式
        try {
            final StructStat stat = Os.stat(mFile.getPath());
            synchronized (this) {
                mStatTimestamp = stat.st_mtime;//更新时间戳
                mStatSize = stat.st_size;//更新文件大小
            }
        } catch (ErrnoException e) {
            // Do nothing
        }
        //写入成功了,删除备份文件
        mBackupFile.delete();

        mDiskStateGeneration = mcr.memoryStateGeneration;//更新磁盘文件状态

        mcr.setDiskWriteResult(true);//记录写入成功了,唤醒等待写入结果的线程

        return;
    } catch (XmlPullParserException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    } catch (IOException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    }
    
    if (mFile.exists()) {
        if (!mFile.delete()) {
            Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
        }
    }
    mcr.setDiskWriteResult(false);
}

在writeToFile()方法中,首先将当前文件重命名为备份文件,然后从当前文件中获取文件输出流,并将MemoryCommitResult中保存的Map数据,写入到文件输出流中。如果写入成功,则删除备份文件,返回true。如果写入失败,则删除当前文件,下一次从备份文件中恢复过来。

通知调用者是否写入成功是通过setDiskWriteResult()方法来完成的,在该方法中,通过MemoryCommitResult的writeToDiskResult变量来保存写入结果,写入成功为true,写入失败为false。不管写入成功还是失败,都会让writtenToDiskLatch闭锁计数减1,唤醒在闭锁上等待的线程。

public void setDiskWriteResult(boolean result) {
    writeToDiskResult = result;//保存是否写入磁盘成功的结果
    writtenToDiskLatch.countDown();//减少闭锁计数,唤醒在闭锁上等待的操作
}

既然有通过闭锁计数减1,唤醒等待线程的操作,就应该也有等待闭锁计算计数为0的地方。这个地方,在调用commit()方法时候,会调用MemoryCommitResult上的闭锁writtenToDiskLatch的await()方法。

try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    }

调用者获取到写入完成通知后,接着通知那些监听SharedPreference变化的监听者。具体是通过EditorImpl的notifyListeners()方法完成的。

2.13 EditorImpl.notifyListeners()

private void notifyListeners(final MemoryCommitResult mcr) {
    // 如果监听者为空,或者没有修改过SharedPreference的内容,则直接返回
    if (mcr.listeners == null || mcr.keysModified == null ||
        mcr.keysModified.size() == 0) {
        return;
    }
    //如果当前线程是主线程,则把SharedPreference中的所有key修改通知给所有的监听者
    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 {
        // 如果是在子线程中,则让该通知操作发生在主线程中
        ActivityThread.sMainThreadHandler.post(new Runnable() {
            public void run() {
                notifyListeners(mcr);
            }
        });
    }
}

在通知SharedPreference变化时,首先判断监听者是否空,或者SharedPreference是否发生了变化。然后在应用主线程中将SharedPreference中的所有key修改通知给所有的监听者。

通过2.9~2.13的过程,描述了commit()的流程:

  1. 首先构建一个写入磁盘的辅助对象MemoryCommitResult,把mModified集合中的数据拷贝到mMap中,并把它保存到MemoryCommitResult的mapToWriteToDisk变量中;
  2. 如果当前没有写入操作,则直接在当前线程中执行写入操作;否则,封装写入操作到单线程任务队列中,等待在其他线程中随后执行写入操作;
  3. 写入操作主要是将MemoryCommitResult中的mapToWriteToDisk集合内容写入到磁盘文件中,写入完成后,再通过setDiskWriteResult()方法返回结果,并唤醒等待结果的线程;
  4. 等待写入结果的线程被唤醒之后,通过notifyListeners()方法,在主线程中将SharedPreference中修改的key通知给监听者;
  5. 返回写入结果给调用者;

2.14 EditorImpl.apply()

public void apply() {
    final MemoryCommitResult mcr = commitToMemory();//构建需要写入磁盘的数据,见2.10
    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);//从等待结束任务队列中移除
            }
        };

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);//见2.11

    // 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);//通知监听者
}

可以看到apply()与commit()的主要区别是传递给enqueueDiskWrite()方法的第二个参数不同。在commit()方法中,postWriteRunnable为null,因此执行的同步写入操作,而在apply()方法中,postWriteRunnable不为null,因此apply()中的所有写入操作都是在单线程的Executor中执行。

在写入操作完成后,会执行postWriteRunnable里面的run()方法,在该run()方法中,又执行awaitCommit里面的run()方法,在该run()方法中,主要是等待写入操作完成。由于postWriteRunnable是在写入操作完成后执行的,因此该等待操作立即返回。

因为apply()方法的写入操作,都是在单线程的Executor中执行的,不能确切知道什么时候执行完成。那么如果想等待异步操作完成后立即返回,该如何做呢?在QueuedWork中,有一个等待执行结束的任务队列,在执行任务之前,先将任务添加到任务队列中,等待任务执行完成后,则再从任务队列中移除该任务。

public class QueuedWork {
    private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =
        new ConcurrentLinkedQueue<Runnable>();

    /*
    * 添加一个任务到等待结束任务队列中
    */
    public static void add(Runnable finisher) {
        sPendingWorkFinishers.add(finisher);
    }
    /*
    * 从等待结束任务队列中移除该任务
    */
    public static void remove(Runnable finisher) {
        sPendingWorkFinishers.remove(finisher);
    }

    /*
    * 等待异步操作完成,如果异步操作没有,则一直循环等待。
    */
    public static void waitToFinish() {
        Runnable toFinish;
        while ((toFinish = sPendingWorkFinishers.poll()) != null) {
            toFinish.run();
        }
    }
}

从QueuedWork中,可以看到,如果需要等待异步操作完成,只需在任务执行前先通过QueuedWork.add()方法将任务添加到等待结束的任务队列中,然后调用QueuedWork.waitToFinish()方法等待异步操作执行完成,异步操作执行完成后,会调用QueuedWork.remove()方法,从等待结束任务队列中移除该任务。

可以看到在apply()方法中,在执行异步写入操作之前,通过QueuedWork.add()方法,将任务添加到了等待结束的任务队列中,当执行完写入操作后,再通过QueuedWork.remove()方法移除在结束等待任务队列中的任务。

前面主要是介绍了数据的存储过程,主要是借助Editor类的putXXX()方法来保存数据,并最后通过commit()或apply()方法将内存中的数据同步到磁盘中。

接下来看看SharedPreference是如何获取数据的?

2.15 SharedPreferencesImpl.getString()

public String getString(String key, @Nullable String defValue) {
    synchronized (this) {
        awaitLoadedLocked();//等待SharedPreference加载到内存中,见2.7
        String v = (String)mMap.get(key);//直接从mMap中获取值
        return v != null ? v : defValue;//如果值不存在,则返回默认的值
    }
}

可以看到,获取数据的过程比较简单,首先是等待SharedPreference加载到内存中,加载完成后,直接从mMap集合查看对应key的value是否存在。如果存在,则直接返回,如果不存在,则返回默认值。

在commitToMemory()方法中,我们可以看到,在写入磁盘之前,其实已经将数据先从mModified集合拷贝到mMap集合中。这样做的一个目的是,当一个线程执行putXXX()操作后,另外一个线程就可以通过getXXX()立即获得相关的值,因为这些数据都是保存在内存中,可以立即返回,而不用等待数据写入到磁盘后,再从磁盘中获取数据。这是因为磁盘操作是一个耗时的操作,所以通过mMap集合在内存中缓存结果。

3.小结

  • SharedPreference主要用来保存一些简单的值,例如int、String、Boolean等类型。

  • SharedPreference数据的存储必须通过Editor类的putXXX()方法进行保存,然后通过Editor的commit()和apply()方法将数据同步到磁盘中。

  • SharedPreference数据的获取可以直接通过SharedPreference的getXXX()方法进行获取。

  • SharedPreference的数据本质上是保存在一个xml文件中,这个xml文件存放在/data/data/应用包名/shared_prefs/目录下。

  • 如果不需要数据写入磁盘的结果,则可以使用apply()方法进行磁盘写入,该方法是在子线程中执行。如果需要磁盘写入结果,则可以使用commit()方法进行磁盘写入操作。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容