Android 之 SharedPreferences 相关

1 SharedPreferences 的使用

1.1 SharedPreferences 存储的特点:
  • SharedPreferences 是一种 Android 自带的轻量级数据 K-V 存储库,它使用了 .xml 的方式来存储数据,比如说我们经常用它来保存一些用户登录信息等轻量级的数据。
  • 只支持 Java 基本数据类型,不支持自定义的数据类型
  • 应用内数据可以共享
  • 使用简单,方便
1.2 SharedPreferences 的操作模式:
  • Context.MODE_PRIVATE:为默认操作模式,表示该文件是私有的,只能被应用本身访问,在该模式下,写入的内通会覆盖原文件的内容——目前只用这一种
  • Context.MODE_APPEND:模式会检查文件是否存储,存在就往文件追加内容,否则就创建新文件(没有用过)
  • Context.MODE_WORLD_REASABLE:表示当前文件可以被其他应用读取(Android 4.2 中弃用)
  • Context.MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入(Android 4.2 中弃用)
1.3 获取 SharedPreferences 的3种方式:
  • Context 对象的 getSharedPreferences(String name, int mode) 方法获取

  • Activity 对象的 getPreferences(int mode) 方法获取

  • PreferenceManager 对象的 getDefaultSharedPreferences(Context context) 方法获取

1.4 SharedPerferences 存数据:
SharedPreferences sp = getSharedPreferences("test", Context.MODE_PRIVATE);

// 1. 第一种写法
sp.edit().putString("name", "Eileen").putInt("age", 30).commit();

// 2. 第二种写法
SharedPreferences.Editor editor = sp.edit();
editor.putString("name", "Eileen");
editor.putInt("age", 30);
editor.commit();

// 3. 错误写法
sp.edit().putString("name", "Eileen");
sp.edit().putInt("age", 30);
sp.edit().commit();

因为 sp.edit() 每次都会返回一个新的 Editor 对象,Editor 的实现类 EditorImpl 里面会有一个缓存的 Map,最后 commit() 的时候先将缓存里面的 Map 写入到内存中的 Map,然后将内存中的 Map 写入到 .xml 文件中。使用上面的方式 commit,由于 sp.edit()又重新返回了一个新的 Editor 对象,缓存中的 Map 是空的,所以导致数据无法被存储。

1.5 SharedPreferences 取数据:
SharedPreferences sp = getSharedPreferences("test", Context.MODE_PRIVATE);
String name = sp.getString("name", null);
int age = sp.getInt("age", 0);
Log.e(TAG, "name: " + name); // name: Eileen
Log.e(TAG, "age: " + age); // age: 30
1.6 SharedPreference 查看

2 SharedPreferences 源码

2.1 获取 SharedPreferences

以下是 Activity.getPreference(int mode) 的源码:

public class Activity extends ContextThemeWrapper 
  	implements Window.Callback, KeyEvent.Callback, Window.OnWindowDismissedCallback {
  
  public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {
    // 最终也是调用ContextWrapper的getSharedPreferences方法
    return getSharedPreferences(getLocalClassName(), mode);
  }  
}

public class ContextWrapper extends Context {
  Context mBase;

  @Override
  public SharedPreferences getSharedPreferences(String name, int mode) {
    return mBase.getSharedPreferences(name, mode);
  }
}

public abstract class Context {
  public abstract SharedPreferences getSharedPreferences(
    String name, @PreferencesMode int mode);
}

以下是 PreferenceManager.getDefaultSharedPreferences(Context context) 的源码:

public class PreferenceManager {
  	private Context mContext;
  	
    public static SharedPreferences getDefaultSharedPreferences(Context context) {
      return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
                                          getDefaultSharedPreferencesMode());
    }
}

最终调用的都是 Context.getSharedPreferences ,而 Context 的最终实现类是 ContextImpl,因此,最终调用的是 ContextImpl.getSharedPreferences 方法。

以下是 ContextImpl.getSharedPreferences 的源码:

class ContextImpl extends Context {  
  // 1. 定义了一个key为String类型,value为File类型的HashMap
  private ArrayMap<String, File> mSharedPrefsPaths;
  
