Android本地存储之SharedPreferences源码解析

简介: SharedPreferences源码解析

SharedPreferences的使用

SharedPreferences存数据

//获得SharedPreferences的实例 sp_name是文件名
SharedPreferences sp = getSharedPreferences("sp_name", Context.MODE_PRIVATE);
//获得Editor 实例
SharedPreferences.Editor editor = sp.edit();
//以key-value形式保存数据
editor.putString("data_key", "data");
//apply()是异步写入数据
editor.apply();   
//commit()是同步写入数据 
//editor.commit(); 

SharedPreferences取数据

//获得SharedPreferences的实例
SharedPreferences sp = getSharedPreferences("sp_name", Context.MODE_PRIVATE);
//通过key值获取到相应的data,如果没取到,则返回后面的默认值
String data = sp.getString("data_key", "defaultValue");

数据以xml形式存储在/data/data/项目包名/shared_prefs/sp_name.xml里,如图:

xml.png

sp_name.png

源码解析

以上是SharedPreferences的简单用法,下面从源码角度来看下整个过程:
先把结论贴出来

  • SharedPreferences读取xml文件时,会以DOM方式解析(把整个xml文件直接加载到内存中解析),在调用getXXX()方法时取到的是内存中的数据,方法执行时会有个锁来阻塞,目的是等待文件加载完毕,没加载完成之前会wait()
  • SharedPreferences写文件时,如果调用的commit(),会将数据同步写入内存中,内存数据更新,再同步写入磁盘中;如果调用的apply(),会将数据同步写入内存中,内存数据更新,然后异步写人磁盘,也就是说可能写磁盘操作还没有完成就直接返回了。在主线程中建议使用apply(),因为同步写磁盘,当文件较大时,commit()会等到写磁盘完成再返回,可能会有ANR问题。
  • SP第一次初始化到读取到数据存在一定延迟,因为需要到文件中读取数据,因此可能会对UI线程流畅度造成一定影响。

获取SharedPreferences实例

我们通过context.getSharedPreferences方法获取SharedPreferences实例

//name是存储的文件名,mode是创建文件时的模式
public abstract SharedPreferences getSharedPreferences(String name, int mode);

Context里面的getSharedPreferences方法是抽象方法,接着就找到了Context的实现类是ContextImpl:

//Map from preference name to generated path. 
//mSharedPrefsPaths为保存文件地址的Map
private ArrayMap<String, File> mSharedPrefsPaths;

@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) { 
   //如果参数name为null,则直接创建一个null.xml的文件
       if (name == null) { 
           name = "null"; 
       }    }   
    File file;
    synchronized (ContextImpl.class) {
        //第一次进的时候初始化
        if (mSharedPrefsPaths == null) {
            mSharedPrefsPaths = new ArrayMap<>();
        }
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
         //通过getPreferencesDir()来获取shared_prefs目录,然后根据文件名加上xml后缀
            file = getSharedPreferencesPath(name); 
           mSharedPrefsPaths.put(name, file);
        }    }
    return getSharedPreferences(file, mode);}

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
//Android N以后不支持Context.MODE_WORLD_WRITEABLE和Context.MODE_WORLD_READABLE模式
    checkMode(mode);
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
    //通过getSharedPreferencesCacheLocked()根据包名来获得缓存preferences的Map
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
    //如果静态内存缓存中有,直接取出来
        sp = cache.get(file);
        if (sp == null) {
             //缓存中没有,new一个sp出来(SharedPreferences是一个接口,SharedPreferencesImpl是其实现类)
            sp = new SharedPreferencesImpl(file, mode);
            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. 
        //MODE_MULTI_PROCESS多进程模式或者SDK<11时,会重新从磁盘加载文件,不过多进程模式
        //已经被deprecated了,官方建议使用ContentProvider来处理多进程访问.
       sp.startReloadIfChangedUnexpectedly();
    } 
   return sp;}

获取缓存目录

//通过getPreferencesDir()来获取shared_prefs目录,然后根据文件名加上xml后缀
@Override
public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}
//Android N以后不支持Context.MODE_WORLD_WRITEABLE和Context.MODE_WORLD_READABLE模式
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");
        }    }}
/** * Map from package name, to preference name, to cached preferences. */
//sSharedPrefsCache根据包名来缓存preferences的Map
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }
    final String packageName = getPackageName();
   //首先在静态缓存sSharedPrefsCache中查找preferences的Map,如果有,直接取出来返回
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    //如果静态缓存中没有,直接new一个Map并且加到静态缓存sSharedPrefsCache中
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);
    }
    return packagePrefs;}

下面再来分析一下SharedPreferencesImpl实例化过程,也是从磁盘读取文件到内存中的过程:

SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    //产生一个.bak结尾的临时File
    mBackupFile = makeBackupFile(file);
    //加载模式
    mMode = mode;
    //标志位,表示从磁盘加载到内存中是否完成
    mLoaded = false;
    //保存在内存中sp对象的Map
    mMap = null;
    //从磁盘中加载文件到内存中
    startLoadFromDisk();
}
static File makeBackupFile(File prefsFile) {
    return new File(prefsFile.getPath() + ".bak");}

private void startLoadFromDisk() {
    synchronized (this) {
        mLoaded = false;
    }
   //新起一个Thread开始加载
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();}

private void loadFromDisk() {
    synchronized (SharedPreferencesImpl.this) {
        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 {
                //SharedPreferences文件以流形式读出来
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16*1024);
                //读取xml中的内容,构造一个Map赋值给下面的mMap
                map = XmlUtils.readMapXml(str);
            } catch (XmlPullParserException | IOException e) {
                Log.w(TAG, "getSharedPreferences", e);
            } finally {
                IoUtils.closeQuietly(str);
            } 
       }    } catch (ErrnoException e) {
        /* ignore */    }
    synchronized (SharedPreferencesImpl.this) {
        //标记位,数据已经加载完成
        mLoaded = true;
        if (map != null) {
            //赋值给内存中的mMap 
            mMap = map;
            //设置时间戳和文件的大小
            mStatTimestamp = stat.st_mtime;
            mStatSize = stat.st_size;
        } else {
            mMap = new HashMap<>();
        }
        // 通知所有线程被SharedPreferencesImpl.this对象锁住的数据已经加载完成了,数据可以使用了。
        notifyAll();    }}

最后来看下读文件和写文件,首先是读文件(文件从磁盘加载到内存中),这里看的SharedPreferencesImpl类中getString(String key, String defValue)地源码,其他getXXX()也是一样的。

读文件(先从内存读,再从磁盘读)

@Nullable
public String getString(String key, @Nullable String defValue) {
    synchronized (this) {
        //同步等待文件从磁盘加载到内存完成为止,否自wait()
        awaitLoadedLocked();
        //从内存中的mMap直接取值
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;    }}

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

写文件(内存->磁盘)

写文件(先把要修改的数据写到内存中,再写入磁盘中):

public final class EditorImpl implements Editor {
    // 保存putXXX()方法时提供的所有要提交修改的数据
    private final Map<String, Object> mModified = Maps.newHashMap();
    private boolean mClear = false;
    public Editor putString(String key, @Nullable String value) {
        synchronized (this) {
            mModified.put(key, value);
            return this;
        }    }

public boolean commit() {
    //数据提交到内存中
    MemoryCommitResult mcr = commitToMemory();
    //将数据写入磁盘中
    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */); 
   try {
       //阻塞等待写操作完成
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;}
}


public void apply() {
    //同步写入内存中
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            public void run() {
                try {
                    //阻塞等待写操作完成
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
            }        };
    QueuedWork.add(awaitCommit);
    Runnable postWriteRunnable = new Runnable() {
            public void run() {
                awaitCommit.run();
                QueuedWork.remove(awaitCommit);
            }        };
      //异步写入磁盘
    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);
}

private void notifyListeners(final MemoryCommitResult mcr) {
    if (mcr.listeners == null || mcr.keysModified == null ||
        mcr.keysModified.size() == 0) {
        return;
    }    if (Looper.myLooper() == Looper.getMainLooper()) {
        for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {
            final String key = mcr.keysModified.get(i);
            for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
                if (listener != null) {
                    listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key);                }
            }
        }    } else {
        // Run this function on the main thread.
        ActivityThread.sMainThreadHandler.post(new Runnable() {
                public void run() {
                    notifyListeners(mcr);
                }
            });
    }}

// Returns true if any changes were madeprivate MemoryCommitResult 
//commit()和apply()两个方法都调用了commitToMemory。该方法主要根据mModified和是否被
//clear修改内存中mMap的值,然后返回写磁盘需要的一些相关值
commitToMemory() {
    MemoryCommitResult mcr = new MemoryCommitResult();
    synchronized (SharedPreferencesImpl.this) {
        // 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
           // 当有多个写操作时,clone一份
            mMap = new HashMap<String, Object>(mMap);
        }
        mcr.mapToWriteToDisk = mMap;
        //未完成的写操作数+1
        mDiskWritesInFlight++;
        boolean hasListeners = mListeners.size() > 0;
        if (hasListeners) {
            mcr.keysModified = new ArrayList<String>();
            mcr.listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());        }
        synchronized (this) {
            if (mClear) {
                if (!mMap.isEmpty()) {
                    mcr.changesMade = true;
                    mMap.clear();
                }
                mClear = false;
            }
            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;
                        }
                    }
                    mMap.put(k, v);
                }
                mcr.changesMade = true;
                if (hasListeners) { 
                   mcr.keysModified.add(k);
                }
            }
            mModified.clear();
        }    }
    return mcr;}

