SharedPreferences

  • 简言
  • Shared Preferences
  • 使用步骤
  • API
  • Editor类API
  • SharedPreferences类API
  • 获取SharedPreferences源码解析
  • Context.getSharedPreferences
  • ContextImpl.getSharedPreferencesPath
  • ContextImpl.getSharedPreferences 重载方法
  • SharedPreferencesImpl
  • 获取SharedPreferences总结
  • 查询getXXX源码解析
  • 数据修改源码解析
  • SharedPreferences.edit
  • Editor
  • 数据提交源码解析
  • Editor.commit
  • Editor.commitToMemory
  • SharedPreferences.enqueueDiskWrite
  • commit总结
  • Editor.apply
  • 使用流程总结
  • commit和apply区别
  • 获取SharedPreferences和Editor区别
  • 使用注意
  • 封装Demo


简言

众所周知,数据存储在每个应用中都会用到,那所用到的技术应该怎么选呢,这里Android给开发者提供了几种方法去保存常用应用数据,至于你想选择哪一种方式,取决于你的特定需求;例如这个数据是本应用私有的还是跟其它应用共享以及数据存储所需的内存空间等

  • Shared Preferences:这种方式通常用来保存私有原始数据,以键值对形式存储;这也就意味着这些数据只能由本应用访问
  • Internal Storage:这种方式是将私有数据保存在内部存储(设备内存)中,实际上是使用文件流进行读写
  • External Storage:这种方式是将公共数据存储在共享外部存储上;也就是将数据存储在SD卡上,存储在这上面说明数据是开放的,其它应用可以直接访问;这跟上面一种都是平常所说的文件存储
  • SQLite Databases:这种方式是将结构化数据存储在私有数据库中;这也就是常说的数据库存储,使用SQLite保存数据,这些数据是私有的
  • Network Connection:这种方式是将数据存储在Web服务器上,也就是通常所说的网络存储

Shared Preferences

本文所含代码随时更新,可从这里下载最新代码
传送门Mango

这篇文章记录Shared Preferences的使用及从源码分析下它的工作逻辑

以下代码基于API24

SharedPreferences是Android平台提供的一个轻量级存储类,它本身是一个接口,实现类是SharedPreferencesImpl;它提供给开发者存储和检索一些基本数据类型的数据的方式,比如int,long,float,boolean,string等;这些数据在APP未被卸载前都可以访问;同时SharedPreferences对象本身只支持访问数据,修改和存储数据操作是由它的内部接口类Editor完成,其实现类是SharedPreferencesImpl内部类EditorImpl

SharedPreferences存储数据的方式是以键值对的形式保存在XML文件中,存储位置在/data/data/<包名>/shared_prefs目录下;不建议使用这种方式存储大批量的数据,会影响性能表现;最好使用它来存储一些配置信息,比如用户名,用户设置信息,解锁口令等数据

使用步骤

  • 使用Context对象获取SharedPreferences对象
  • 使用SharedPreferences的getXXX等方法获取数据
  • 使用SharedPreferences.edit()获取Editor对象
  • 使用Editor.putXXX等方法提交数据
  • 使用Editor.commit保存数据到文件中
Context context = MainActivity.this;
// 在/data/data/<包名>/shared_prefs目录下生成了一个Mango.xml文件,一个应用可以创建多个这样的xml文件
SharedPreferences sp = context.getSharedPreferences("Mango", Context.MODE_PRIVATE);
String va = sp.getString("blog","");//获取数据

SharedPreferences.Editor edit = sp.edit();
edit.putString("blog","");
edit.putInt("years",2);
edit.putBoolean("master", true);
edit.commit();//保存数据 或者使用edit.apply()

其生成的XML文件如下

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="blog"></string>
    <int name="year" value="2" />
    <boolean name="master" value="true" />
</map>

