Android SharedPreferences★★★★

1.SharedPreferences

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

SharedPreferences是一个轻量级的存储类,特别适合用于保存软件配置参数。其背后是用xml文件存放数据,文件存放在/data/data//shared_prefs目录下。SharedPreferences所保存的数据会一直存在,除非被覆盖、移除、清空或文件被删除。(SharedPreferences保存的数据会随着应用的卸载而被删除)

SharedPreferences可以保存的数据类型有:int、boolean、float、long、String、StringSet。

SharedPreferences优点:

相对于文件存储来说比较方便,支持多种数据类型的存储。

SharedPreferences缺点:

①不安全,一般只用来存储配置信息

②对数据的操作单一

③存储相同的key值时,存入的数据会被覆盖

2.SharedPreferences使用

①获取到应用中的SharedPreferences

有三种方式。

1)getSharedPreferences(String,mode)

如果需要多个通过名称参数来区分的sharedpreference文件, 名称可以通过第一个参数来指定。可在app中通过任何一个Context 执行该方法。

SharedPreferences sp = context.getSharedPreferences("setting, Context.MODE_PRIVATE);

第一次访问名为"setting"的SharedPreferences文件时,系统会在应用数据目录下(/data/data/packageName/)的shared_prefs文件夹下,创建一个同名的xml文件。也就是说该文件不存在时,直接创建;如果已经存在,则直接使用。

mode指定为MODE_PRIVATE,则该配置文件只能被自己的应用程序访问。

2)getPreferences(mode)

这个方法默认使用当前类不带包名的类名作为文件的名称,配置文件仅可以被调用的Activity使用。当Activity只需要创建一个SharedPreferences对象的时候,可以使用该方法。

SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);

3)PreferenceManager.getDefaultSharedPreferences(Context)

每个应用都有一个默认的配置文件preferences.xml,可以使用getDefaultSharedPreferences获取。

每个应用默认的配置文件的名字是:包名+_preferences,其文件读取类型为Context.MODE_PRIVATE。

看一下它的源码:

public static SharedPreferences getDefaultSharedPreferences(Context context) {

    return context.getSharedPreferences( getDefaultSharedPreferencesName(context),getDefaultSharedPreferencesMode());

}

 private static String getDefaultSharedPreferencesName(Context context) {

    return context.getPackageName() + "_preferences";

}

private static int getDefaultSharedPreferencesMode() {

    return Context.MODE_PRIVATE;

}

以上三种获取SharedPreferences的方法都提到了mode,来看一下关于mode的指定:

1)私有模式

​ Context.MODE_PRIVATE 的值是 0;

​ 只能被创建这个文件的当前应用访问。若文件不存在会创建文件;若创建的文件已存在则会覆盖掉原来的文件。

2)追加模式

​ Context.MODE_APPEND 的值是 32768;

​ 只能被创建这个文件的当前应用访问。若文件不存在会创建文件;若文件存在则在文件的末尾进行追加内容。

3)可读模式

​ Context.MODE_WORLD_READABLE的值是1;

​ 创建出来的文件可以被其他应用所读取

4)可写模式

​ Context.MODE_WORLD_WRITEABLE的值是2;

​ 允许其他应用对其进行写入。

②读写SharedPreferences

1)写SharedPreferences

为了写sharedPreferences文件,需要通过执行edit()创建一个 SharedPreferences.Editor。通过putXXX()方法传递keys与values,最后通过commit() 或apply()提交改变。

SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);

SharedPreferences.Editor editor = sharedPref.edit();

editor.putString("number",number);

editor.putString("password",pwd);

editor.apply(); 或 editor.commit();

commit表示同步提交到SharedPreferences文件,获取是否同步成功的结果:boolean success = editor.commit();

apply表示异步提交到SharedPreferences文件:editor.apply();

2)读SharedPreferences

通过getXXX()方法从sharedPreferences中读取数据。在这些方法里面传入想要获取的value对应的key,并提供一个默认的value作为查找的key不存在时函数的返回值。如下:

SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);

String number=sp.getString("number","");

String pasword=sp.getString("password","");

③移除数据

1)移除指定key的数据(由Editor对象调用)

abstract SharedPreferences.Editor remove(String key)

参数key:指定数据的key

2)清空数据(由Editor对象调用)

abstract SharedPreferences.Editor clear()

④系统默认的SharedPreferences

每个应用都有一个默认编好的preferences.xml文件,使用getDefaultSharedPreferences获取,其余操作是一样的。

SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);

SharedPreferences.Editor editor = preferences.edit();

editor.putBoolean("if_set_location", false);

editor.commit();

⑤访问其他应用的SharedPreferences

