美文网首页
SharedPreferences

SharedPreferences

作者: MrDecoder | 来源:发表于2020-10-30 15:59 被阅读0次
  • 概述
  • 源码分析
  • 总结

一、概述

Android系统里面大致有三种数据存储方案:

  1. 文件存储。
  2. 数据库存储。
  3. SharedPreferences存储。

SharedPreferences是这三种方案中最轻量级的存储方案。具有以下特点:

  1. 使用键值对的方式存储数据。
  2. 全量读写。全量读写的特性决定了SharedPreferences适合于小数据存储,轻便快捷。

二、源码分析

2.1 初始化

我们可以通过Context的getSharedPreferences方法获取一个SharedPreferences对象。
SharedPreferences本身是一个顶层接口定义了一系列的get方法以及获取Editor对象对内容进行提交的方法。

Context本身也是个抽象类,其具体实现是ContextImpl。接下来直接分析ContextImpl中的getSharedPreferences方法。

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
    //创建sSharedPrefs对象--全局cache
    //以应用包名作为key,记录所有应用创建的SharedPreferences对象。
        if (sSharedPrefs == null) {
            sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
        }

        final String packageName = getPackageName();
        ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
        
        //创建packagePrefs对象--应用cache
        //记录当前应用创建的SharedPreferences对象
        if (packagePrefs == null) {
            packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
            sSharedPrefs.put(packageName, packagePrefs);
        }

        // 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";
            }
        }

        sp = packagePrefs.get(name);
        //根据name创建具体的SharedPreferencesImpl对象
        //记录在packagePrefs
        if (sp == null) {
            //如果文件不存在,则创建文件
            File prefsFile = getSharedPrefsFile(name);
            sp = new SharedPreferencesImpl(prefsFile, mode);
            packagePrefs.put(name, sp);
            return sp;
        }
    }
    //MODE_MULTI_PROCESS模式下,判断是否需要重新加载文件
    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;
}

getSharedPreferences方法接受两个输入参数,name和mode。name表示当前的SharedPreferences文件的名称,mode表示接受的访问方式。mode支持以下几种类型:

  • MODE_PRIVATE:默认模式,仅限创建SharedPreferences的应用内可以访问。
  • MODE_WORLD_READABLE:共享读,所有的应用有读权限。已经废弃,不推荐使用
  • MODE_WORLD_WRITEABLE:共享写,所有的应用有写权限。已经废弃,不推荐使用
  • MODE_MULTI_PROCESS:多进程模式。设置了这个flag,在加载文件的时候会根据文件的lastModifiedTime和size判断文件是否被修改。如果检查到文件被修改了,则会重新从磁盘加载SharedPreferences文件。
2.1.1 getSharedPrefsFile
@Override
public File getSharedPrefsFile(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}

private File makeFilename(File base, String name) {
    if (name.indexOf(File.separatorChar) < 0) {
        return new File(base, name);
    }
    throw new IllegalArgumentException(
            "File " + name + " contains a path separator");
}

private File getPreferencesDir() {
    synchronized (mSync) {
        if (mPreferencesDir == null) {
            mPreferencesDir = new File(getDataDirFile(), "shared_prefs");
        }
        return mPreferencesDir;
    }
}

getSharedPrefsFile根据名字在mPreferencesDir目录下创建文件。如果mPreferencesDir为空则先创建mPreferencesDir,其路径为:/data/data/package_name/shared_prefs

2.1.2 SharedPreferencesImpl初始化
SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    startLoadFromDisk();
}

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

SharedPreferencesImpl在初始化过程中创建后缀名为.bak的备份文件,调用startLoadFromDisk加载文件。

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

首先判断文件有没有改变,文件改变则调用startLoadFromDisk方法从磁盘加载文件。

2.1.4 hasFileChangedUnexpectedly
// Has the file changed out from under us?  i.e. writes that
// we didn't instigate.
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;
    }
}

根据文件的修改时间和大小判断文件是否有改变。

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

开启子线程加载文件。具体的加载过程交给loadFromDiskLocked执行。

2.1.6 loadFromDiskLocked

loadFromDiskLocked是真正执行从磁盘加载的逻辑。