API

  • getSharedPreferences(String name, int mode):获取SharedPreferences实例,第一个参数是XML文件名称,第二个参数是文件创建模式,有如下可选值
  • Context.MODE_PRIVATE:默认模式,其中创建的文件只能由调用应用程序(或共享相同用户ID的所有应用程序)访问
  • Context.MODE_WORLD_READABLE:允许所有其他应用程序对创建的文件具有读的访问权限,使用这个模式Android N(7.0)开始将抛出SecurityException,这个模式已标记被弃用,创建全局可读文件非常危险,可能引发安全漏洞,应用应该使用更安全正式的交互机制,比如ContentProvider,BroadcastReceiver和android.app.Service
  • Context.MODE_WORLD_WRITEABLE:允许所有其他应用程序具有对创建文件的写访问权,,使用这个模式Android N(7.0)开始将抛出SecurityException,这个模式已标记被弃用,创建全局可写文件非常危险,可能引发安全漏洞,应用应该使用更安全正式的交互机制,比如ContentProvider,BroadcastReceiver和android.app.Service
  • Context.MODE_MULTI_PROCESS:多进程模式,每次getSharedPreferences过程, 会检查XML文件上次修改时间和文件大小, 一旦有修改则会重新从磁盘加载文件,强烈不推荐此模式,暂时还没被弃用,后续将会被谷歌抛弃,因为进程间有更好的通信方式

Editor类API

  • putString(String key, @Nullable String value):将一个String值添加到editor,后续可通过key找到这个String
  • putInt(String key, int value):同上,只不过value变成了int型数
  • putLong(String key, long value):同上
  • putFloat(String key, float value):同上
  • putBoolean(String key, boolean value):同上
  • putStringSet(String key, @Nullable Set< String> values):可以看到editor还可以添加一个Set集合
  • remove(String key):根据key将上面添加的value去除
  • clear():将构建SharedPreferences 对象时指定的xml文件中的内容清空
  • commit():将editor所添加的内容写到XML文件中,操作成功返回true,反之返回false;如果同时有两个editor操作,最后一个会成功;如果不需要返回值且在主线程使用,请调用apply方法
  • apply():该方法也是将editor内容写到XML文件中,但是它与commit方法不同,具体看下方

SharedPreferences类API

  • edit():获取Editor实例,用来保存数据
  • getString(String key, @Nullable String defValue):根据key获取Editor类put进去的值,第二个参数是默认值
  • getInt(String key, int defValue):同上
  • getLong(String key, long defValue):同上
  • getFloat(String key, float defValue):同上
  • getBoolean(String key, boolean defValue):同上
  • getStringSet(String key, @Nullable Set defValues):同上
  • contains(String key):判断XML文件中是否包含这个key对应的数据

获取SharedPreferences源码解析

Context.getSharedPreferences

我们从getSharedPreferences开始入手,具体实现在ContextImpl类

//记录/data/data/<包名>/shared_prefs目录下所有xml文件,key是文件名,value是文件
	private ArrayMap<String, File> mSharedPrefsPaths;
		
	@Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        // 在低于API19的版本,如果name为null,就重置name
        if (mPackageInfo.getApplicationInfo().targetSdkVersion <
                Build.VERSION_CODES.KITKAT) {
            if (name == null) {
                name = "null";
            }
        }

        File file;//获取指定的xml文件
        //可以看出来获取SharedPreferences对象是同步操作
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
            	//存放文件名-文件的Map
                mSharedPrefsPaths = new ArrayMap<>();
            }
            //先从mSharedPrefsPaths查询是否存在相应文件
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
            	//第一次进来肯定是空的,需要去实例化
                file = getSharedPreferencesPath(name);
                //添加到Map
                mSharedPrefsPaths.put(name, file);
            }
        }
        //调用另外一个重载方法
        return getSharedPreferences(file, mode);
    }