如果应用B要读写访问A应用中的Preference前提条件是,A应用中该preference创建时指定了Context.MODE_WORLD_READABLE或者Context.MODE_WORLD_WRITEABLE权限,代表其他的应用能访问读取或者写入。

具体步骤:

在B中创建一个指向A应用的Context:

Context otherAppsContext = createPackageContext("A应用的包名", Context.CONTEXT_IGNORE_SECURITY);

然后通过context获取到SharedPreferences实体:

SharedPreferences sharedPreferences = otherAppsContext.getSharedPreferences("SharedPreferences的文件名", Context.MODE_WORLD_READABLE);

String name = sharedPreferences.getString("key", "");

注:如果不通过创建Context访问其他应用的preference,也可以以读取xml文件方式直接访问其他应用preference对应的xml文件,如:

File xmlFile = new File(“/data/data/<package name>/shared_prefs/itcast.xml”);

//<package name>应替换成应用的包名。

3.SharedPreferences变化监听

这是当SharedPreferences改变时的回调,是SharedPreferences的一个接口。

通过registerOnSharedpreferenceListener方法设置监听:

SharedPreferences sp = getSharedPreferences( "testSP", Context.MODE_PRIVATE);

sp. registerOnSharedPreferenceChangeListener( new SharedPreferences. OnSharedPreferenceChangeListener() {

    @Override

    public void onSharedPreferenceChanged( SharedPreferences sharedPreferences, String s) {

        Log.i("spTest","sp changed, key is "+ s);

    }

});

关于这个监听,官方文档是这样描述的:

Called when a shared preference is changed, added, or removed.

This may be called even if a preference is set to its existing value.

This callback will be run on your main thread.

使用这个监听时,如果你用匿名对象也就是下面这样,可能会被当作垃圾回收,导致会回调一次你的callback,达不到监听的效果。

//匿名回调

protected void onCreate(Bundle savedInstanceState)  {   

    SharedPreferences sp = getSharedPreferences( "testSP", Context.MODE_PRIVATE);

     sp.registerOnSharedPreferenceChangeListe ner(new OnSharedPreferenceChangeListener() {

        @Override

        public void onSharedPreferenceChanged( SharedPreferences sharedPreferences, String key) {

            Log.i(LOGTAG, "testOnSharedPreference ChangedWrong key =" + key);

        }

    });

这种写法看上去没有什么问题,而且很多时候开始几次onSharedPreferenceChanged方法也可以被调用。但是过一段时间(简单demo 不容易出现,但是使用DDMS中的gc会立刻导致接下来的问题),你会发现前面的方法突然不再被调用,进而影响到程序的处理。

原因剖析:

真正的原因就是注册的监听器被移除掉了。

首先我们先了解一下registerOnSharedPreferenceChangeListener注册的实现。

private final WeakHashMap<OnSharedPreferenc eChangeListener, Object> mListeners = new WeakHashMap<OnSharedPreferenceChangeListener, Object>();

registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {

    synchronized(this) {

        mListeners.put(listener, mContent);

    }

从上面的代码可以得知,一个OnSharedPreferenceChangeListener对象实际上是放到了一个WeakHashMap的容器中,执行完示例中的onCreate方法,这个监听器对象很快就会成为垃圾回收的目标,由于放在WeakHashMap中作为key不会阻止垃圾回收, 所以当监听器对象被回收之后,这个监听器也会从mListeners中移除。所以就造成了onSharedPreferenceChanged不会被调用。

解决办法:改为对象成员变量(推荐)

将监听器作为Activity的一个成员变量,在Activity的onResume进行注册,在onPause时进行注销。推荐在这两个Activity生命周期中进行处理,尤其是当SharedPreference值发生变化后,对Activity展示的UI进行处理操作的情况。这种方法是最推荐的解决方案。

private OnSharedPreferenceChangeListener mListener = new OnSharedPreferenceChangeL istener() {

    @Override

    public void onSharedPreferenceChanged( SharedPreferences sharedPreferences, String key) {

        Log.i(LOGTAG, "instance variable key=" + key);

    }

};

@Override

protected void onResume() {

 sp.registerOnSharedPreferenceChangeListener(mListener);

    super.onResume();

}

@Override

protected void onPause() {   

    sp.unregisterOnSharedPreferenceChangeList ener(mListener);

    super.onPause();

}

总结一下:

①仅当添加或更改值时,监听器才会触发,设置相同的值将不会调用它;

②监听器需要保存在成员变量中,而不是匿名类,因为registerOnSharedPreferenceChangeListener使用弱引用进行存储,因此将被垃圾回收;

③除了使用成员变量,它也可以由类直接实现,然后调用 registerOnSharedPreferenceChangeListener(this);

④当不再需要使用时,请记住注销该侦听器unregisterOnSharedPreferenceChangeListener。

4.实现原理

①创建SharedPreferences

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

ContextImpl.java

public SharedPreferences getSharedPreferences(String name, int mode) {

    File file;

    synchronized (ContextImpl.class) {

        if (mSharedPrefsPaths == null) {

            mSharedPrefsPaths = new ArrayMap<>();

        }

        //先从缓存mSharedPrefsPaths中查找sp文件是否存在

        file = mSharedPrefsPaths.get(name);

        //如果缓存中不存在,则新建sp文件,文件名为name.xml

        if (file == null) {

            file = getSharedPreferencesPath(name);

            mSharedPrefsPaths.put(name, file);

        }

    }

    //获取File对象对应的SharedPreferences对象

    return getSharedPreferences(file, mode);

}

这里出现了一个变量 mSharedPrefsPaths,找一下它的定义:

//文件名为 key,具体文件为 value。存储所有 sp 文件,由 ContextImpl.class 锁保护

@GuardedBy("ContextImpl.class")

private ArrayMap<String, File> mSharedPrefsPaths;

mSharedPrefsPaths 是一个 ArrayMap ,缓存了文件名和sp文件的对应关系。首先会根据参数中的文件名name查找缓存中是否存在对应的sp文件。如果不存在的话,会新建名称为 [name].xml 的文件,并存入缓存 mSharedPrefsPaths 中。最后会调用另一个重载的getSharedPreferences()方法,参数是 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){

    return ensurePrivateDirExists(file,0771,-1,null);

}

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) {

        final File result = new File(base, name);

        return res;

    }

}

