读源码 | SharedPreferences 源码解析(上)

2,138 阅读6分钟

前言

SharedPreferences 是 Android 数据持久化的一种方式,它通过一个 xml 文件来存储键值对,适用于简单的数据,比如应用配置等。

虽然 SharedPreferences 用起来简单,但其源码实现还是有许多值得研究的地方,而且这部分内容也是面试时的高频考点,因此研读 SharedPreferences 的源码是非常有必要的。

为了保证阅读体验以及降低写作难度,我把这部分内容拆分成两篇来完成。这是第一篇,主要分析 SharedPreferences 是如何被创建出来的,具体来说就是,当调用 context.getSharedPreferences() 后发生了什么。

另外,为了简化描述,文章正文的 SharedPreferences 将使用缩写 Sp 来代替。

getSharedPreferences(String name, int mode)

‌在平时开发过程中,我们一般通过调用 context.getSharedPreferences(String name, int mode) 方法获取 Sp 实例,第一个参数是 Sp 的名字,第二个参数是访问模式。

这个方法的具体实现在 ContextImpl 类中,方法源码如下:

@Override
public SharedPreferences getSharedPreferences(String name, int mode) { 
    // 处理 name 为 null 的情况 
    if (mPackageInfo.getApplicationInfo().targetSdkVersion < 
            Build.VERSION_CODES.KITKAT) { 
        if (name == null) { 
            name = "null"; 
        }
    }

    File file; 
    synchronized (ContextImpl.class) { 
        if (mSharedPrefsPaths == null) { 
            //创建缓存的 map 
            mSharedPrefsPaths = new ArrayMap<>(); 
        }
        //先从缓存中根据 name 获取 File  
        file = mSharedPrefsPaths.get(name); 
        if (file == null) { 
            //缓存中没有,生成 file 
            file = getSharedPreferencesPath(name); 
            //加入缓存 
            mSharedPrefsPaths.put(name, file); 
        }
    }
    //通过文件生成 SharedPreferences 
    return getSharedPreferences(file, mode); 
}

在该方法中,首先针对低版本时 name 为 null 的情况进行兼容(在高版本中值为 null 的 String 在拼接时会转为 "null",关于低于 KITKAT 版本中字符串拼接的具体实现还有待研究),然后从缓存 mSharedPrefsPaths 中根据 name 来找到对应的 File 对象。

如果对应的 File 对象存在,接着调用重载方法 getSharedPreferences 生成 Sp 实例(这个方法的第一个参数类型是 File );如果 File 不存在,则调用 getSharedPreferencesPath 方法根据 name 生成 File ,该方法代码如下:

@Override
public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}

方法又调用了makeFilename方法,第一个参数是文件目录,通过 getPreferencesDir 方法获取;第二个是文件名,也就是 name 加上一个 ".xml" 文件后缀。

getPreferencesDir 代码如下:

private File getPreferencesDir() { 
    synchronized (mSync) { 
        if (mPreferencesDir == null) { 
            //应用的data 目录下 shared_prefs 文件夹 
            mPreferencesDir = new File(getDataDir(), "shared_prefs"); 
        } 
        return ensurePrivateDirExists(mPreferencesDir); 
    }
}

其实就是获取 data 目录下应用私有的 "shared_prefs" 目录。一个应用的私有目录通常像下面这样,Sp 对应的文件就保存在 shared_prefs 中:

makeFilename 方法通过目录和文件名创建了一个 File 对象:

private File makeFilename(File base, String name) { 
    //文件名包含分隔符时抛出异常 
    if (name.indexOf(File.separatorChar) < 0) { 
        final File res = new File(base, name); 
        return res; 
    }
    throw new IllegalArgumentException( 
            "File " + name + " contains a path separator"); 
}

File 对象创建完成后,会先被加入到 mSharedPrefsPaths 缓存中,避免下次再创建,然后作为参数传给重载的 getSharedPreferences 方法用于生成 Sp 实例。

注:上面的 mSharedPrefsPaths 是一个 ArrayMap,如果对 ArrayMap 的实现感兴趣,可以查看我写的 ArrayMap 源码解析

getSharedPreferences(File file, int mode)

这个方法是上面方法的重载,第一个参数类型为 File ,方法源码如下:

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        // 先从缓存中获取
        sp = cache.get(file);
        if (sp == null) {
            //一些权限检查
            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");
                }
            }
            //创建 SharedPreferences 实例
            sp = new SharedPreferencesImpl(file, mode);
            // 放入缓存
            cache.put(file, sp);
            return sp;
        }
    }
    //....
    return sp;
}

在这个方法中,依旧是先从 cache 中获取 File 对应的 Sp 实例,如果找到就直接返回;否则,就执行创建过程。

在创建前会先进行一些权限检查,包括访问模式和文件权限。

其中 checkMode() 方法用于检查访问模式:

private void checkMode(int mode) { 
    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 (API 24) 以及更高版本中,使用 MODE_WORLD_READABLEMODE_WORLD_WRITEABLE 将会抛出 SecurityException,因此我们应该使用 MODE_PRIVATE

而之后的代码则用于检查文件权限:

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

在 Android O 之后,如果在用户未解锁时试图访问应用私有数据,将会抛出异常。

关于 CredentialProtectedStorage 我只在一个官方样例 DirectBoot 里找到了一些解释:应用数据默认都是 CredentialProtected (必须在用户解锁后才能访问),还有一种是 Device protected(不受用户解锁限制,可以直接访问),这两种模式从 Android N 开始引入的。

回到 Sp 的创建过程。当权限检查没有问题后,通过实例化 SharedPreferencesImpl 创建 Sp 实例并放入缓存。

SharedPreferencesImpl 是 SharedPreferences 接口的实现类,它的构造方法源码如下:

SharedPreferencesImpl(File file, int mode) { 
    // Sp 对应的文件
    mFile = file; 
    //备份文件 
    mBackupFile = makeBackupFile(file);
    mMode = mode; 
    //标识 Sp是否已从文件加载  
    mLoaded = false; 
    //存储键值对的 Map 
    mMap = null; 
    mThrowable = null;
    //从文件中读取 
    startLoadFromDisk();  
}

备份文件 mBackupFile 的创建调用了makeBackupFile 方法,这个方法会基于 Sp 的文件创建了一个后缀为 ".bak" 的文件。

static File makeBackupFile(File prefsFile) {
    return new File(prefsFile.getPath() + ".bak");
}

接下来是调用 startLoadFromDisk 来通过文件加载 Sp:

private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false; 
    } 
    //开启一个线程读取文件 
    new Thread("SharedPreferencesImpl-load") { 
        public void run() { 
            loadFromDisk(); 
        } 
    }.start(); 
}

startLoadFromDisk 方法中创建并启动了一个新的线程,线程会执行 loadFromDisk 方法,这才是真正读取文件的地方:

private void loadFromDisk() { 
    synchronized (mLock) { 
        //已经读取成功,直接返回 
        if (mLoaded) { 
            return; 
        } 
        if (mBackupFile.exists()) { 
            mFile.delete();  
            mBackupFile.renameTo(mFile); 
        } 
    } 

    Map<String, Object> map = null; 
    Throwable thrown = null; 
    try { 
        if (mFile.canRead()) { 
            BufferedInputStream str = null; 
            try { 
                //创建文件输入流 
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024); 
                //将读取的文件解析成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) { 
        // An errno exception means the stat failed. Treat as empty/non-existing by 
        // ignoring. 
    } catch (Throwable t) { 
        thrown = t; 
    } 

    synchronized (mLock) { 
        //读取完成后设置读取状态为true 
        mLoaded = true; 
        mThrowable = thrown; 

        // It's important that we always signal waiters, even if we'll make 
        // them fail with an exception. The try-finally is pretty wide, but 
        // better safe than sorry. 
        try { 
            if (thrown == null) { 
                if (map != null) { 
                    //将 mMap 赋值 
                    mMap = map; 
                } else { 
                    mMap = new HashMap<>(); 
                } 
            } 
            // In case of a thrown exception, we retain the old map. That allows 
            // any open editors to commit and store updates. 
        } catch (Throwable t) { 
            mThrowable = t; 
        } finally { 
            //通知所有可能正在等待的线程 
            mLock.notifyAll(); 
        }
    }
}

虽然 loadFromDisk 的代码看着有点长,但是做的事情其实很简单。

  • 先判断 mLoaded 是否为 true,如果为 true 则代表该文件已经被加载过,无须重复加载。因为可能面临多线程并发问题,所有这里对 mLoaded 都加了互斥锁。

  • 如果文件未被加载,那么就创建输入流、读取 xml 文件内容并解析成 HashMap。

xml 解析过程通过工具类 XmlUtils.readMapXml 来完成,这部分不是我们要关注的重点,所以不展开描述。

在解析完文件后,将 mLoaded 设为 true。如果在上面过程中没有异常抛出,那么就将解析后的 map 赋值给 mMap ;如果解析后 map 为空,会创建一个新的 HashMap 赋值给 mMap。

最后,调用 mLock.notifyAll() 通知所有在 mLock 上等待的线程。

这时,Sp 实例已被创建,我们就可以对 Sp 中的键值对进行读取和写入了。

总结

Sp 的创建过程可以简单描述为:先通过 name 创建对应的 File 对象,然后通过 File 对象创建 Sp 实例。为了性能考虑,这两步都加入了缓存。

Sp 实例的最终创建逻辑是在 SharedPreferencesImpl 的 startLoadFromDisk 中完成的,该方法会重新创建一个线程,进行 xml 文件读取和解析,最终生成一个HashMap 对象,此后对键值对的操作都将会围绕这个 HashMap 进行。

我把 Sp 创建过程中涉及的主要方法的调用关系和关键点画了个草图,方便理解和记忆。

关于 Sp 创建过程就介绍到这里,下一篇文章将会详细介绍与 Sp 的读写相关的源码。

欢迎讨论。