ContextImpl.getSharedPreferencesPath
//SharedPreferences文件所在目录,/data/data/<包名>/shared_prefs
	File mPreferencesDir;

	@Override
    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);//直接根据父文件夹和文件名创建文件返回
        }
        throw new IllegalArgumentException(
                "File " + name + " contains a path separator");
    }
    
    private File getPreferencesDir() {
        synchronized (mSync) {
            if (mPreferencesDir == null) {
            	//创建xml文件的父文件夹
                mPreferencesDir = new File(getDataDir(), "shared_prefs");
            }
            return ensurePrivateDirExists(mPreferencesDir);
        }
    }
    
    @Override
    public File getDataDir() {
        if (mPackageInfo != null) {
            File res = null;
            //根据包名创建文件
            if (isCredentialProtectedStorage()) {
                res = mPackageInfo.getCredentialProtectedDataDirFile();
            } else if (isDeviceProtectedStorage()) {
                res = mPackageInfo.getDeviceProtectedDataDirFile();
            } else {
                res = mPackageInfo.getDataDirFile();
            }

            if (res != null) {
                if (!res.exists() && android.os.Process.myUid() == android.os.Process.SYSTEM_UID) {
                    Log.wtf(TAG, "Data directory doesn't exist for package " + getPackageName(),
                            new Throwable());
                }
                return res;
            } else {
                throw new RuntimeException(
                        "No data directory found for package " + getPackageName());
            }
        } else {
            throw new RuntimeException(
                    "No package details found for package " + getPackageName());
        }
    }
    
    private static File ensurePrivateDirExists(File file) {
    	//确保应用程序目录存在
        if (!file.exists()) {
            try {
                Os.mkdir(file.getAbsolutePath(), 0771);
                Os.chmod(file.getAbsolutePath(), 0771);
            } catch (ErrnoException e) {
                if (e.errno == OsConstants.EEXIST) {
                    // We must have raced with someone; that's okay
                } else {
                    Log.w(TAG, "Failed to ensure " + file + ": " + e.getMessage());
                }
            }
        }
        return file;
    }

从这一段可以看出来这些XML文件保存在/data/data/<包名>/shared_prefs下面

ContextImpl.getSharedPreferences 重载方法

//以包名为key, 二级key是以SP文件名, 以SharedPreferencesImpl为value的嵌套map结构. sSharedPrefsCache是静态类成员变量, 每个进程保存唯一一份, 且由ContextImpl.class锁保护
	//这里要记住每个xml文件对应一个SharedPreferencesImpl对象,后续对这个文件的修改由它负责
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
		
		@Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        checkMode(mode);
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);//根据文件获取对应的SharedPreferencesImpl对象,第一次为null,需要实例化
            if (sp == null) {
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);//保存到Map
                return sp;//返回给开发者
            }
        }
        //当开发者设置了多进程模式, 则当文件被其他进程改变时,则会重新加载
        if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }
    
    
    private void checkMode(int mode) {
    		//在Android7.0及以后,使用如下两种文件模式将会抛出异常
        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");
            }
        }
    }
    
    private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
        if (sSharedPrefsCache == null) {
            sSharedPrefsCache = new ArrayMap<>();
        }

        final String packageName = getPackageName();//获取包名
        //通过包名获取map,第一次肯定为null
        ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
        if (packagePrefs == null) {
            packagePrefs = new ArrayMap<>();
            sSharedPrefsCache.put(packageName, packagePrefs);
        }

        return packagePrefs;
    }

从以上代码可以得出

  • 在Android7.0及以后不要设置成 MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE模式
  • SharedPreferencesImpl实例全局只实例一次,保存一份

上面代码看完后,可以知道开发者已经获取了指定的XML文件对应的SharedPreferences对象,但是假如你在某次获取前,里面已经保存数据了,那你这次获取的时候文件里的内容什么时候加载进去的呢?因为目前的代码只看到了创建文件的过程,没有看到获取文件内容的代码,不着急,继续看上面有一段代码new SharedPreferencesImpl,就是实例化每个文件对应的SharedPreferencesImpl

SharedPreferencesImpl