  @Override
  public SharedPreferences getSharedPreferences(String name, int mode) {  
    File file;
    synchronized (ContextImpl.class) { // 2. 加锁操作
      if (mSharedPrefsPaths == null) {
        mSharedPrefsPaths = new ArrayMap<>();
      }
      file = mSharedPrefsPaths.get(name);
      if (file == null) {
        // 3. 构建出和name对应的File,并将其放入map中
        file = getSharedPreferencesPath(name);
        mSharedPrefsPaths.put(name, file);
      }
    }
    return getSharedPreferences(file, mode);
  }

  @Override
  public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
      // 5. 这里有一个缓存机制,以加快之后获取SP的速度
      final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
      // 6. 获取SharedPreferences的实现类SharedPreferencesImpl(简称SPI)
      sp = cache.get(file);
      if (sp == null) {
        checkMode(mode);
        sp = new SharedPreferencesImpl(file, mode);
        cache.put(file, sp);
        return sp;
      }
    }
    return sp;
  }
  
  @Override
  public File getSharedPreferencesPath(String name) {
    // 4. 创建一个名为name的.xml文件
    return makeFilename(getPreferencesDir(), name + ".xml");
  }
  
  private File makeFilename(File base, String name) {
    if (name.indexOf(File.separatorChar) < 0) {
      final File res = new File(base, name);
      BlockGuard.getVmPolicy().onPathAccess(res.getPath());
      return res;
    }
    throw new IllegalArgumentException(
      "File " + name + " contains a path separator");
  } 
}

由此可以看出,SharedPreferences 确实是用 .xml 来保存其中的 K-V 数据的。

缓存机制

以下是 getSharedPreferencesCacheLocked 方法的源码:

class ContextImpl extends Context {
  private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
  
  @GuardedBy("ContextImpl.class")
  private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
      sSharedPrefsCache = new ArrayMap<>();
    }

    final String packageName = getPackageName();
    // 1. 根据PackageName来保存不同的SP缓存,通过这样的方式,就保证了不同的PackageName,即使有相同name的SP从缓存中拿到的数据也是各自的
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
      packagePrefs = new ArrayMap<>();
      sSharedPrefsCache.put(packageName, packagePrefs);
    }

    return packagePrefs;
  }
}
SharedPreferencesImpl

SharedPreferences 其实只是一个接口,SharedPreferencesImpl 才是具体的实现类:

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

final class SharedPreferencesImpl implements SharedPreferences {
  private Map<String, Object> mMap;
  
  SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    // 1. 进行备份文件创建
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    // 2. 从Disk载入信息
    startLoadFromDisk();
  }
  
  private void startLoadFromDisk() {
    synchronized (mLock) {
      mLoaded = false;
    }
    // 3. 开启新线程来加载数据
    new Thread("SharedPreferencesImpl-load") {
      public void run() {
        loadFromDisk();
      }
    }.start();
  }

  private void loadFromDisk() {
    synchronized (mLock) {
      if (mLoaded) {
        return;
      }
      if (mBackupFile.exists()) {
        mFile.delete();
        mBackupFile.renameTo(mFile);
      }
    }

    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);
          // 通过XmlUtil将xml的数据读取为一个Map,这是一个非常耗时的操作
          map = (Map<String, Object>) XmlUtils.readMapXml(str);
        } 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();
      }
    }
  }
}
2.2 编辑 SharedPreferences
2.2.1 获取 Editor

以下是 SharedPreferencesImpl.edit() 的源码

final class SharedPreferencesImpl implements SharedPreferences {
  @Override
  public Editor edit() {
    synchronized (mLock) {
      // 1. 等待读取完成
      awaitLoadedLocked();
    }
	// 2. 读取完成之后才会真正的创建并返回EditorImpl对象。因为每次都创建一个新的对象,所以要做到数据更新的操作一次性完成
    return new EditorImpl();
  }
  
  public final class EditorImpl implements Editor {
    
  }
}
2.2.2 EditorImpl

真正操作 SharedPreferences 的是 Editor。Editor 是一个接口,具体的实现类为 EditorImpl。 EditorImpl 是 SharedPreferencesImpl 的一个内部类。

final class SharedPreferencesImpl implements SharedPreferences {
  
  private Map<String, Object> mMap;
  
  public final class EditorImpl implements Editor {
    @GuardedBy("mEditorLock")
    // 1. 通过mModified对SharedPreferences进行操作
    private final Map<String, Object> mModified = new HashMap<>();
    
