- CompoundButton 源码分析
- LinearLayout 源码分析
- SearchView 源码解析
- LruCache 源码解析
- ViewDragHelper 源码解析
- BottomSheets 源码解析
- Media Player 源码分析
- NavigationView 源码解析
- Service 源码解析
- Binder 源码分析
- Android 应用 Preference 相关及源码浅析 SharePreferences 篇
- ScrollView 源码解析
- Handler 源码解析
- NestedScrollView 源码解析
- SQLiteOpenHelper/SQLiteDatabase/Cursor 源码解析
- Bundle 源码解析
- LocalBroadcastManager 源码解析
- Toast 源码解析
- TextInputLayout
- LayoutInflater 和 LayoutInflaterCompat 源码解析
- TextView 源码解析
- NestedScrolling 事件机制源码解析
- ViewGroup 源码解析
- StaticLayout 源码分析
- AtomicFile 源码解析
- AtomicFile 源码解析
- Spannable 源码分析
- Notification 之 Android 5.0 实现原理
- CoordinatorLayout 源码分析
- Scroller 源码解析
- SwipeRefreshLayout 源码分析
- FloatingActionButton 源码解析
- AsyncTask 源码分析
- TabLayout 源码解析
3.2 SharePreferences 实现类 SharePreferencesImpl 分析
我们从上面 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论