//保存xml文件中的内容
	private Map<String, Object> mMap

	//实例化SharedPreferencesImpl对象
    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        //创建.bak备份文件 用于发生异常时, 可通过备份文件来恢复数据
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        //开启线程加载指定文件数据
        startLoadFromDisk();
    }
    
    static File makeBackupFile(File prefsFile) {
        return new File(prefsFile.getPath() + ".bak");
    }
    
    private void startLoadFromDisk() {
    
        synchronized (this) {
            mLoaded = false;//用于标记xml文件是否已加载到内存中
        }
        //创建线程去实现从磁盘加载xml文件内容的工作
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }

    private void loadFromDisk() {
        synchronized (SharedPreferencesImpl.this) {
            if (mLoaded) {//如果这个xml文件已经加载过了,直接返回
                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);//解析Xml文件流
                } catch (XmlPullParserException | IOException e) {
                    Log.w(TAG, "getSharedPreferences", e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            /* ignore */
        }

        synchronized (SharedPreferencesImpl.this) {
            mLoaded = true;//设置已经加载
            if (map != null) {
                mMap = map;//解析出来的信息保存到mMap,后续操作都是操作mMap
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<>();
            }
            notifyAll();//唤醒其它阻塞的线程
        }
    }

到这里获取SharedPreferences的过程就结束了

获取SharedPreferences总结

  • 首次使用需要创建相应的xml文件,文件目录是/data/data/<包名>/shared_prefs/name.xml
  • 在Android7.0及以后不要设置 MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE模式
  • 创建每个文件对应的SharedPreferencesImpl对象,这个全局只保存一份
  • 解析XML文件中的内容是异步的,此时进行getXXX和putXXX操作会进入阻塞,直到解析完成

查询getXXX源码解析

查询方法都是类似的,这里以getString(String key, @Nullable String defValue)为例

@Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (this) {//获取锁
            awaitLoadedLocked();//检查xml数据是否加载完成
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }
    
private void awaitLoadedLocked() {
    if (!mLoaded) {
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }
    while (!mLoaded) {
        try {
            wait(); //当没有加载完成,则进入等待状态
        } catch (InterruptedException unused) {
        }
    }
}

从这里看出,当loadFromDisk方法没有执行完成, 则会阻塞查询操作;当数据加载完成, 则直接从mMap来查询相应数据


数据修改源码解析

从上面的使用步骤一节中可以知道,任何put操作前都需要先获取Editor对象

SharedPreferences.edit

public Editor edit() {
        // 检查xml数据是否加载完成
        synchronized (this) {
            awaitLoadedLocked();
        }
				//实例化EditorImpl对象
        return new EditorImpl();
    }

Editor只是个接口,具体实现类是EditorImpl,再看这个类有关数据修改的方法

Editor

private final Map<String, Object> mModified = Maps.newHashMap();
    private boolean mClear = false;
		
		//插入数据
    public Editor putString(String key, @Nullable String value) {
        synchronized (this) {
            //插入数据, 先暂存到mModified对象
            mModified.put(key, value);
            return this;
        }
    }
    //移除数据
    public Editor remove(String key) {
        synchronized (this) {
            mModified.put(key, this);
            return this;
        }
    }

    //清空全部数据
    public Editor clear() {
        synchronized (this) {
            mClear = true;
            return this;
        }
    }

这些方法表明调用到这里,还只是修改mModified ,即只在应用内存中操作,还没有操作到XML文件


数据提交源码解析

数据提交有commit和apply两个方法,属于Editor类

Editor.commit

public boolean commit() {
			//将数据同步到内存中即mMap
            MemoryCommitResult mcr = commitToMemory();
            //将数据同步到磁盘文件中即xml文件
            SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
            try {
            	//等待同步操作结束
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException e) {
                return false;
            }
            //如果注册了监听,则在主线程回调onSharedPreferenceChanged()方法
            notifyListeners(mcr);
            //返回执行结果
            return mcr.writeToDiskResult;
}
Editor.commitToMemory
private MemoryCommitResult commitToMemory() {
            MemoryCommitResult mcr = new MemoryCommitResult();
            synchronized (SharedPreferencesImpl.this) {
                if (mDiskWritesInFlight > 0) {
                    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) {
                		//如果执行了clear方法,那就在这里清空内存中的mMap
                    if (mClear) {
                        if (!mMap.isEmpty()) {
                            mcr.changesMade = true;
                            mMap.clear();
                        }
                        mClear = false;
                    }

										//对Editor中的mModified进行遍历,这个map保存了用户添加的数据
                    for (Map.Entry<String, Object> e : mModified.entrySet()) {
                        String k = e.getKey();
                        Object v = e.getValue();
                        // 如果value是‘this’,这是一个特殊的值,不能用,需要移出,请大家注意
                        if (v == this || v == null) {
                            if (!mMap.containsKey(k)) {
                                continue;
                            }
                            mMap.remove(k);
                        } else {
                            if (mMap.containsKey(k)) {
                                Object existingValue = mMap.get(k);
                                //如果value已经存在,就不再添加了
                                if (existingValue != null && existingValue.equals(v)) {
                                    continue;
                                }
                            }
                            mMap.put(k, v);
                        }

                        mcr.changesMade = true;//changesMade代表数据是否有改变
                        if (hasListeners) {
                            mcr.keysModified.add(k);//记录发生改变的key
                        }
                    }
										//清空editor中的mModified数据,防止下次重复添加
                    mModified.clear();
                }
            }
            return mcr;
        }
SharedPreferences.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--;
                    }
                    //此时postWriteRunnable为null不执行该方法
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

        final boolean isFromSyncCommit = (postWriteRunnable == null);

        //commit方法会进入该分支
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (SharedPreferencesImpl.this) {
            		//commitToMemory过程会加1,则wasEmpty=true
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
            		//那就执行线程吧
                writeToDiskRunnable.run();
                return;
            }
        }
		//commit操作不执行该方法,上面已经return
        QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
    }

