返回介绍

3.2 SharePreferences 实现类 SharePreferencesImpl 分析

发布于 2024-12-23 21:33:34 字数 8230 浏览 0 评论 0 收藏 0

我们从上面 SharePreference 的使用入口可以分析,具体可以知道 SharePreference 的实例获取可以通过两种方式获取,一种是 Activity 的 getPreferences 方法,一种是 Context 的 getSharedPreferences 方法。所以我们如下先来看下这两个方法的源码。

先来看下 Activity 的 getPreferences 方法源码,如下:

  public SharedPreferences getPreferences(int mode) {
    return getSharedPreferences(getLocalClassName(), mode);
  }

哎?可以发现,其实 Activity 的 SharePreference 实例获取方法只是对 Context 的 getSharedPreferences 再一次封装而已,使用 getPreferences 方法获取实例默认生成的 xml 文件名字是当前 activity 类名而已。既然这样那我们还是转战 Context(其实现在 ContextImpl 中,至于不清楚 Context 与 ContextImpl 及 Activity 关系的请先看这篇博文, 点我迅速脑补 )的 getSharedPreferences 方法,具体如下:

//ContextImpl 类中的静态 Map 声明,全局的一个 sSharedPrefs
private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs;

//获取 SharedPreferences 实例对象
public SharedPreferences getSharedPreferences(String name, int mode) {
  //SharedPreferences 的实现类对象引用声明
  SharedPreferencesImpl sp;
  //通过 ContextImpl 保证同步操作
  synchronized (ContextImpl.class) {
    if (sSharedPrefs == null) {
      //实例化对象为一个复合 Map,key-package,value-map
      sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
    }
  //获取当前应用包名
    final String packageName = getPackageName();
    //通过包名找到与之关联的 prefs 集合 packagePrefs
    ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
    //懒汉模式实例化
    if (packagePrefs == null) {
      //如果没找到就 new 一个包的 prefs,其实就是一个文件名对应一个 SharedPreferencesImpl,可以有多个对应,所以用 map
      packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
      //以包名为 key,实例化的所有文件 map 作为 value 添加到 sSharedPrefs
      sSharedPrefs.put(packageName, packagePrefs);
    }

    if (mPackageInfo.getApplicationInfo().targetSdkVersion <
        Build.VERSION_CODES.KITKAT) {
      if (name == null) {
        //nice 处理,name 传 null 时用"null"代替
        name = "null";
      }
    }
  //找出与文件名 name 关联的 sp 对象
    sp = packagePrefs.get(name);
    if (sp == null) {
      //如果没找到则先根据 name 构建一个 File 的 prefsFile 对象
      File prefsFile = getSharedPrefsFile(name);
      //依据上面的 File 对象创建一个 SharedPreferencesImpl 对象的实例
      sp = new SharedPreferencesImpl(prefsFile, mode);
      //以 key-value 方式添加到 packagePrefs 中
      packagePrefs.put(name, sp);
      返回与 name 相关的 SharedPreferencesImpl 对象
      return sp;
    }
  }
  //如果不是第一次,则在 3.0 之前(默认具备该 mode)或者 mode 为 MULTI_PROCESS 时调用 reload 方法
  if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
      getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
    //重新加载文件数据
    sp.startReloadIfChangedUnexpectedly();
  }
  //返回 SharedPreferences 实例对象 sp
  return sp;
}

我们可以发现,上面方法中首先调运了 getSharedPrefsFile 来获取一个 File 对象,所以我们继续先来看下这个方法,具体如下:

  public File getSharedPrefsFile(String name) {
    //依据我们传入的文件名字符串创建一个后缀为 xml 的文件
    return makeFilename(getPreferencesDir(), name + ".xml");
  }

  private File getPreferencesDir() {
    synchronized (mSync) {
      if (mPreferencesDir == null) {
        //获取当前 app 的 data 目录下的 shared_prefs 目录
        mPreferencesDir = new File(getDataDirFile(), "shared_prefs");
      }
      return mPreferencesDir;
    }
  }

可以看见,原来 SharePreference 文件存储路径和文件创建是这个来的。继续往下看可以发现接着调运了 SharedPreferencesImpl 的构造函数,至于这个构造函数用来干嘛,下面会分析。

好了,到这里我们先回过头稍微总结一下目前的源码分析结论,具体如下:

前面我们有文章分析了 Android 中的 Context,这里又发现 ContextImpl 中有一个静态的 ArrayMap 变量 sSharedPrefs。这时候你想到了啥呢?无论有多少个 ContextImpl 对象实例,系统都共享这一个 sSharedPrefs 的 Map,应用启动以后首次使用 SharePreference 时创建,系统结束时才可能会被垃圾回收器回收,所以如果我们一个 App 中频繁的使用不同文件名的 SharedPreferences 很多时这个 Map 就会很大,也即会占用移动设备宝贵的内存空间,所以说我们应用中应该尽可能少的使用不同文件名的 SharedPreferences,取而代之的是合并他们,减小内存使用。同时上面最后一段代码也及具有隐藏含义,其表明了 SharedPreferences 是可以通过 MODE_MULTI_PROCESS 来进行夸进程访问文件数据的,其 reload 就是为了夸进程能更好的刷新访问数据。