在shared_prefs文件夹下,创建一个指定名字的xml文件,用来存储键值对数据。

至此,生成了一个相应SharedPreferences的File文件,并进行缓存。

生成File对象后,接下来通过File对象获取SharedPreferences对象。

ContextImpl.java:

public SharedPreferences getSharedPreferences(File file, int mode) {

    SharedPreferencesImpl sp;

    synchronized (ContextImpl.class) {

        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();

        sp = cache.get(file); //先从缓存中尝试获取sp

        if(sp == null) { //如果缓存中获取失败

            checkMode(mode);//检查mode

            sp = new SharedPreferencesImpl(file, mode);

            cache.put(file, sp);

            return sp;

        }

    }

    //mode为MODE_MULTI_PROCESS时,由于文件可能被其他进程修改,则重新加载。该模式下每次获取SharedPreferences实例的时候都会尝试从磁盘中加载修改过的数据,并且读取是在异步线程中,因此一个线程的修改最终会反映到另一个线程,但不能立即反映到另一个进程,所以通过SharedPreferences无法实现多进程同步。如果仅仅让多进程可访问同一个SharedPref文件,不需要设置MODE_MULTI_PROCESS,,如果需要实现多进程同步,必须设置这个参数,但也只能实现最终一致,无法即时同步。显然这并不足以保证跨进程安全。

    if ((mode & Context.MODE_MULTI_PROCESS) != 0 || getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {

        sp.startReloadIfChangedUnexpectedly();

    }

    return sp;

}

SharedPreferences只是接口而已,我们要获取的实际上是它的实现类 SharedPreferencesImpl 。通过getSharedPreferencesCacheLocked()方法可以获取已经缓存的SharedPreferencesImpl对象和其sp文件。返回的SharedPreferences实际对象是一个SharedPreferencesImpl对象实例,并且也通过一个ArrayMap做了一个缓存,也就是说,一个name会对应一个SharedPreferences的File实例,而一个File会对应一个SharedPreferencesImpl实例。

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

    }

    return packagePrefs;

}

sSharedPrefsCache是一个嵌套的 ArrayMap,其定义如下:

private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

以包名为key,以一个存储了sp文件及其SharedPreferencesImp对象的ArrayMap为 value。如果存在直接返回,反之创建一个新的ArrayMap作为值并存入缓存。

private void checkMode(int mode) {

    // 从N开始,如果使用 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE,直接抛出异常

    if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {

        if((mode & MODE_WORLD_READABLE) != 0){

            throw new SecurityException( "MODE_WORLD_READABLE no longer supported");

        }

      if((mode & MODE_WORLD_WRITEABLE) != 0){

            throw new SecurityException( "MODE_WORLD_WRITEABLE no longer supported");

        }

    }

}

从Android N开始,明确不再支持 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE,再加上 MODE_MULTI_PROCESS 并不能保证线程安全,一般就使用 MODE_PRIVATE 就可以了。