private void loadFromDiskLocked() {
    //mLoaded是一个标记量,用来判断是否已经加载过
    if (mLoaded) {
        return;
    }
    //备份文件存在,则删除原文件,从备份文件加载
    if (mBackupFile.exists()) {
        mFile.delete();
        mBackupFile.renameTo(mFile);
    }

    // Debugging
    if (mFile.exists() && !mFile.canRead()) {
        Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
    }
    
    //读取xml文件,将其保存在map中
    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);
            } catch (XmlPullParserException e) {
                Log.w(TAG, "getSharedPreferences", e);
            } catch (FileNotFoundException e) {
                Log.w(TAG, "getSharedPreferences", e);
            } catch (IOException e) {
                Log.w(TAG, "getSharedPreferences", e);
            } 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();
}
  1. 通过mLoaded标记量判断是否已经加载,已经加载过不再重复加载。

  2. 如果有备份文件,则直接从备份文件读取。删除原文件,将备份文件重命名为原文件名。

  3. 对Xml文件进行解析,将解析完的数据保存在变量mMap中,mMap类型为HashMap.

  4. 通知所有处于等待状态的线程。这里用到的是多线程里面的生产者-消费者模式。getXXX执行的是获取操作,如果数据没有准备好(通过mLoaded标记量判断)则wait。等到数据setXXX(初始化,初始化过程中从磁盘读取数据为异步操作)完成notify生产者获取。

2.2 Editor写数据

SharedPreferences内部所有的写请求都是通过Editor去实现的。通过edit方法可以获得Editor对象。Editor本身也是接口,其对应的具体实现类为EditorImpl。

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 (this) {
        awaitLoadedLocked();
    }
    
    return new EditorImpl();
}
2.2.1 Editor初始化
public final class EditorImpl implements Editor {
    private final Map<String, Object> mModified = Maps.newHashMap();
    private boolean mClear = false;
}
  • mModified对应的是Editor的内存cache,所有的put的操作都是直接修改内存里面的数据,在经过commit或者apply提交修改到文件本身。
  • clear是一个清除标记量
2.2.2 putXXX
public Editor putString(String key, @Nullable String value) {
    synchronized (this) {
        mModified.put(key, value);
        return this;
    }
}

public Editor putInt(String key, int value) {
    synchronized (this) {
        mModified.put(key, value);
        return this;
    }
}
...

put系列方法提供了直接对内存cache进行修改的操作。

2.2.3 commit
public boolean commit() {
    MemoryCommitResult mcr = commitToMemory();
    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

commit的内部的执行逻辑:

  1. commitToMemory首先同步提交到内存的修改;
  2. enqueueDiskWrite执行同步磁盘文件更新。
2.2.3.1 commitToMemory
MemoryCommitResult
// Return value from EditorImpl#commitToMemory()
private static class MemoryCommitResult {
    public boolean changesMade;  // any keys different?
    public List<String> keysModified;  // may be null
    public Set<OnSharedPreferenceChangeListener> listeners;  // may be null
    public Map<?, ?> mapToWriteToDisk;
    public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
    public volatile boolean writeToDiskResult = false;

    public void setDiskWriteResult(boolean result) {
        writeToDiskResult = result;
        writtenToDiskLatch.countDown();
    }
}
  • changesMade:用来判断key是否发生改变。
  • keysModified:保存发生改变的key。
  • writtenToDiskLatch:同步门闩,apply提交修改时,用来控制一次只有一个线程进行writeToFile操作。
  • mapToWriteToDisk:记录当前需要写入xml文件的数据。
  • writeToDiskResult:标记量表示内存提交到磁盘的写入操作是否成功。
private MemoryCommitResult commitToMemory() {
    MemoryCommitResult mcr = new MemoryCommitResult();
    synchronized (SharedPreferencesImpl.this) {
        // 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);
        }
        mcr.mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;

        boolean hasListeners = mListeners.size() > 0;
        if (hasListeners) {
            mcr.keysModified = new ArrayList<String>();
            mcr.listeners =
                    new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
        }

        synchronized (this) {
            if (mClear) {
                if (!mMap.isEmpty()) {
                    mcr.changesMade = true;
                    mMap.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 (!mMap.containsKey(k)) {
                        continue;
                    }
                    mMap.remove(k);
                } else {
                    if (mMap.containsKey(k)) {
                        Object existingValue = mMap.get(k);
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    mMap.put(k, v);
                }

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

            mModified.clear();
        }
    }
    return mcr;
}
  • commitToMemory返回的是一个MemoryCommitResult对象,MemoryCommitResult中有一个成员变量mapToWriteToDisk用来记录需要更新的磁盘的数据,其类型为HashMap。
  • commitToMemory会首先创建一个MemoryCommitResult对象mcr。并且将SharedPreferenceImpl里面的mMap记录在mapToWriteToDisk变量中。
  • 检测清除标记量mClear是否为true。如果设置mClear为true,则更新mcr当中changesMade变量为true。同时清除SharedPreferenceImpl的成员变量mMap。
  • 然后遍历Editor里面保存更改的数据mModified,将其中的数据添加到mMap中,添加完成后清除mModified。
2.2.3.2 enqueueDiskWrite
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    final Runnable writeToDiskRunnable = new Runnable() {
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr);
                }
                synchronized (SharedPreferencesImpl.this) {
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };

    final boolean isFromSyncCommit = (postWriteRunnable == null);

    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (SharedPreferencesImpl.this) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }

    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
  • commit调用enqueueDiskWrite时,传入的postWriteRunnable对象为空。isFromSyncCommit为true表明执行同步写操作。直接执行writeToDiskRunnable对象的run方法,将操作转发给writeToFile方法。
  • apply对象执行的是异步写操作,通过单一线程线程池执行writeToDiskRunnable任务。
