美文网首页
Android-SharedPreferences

Android-SharedPreferences

作者: 有腹肌的豌豆Z | 来源:发表于2020-10-20 11:52 被阅读0次

一、简介

在Android中,我们通常会需要存储一些数据,有一些大型的数据诸如图片、JSON数据等,可以通过读写File的方式实现;有一些大量级的关系型数据,可以通过数据库SQLite实现;还有一些简单的、无安全风险的键值对数据,可以通过Android提供的SharedPreferences实现。

SharedPreferences

public interface SharedPreferences {
   
    public interface OnSharedPreferenceChangeListener {
        void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
    }
  
    public interface Editor {
        Editor putString(String key, @Nullable String value);
        Editor putStringSet(String key, @Nullable Set<String> values);
        Editor putInt(String key, int value);
        Editor putLong(String key, long value);
        Editor putFloat(String key, float value);
        Editor putBoolean(String key, boolean value);
        Editor remove(String key);
        Editor clear();
        boolean commit();
        void apply();
    }

    Map<String, ?> getAll();

    @Nullable
    String getString(String key, @Nullable String defValue);
   
    @Nullable
    Set<String> getStringSet(String key, @Nullable Set<String> defValues);
   
    int getInt(String key, int defValue);
    long getLong(String key, long defValue);
    float getFloat(String key, float defValue);
    boolean getBoolean(String key, boolean defValue);
    boolean contains(String key);
    Editor edit();
  
    void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
   
    void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
}

SharedPreferences是一个轻量级的xml键值对文件,使用起来也是很方便。

        //根据文件名,获取SharedPreferences对象;mode一般都使用MODE_PRIVATE,只能由该App访问
        SharedPreferences sp = getSharedPreferences("setting", Context.MODE_PRIVATE);
        //根据key,获取指定值
        boolean needInitChannels = sp.getBoolean("isDebug", false);
        //获取Editor编辑对象,用于编辑SharedPreferences
        SharedPreferences.Editor editor = sp.edit();
        editor.putBoolean("isDebug", true);
        //同步提交到SharedPreferences文件,获取是否同步成功的结果
        editor.commit();
        //异步提交到SharedPreferences文件
        editor.apply();

当我们第一次访问一个名为"setting"的SharedPreferences文件,系统会在应用数据目录下(/data/data/packageName/)的shared_prefs文件夹下,创建一个同名的xml文件。

二、实现原理

1.创建

我们是通过Context的getSharedPreferences()方法来获取一个SharedPreferences对象,而Context的实际逻辑载体,是在ContextImpl里,所以我们来看ContextImpl的该方法。

public SharedPreferences getSharedPreferences(String name, int mode) {
    ...
    File file;
    synchronized (ContextImpl.class) {
        if (mSharedPrefsPaths == null) {
            mSharedPrefsPaths = new ArrayMap<>();
        }
        //1.获取缓存File对象
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            //生成File对象
            file = getSharedPreferencesPath(name);
            mSharedPrefsPaths.put(name, file);
        }
    }
    //2.获取File对象对应的SharedPreferences对象
    return getSharedPreferences(file, mode);
}

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

这里第一步我们可以看到,有一个ArrayMap会缓存每一个SharedPreferences文件对象,如果第一次加载的话,会调用getSharedPreferencesPath()生成File对象。

public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}
private File getPreferencesDir() {
    synchronized (mSync) {
        if (mPreferencesDir == null) {
            ///data/data/packageName/目录
            mPreferencesDir = new File(getDataDir(), "shared_prefs");
        }
        return ensurePrivateDirExists(mPreferencesDir);
    }
}
private static File ensurePrivateDirExists(File file, int mode, int gid, String xattr) {
    if (!file.exists()) {
        final String path = file.getAbsolutePath();
        try {
            Os.mkdir(path, mode);
            ...
        }...
    }
    return file;
}

我们会看到,如果应用目录下还没有shared_prefs文件夹,则创建一个该文件夹。

public File getSharedPreferencesPath(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);
    }
}

然后,会在shared_prefs文件夹下,创建一个我们指定名字的xml文件,用来存储键值对数据。
至此,会生成一个相应SharedPreferences的File文件,并进行缓存。
第二步,通过File获取SharedPreferences对象。

public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        ...
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
    }
    ...
    return sp;
}

这里我们看到,返回的SharedPreferences实际对象,是一个SharedPreferencesImpl对象实例,并且也通过一个ArrayMap做了一个缓存,也就是说,一个name会对应一个SharedPreferences的File实例,而一个File会对应一个SharedPreferencesImpl实例。