从源码中可以看到,如果缓存中没有对应的SharedPreferencesImpl对象,就得自己创建了。看一下它的构造函数:

SharedPreferencesImpl(File file, int mode) {
    mFile = file; // sp 文件
    mBackupFile = makeBackupFile(file); // 创建备份文件
    mMode = mode; 
    mLoaded = false; // 标识sp文件是否已经加载到内存
    mMap = null; // 存储sp文件中的键值对
    mThrowable = null;
    startLoadFromDisk(); // 加载数据
}
注意这里的mMap,它是一个 Map<String, Object>,存储了sp文件中的所有键值对。所以 SharedPreferences文件的所有数据都是存在于内存中的,既然存在于内存中,就注定它不适合存储大量数据。

private void startLoadFromDisk() {

    synchronized (mLock) {

        mLoaded = false;

    }

    //工作线程中进行

    new Thread("SharedPreferencesImpl-load") {

        public void run() {

            loadFromDisk(); // 异步加载

        }

    }.start();

}

private void loadFromDisk() {

    synchronized (mLock) { // 获取 mLock 锁

        if (mLoaded) { // 已经加载进内存,直接返回,不再读取文件

            return;

        }

        if (mBackupFile.exists()) { // 如果存在备份文件,直接将备份文件重命名为 sp 文件

            mFile.delete();

            mBackupFile.renameTo(mFile);

        }

    }

    if (mFile.exists() && !mFile.canRead()) {

        Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");

    }

    Map<String, Object> map = null;

    StructStat stat = null;

    Throwable thrown = null;

    try { // 读取 sp 文件

        stat = Os.stat(mFile.getPath());

        if (mFile.canRead()) {

            BufferedInputStream str = null;

            try {

                //读取文件内容

                str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);

                map = (Map<String, Object>) XmlUtils.readMapXml(str);//解析XML生成Map

            } catch (Exception e) {

                Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);

            } finally {

                IoUtils.closeQuietly(str);

            }

        }

    } catch (ErrnoException e) {

    } catch (Throwable t) {

        thrown = t;

    }

    synchronized (mLock) {

        mLoaded = true;

        mThrowable = thrown;

        try {

            if (thrown == null) {

                if (map != null) {

                    mMap = map;

                    mStatTimestamp = stat.st_mtim; // 更新修改时间

                    mStatSize = stat.st_size; // 更新文件大小

                } else {

                    mMap = new HashMap<>();

                }

            }

        } catch (Throwable t) {

            mThrowable = t;

        } finally {

            mLock.notifyAll(); // 唤醒处于等待状态的线程

        }

    }

}

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

简单看一下流程:

(1)判断是否已经加载进内存

(2)判断是否存在遗留的备份文件,如果存在,重命名为 sp 文件

(3)读取 sp 文件,并存入内存

(4)更新文件信息

(5)释放锁,唤醒处于等待状态的线程

loadFromDisk() 是异步执行的,而且是线程安全的,读取过程中持有锁 mLock,看起来设计的都很合理,但是在不合理的使用情况下就会出现问题。

别忘了此时还停留在getSharedPreferences()方法,也就是获取SharedPreferences的过程中。如果在使用过程中,调用getSharedPreferences()之后,直接调用 getXXX() 方法来获取数据,恰好sp文件数据量又比较大,读取过程比较耗时,那么getXXX() 方法就会被阻塞。后面看到getXXX()方法的源码时,就会看到它需要等待sp文件加载完成,否则就会阻塞。所以在使用过程中,可以提前异步初始化SharedPreferences对象,加载sp文件进内存,避免发生潜在可能的卡顿。这是 SharedPreferences 的一个槽点,也是使用过程中需要注意的。

2.读取sp数据

获取sp文件中的数据使用的是SharedPreferencesImpl中的七个getXXX方法。它们都是一样的逻辑,以getInt()为例看一下源码:

public int getInt(String key, int defValue) {

    synchronized (mLock) {

        awaitLoadedLocked(); //sp文件尚未加载完成时,会阻塞在这里

        Integer v = (Integer)mMap.get(key); // 加载完成后直接从内存中读取

        return v != null ? v : defValue;

    }

}

一旦sp文件加载完成,所有获取数据的操作都是从内存中读取的。这样的确提升了效率,但是很显然将大量的数据直接放在内存是不合适的,所以注定SharedPreferences不适合存储大量数据。

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

@GuardedBy("mLock")

private void awaitLoadedLocked() {

    if (!mLoaded) {

        BlockGuard.getThreadPolicy(). onReadFromDisk();

    }

    while (!mLoaded) { //sp文件未加载完成时, 等待

        try {

            mLock.wait();

        } catch (InterruptedException unused) {

        }

    }

    if (mThrowable != null) {

        throw new IllegalStateException( mThrowable);

    }

}