private void writeToFile(MemoryCommitResult mcr) {
    if (mFile.exists()) {
        if (!mcr.changesMade) { //没有key发生改变, 则直接返回
            mcr.setDiskWriteResult(true);
            return;
        }
        if (!mBackupFile.exists()) {
            //当备份文件不存在, 则把mFile重命名为备份文件
            if (!mFile.renameTo(mBackupFile)) {
                mcr.setDiskWriteResult(false);
                return;
            }
        } else {
            mFile.delete(); //否则,直接删除mFile
        }
    }

    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) {
            ...
        }
        //写入成功, 则删除备份文件
        mBackupFile.delete();
        //返回写入成功, 唤醒等待线程
        mcr.setDiskWriteResult(true);
        return;
    } catch (XmlPullParserException e) {
        ...
    } catch (IOException e) {
        ...
    }
    //如果写入文件的操作失败, 则删除未成功写入的文件
    if (mFile.exists()) {
        if (!mFile.delete()) {
            ...
        }
    }
    //返回写入失败, 唤醒等待线程
    mcr.setDiskWriteResult(false);
}

至此数据提交操作结束

commit总结
  • 先通过commitToMemory方法将数据从EditorImpl更新到SharedPreferences,也就是将 mModified数据添加到mMap
  • 在该方法中 将mMap信息赋值给mapToWriteToDisk, 并mDiskWritesInFlight加1
  • 当mClear为true, 则直接清空mMap
  • 当value值为this或null, 则移除相应的key
  • 当value值发生改变, 则会更新到mMap
  • 只要有key/value发生改变(新增, 删除), 则设置mcr.changesMade = true
  • 清空EditorImpl中的mModified数据
  • 再通过enqueueDiskWrite#方法将数据写入到磁盘的xml文件中
  • 当没有key发生改变, 则直接返回
  • 将mMap全部信息写入文件, 如果写入成功则删除备份文件,如果写入失败则删除mFile
  • 要注意commit操作是同步的,会等到数据写入到磁盘以后才会返回

Editor.apply

public void apply() {
			//将数据同步到内存中即mMap
            final MemoryCommitResult mcr = commitToMemory();
            //新起线程
            final Runnable awaitCommit = new Runnable() {
                    public void run() {
                        try {
                        	//进入等待状态
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }
                    }
                };

			//将awaitCommit添加到QueuedWork
            QueuedWork.add(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    public void run() {
                    	//执行awaitCommit
                        awaitCommit.run();
                        //从QueuedWork移除
                        QueuedWork.remove(awaitCommit);
                    }
                };
			//将数据同步到磁盘文件中即xml文件
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

            // 如果注册了监听,则在主线程回调onSharedPreferenceChanged()方法
            notifyListeners(mcr);
        }
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--;
                    }
                    //此时postWriteRunnable不为null执行该方法
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

        ......
        
		//将任务放入单线程的线程池来执行
        QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
    }

