前(fei)言(hua)

SharedPreference(简称SP)是Android上一种非常易用的轻量级存储方式。SP采用key-value(键值对)形式来存储数据,最终以xml格式的文件来持久化到存储介质上。默认的存储位置在/data/data/<包名>/shared_prefs目录下,当用户卸载此应用程序或者清除应用数据时,SP保存的数据会一并清除。

运用场景

  • 保存应用的偏好设置(登录状态,业务的开关,临时标志位等);
  • 保存格式简单的,数据量较小的数据信息;

使用步骤

获取SharedPreferences对象

  • PreferenceManager.java

PreferenceManager.getDefaultSharedPreferences(context)

  • ContextImpl.java
@Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        //....此处省略若干行 
        return getSharedPreferences(file, mode);
    }

    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
         //....此处省略若干行 
        return sp;
    }

复制代码

name:要保存的文件名
mode:Context.MODE_PRIVATE:默认操作模式,代表该文件是私有数据,只能被应用本身访问;Context.MODE_APPEND:该模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件。

获取Sharedpreferences.Editor对象

SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
 SharedPreferences.Editor editor = sp.edit();
复制代码

通过Sharedpreferences.Editor接口的putXxx方法保存键值对

editor.putString("author", "wragony");
editor.putInt("year", 2018);
//....
复制代码

通过Sharedpreferences.editor接口的commit()方法或apply()方法保存键值对

editor.commit();
//editor.apply();

复制代码

完整示例

public void spDemo(Context context) {

    SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
    SharedPreferences.Editor editor = sp.edit();
    editor.putString("author", "wragony");
    editor.putInt("year", 2018);
    editor.commit();

    String author = sp.getString("author", "");
    int year = sp.getInt("year", 0);
    Log.d("SP-DEMO", String.format("author:%s,year:%d", author, year));
}

复制代码

查看Sharedpreferences保存的xml文件内容



源(zhuang)码(bi)分(kai)析(shi)

篇幅有些过长,关键点写在了注释里;

1.获取SharedPreferences对象流程

获取SharedPreferences对象的几种方式最终都会调用ContextImpl.java 中的 getSharedPreferences(String name, int mode) 方法;

【1】 public SharedPreferences getSharedPreferences(String name, int mode) //ContextImpl.java

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    // 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";
        }
    }

    File file;
    synchronized (ContextImpl.class) {
        //如果SP文件路径的缓存为null则创建一个Map用于缓存SP文件路径
        if (mSharedPrefsPaths == null) {
            mSharedPrefsPaths = new ArrayMap<>();
        }
        //先从缓存SP文件路径查找否存在相应文件
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            //如果文件不存在, 则创建新的文件,并放入缓存中
            file = getSharedPreferencesPath(name);
            mSharedPrefsPaths.put(name, file);
        }
    }
    //详见【2】中的注释
    return getSharedPreferences(file, mode);
}
复制代码

【2】 public SharedPreferences getSharedPreferences(File file, int mode) //ContextImpl.java

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    //实例的构造被 synchronized 关键字包裹, 因此构造过程是多线程安全的
    synchronized (ContextImpl.class) {
     
        //先从缓存中获取SharedPreferencesImpl 对象
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
          //如果缓存未命中, 构造SharedPreferencesImpl对象
            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对象 详见【3】
            sp = new SharedPreferencesImpl(file, mode);
            
            // 将构造的SharedPreferencesImpl对象放入缓存中
            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;
}

复制代码

【3】 SharedPreferencesImpl(File file, int mode) //SharedPreferencesImpl.java

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

private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    //开启子线程从磁盘文件读取
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
        	  //详见【4】
            loadFromDisk();
        }
    }.start();
}

复制代码

【4】private void loadFromDisk() //SharedPreferencesImpl.java