mLoaded初始值为false,在loadFromDisk()方法中读取 sp 文件之后会被置为 true,并调用mLock.notifyAll()通知等待的线程。

③存储sp数据

SharedPreferences存储数据时,需要使用edit()方法,该方法会返回一个Editor()对象Editor和SharedPreferences一样都只是接口,它们的实现类分别是EditorImpl和 SharedPreferencesImpl。

SharedPreferencesImpl.java:

public Editor edit() {

    synchronized (mLock) {

        awaitLoadedLocked();//等待sp文件加载完成

    }

    return new EditorImpl();

}

可见,edit()方法同样需要等待sp文件加载完成,然后再进行EditImpl()的初始化。每次调用edit()方法都会实例化一个新的EditorImpl对象。所以在使用的时候要注意不要每次 put() 都去调用edit()方法,在封装SharedPreferences工具类的时候可能会犯这个错误。

接下来看看EditorImpl实现类:

SharedPreferencesImpl.java:

public final class EditorImpl implements Editor {

    private final Object mEditorLock = new Object();

    @GuardedBy("mEditorLock")

    private final Map<String, Object> mModified = new HashMap<>(); // 存储要修改的数据

    @GuardedBy("mEditorLock")

    private boolean mClear = false; // 清除标记

    @override

    public Editor putString(String key, String value){

        synchronized (mEditorLock) {

            mModified.put(key, value);

            return this;

        }

    }

    @override

    public Editor remove(String key) {

        synchronized (mEditorLock) {

            mModified.put(key, this);

            return this;

        }

    }

    @Override

    public Editor clear() {

        synchronized (mEditorLock) {

            mClear = true;

            return this;

        }

    }

    @Override

    public boolean commit() { } 

    @Override

    public boolean apply() { }

}

EditorImpl有两个成员变量 : mModified, mClear。mModified 是一个HashMap,存储了所有通过putXXX()方法添加的需要添加或者修改的键值对。mClear是清除标记,在clear()方法中会被置为 true。

所有的putXXX()方法都只是改变了mModified集合,当调用commit()或者apply()时才会去修改sp文件。下面分别看一下这两个方法。

④commit()

SharedPreferencesImpl.java:

@override

public boolean commit() {

    long startTime = 0;

    if (DEBUG) {

        startTime = System.currentTimeMillis();

    }

    // 先将 mModified 同步到内存

    MemoryCommitResult mcr = commitToMemory(); 

    // 再将内存数据同步到文件

    SharedPreferencesImpl.this. enqueueDiskWrite(mcr, null );

    try {

        mcr.writtenToDiskLatch.await(); // 等待写入操作完成

    } catch (InterruptedException e) {

        return false;

    } finally {

        if (DEBUG) {

            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration + " committed after " + (System.currentTimeMillis() - startTime) + " ms");

        }

    }

    notifyListeners(mcr); // 通知监听者,回调OnSharedPreferenceChangeListener

    return mcr.writeToDiskResult; // 返回写入操作结果

}

commit()的大致流程是:

(1)首先调用commitToMemory()方法同步mModified到内存中 

(2)然后调用enqueueDiskWrite()方法同步内存数据到sp文件中

(3)等待写入操作完成,并通知监听者

内存同步是commitToMemory()方法,写入文件是enqueueDiskWrite()方法。来详细看一下这两个方法。

private MemoryCommitResult commitToMemory() {

    long memoryStateGeneration;

    List<String> keysModified = null;

    Set<OnSharedPreferenceChangeListener> listeners = null;

    Map<String, Object> mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this. mLock) {

        // 在commit()写入本地文件过程中,会将mDiskWritesInFlight置为 1。写入过程尚未完成时,又调用了commitToMemory(),直接修改 mMap可能会影响写入结果,所以这里要对 mMap 进行一次深拷贝

        if (mDiskWritesInFlight > 0) {   

            mMap = new HashMap<String, Object>(mMap);

        }

        mapToWriteToDisk = mMap;

        mDiskWritesInFlight++;

        boolean hasListeners = mListeners.size() > 0;

        if (hasListeners) {

            keysModified = new ArrayList<String>();

            listeners = new HashSet<OnSharedPre ferenceChangeListener>(mListeners.keySet());

        }

        synchronized (mEditorLock) {

            boolean changesMade = false;

            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();

                // v == this 和 v == null 都表示删除此 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++;

            }

            memoryStateGeneration = mCurrentMemoryStateGeneration;

        }

    }

    return new MemoryCommitResult( memoryStateGeneration, keysModified, listeners, mapToWriteToDisk);

}