QueuedWork是一个线程池,而且只有一个核心线程,提交的任务到会加入到一个等待队列中按照顺序执行

可见apply跟commit的最大区别 commit发生在UI线程而apply发生在工作线程(apply的写入文件操作是在单线程的线程池来完成)


使用流程总结

当我们首次创建SharedPreferences对象时,会根据文件名将文件下内容一次性加载到mMap容器中,每当我们edit都会创建一个新的EditorImpl对象,当修改或者添加数据时会将数据添加到mModifiled容器中,然后commit或者apply操作比较mMap与mModifiled数据,修正mMap中最后一次提交数据,然后写入到文件中;而get直接从mMap中读取

commit和apply区别

  • apply没有返回值, commit有返回值能知道修改是否提交成功
  • commit方法是同步提交到磁盘文件,所以可以有返回值,同时也说明如果添加内容太多,有出现ANR的可能;apply方法是异步提交到磁盘文件,所以没有返回值,如果一次apply操作还没结束又来了一次commit操作,那commit操作会阻塞,直到apply完成才会执行
  • 多并发的提交commit时,需等待正在处理的commit数据更新到磁盘文件后才会继续往下执行,从而降低效率; 而apply只是原子更新到内存,后调用apply函数会直接覆盖前面内存数据,从一定程度上提高很多效率

获取SharedPreferences和Editor区别

  • edit()方法每次都是创建新的EditorImpl对象
  • getSharedPreferences()方法是从ContextImpl.sSharedPrefsCache获取唯一的对象

使用注意

  • 强烈建议不要使用SharedPreferences存储大的key/values,以减少卡顿/anr;同时这些key/value都是存储在内存中的,不能及时得到释放,内存使用过高会频发引发GC,导致丢帧卡顿
  • 不要频繁使用apply和commit,尽量批量提交
  • 尽量使用Context.MODE_PRIVATE模式,不要使用其它三种
  • 不要把所有的数据都写到同一个文件中,减少同步锁竞争,影响读取速度
  • 不要频繁调用edit(),耗费内存
  • 尽量不要存放json和html,这种可以直接文件缓存

这里还有一个问题:某些情况为了避免出现卡顿情况,我们会使用apply代替commit,但是apply是否就一定安全呢?

答案: 不是

如果大家研究过ActivityThread这个类的话(笔者前面有文章分析过这个类),应该了解在Activity执行onStop方法的时候,是由ActivityThread的handleStopActivity回调的

private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {
		......
        // Make sure any pending writes are now committed.
        if (!r.isPreHoneycomb()) {
            QueuedWork.waitToFinish();
        }

	......
    }
public class QueuedWork {

    // The set of Runnables that will finish or wait on any async
    // activities started by the application.
    private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =
            new ConcurrentLinkedQueue<Runnable>();
 }

这里有这么一句话QueuedWork.waitToFinish(),干什么用的呢,看名字也知道,在等QueuedWork线程池中的线程执行完毕;上面在说apply的时候,它会使用QueuedWork添加工作线程到这个单线程池去,由于该线程池队列是串行执行;这样当我们关闭Activity时,会检查sPendingWorkFinishers队列中任务是否已经全部执行完成,否则一直等到全部执行完成。如果此时等待超过5s,等待你的将是无穷的罪孽啊;所以建议不要频繁的apply!!!

封装Demo

鉴于上面的一系列分析,就可以封装一个合适的工具类来正确使用SharedPreferfences了

/**
 * @Description TODO(SharedPreferences使用封装)
 * @author cxy
 * @Date 2018/10/30 10:07
 */
public class SharedPTools {

    private String TAG = SharedPTools.class.getSimpleName();

