美文网首页
Android-SharedPreferences

Android-SharedPreferences

作者: 有腹肌的豌豆Z | 来源:发表于2020-10-20 11:52 被阅读0次

    一、简介

    在Android中,我们通常会需要存储一些数据,有一些大型的数据诸如图片、JSON数据等,可以通过读写File的方式实现;有一些大量级的关系型数据,可以通过数据库SQLite实现;还有一些简单的、无安全风险的键值对数据,可以通过Android提供的SharedPreferences实现。

    SharedPreferences

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

    SharedPreferences是一个轻量级的xml键值对文件,使用起来也是很方便。

            //根据文件名,获取SharedPreferences对象;mode一般都使用MODE_PRIVATE,只能由该App访问
            SharedPreferences sp = getSharedPreferences("setting", Context.MODE_PRIVATE);
            //根据key,获取指定值
            boolean needInitChannels = sp.getBoolean("isDebug", false);
            //获取Editor编辑对象,用于编辑SharedPreferences
            SharedPreferences.Editor editor = sp.edit();
            editor.putBoolean("isDebug", true);
            //同步提交到SharedPreferences文件,获取是否同步成功的结果
            editor.commit();
            //异步提交到SharedPreferences文件
            editor.apply();
    

    当我们第一次访问一个名为"setting"的SharedPreferences文件,系统会在应用数据目录下(/data/data/packageName/)的shared_prefs文件夹下,创建一个同名的xml文件。

    二、实现原理

    1.创建

    我们是通过Context的getSharedPreferences()方法来获取一个SharedPreferences对象,而Context的实际逻辑载体,是在ContextImpl里,所以我们来看ContextImpl的该方法。

    public SharedPreferences getSharedPreferences(String name, int mode) {
        ...
        File file;
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            //1.获取缓存File对象
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                //生成File对象
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
        //2.获取File对象对应的SharedPreferences对象
        return getSharedPreferences(file, mode);
    }
    
        @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");
                        }
                    }
                    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.
                sp.startReloadIfChangedUnexpectedly();
            }
            return sp;
        }
    
    

    这里第一步我们可以看到,有一个ArrayMap会缓存每一个SharedPreferences文件对象,如果第一次加载的话,会调用getSharedPreferencesPath()生成File对象。

    public File getSharedPreferencesPath(String name) {
        return makeFilename(getPreferencesDir(), name + ".xml");
    }
    private File getPreferencesDir() {
        synchronized (mSync) {
            if (mPreferencesDir == null) {
                ///data/data/packageName/目录
                mPreferencesDir = new File(getDataDir(), "shared_prefs");
            }
            return ensurePrivateDirExists(mPreferencesDir);
        }
    }
    private static File ensurePrivateDirExists(File file, int mode, int gid, String xattr) {
        if (!file.exists()) {
            final String path = file.getAbsolutePath();
            try {
                Os.mkdir(path, mode);
                ...
            }...
        }
        return file;
    }
    
    

    我们会看到,如果应用目录下还没有shared_prefs文件夹,则创建一个该文件夹。

    public File getSharedPreferencesPath(String name) {
       return makeFilename(getPreferencesDir(), name + ".xml");
    }
    private File makeFilename(File base, String name) {
        if (name.indexOf(File.separatorChar) < 0) {
            return new File(base, name);
        }
    }
    

    然后,会在shared_prefs文件夹下,创建一个我们指定名字的xml文件,用来存储键值对数据。
    至此,会生成一个相应SharedPreferences的File文件,并进行缓存。
    第二步,通过File获取SharedPreferences对象。

    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            ...
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);
                return sp;
            }
        }
        ...
        return sp;
    }
    

    这里我们看到,返回的SharedPreferences实际对象,是一个SharedPreferencesImpl对象实例,并且也通过一个ArrayMap做了一个缓存,也就是说,一个name会对应一个SharedPreferences的File实例,而一个File会对应一个SharedPreferencesImpl实例。

    sharedPreferences是一个接口,在Content类中,声明了getSharedPreferences()空实现的一个方法,先看源码
    @Override
        public SharedPreferences getSharedPreferences(String name, int mode) {
            SharedPreferencesImpl sp;
            synchronized (ContextImpl.class) {
                if (sSharedPrefs == null) {
                    sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
                }
     
                final String packageName = getPackageName();
                ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
                if (packagePrefs == null) {
                    packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
                    sSharedPrefs.put(packageName, packagePrefs);
                }
     
                // 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) {
                    if (name == null) {
                        name = "null";
                    }
                }
     
                sp = packagePrefs.get(name);
                if (sp == null) {
                    File prefsFile = getSharedPrefsFile(name);
                    sp = new SharedPreferencesImpl(prefsFile, mode);
                    packagePrefs.put(name, 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.
                sp.startReloadIfChangedUnexpectedly();
            }
            return sp;
        }
    

    我们可以很明显的看到

    创建ArrayMap集合,集合的key:string,value:ArrayMap<String,SharedPreferenceImpl>
    getPackageName(),拿到项目包名;
    根据包名,在第一步创建的ArrayMap集合中找,有没有包名为key的键值对,if逻辑判断
    如果没有,创建一个ArrayMap集合,作为value,包名作为key,存储在第一步创建的集合ArrayMap中
    根据方法传入的SharedPreferences名称,也就是name的String类型参数,在第四步创建的ArrayMap中get查找有没有创建对应名称的SharedPreferenceImpl对象,if逻辑判断
    如果sp为空,创建对应的file文件,将包名称对应的SharedPreferenceImpl对象存储到第四步的ArrayMap中,name作为key
    然后返回第四步以包名称为key的ArrayMap。
    

    2.取值

    通过上述内容,可以知道我们实际操作的对象,其实都是SharedPreferencesImpl对象。这里我们以getBoolean()为例,看一下如何获取值。

    public boolean getBoolean(String key, boolean defValue) {
        synchronized (mLock) {
            //等待从磁盘加载数据(需要的话)
            awaitLoadedLocked();
            //直接从map取值返回
            Boolean v = (Boolean)mMap.get(key);
            return v != null ? v : defValue;
        }
    }
    

    那么从磁盘读取文件内容,并生成Map对象的初始化过程,是在何时进行的呢?是在SharedPreferencesImpl的构造函数中进行。

    SharedPreferencesImpl(File file, int mode) {
        ...
        startLoadFromDisk();
    }
    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        //工作线程进行
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }
    private void loadFromDisk() {
        synchronized (mLock) {
            if (mLoaded) {
                return;
            }
            ...
        }
        ...
        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);
                    //解析XML生成Map
                    map = (Map<String, Object>) XmlUtils.readMapXml(str);
                }...
            }
        }
        ...
        mMap = map;
        ...
        mLock.notifyAll();
    }
    

    可以看到,SharedPreferencesImpl创建的时候,简单粗暴的开启了一个工作线程,进行File的读取和解析,并生成了Map对象,赋值给mMap变量。

    而在我们的getBoolean()等操作Map的方法调用时,会通过awaitLoadedLocked()方法判断是否map已经生成,如果没有则等待。

    private void awaitLoadedLocked() {
        ...
        while (!mLoaded) {
            try {
                mLock.wait();
            }...
        }
    }
    

    3.更新

    再来看更新,上面说到过,更新是通过SharedPreferences的edit()方法,获取一个Editor对象进行。

    public Editor edit() {
        synchronized (mLock) {
            awaitLoadedLocked();
        }
        return new EditorImpl();
    }
    

    可见每次都是创建一个新的EditorImpl对象实例进行操作。通过源码可知多个数据编辑的时候 尽可能创建一个edit 对象

    public Editor putBoolean(String key, boolean value) {
        synchronized (mEditorLock) {
            mModified.put(key, value);
            return this;
        }
    }
    

    所有的更新操作,都会维护在EditorImpl对象的一个HashMap中,等待后续应用。

     @GuardedBy("mEditorLock")
            private final Map<String, Object> mModified = new HashMap<>();
    

    4.应用

    更新后,我们需要写入到磁盘中,EditorImpl为我们提供了两个方法,一个是commit()方法,一个是apply()方法,他们的区别,主要是同步和异步的差别。

    (1)commit()
    public boolean commit() {
       ...
       //1.将改动先同步到内存
       MemoryCommitResult mcr = commitToMemory();
       //2.同步写入磁盘
       SharedPreferencesImpl.this.enqueueDiskWrite(
           mcr, null /* sync write on this thread okay */);
       try {
           mcr.writtenToDiskLatch.await();
       }...
       return mcr.writeToDiskResult;
    }
    

    第一步是调用commitToMemory()方法,将EditorImpl的modify改动,应用到内存中,即应用到mMap中。

    private MemoryCommitResult commitToMemory() {
        long memoryStateGeneration;
        List<String> keysModified = null;
        Set<OnSharedPreferenceChangeListener> listeners = null;
        Map<String, Object> mapToWriteToDisk;
    
        synchronized (SharedPreferencesImpl.this.mLock) {
            if (mDiskWritesInFlight > 0) {
                mMap = new HashMap<String, Object>(mMap);
            }
            mapToWriteToDisk = mMap;
            //当前更新操作的线程数+1
            mDiskWritesInFlight++;
            ...
            synchronized (mEditorLock) {
                ...
                for (Map.Entry<String, Object> e : mModified.entrySet()) {
                    String k = e.getKey();
                    Object v = e.getValue();
                    if (v == this || v == null) {//remove
                        if (!mapToWriteToDisk.containsKey(k)) {
                            continue;
                        }
                        mapToWriteToDisk.remove(k);
                    } else {//update or insert
                        if (mapToWriteToDisk.containsKey(k)) {
                            Object existingValue = mapToWriteToDisk.get(k);
                            if (existingValue != null && existingValue.equals(v)) {
                                continue;
                            }
                        }
                        mapToWriteToDisk.put(k, v);
                    }
                }...
            }
        }
        return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                mapToWriteToDisk);
    }
    

    第二步,将内存中最新的数据,写到文件中。

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                      final Runnable postWriteRunnable) {
        //commit()方法时为true
        final boolean isFromSyncCommit = (postWriteRunnable == null);
    
        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        //将Map写入File
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        //当前更新操作的线程数-1
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };
    
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                //只有当前线程调用时,因为同步进行,所以一直为1;有多个线程同时调用时,会大于1
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                //当前线程直接调用
                writeToDiskRunnable.run();
                return;
            }
        }
        //异步调用
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }
    

    当只有一个线程操作SharedPreferences的话,mDiskWritesInFlight计数器始终为1,因为是同步写入File,写入后计数器会-1。

    而写入文件就是很简单的IO操作,只不过需要把Map转换为xml的格式。

    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        ...
        try {
            //OutputStream
            FileOutputStream str = createFileOutputStream(mFile);
            ...
            //将map数据写成xml格式到file中
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
            ...
            //设置成功写入的结果
            mcr.setDiskWriteResult(true, true);
    }
    
    (2)apply()

    再来看apply()方法。

    public void apply() {
        final MemoryCommitResult mcr = commitToMemory();
        ...
        Runnable postWriteRunnable = new Runnable() {
                @Override
                public void run() {
                    awaitCommit.run();
                    QueuedWork.removeFinisher(awaitCommit);
                }
            };
        //postWriteRunnable不为null
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
        ...
    }
    

    这里可以看到,调用的方法和commit()都一样,唯独调用enqueueDiskWrite()方法时,第二个Runnable对象不为null,按照上面说,不为null时,走的方法就是异步方法QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);

    public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();
        synchronized (sLock) {
            //将任务加入到队列中等待执行
            sWork.add(work);
            //通过handler发送消息
            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }
    private static class QueuedWorkHandler extends Handler {
        ...
        public void handleMessage(Message msg) {
            if (msg.what == MSG_RUN) {
                processPendingWork();
            }
        }
    }
    private static void processPendingWork() {
        ...
        for (Runnable w : work) {
            //执行任务
             w.run();
         }
    }
    

    可以看到,内部就是通过handler发送消息执行任务的,任务还是一样的writeFile()方法,那么为何是异步的呢?就得来看看handler是啥handler了。

    private static Handler getHandler() {
        synchronized (sLock) {
            if (sHandler == null) {
                HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                        Process.THREAD_PRIORITY_FOREGROUND);
                handlerThread.start();
    
                sHandler = new QueuedWorkHandler(handlerThread.getLooper());
            }
            return sHandler;
        }
    }
    

    现在就明白了吧,是一个全局的HandlerThread对象,也就是一个工作线程,所以apply()方法,会通过一个全局唯一的异步线程进行写文件的操作。

    三、总结

    • SharedPreferences的File创建和内容解析,在内存中是有缓存的。
    • SharedPreferences的提交,commit()方法是在当前线程完成,而apply()方法在全局唯一的一个工作线程中完成。
    • 所有的文件和内存读写操作,都通过锁对象进行加锁,保证了多线程同步。
    • sp的数据保存写入是全量的。
    • sp在保存大数据量的时候会有可能导致ANR。

    四、QA

    (1)每次调用getSharedPreferences时都会创建一个SharedPreferences对象吗?这个对象具体是哪个类对象?
    答:不是,只要name相同,就会返回同一个,SharedPreferencesImpl对象,packagePrefs存放文件name与SharedPreferencesImpl键值对,sSharedPrefs存放包名与ArrayMap键值对。注意sSharedPrefs是static变量,也就是一个类只有一个实例,因此你每次getSharedPreferences其实拿到的都是同一个SharedPreferences对象。

    (2)在UI线程中调用getXXX有可能导致ANR吗?
    答:有可能的,getXXX之前,会给当前线程加锁,如果sp文件特别大,查询非常耗时的时候,有可能ANR。

    (3)为什么SharedPreferences只适合用来存放少量数据,为什么不能把SharedPreferences对应的xml文件当成普通文件一样存放大量数据?
    答:其实这和第二个问题没有区别,因为SharedPreference是整个文件都加载到内存中,文件太大了会对内存造成压力。

    (4)commit和apply有什么区别?
    (5)SharedPreferences每次写入时是增量写入吗?

    答:不是,每次都是重新写入,说一下那个mBackupFile,SharedPreferences在写入时会先把之前的xml文件改成名成一个备份文件,然后再将要写入的数据写到一个新的文件中,如果这个过程执行成功的话,就会把备份文件删除。由此可见每次即使只是添加一个键值对,也会重新写入整个文件的数据,这也说明SharedPreferences只适合保存少量数据,文件太大会有性能问题。SharedPreferences每次写入都是整个文件重新写入,不是增量写入。

    相关文章

      网友评论

          本文标题:Android-SharedPreferences

          本文链接:https://www.haomeiwen.com/subject/pbarmktx.html