简单说,commitToMemory()方法会将所有需要改动的数据mModified和原sp文件数据mMap进行合并生成一个新的数据集合mapToWriteToDisk,从名字也可以看出来,这就是之后要写入文件的数据集。没错,SharedPreferences的写入都是全量写入,即使你只改动了其中一个配置项,也会重新写入所有数据。针对这一点,可以做的优化是,将需要频繁改动的配置项使用单独的sp文件进行存储,避免每次都要全量写入。

接下来是enqueueDiskWrite方法:

SharedPreferencesImpl.java:

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) {

                writeToFile(mcr, isFromSyncCommit); 

            }

            synchronized (mLock) {

               //当前更新操作的线程数-1

                mDiskWritesInFlight--;

            }

            if (postWriteRunnable != null) {

                postWriteRunnable.run();

            }

        }

    };

    // commit()直接在当前线程进行写入操作

    if (isFromSyncCommit) {

        boolean wasEmpty = false;

        synchronized (mLock) {

            //只有当前线程调用时,因为同步进行,所以一直为1;有多个线程同时调用时,会大于1

            wasEmpty = mDiskWritesInFlight == 1;

        }

        if (wasEmpty) {

            writeToDiskRunnable.run();//当前线程直接调用

            return;

        }

    }

    //  //异步调用,apply()方法执行此处,由 QueuedWork.QueuedWorkHandler处理

    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);

}

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

回头先看一下commit()方法中是如何调用 enqueueDiskWrite() 方法的:

SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);

第二个参数postWriteRunnable是null,所以 isFromSyncCommit为true,会执行上面的if代码块,而不执行QueuedWork.queue()。由此可见,commit()方法最后的写文件操作是直接在当前调用线程执行的,如果在主线程调用该方法,就会直接在主线程进行 IO 操作。显然,这是不建议的,可能造成卡顿或者 ANR。在实际使用中应该尽量使用apply()方法来提交数据。当然,apply()也并不是十全十美的,后面会提到。

commit()方法的最后一步了,将mapToWriteToDisk写入sp文件。而写入文件就是很简单的IO操作,只不过需要把Map转换为xml的格式。

private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {

    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;

        // 仅当磁盘状态比当前提交旧时才需要写入文件

        if (mDiskStateGeneration < mcr.memoryStateGeneration) {

            if (isFromSyncCommit) {

                needsWrite = true;

            } else {

                synchronized (mLock) {

                    if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {

                        needsWrite = true;

                    }

                }

            }

        }

        if (!needsWrite) { // 无需写入,直接返回

            mcr.setDiskWriteResult(false, true);

            return;

        }

        boolean backupFileExists = mBackupFile.exists(); // 备份文件是否存在

        // 如果备份文件不存在,将 mFile 重命名为备份文件,供以后遇到异常时使用

        if (!backupFileExists) {

            if (!mFile.renameTo(mBackupFile)) {

                Log.e(TAG, "Couldn't rename file " + mFile  + " to backup file " + mBackupFile);

                mcr.setDiskWriteResult(false, false);

                return;

            }

        } else {

            mFile.delete();

        }

    }   

    try {

        FileOutputStream str = createFileOutputStream(mFile);

        if (str == null) {

            mcr.setDiskWriteResult(false, false);

            return;

        }           

        XmlUtils.writeMapXml( mcr.mapToWriteToDisk, str); // 全量写入,将map数据写成xml格式到file中

        writeTime = System.currentTimeMillis();

        FileUtils.sync(str);

        str.close();

        ContextImpl.setFilePermissionsFromMode( mFile.getPath(), mMode, 0);

        try {

            final StructStat stat = Os.stat(mFile.getPath());

            synchronized (mLock) {

                mStatTimestamp = stat.st_mtim; // 更新文件时间

                mStatSize = stat.st_size; // 更新文件大小

            }

        } catch (ErrnoException e) {

        }

        // 写入成功,删除备份文件

        mBackupFile.delete();

        mDiskStateGeneration = mcr.memoryStateGeneration;

        // 返回写入成功,唤醒等待线程

        mcr.setDiskWriteResult(true, true);

        long fsyncDuration = fsyncTime - writeTime;

        mSyncTimes.add((int) fsyncDuration);

        mNumSync++;

        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, false); // 返回写入失败

}

⑤apply()

SharedPreferencesImpl.java:

@Override