// Return value from EditorImpl#commitToMemory()
//写磁盘需要的相关值
private static class MemoryCommitResult {
    public boolean changesMade;
  // any keys different? public List<String> keysModified;
  // may be null    public Set<OnSharedPreferenceChangeListener> listeners;
  // may be null    public Map<?, ?> mapToWriteToDisk;
    public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
    public volatile boolean writeToDiskResult = false;
    public void setDiskWriteResult(boolean result) {
        writeToDiskResult = result;
        writtenToDiskLatch.countDown();
    }}

commit()和apply()

commit()apply()这两个方法都是首先修改内存中缓存的mMap的值,然后将数据写到磁盘中。它们的主要区别是commit会等待写入磁盘后再返回,而apply则在调用写磁盘操作后就直接返回了,但是这时候可能磁盘中数据还没有被修改。

相关文章
|
4月前
|
安全 Java Android开发
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
208 0
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
|
5月前
|
XML 搜索推荐 Android开发
Android改变进度条控件progressbar的样式(根据源码修改)
本文介绍了如何基于Android源码自定义ProgressBar样式。首先分析了系统源码中ProgressBar样式的定义,发现其依赖一张旋转图片实现动画效果。接着分两步指导开发者实现自定义:1) 模仿源码创建一个旋转动画XML文件(放置在drawable文件夹),修改图片为自定义样式;2) 在UI控件中通过`indeterminateDrawable`属性应用该动画。最终实现简单且个性化的ProgressBar效果,附带效果图展示。
300 2
|
7月前
|
算法 测试技术 C语言
深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
676 29
|
6月前
|
NoSQL 应用服务中间件 PHP
布谷一对一直播源码android版环境配置流程及功能明细
部署需基于 CentOS 7.9 系统,硬盘不低于 40G,使用宝塔面板安装环境,包括 PHP 7.3(含 Redis、Fileinfo 扩展)、Nginx、MySQL 5.6、Redis 和最新 Composer。Swoole 扩展需按步骤配置。2021.08.05 后部署需将站点目录设为 public 并用 ThinkPHP 伪静态。开发环境建议 Windows 操作系统与最新 Android Studio,基础配置涉及 APP 名称修改、接口域名更换、包名调整及第三方登录分享(如 QQ、微信)的配置,同时需完成阿里云与腾讯云相关设置。
|
7月前
|
XML JavaScript Android开发
【Android】网络技术知识总结之WebView,HttpURLConnection,OKHttp,XML的pull解析方式
本文总结了Android中几种常用的网络技术,包括WebView、HttpURLConnection、OKHttp和XML的Pull解析方式。每种技术都有其独特的特点和适用场景。理解并熟练运用这些技术,可以帮助开发者构建高效、可靠的网络应用程序。通过示例代码和详细解释,本文为开发者提供了实用的参考和指导。
195 15
|
7月前
|
监控 Shell Linux
Android调试终极指南:ADB安装+多设备连接+ANR日志抓取全流程解析,覆盖环境变量配置/多设备调试/ANR日志分析全流程,附Win/Mac/Linux三平台解决方案
ADB(Android Debug Bridge)是安卓开发中的重要工具,用于连接电脑与安卓设备,实现文件传输、应用管理、日志抓取等功能。本文介绍了 ADB 的基本概念、安装配置及常用命令。包括:1) 基本命令如 `adb version` 和 `adb devices`;2) 权限操作如 `adb root` 和 `adb shell`;3) APK 操作如安装、卸载应用;4) 文件传输如 `adb push` 和 `adb pull`;5) 日志记录如 `adb logcat`;6) 系统信息获取如屏幕截图和录屏。通过这些功能,用户可高效调试和管理安卓设备。
|
7月前
|
前端开发 数据安全/隐私保护 CDN
二次元聚合短视频解析去水印系统源码
二次元聚合短视频解析去水印系统源码
194 4
|
7月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
7月前
|
存储 前端开发 JavaScript
在线教育网课系统源码开发指南:功能设计与技术实现深度解析
在线教育网课系统是近年来发展迅猛的教育形式的核心载体,具备用户管理、课程管理、教学互动、学习评估等功能。本文从功能和技术两方面解析其源码开发,涵盖前端(HTML5、CSS3、JavaScript等)、后端(Java、Python等)、流媒体及云计算技术,并强调安全性、稳定性和用户体验的重要性。
|
7月前
|
负载均衡 JavaScript 前端开发
分片上传技术全解析:原理、优势与应用(含简单实现源码)
分片上传通过将大文件分割成多个小的片段或块,然后并行或顺序地上传这些片段,从而提高上传效率和可靠性,特别适用于大文件的上传场景,尤其是在网络环境不佳时,分片上传能有效提高上传体验。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~

热门文章

最新文章

推荐镜像

更多