sharedPreferences是一个接口,在Content类中,声明了getSharedPreferences()空实现的一个方法,先看源码
@Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            if (sSharedPrefs == null) {
                sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
            }
 
            final String packageName = getPackageName();
            ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
            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);
            if (sp == null) {
                File prefsFile = getSharedPrefsFile(name);
                sp = new SharedPreferencesImpl(prefsFile, mode);
                packagePrefs.put(name, 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;
    }

我们可以很明显的看到

创建ArrayMap集合,集合的key:string,value:ArrayMap<String,SharedPreferenceImpl>
getPackageName(),拿到项目包名;
根据包名,在第一步创建的ArrayMap集合中找,有没有包名为key的键值对,if逻辑判断
如果没有,创建一个ArrayMap集合,作为value,包名作为key,存储在第一步创建的集合ArrayMap中
根据方法传入的SharedPreferences名称,也就是name的String类型参数,在第四步创建的ArrayMap中get查找有没有创建对应名称的SharedPreferenceImpl对象,if逻辑判断
如果sp为空,创建对应的file文件,将包名称对应的SharedPreferenceImpl对象存储到第四步的ArrayMap中,name作为key
然后返回第四步以包名称为key的ArrayMap。

2.取值

通过上述内容,可以知道我们实际操作的对象,其实都是SharedPreferencesImpl对象。这里我们以getBoolean()为例,看一下如何获取值。

public boolean getBoolean(String key, boolean defValue) {
    synchronized (mLock) {
        //等待从磁盘加载数据(需要的话)
        awaitLoadedLocked();
        //直接从map取值返回
        Boolean v = (Boolean)mMap.get(key);
        return v != null ? v : defValue;
    }
}

那么从磁盘读取文件内容,并生成Map对象的初始化过程,是在何时进行的呢?是在SharedPreferencesImpl的构造函数中进行。

SharedPreferencesImpl(File file, int mode) {
    ...
    startLoadFromDisk();
}
private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    //工作线程进行
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}
private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        ...
    }
    ...
    Map<String, Object> map = null;
    StructStat stat = null;
    Throwable thrown = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                //读取文件内容
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                //解析XML生成Map
                map = (Map<String, Object>) XmlUtils.readMapXml(str);
            }...
        }
    }
    ...
    mMap = map;
    ...
    mLock.notifyAll();
}

可以看到,SharedPreferencesImpl创建的时候,简单粗暴的开启了一个工作线程,进行File的读取和解析,并生成了Map对象,赋值给mMap变量。

而在我们的getBoolean()等操作Map的方法调用时,会通过awaitLoadedLocked()方法判断是否map已经生成,如果没有则等待。

private void awaitLoadedLocked() {
    ...
    while (!mLoaded) {
        try {
            mLock.wait();
        }...
    }
}

3.更新

再来看更新,上面说到过,更新是通过SharedPreferences的edit()方法,获取一个Editor对象进行。

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

可见每次都是创建一个新的EditorImpl对象实例进行操作。通过源码可知多个数据编辑的时候 尽可能创建一个edit 对象

public Editor putBoolean(String key, boolean value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}

所有的更新操作,都会维护在EditorImpl对象的一个HashMap中,等待后续应用。

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

4.应用

更新后,我们需要写入到磁盘中,EditorImpl为我们提供了两个方法,一个是commit()方法,一个是apply()方法,他们的区别,主要是同步和异步的差别。

(1)commit()
public boolean commit() {
   ...
   //1.将改动先同步到内存
   MemoryCommitResult mcr = commitToMemory();
   //2.同步写入磁盘
   SharedPreferencesImpl.this.enqueueDiskWrite(
       mcr, null /* sync write on this thread okay */);
   try {
       mcr.writtenToDiskLatch.await();
   }...
   return mcr.writeToDiskResult;
}

第一步是调用commitToMemory()方法,将EditorImpl的modify改动,应用到内存中,即应用到mMap中。

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);
        }
        mapToWriteToDisk = mMap;
        //当前更新操作的线程数+1
        mDiskWritesInFlight++;
        ...
        synchronized (mEditorLock) {
            ...
            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                if (v == this || v == null) {//remove
                    if (!mapToWriteToDisk.containsKey(k)) {
                        continue;
                    }
                    mapToWriteToDisk.remove(k);
                } else {//update or insert
                    if (mapToWriteToDisk.containsKey(k)) {
                        Object existingValue = mapToWriteToDisk.get(k);
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    mapToWriteToDisk.put(k, v);
                }
            }...
        }
    }
    return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
            mapToWriteToDisk);
}