public void apply() {

    final long startTime = System.currentTimeMillis();

    // 先将mModified同步到内存

    final MemoryCommitResult mcr = commitToMemory();

    final Runnable awaitCommit =new Runnable() {

        @Override

        public void run() {

            try {

                mcr.writtenToDiskLatch.await();

            } catch (InterruptedException ignored) {

            }

            if (DEBUG && mcr.wasWritten) {

                Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration + " applied after " + (System.currentTimeMillis() - startTime) + " ms");

            }

        }

    };

    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable=new Runnable(){

        @Override

        public void run() {

            awaitCommit.run();

            QueuedWork.removeFinisher( awaitCommit);

        }

    };

    SharedPreferencesImpl.this. enqueueDiskWrite(mcr, postWriteRunnable);

    notifyListeners(mcr);

}

同样也是先调用commitToMemory()同步到内存,再调用enqueueDiskWrite()同步到文件。和commit()不同的是,enqueueDiskWrite()方法的 Runnable 参数不再是null了,传进来一个postWriteRunnable,不为null时,走的方法就是异步方法。所以其内部的执行逻辑和 commit() 方法是完全不同的。commit() 方法会直接在当前线程执行 writeToDiskRunnable(),而 apply() 会由QueuedWork来处理:

QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); 

queue()方法的源码为:

Queue work.java:

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

        }

    }

}

这里的handler是一个全局的HandlerThread对象,handler所在的线程就是执行Runnable的线程了,也就是一个工作线程,所以apply()方法,会通过一个全局唯一的异步线程进行写文件的操作,任务还是一样的writeFile()方法。看一下 getHandler 源码:

QueuedWork.java:

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;

    }

}

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();

  }

}

可以看到,此时写sp文件的操作会异步执行在一个单独的线程上。

QueuedWork 除了执行异步操作之外,还有一个作用。它可以确保当 Activity onPause()/onStop() 之后,或者 BroadCast onReceive() 之后,异步任务可以执行完成。以 ActivityThread.java 中 handlePauseActivity() 方法为例:

@Override