private void loadFromDisk() {
        synchronized (mLock) {
            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");
        }

        Map map = null;
        StructStat stat = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                  //从文件读取并解析xml为map对象
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16*1024);
                    map = XmlUtils.readMapXml(str);
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            /* ignore */
        }

        synchronized (mLock) {
           // 标记为已加载状态
            mLoaded = true;
            if (map != null) {
                //将文件信息保存到mMap缓存中
                mMap = map;
                // 更新xml文件修改时间及文件大小
                mStatTimestamp = stat.st_mtim;
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<>();
            }
            //唤醒处于等待状态的线程
            mLock.notifyAll();
        }
    }
    
复制代码
2.获取数据 getXxx() 的流程

【4】getString(String key, @Nullable String defValue) //SharedPreferencesImpl.java

可见, 所有的 get 操作都是线程安全的. 并且 get 仅仅是从内存中(mMap) 获取数据, 所以无性能问题.

@Nullable
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        //阻塞等待异步加载线程加载完成
        awaitLoadedLocked();
        //获取map中key对应的string
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

复制代码

考虑到 配置文件的加载是在单独的线程中异步进行的(参考 ‘SharedPreferences 的构造’), 所以这里的 awaitLoadedLocked 是在等待配置文件加载完毕. 也就是说如果我们第一次构造 SharedPreferences 后就立刻调用 getXxx 方法, 很有可能读取配置文件的线程还未完成, 所以这里要等待该线程做完相应的加载工作. 来看看awaitLoadedLocked 的源码:

【5】awaitLoadedLocked() //SharedPreferencesImpl.java

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 {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
}

复制代码

如果加载还未完成(mLoaded == false), getXxx 会卡在 awaitLoadedLocked, 一旦加载配置文件的线程工作完毕, 则这个加载线程会通过 notifyAll 会通知所有在 awaitLoadedLocked 中等待的线程, getXxx 就能够返回了. 不过大部分情况下, mLoaded == true. 这样的话 awaitLoadedLocked 会直接返回

3.存储数据 putXxx() 的流程

【6】putString(String key, @Nullable String value) //SharedPreferencesImpl.EditorImpl.java

public final class EditorImpl implements Editor {
    private final Object mLock = new Object();

    @GuardedBy("mLock")
    private final Map<String, Object> mModified = Maps.newHashMap();

    @GuardedBy("mLock")
    private boolean mClear = false;

    public Editor putString(String key, @Nullable String value) {
       //加了一个EditorImpl对象的同步锁,主要用来控制mModified的单线程操作
        synchronized (mLock) {
            mModified.put(key, value);
            return this;
        }
    }
  
     // 此处省略若干行代码....
    
    public Editor clear() {
	    synchronized (mLock) {
	        //修改mClear标志为true,在后续 commit及apply操作的时候,修改内存数据 commitToMemory 方法中会使用到
	        mClear = true;
	        return this;
	    }
	}
	 // 此处省略若干行代码....
	
  }
    
复制代码

EditorImpl中创建了mModified缓存用于保存将被put的数据;mClear 表示是否要清空配置

可见,在调用putXxx() 方法之前,需要先获取Editor对象,SP则通过edit()方法获取Editor对象,源码如下:

【7】edit() //SharedPreferencesImpl.java

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 (mLock) {
       //等待异步加载线程完成加载
        awaitLoadedLocked();
    }

    return new EditorImpl();
}

复制代码

可以看到 putXxx()方法并没有真正的将数据保存到磁盘文件,只是临时保存了自身mModified中,只有最终执行commit()或者apply()同步数据到SharedPreferences中的map,随后同步或者异步提交数据到磁盘文件。下面请看commit()或者apply()的源码:

【8】apply() //SharedPreferencesImpl.EditorImpl.java

public void apply() {
    final long startTime = System.currentTimeMillis();

    //详见【10】
    final MemoryCommitResult mcr = commitToMemory();
   
    final Runnable awaitCommit = new Runnable() {
            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() {
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };

   // 调用enqueueDiskWrite方法执行文件写入操作,详见【11】
    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);
}
    