第二步,将内存中最新的数据,写到文件中。

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
    //commit()方法时为true
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    final Runnable writeToDiskRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (mWritingToDiskLock) {
                    //将Map写入File
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    //当前更新操作的线程数-1
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };

    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            //只有当前线程调用时,因为同步进行,所以一直为1;有多个线程同时调用时,会大于1
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            //当前线程直接调用
            writeToDiskRunnable.run();
            return;
        }
    }
    //异步调用
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

当只有一个线程操作SharedPreferences的话,mDiskWritesInFlight计数器始终为1,因为是同步写入File,写入后计数器会-1。

而写入文件就是很简单的IO操作,只不过需要把Map转换为xml的格式。

private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    ...
    try {
        //OutputStream
        FileOutputStream str = createFileOutputStream(mFile);
        ...
        //将map数据写成xml格式到file中
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        ...
        //设置成功写入的结果
        mcr.setDiskWriteResult(true, true);
}
(2)apply()

再来看apply()方法。

public void apply() {
    final MemoryCommitResult mcr = commitToMemory();
    ...
    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };
    //postWriteRunnable不为null
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    ...
}

这里可以看到,调用的方法和commit()都一样,唯独调用enqueueDiskWrite()方法时,第二个Runnable对象不为null,按照上面说,不为null时,走的方法就是异步方法QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);

public static void queue(Runnable work, boolean shouldDelay) {
    Handler handler = getHandler();
    synchronized (sLock) {
        //将任务加入到队列中等待执行
        sWork.add(work);
        //通过handler发送消息
        if (shouldDelay && sCanDelay) {
            handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
        } else {
            handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
        }
    }
}
private static class QueuedWorkHandler extends Handler {
    ...
    public void handleMessage(Message msg) {
        if (msg.what == MSG_RUN) {
            processPendingWork();
        }
    }
}
private static void processPendingWork() {
    ...
    for (Runnable w : work) {
        //执行任务
         w.run();
     }
}

可以看到,内部就是通过handler发送消息执行任务的,任务还是一样的writeFile()方法,那么为何是异步的呢?就得来看看handler是啥handler了。

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

现在就明白了吧,是一个全局的HandlerThread对象,也就是一个工作线程,所以apply()方法,会通过一个全局唯一的异步线程进行写文件的操作。

三、总结

  • SharedPreferences的File创建和内容解析,在内存中是有缓存的。
  • SharedPreferences的提交,commit()方法是在当前线程完成,而apply()方法在全局唯一的一个工作线程中完成。
  • 所有的文件和内存读写操作,都通过锁对象进行加锁,保证了多线程同步。
  • sp的数据保存写入是全量的。
  • sp在保存大数据量的时候会有可能导致ANR。

四、QA

(1)每次调用getSharedPreferences时都会创建一个SharedPreferences对象吗?这个对象具体是哪个类对象?
答:不是,只要name相同,就会返回同一个,SharedPreferencesImpl对象,packagePrefs存放文件name与SharedPreferencesImpl键值对,sSharedPrefs存放包名与ArrayMap键值对。注意sSharedPrefs是static变量,也就是一个类只有一个实例,因此你每次getSharedPreferences其实拿到的都是同一个SharedPreferences对象。

(2)在UI线程中调用getXXX有可能导致ANR吗?
答:有可能的,getXXX之前,会给当前线程加锁,如果sp文件特别大,查询非常耗时的时候,有可能ANR。

(3)为什么SharedPreferences只适合用来存放少量数据,为什么不能把SharedPreferences对应的xml文件当成普通文件一样存放大量数据?
答:其实这和第二个问题没有区别,因为SharedPreference是整个文件都加载到内存中,文件太大了会对内存造成压力。

(4)commit和apply有什么区别?
(5)SharedPreferences每次写入时是增量写入吗?

答:不是,每次都是重新写入,说一下那个mBackupFile,SharedPreferences在写入时会先把之前的xml文件改成名成一个备份文件,然后再将要写入的数据写到一个新的文件中,如果这个过程执行成功的话,就会把备份文件删除。由此可见每次即使只是添加一个键值对,也会重新写入整个文件的数据,这也说明SharedPreferences只适合保存少量数据,文件太大会有性能问题。SharedPreferences每次写入都是整个文件重新写入,不是增量写入。

相关文章

网友评论

      本文标题:Android-SharedPreferences

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