public void handleStopActivity(IBinder token, boolean show, int configChanges, PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {

    final ActivityClientRecord r = mActivities.get(token);

    r.activity.mConfigChangeFlags |= configChanges;

    final StopInfo stopInfo = new StopInfo();

    performStopActivityInner(r, stopInfo, show, true, finalStateRequest, reason);

    updateVisibility(r, show);

    // 可能因等待写入造成卡顿甚至 ANR

    if (!r.isPreHoneycomb()) {

        QueuedWork.waitToFinish();

    }

    stopInfo.setActivity(r);

    stopInfo.setState(r.state);

    stopInfo.setPersistentState(r.persistentState);

    pendingActions.setStopInfo(stopInfo);

    mSomeActivitiesChanged = true;

}

初衷可能是好的,但是我们都知道在 Activity() 的 onPause()/onStop() 中不应该进行耗时任务。如果 sp 数据量很大的话,这里无疑会出现性能问题,可能造成卡顿甚至 ANR。

5.总结

①SharedPreferences的File创建和内容解析,在内存中是有缓存的

②SharedPreferences的提交,commit()方法是在当前线程完成,而apply()方法在全局唯一的一个工作线程中完成

③所有的文件和内存读写操作,都通过锁对象进行加锁,保证了多线程同步

④ShredPreferences是单例对象,第一次打开后,之后获取都无需创建,速度很快。

当第一次获取数据后,数据会被加载到一个缓存的Map中,之后的读取都会非常快。

当由于是XML<->Map的存储方式,所以,数据越大,操作越慢,get、commit、apply、remove、clear都会受影响,所以尽量把数据按功能拆分成若干份。

⑤同时执行这两句代码的时候,第一行代码所写的内容会被第二行代码取代。
editor.putInt("age", 20);
//覆盖key为age的数据,得到的结果:age = 32
editor.putInt("age", 32);


editor.putString("age", "20");
//覆盖key为age的数据,得到的结果:age = 32 (int类型)
editor.putInt("age", 32);

⑥执行以下代码会出现异常。
(指定key所保存的类型和读取时的类型不同)
editor.putInt("age", 32);//保存为int类型
String age = userInfo.getString("age", "null");//读取时为String类型,出现异常

⑦在这些动作之后,记得commit
editor.putInt("age", 20);//写入操作
editor.remove("age");   //移除操作
editor.clear();     //清空操作
editor.commit();//记得commit

同时,SharedPreferences的槽点也不少:

①不支持跨进程,MODE_MULTI_PROCESS 也没用。跨进程频繁读写可能导致数据损坏或丢失。

②初始化的时候会读取sp文件,可能导致后续 getXXX() 方法阻塞。建议提前异步初始化 SharedPreferences。

③sp文件的数据会全部保存在内存中,所以不宜存放大数据。

④edit()方法每次都会新建一个EditorImpl对象。建议一次 edit(),多次 putXXX() 。

⑤无论是commit()还是apply() ,针对任何修改都是全量写入。建议针对高频修改的配置项存在单独的sp文件中。

⑥commit()同步保存,有返回值。apply()异步保存,无返回值。按需取用。

⑦onPause() 、onReceive() 等时机会等待异步写操作执行完成,可能造成卡顿或者 ANR。

如果不需要跨进程,仅仅存储少量的配置项,SharedPreferences 仍然是一个很好的选择。

6.SharedPreferences缺点

google对SP的定义为轻量级存储,如果存储的数据少,使用起来没有任何问题,当需要存储数据比较多时,SP可能会导致以下问题:

①SP第一次加载数据时需要全量加载,当数据量大时可能会阻塞UI线程造成卡顿

②SP读写文件不是类型安全的,且没有发出错误信号的机制,缺少事务性API

③commit() / apply()操作可能会造成ANR问题:

commit()是同步提交,会在UI主线程中直接执行IO操作,当写入操作耗时比较长时就会导致UI线程被阻塞,进而产生ANR;apply()虽然是异步提交,但异步写入磁盘时,如果执行了Activity / Service中的onStop()方法,那么一样会同步等待SP写入完毕,等待时间过长时也会引起ANR问题。针对apply()展开来看一下:

SharedPreferencesImpl#EditorImpl.java中最终执行了apply()函数:

public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

public void apply() {

    final MemoryCommitResult mcr = commitToMemory();

    final Runnable awaitCommit = new Runnable(){

         public void run() {

             try {

                   mcr.writtenToDiskLatch.await();

               } catch (InterruptedException ignored) {

               }

          }

      };

    //8.0之前

   QueuedWork.add(awaitCommit);

   //8.0之后

   QueuedWork.addFinisher(awaitCommit);   

   //异步执行磁盘写入操作  SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

   //......

}

构造一个名为awaitCommit的Runnable任务并将其加入到QueuedWork中,该任务内部直接调用了CountDownLatch.await()方法,即直接在UI线程执行等待操作,那么需要看QueuedWork中何时执行这个任务。

QueuedWork类在Android8.0以上和8.0以下的版本实现方式有区别:

8.0之前QueuedWork.java:

public class QueuedWork {

    private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers = new ConcurrentLinkedQueue<Runnable>();

    public static void add(Runnable finisher) {

        sPendingWorkFinishers.add(finisher);

    }

    public static void waitToFinish() {

        Runnable toFinish;

        // 从队列中取出任务:如果任务为空,则跳出循环,UI线程可以继续往下执行;反之任务不为空,取出任务并执行,实际执行的CountDownLatch.await(),即UI线程会阻塞等待

        while ((toFinish = sPendingWorkFinishers.poll()) != null) {

            toFinish.run();

        }

    }

  //......

}

8.0之后QueuedWork.java:

public class QueuedWork {

   private static final LinkedList<Runnable> sFinishers = new LinkedList<>();  

   public static void waitToFinish() {

       Handler handler = getHandler();

       StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();

       try {

          //8.0之后优化,会主动尝试执行写磁盘任务

           processPendingWork();

       } finally {

           StrictMode.setThreadPolicy(oldPolicy);

       }

      try {

          while (true) {

              Runnable finisher;

              synchronized (sLock) {

                  //从队列中取出任务

                  finisher = sFinishers.poll();

              }

              //如果任务为空,则跳出循环,UI线程可以继续往下执行

              if (finisher == null) {

                  break;

              }

              //任务不为空,执行CountDownLatch.await(),即UI线程会阻塞等待

              finisher.run();

         }

     } finally {

         sCanDelay = true;

     }

  }

}

可以看到不管8.0之前还是之后,waitToFinish()都会尝试从Runnable任务队列中取任务,如果有的话直接取出并执行,直接看哪里调用了waitToFinish():

ActivityThread.java

private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {

  //......

  QueuedWork.waitToFinish();

}

private void handleStopService(IBinder token) {

  //......

  QueuedWork.waitToFinish();

}

省略了一些代码细节,可以看到在ActivityThread中handleStopActivity、handleStopService方法中都会调用waitToFinish()方法,即在Activity的onStop()中、Service的onStop()中都会先同步等待写入任务完成才会继续执行。

所以apply()虽然是异步写入磁盘,但是如果此时执行到Activity/Service的onStop(),依然可能会阻塞UI线程导致ANR。

猜你喜欢

转载自blog.csdn.net/zenmela2011/article/details/123910319