    @Override
    public Editor putString(String key, @Nullable String value) {
      synchronized (mEditorLock) {
        // 2. 此时还不会提交到SharedPreferencesImpl的mMap,操作改变的都是mModified
        mModified.put(key, value);
        return this;
      }
    }
    
  }
}
2.3 提交 SharedPreferences

SharedPreferences 的提交有两种方式:apply() 和 commit()

  • apply():线程不安全,性能高,异步处理 I/O 操作,一定会把这个写文件操作放入一个SingleThreadExecutor 线程池中处理
  • commit():线程安全,性能慢,一般来说在当前线程完成写文件操作
2.3.1 apply()
final class SharedPreferencesImpl implements SharedPreferences {
  public final class EditorImpl implements Editor {
    @Override
    public void apply() {
      final long startTime = System.currentTimeMillis();
	  // 1. 将原先读取mMap与刚刚修改过的mModified进行合并,并存储于返回的MemoryCommitResult中
      final MemoryCommitResult mcr = commitToMemory();
      final Runnable awaitCommit = new Runnable() {
        @Override
        public void run() {
          try {
            mcr.writtenToDiskLatch.await();
          } catch (InterruptedException ignored) {
          }
        }
      };

      QueuedWork.addFinisher(awaitCommit);

      Runnable postWriteRunnable = new Runnable() {
        @Override
        public void run() {
          awaitCommit.run();
          QueuedWork.removeFinisher(awaitCommit);
        }
      };

      // 2. 传入postWriteRunnable,进行一个异步写操作
      SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

      // Okay to notify the listeners before it's hit disk
      // because the listeners should always get the same
      // SharedPreferences instance back, which has the
      // changes reflected in memory.
      notifyListeners(mcr);
    }
  }
}

也就是说,apply() 方法会先将数据提交到内存,再开启一个线程将数据写入到硬盘,是个异步过程。

2.3.2 commit()
final class SharedPreferencesImpl implements SharedPreferences {
  public final class EditorImpl implements Editor {
    @Override
    public boolean commit() {
      long startTime = 0;

      if (DEBUG) {
        startTime = System.currentTimeMillis();
      }
	  // 1. 将原先读取mMap与刚刚修改过的mModified进行合并,并存储于返回的MemoryCommitResult中 
      MemoryCommitResult mcr = commitToMemory();
	  // 2. enqueueDiskWrite传入的第二个参数是null,会在当前线程执行写操作
      SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
      try {
        mcr.writtenToDiskLatch.await();
      } catch (InterruptedException e) {
        return false;
      } finally {
        
      }
      notifyListeners(mcr);
      return mcr.writeToDiskResult;
    }
  }
}

也就是说,commit 方法会先将数据提交到内存,然后在当前线程写入硬盘,是个同步过程。

2.3.3 同步数据到内存

以下是 EditorImpl.commitToMemory 的源码:

private MemoryCommitResult commitToMemory() {
  Map<String, Object> mapToWriteToDisk;

  synchronized (SharedPreferencesImpl.this.mLock) {
    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<OnSharedPreferenceChangeListener>(mListeners.keySet());
    }

    synchronized (mEditorLock) {
      boolean changesMade = false;

      if (mClear) {
        if (!mapToWriteToDisk.isEmpty()) {
          changesMade = true;
          mapToWriteToDisk.clear();
        }
        keysCleared = true;
        mClear = false;
      }

      for (Map.Entry<String, Object> e : mModified.entrySet()) {
        String k = e.getKey();
        Object v = e.getValue();
        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, keysCleared, keysModified,
                                listeners, mapToWriteToDisk);
}

将 mMap 的数据与 mModified 的数据进行了整合,之后将 mModified 重新清空。最后将合并的数据放入到 MemoryCommitResult 中。

2.3.4 写入数据至硬盘

以下是 SharedPreferencesImpl.enqueueDiskWrite 的源码:

private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
  // 1. 若第二个Runnable为null的话,则会将isFromSyncCommit设置为true,也就是一个同步过程
  final boolean isFromSyncCommit = (postWriteRunnable == null);

  final Runnable writeToDiskRunnable = new Runnable() {
    @Override
    public void run() {
      synchronized (mWritingToDiskLock) {
        // 4. XmlUtils来进行XML的写入
        writeToFile(mcr, isFromSyncCommit);
      }
      synchronized (mLock) {
        mDiskWritesInFlight--;
      }
      if (postWriteRunnable != null) {
        postWriteRunnable.run();
      }
    }
  };

  // Typical #commit() path with fewer allocations, doing a write on
  // the current thread.
  if (isFromSyncCommit) {
    boolean wasEmpty = false;
    synchronized (mLock) {
      wasEmpty = mDiskWritesInFlight == 1;
    }
    if (wasEmpty) {
      // 2. 同步写入,否则会构造一个Runnable来提供给QueueWork进行异步写入
      writeToDiskRunnable.run();
      return;
    }
  }

  // 3.QueueWork类内部维护了一个线程池,这样可以达到一步写入的目的
  QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
2.4 总结

SharedPreferences 其实是一个用 .xml 进行保存的 K-V 存储库。有一个两级缓存系统,包括了内存缓存和磁盘缓存。

在获取 SharedPreferences 时会进行数据加载,将 name 对应的 .xml 文件以 Map (SharedPreferences.mMap)的形式读入到内存中。

SharedPreferences 的编辑操作其实都是在其内部类 Editor 内实现。在 Editor 的内部维护了一个新的 Map(Editor.mModified),而所有的编辑其实都是在操作这个 Map,只有提交时才会和之前读取的数据进行合并。

apply

  • 先将数据提交到内存,再开启一个异步过程来将数据写入硬盘
  • 返回值时可能写入操作还没有结束,写入失败不会有任何提示

commit

  • 先将数据提交到内存,之后是一个同步过程将数据写入硬盘
  • 写入操作结束后才会返回值,写入失败会有提示

apply 的写磁盘是异步行为,commit 的写磁盘是同步行为。两者在写磁盘前都会先同步的写到内存缓存中。

当我们对写入的结果不那么关心的情况下,可以使用 apply 进行异步写入,当我们对写入的结果十分关心且有后续操作的话,最好使用 commit 的方法进行同步写入的。

3 SharedPreferences 使用封装

由于 SharedPreferences 的 key 和 value 其实最终都是以 String 类型存在,所以可以写这样一个 SharedPreferences 工具类:

class PreferenceManager {
    private static final String PERF_NAME = "com.cah.androidtest.my_pref";
    private static final int CURRENT_VERSION_CODE = 1;
    private volatile static PreferenceManager instance;
    private final SharedPreferences preferences;

    private PreferenceManager(Context context) {
        preferences = context.getSharedPreferences(PERF_NAME, Context.MODE_PRIVATE);
        checkPreVersion();
    }

    public static PreferenceManager getInstance(Context context) {
        if (instance == null) {
            synchronized (PreferenceManager.class) {
                if (instance == null) {
                    instance = new PreferenceManager(context);
                }
            }
        }
        return instance;
    }

    public final void putValue(String key, String value) {
        preferences.edit().putString(key, value).apply();
    }

    public final String getValue(String key) {
        checksLegal(key);
        return preferences.getString("key", "");
    }

    public final void deleteValue(String key) {
        checksLegal(key);
        preferences.edit().remove(key).apply();
    }

    public final void clear() {
        preferences.edit().clear().apply();
    }

    private void checksLegal(String key) {
        if (TextUtils.isEmpty(key))
            throw new IllegalArgumentException("This parameter is illegal, key: " + key);
    }

    private void checkPreVersion() {
        final int oldVersion = preferences.getInt(PERF_NAME, 0);
        if (oldVersion < CURRENT_VERSION_CODE) {
            preferences.edit().clear().putInt(PERF_NAME, CURRENT_VERSION_CODE).apply();
        }
    }
}

由于版本升级时不会删除 SharedPreferences 文件,所以可以根据版本判断,来进行一些数据更新,从上面看来,由于每一次调用 getSharedPreferences() 都会有 I/O 操作,当内容比较多时,就不适宜在 Application 的 onCreate 中进行 SharedPreferences 文件初始化了,最好的办法就是开个子线程去完成它的创建和数据的预加载。

参考

https://blog.csdn.net/u013441613/article/details/80361817
SharedPreference 的commit和apply

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值