好了,还记不记得上面我们分析留的尾巴呢?现在我们就来看看这个尾巴,可以发现 SharedPreferencesImpl 类其实就是 SharedPreferences 接口的实现类,其构造函数如下:

final class SharedPreferencesImpl implements SharedPreferences {
  ......
  //构造函数,file 是前面分析 data 目录下创建的传入 name 的 xml 文件,mode 为传入的访问方式
  SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    //依据文件名创建一个同名的.bak 备份文件,当 mFile 出现 crash 的会用 mBackupFile 来替换恢复数据
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    //将文件从 flash 或者 sdcard 异步加载到内存中
    startLoadFromDisk();
  }
  ......
  //创建同名备份文件
  private static File makeBackupFile(File prefsFile) {
    return new File(prefsFile.getPath() + ".bak");
  }
  ......
  private void startLoadFromDisk() {
    //同步操作 mLoaded 标志,写为未加载,这货是关键的关键!!!!
    synchronized (this) {
      mLoaded = false;
    }
    //开启一个线程异步同步加载 disk 文件到内存
    new Thread("SharedPreferencesImpl-load") {
      public void run() {
        synchronized (SharedPreferencesImpl.this) {
          //新线程中在 SharedPreferencesImpl 对象锁中异步 load 数据,如果此时数据还未 load 完成,则其他线程调用 SharedPreferences.getXXX 方法都会被阻塞,具体原因关注 mLoaded 标志变量即可!!!!!
          loadFromDiskLocked();
        }
      }
    }.start();
  }
}

好了,到这里你会发现整个 SharedPreferencesImpl 的构造函数很简单,那我们就继续分析真正的异步加载文件到内存过程,如下:

  private void loadFromDiskLocked() {
    //如果已经异步加载直接 return 返回
    if (mLoaded) {
      return;
    }
    //如果存在备份文件则直接使用备份文件
    if (mBackupFile.exists()) {
      mFile.delete();
      mBackupFile.renameTo(mFile);
    }
    ......
    Map map = null;
    StructStat stat = null;
    try {
      //获取 Linux 文件 stat 信息,Linux 高级 C 中经常出现的
      stat = Os.stat(mFile.getPath());
      //文件至少是可读的
      if (mFile.canRead()) {
        BufferedInputStream str = null;
        try {
          //把文件以 BufferedInputStream 流读出来
          str = new BufferedInputStream(
              new FileInputStream(mFile), 16*1024);
          //使用系统提供的 XmlUtils 工具类将 xml 流解析转换为 map 类型数据
          map = XmlUtils.readMapXml(str);
        } catch (XmlPullParserException e) {
          Log.w(TAG, "getSharedPreferences", e);
        } catch (FileNotFoundException e) {
          Log.w(TAG, "getSharedPreferences", e);
        } catch (IOException e) {
          Log.w(TAG, "getSharedPreferences", e);
        } finally {
          IoUtils.closeQuietly(str);
        }
      }
    } catch (ErrnoException e) {
    }
    //标记置为为已读
    mLoaded = true;
    if (map != null) {
      //把解析的 map 赋值给 mMap
      mMap = map;
      mStatTimestamp = stat.st_mtime;//记录时间戳
      mStatSize = stat.st_size;//记录文件大小
    } else {
      mMap = new HashMap<String, Object>();
    }
    //唤醒其他等待线程(其实就是调运该类的 getXXX 方法的线程),因为在 getXXX 时会通过 mLoaded 标记是否进入 wait,所以这里需要 notify
    notifyAll();
  }

OK,到此整个 Android 应用获取 SharePreference 实例的过程我们就分析完了,简单总结下如下:

创建相关权限和 mode 的 xml 文件,异步同步锁加载 xml 文件并解析 xml 数据为 map 类型到内存中等待使用操作,特别注意,在 xml 文件异步加载未完成时调运 SharePreference 的 getXXX 及 setXXX 方法是阻塞等待的。由此也可以知道,一旦拿到 SharePreference 对象之后的 getXXX 操作其实都不再是文件读操作了,也就不存在网上扯蛋的认为多次频繁使用 getXXX 方法降低性能的说法了。

分析完了构造实例化,我们回忆可以知道使用 SharePreference 可以通过 getXXX 方法直接获取已经存在的 key-value 数据,下面我们就来看下这个过程,这里我们随意看一个方法即可,如下:

  public boolean getBoolean(String key, boolean defValue) {
    //可以看见,和上面异步 load 数据使用的是同一个对象锁
    synchronized (this) {
      //阻塞等待异步加载线程加载完成 notify
      awaitLoadedLocked();
      //加载完成后解析的 xml 数据放在 mMap 对象中,我们从 mMap 中找出指定 key 的数据
      Boolean v = (Boolean)mMap.get(key);
      //存在返回找到的值,不存在返回设置的 defValue
      return v != null ? v : defValue;
    }
  }

先不解释,我们来关注下上面方法调运的 awaitLoadedLocked 方法,具体如下:

  private void awaitLoadedLocked() {
    ......
    //核心,这就是异步阻塞等待
    while (!mLoaded) {
      try {
        wait();
      } catch (InterruptedException unused) {
      }
    }
  }

哈哈,不解释,这也太赤裸裸的明显了,就是阻塞,就是这么任性,没辙。那我们继续攻占高地呗,get 完事了,那就是 set 了呀。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文