复制代码

【9】commit() //SharedPreferencesImpl.EditorImpl.java

public boolean commit() {
    long startTime = 0;

    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }
   //与apply方法一样也是先提交内存
    MemoryCommitResult mcr = commitToMemory();

    SharedPreferencesImpl.this.enqueueDiskWrite(  mcr, null /* sync write on this thread okay */);
    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);
    return mcr.writeToDiskResult;
}


复制代码

【10】commitToMemory() //SharedPreferencesImpl.EditorImpl.java

MemoryCommitResult正如它的命名一样,是表示内存提交结果的数据结构,用于标注是否有数据改变、改变的key列表、监听器、需要写入文件的数据、写文件结果等。

// 将Editor的数据同步到SharedPreferences的缓存map中,它返回的是MemoryCommitResult类型的结果
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
    long memoryStateGeneration;
    List<String> keysModified = null;
    Set<OnSharedPreferenceChangeListener> listeners = null;
    Map<String, Object> mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {
        // 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);
        }
        //将内存中的mMap赋值给要写到disk的Map
        mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;

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

        synchronized (mLock) {
            boolean changesMade = false;

			  // 前面调用了clear方法设置了需要clear
            if (mClear) {
                if (!mMap.isEmpty()) {
                    changesMade = true;
                    mMap.clear();
                }
                mClear = false;
            }
           //遍历mModified,将remove的数据移除,将新数据放进去
            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;
                        }
                    }
                     //把变化和新加的数据更新到SharePreferenceImpl的mMap中
                    mMap.put(k, v);
                }

                changesMade = true;
                if (hasListeners) {
                    keysModified.add(k);
                }
            }
          //清空Editor中临时缓存数据
            mModified.clear();

            if (changesMade) {
                mCurrentMemoryStateGeneration++;
            }

            memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
    }
    return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
            mapToWriteToDisk);
}
    
复制代码

【11】enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable)

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    //是否同步提交,也就是是否调用了commit()方法                        
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    final Runnable writeToDiskRunnable = new Runnable() {
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };

//commit()提交的方式即同步提交
    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
           //当前只有一个写操作
            wasEmpty = mDiskWritesInFlight == 1;
        }
        //直接执行写入操作,注意:这里直接调用了run方法,并没有调用线程的start方法,所以这里是同步提交
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
	//异步的方式,则加入工作队列异步执行写入操作
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
    
复制代码

总结

  • commit()与apply()的区别
    (1)commit 操作是把全部数据更新到文件,同步执行的,所以要确保putXxxt(key,value)里的value数据量不要太大;
    (2)apply 操作是将修改先提交到内存,再异步执行写入文件;
    (3)多并发的提交commit时,需等待正在处理的commit数据更新到磁盘文件后才会继续往下执行,从而降低效率; 而apply只是原子更新到内存,后调用apply函数会直接覆盖前面内存数据,从一定程度上提高很多效率;
  • Sharedpreferences保存的数据不宜过大,如果你的sp文件比较大,那么会带来两个严重问题:
    (1)第一次从sp中获取值的时候,有可能阻塞主线程,使界面卡顿、掉帧。
    (2)解析sp的时候会产生大量的临时对象,导致频繁GC,引起界面卡顿。
    (3)这些key和value会永远存在于内存之中,占用大量内存。
  • 高频写操作的key与高频读操作的key可以适当地拆分文件, 由于减少同步锁竞争;
  • 不要连续多次edit(), 应该获取一次获取edit(),然后多次执行putxxx(), 减少内存波动;
  • 不要高频地使用apply, 尽可能地批量提交;commit直接在主线程操作, 更要注意了;
  • 不是进程安全,所以不要用于在多进程间数据通信;
  • 从 Android N 开始, 不再支持 MODE_WORLD_READABLE & MODE_WORLD_WRITEABLE;

参考

1.彻底搞懂 SharedPreferences

2.全面剖析SharedPreferences