    private static SharedPTools instance;
    //避免内存泄漏
    private WeakReference<Context> mContext ;
    /**
     * 防止直接使用时无限new Editor 消耗内存 避免内存泄漏
     * 退出应用时调用 {@link #clearCache()}
     */
    private Map<SharedPreferences,WeakReference<SharedPreferences.Editor>> mEditorCache = new HashMap<>();

    public SharedPTools(Context context) {
        mContext = new WeakReference<>(context);
    }
    public static SharedPTools getInstance(Context context){
        if (instance == null) {
            instance = new SharedPTools(context);
        }
        return instance;
    }

    private SharedPreferences.Editor editor(SharedPreferences sp){
        WeakReference<SharedPreferences.Editor> weak = mEditorCache.get(sp);
        SharedPreferences.Editor edit ;
        if (weak == null) {
            edit = sp.edit();
            weak = new WeakReference<>(edit);
            mEditorCache.put(sp,weak);
        }
        return weak.get();
    }

    private SharedPreferences getSharedP(String key){
        return mContext.get().getSharedPreferences(key, Activity.MODE_PRIVATE);
    }


    /**==========================================================华丽丽分割线======================================================**/

    /**
     * 从SharedPreferences文件中获取值
     * @param key xml文件名
     * @param valuekey 与值对应的key
     * @param defaultObject 默认值
     * @return
     */
    public Object getValue(String key,String valuekey,Object defaultObject){
        if (defaultObject instanceof String)
            return getSharedP(key).getString(valuekey, (String) defaultObject);
        else if (defaultObject instanceof Boolean)
            return getSharedP(key).getBoolean(valuekey, (Boolean) defaultObject);
        else if (defaultObject instanceof Integer)
            return getSharedP(key).getInt(valuekey, (Integer) defaultObject);
        else if (defaultObject instanceof Float)
            return getSharedP(key).getFloat(valuekey, (Float) defaultObject);
        else if (defaultObject instanceof Long)
            return getSharedP(key).getLong(valuekey, (Long) defaultObject);
        else
            return null;
    }

    /**
     * 往SharedPreferences文件中添加值
     * @param key xml文件名
     * @param valuekey 与值对应的key
     * @param value 要添加的值
     * @return
     */
    public SharedPTools putValue(String key,String valuekey,Object value){
        if (value instanceof String)
            editor(getSharedP(key)).putString(valuekey, (String) value);
        else if (value instanceof Boolean)
            editor(getSharedP(key)).putBoolean(valuekey, (Boolean) value);
        else if (value instanceof Integer)
            editor(getSharedP(key)).putInt(valuekey, (Integer) value);
        else if (value instanceof Float)
            editor(getSharedP(key)).putFloat(valuekey, (Float) value);
        else if (value instanceof Long)
            editor(getSharedP(key)).putLong(valuekey, (Long) value);
        return this;
    }

    public SharedPTools remove(String key, String valuekey){
        editor(getSharedP(key)).remove(valuekey);
        return this;
    }

    public SharedPTools clear(String key){
        editor(getSharedP(key)).clear();
        return this;
    }

    public boolean contains(String key, String valuekey){
        return getSharedP(key).contains(valuekey);
    }

    public boolean commit(String key){
        return editor(getSharedP(key)).commit();
    }

    public void apply(String key){
        editor(getSharedP(key)).apply();
    }

    /**
     * 清空内存
     */
    public void clearCache(){
        for(WeakReference<SharedPreferences.Editor> weak : mEditorCache.values()){
            weak.clear();
        }
        mEditorCache.clear();
    }

}

在Activity中使用样例:

String value = (String) SharedPTools.getInstance(this).getValue("key","m","none");

SharedPTools.getInstance(this)
			.putValue("key","m","ma"+Math.random()*10)
            .putValue("key","b","ma"+Math.random()*10)
            .commit("key");

SharedPTools.getInstance(this).putValue("key","m","ma"+Math.random()*10);
SharedPTools.getInstance(this).putValue("key","b","ma"+Math.random()*10);
SharedPTools.getInstance(this).commit("key");