2.2.3.3 writeToFile
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 (!mcr.changesMade) { //没有key发生改变,则直接返回
            // If the file already exists, but no changes were
            // made to the underlying map, it's wasteful to
            // re-write the file.  Return as if we wrote it
            // out.
            mcr.setDiskWriteResult(true);
            return;
        }
        if (!mBackupFile.exists()) { //备份文件不存在,则把mFile命名为备份文件
            if (!mFile.renameTo(mBackupFile)) {
                Log.e(TAG, "Couldn't rename file " + mFile
                      + " to backup file " + mBackupFile);
                mcr.setDiskWriteResult(false);
                return;
            }
        } else {
            mFile.delete(); //否则删除文件
        }
    }

    // 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 (str == null) {
            mcr.setDiskWriteResult(false);
            return;
        }
        //将mMap全部数据写入文件
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        FileUtils.sync(str);
        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
        }
        //写入成功,删除备份文件
        // Writing was successful, delete the backup file if there is one.
        mBackupFile.delete();
        //返回写入成功,唤醒等待线程
        mcr.setDiskWriteResult(true);
        return;
    } catch (XmlPullParserException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    } catch (IOException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    }
    // 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);
}
  • 如果key没有发生改变,则直接返回。
  • 将mMap全部数据写入xml文件,如果写入成功则删除备份文件,如果写入失败则删除mFile。
2.2.4 apply
public void apply() {
    final MemoryCommitResult mcr = commitToMemory();
    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);

    // 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的内部的执行逻辑:

  1. commitToMemory首先同步提交到内存的修改;
  2. enqueueDiskWrite执行异步磁盘文件更新,这里所有的文件IO都是通过单线程控制的。QueuedWork保存的是等待提交的任务。enqueueDiskWrite通过postWriteRunnable参数是否为空来进行同步或者异步操作。为空时进行同步写,否则将任务提交到单一线程线程池进行任务写。
2.2.5 clear
public Editor clear() {
    synchronized (this) {
        mClear = true;
        return this;
    }
}

clear操作只是提供对mClear标记量进行修改,将其设置为true。

2.3 读取数据

2.3.1 getString
@Nullable
public String getString(String key, @Nullable String defValue) {
    synchronized (this) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

getXXX系列方法执行的是数据获取操作,先判断数据有没有加载完成。如果数据加载完成则直接从内存中或者。mMap就是对应的数据初始化完成之后对应的内存cache。

2.3.2 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();
    }
    while (!mLoaded) {
        try {
            wait();
        } catch (InterruptedException unused) {
        }
    }
}

如果没有加载完成则处于等待状态。

三、总结

  1. SharedPreferences是Android内置的一个轻量级的数据持久化框架。其内部以键值对的形式保存数据。生成的目标文件在/data/data/package_name/shared_prefs目录下。
  2. 所有对SharedPreferences进行Put和Get的操作都是针对内存操作。SharedPreferences内部通过Editor对数据进行写操作,Editor内部支持两种方式写数据:
    • commit:同步更新内存,同步更新磁盘。
    • apply:同步更新内存,异步更新磁盘。
  3. 进行apply提交更改时,如果进行的更改的任务比较多,耗时比较长的话。在页面跳转时会调用QueuedWork.waitToFinish的方法以等待任务执行完成,这会带来ANR隐患,尤其是在一些性能比较差的设备上。解决方案中有一种最简单的方式就是使用异步commit,尽量避免使用apply。
参考链接:
  1. 官方文档
  2. SharedPreferences ANR

相关文章

网友评论

      本文标题:SharedPreferences

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