SharedPreferences源码角度分析 用commit 还是apply ?

前言

最近一段时间一直在Review《祖传代码》, 用AndroidStudio Inspect Code扫描时候发现一段Warning 。出于好奇,扒一扒源码(源码是最好的老师) 。从源码分析一下commit()和apply()到底有什么区别。下文是来自编译器提示warning⚠️ 内容。


Consider using 'apply()' instead; 'commit' writes its data to persistent storage immediately, whereas 'apply' will handle it in the background

  • 翻译成中文大概意思:

开发者应考虑使用apply(),来替代commit()。commit(同步)会将数据写入持久化存储,apply(异步)会在后台处理。

commit方法源码注释

       /**
         * Commit your preferences changes back from this Editor to the
         * {@link SharedPreferences} object it is editing.  This atomically
         * performs the requested modifications, replacing whatever is currently
         * in the SharedPreferences.
         *
         * 

Note that when two editors are modifying preferences at the same * time, the last one to call commit wins. * *

If you don't care about the return value and you're * using this from your application's main thread, consider * using {@link #apply} instead. * * @return Returns true if the new values were successfully written * to persistent storage. */ boolean commit();

  • 针对commit()源码中的注释进行一下解读(英文水平高,直接看就好):

    1. 多并发commit时,需要等待正在处理commit数据更新到磁盘中,才会继续执行,相比apply效率较低。

    2. commit()是有返回值,布尔类型,返回是否成功写入数据,不考虑结果可以使用apply()。

    3. 写入数据是同步操作

apply方法源码注释

       /**
         * Commit your preferences changes back from this Editor to the
         * {@link SharedPreferences} object it is editing.  This atomically
         * performs the requested modifications, replacing whatever is currently
         * in the SharedPreferences.
         *
         * 

Note that when two editors are modifying preferences at the same * time, the last one to call apply wins. * *

Unlike {@link #commit}, which writes its preferences out * to persistent storage synchronously, {@link #apply} * commits its changes to the in-memory * {@link SharedPreferences} immediately but starts an * asynchronous commit to disk and you won't be notified of * any failures. If another editor on this * {@link SharedPreferences} does a regular {@link #commit} * while a {@link #apply} is still outstanding, the * {@link #commit} will block until all async commits are * completed as well as the commit itself. * *

As {@link SharedPreferences} instances are singletons within * a process, it's safe to replace any instance of {@link #commit} with * {@link #apply} if you were already ignoring the return value. * *

You don't need to worry about Android component * lifecycles and their interaction with apply() * writing to disk. The framework makes sure in-flight disk * writes from apply() complete before switching * states. * *

The SharedPreferences.Editor interface * isn't expected to be implemented directly. However, if you * previously did implement it and are now getting errors * about missing apply(), you can simply call * {@link #commit} from apply(). */ void apply();

  • 针对apply()源码中的注释进行一下解读(英文水平高,直接看就好):

    1. apply写入过程中会阻塞同一个SharedPreferences对象其它写入操作。

    2. apply()是void没有任何返回值,

    3. 写入数据是异步操作,与commit方式不同,apply以原子形式将数据提交到内存中,apply()调用的函数直接覆盖之前内存数据,然后异步提交到磁盘上,效率上相比commit要高。

原子性

什么是原子性 ?

这个思想体现在多线程并发操作里,即一个操作或者多个操作,要么全部执行,执行过程不会被任何因素打断,要么全部不执行。

经典的转账问题

比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。
试想一下,如果这2个操作不具备原子性,会造成什么样的后果。

假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。

SharedPreferences 源码流程分析

本文所用的源码为安卓30, AndroidStudio中SDK Platforms里下载。


SDK Platforms.png

从源码中可知 SharedPreferences是个接口,需要找一下SharedPreferences具体实现类。SharedPreferencesImpl 类是SharedPreferences具体实现,从这个方法里能找到commit和apply的具体逻辑。

SharedPreferencesImpl所在路径.png

commit()

简单说一下这块(注释很细可参考)。将Editor对象设置的数据存储到内存中,存储时主线程处于阻塞状态,等到写入物理磁盘有结果(成功或者失败)后,解除阻塞状态。同步回调结果和写入物理磁盘结果。

 public boolean commit() {
            long startTime = 0;
            if (DEBUG) {
                startTime = System.currentTimeMillis();
            }
           //将编辑(Editor)的数据,提交到内存
            MemoryCommitResult mcr = commitToMemory();
            SharedPreferencesImpl.this.enqueueDiskWrite(
             mcr, null /* sync write on this thread okay */);
            try {
                //阻塞等待写入完成
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException e) {
                return false;
            } finally {
                if (DEBUG) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " committed after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
          //就是对onSharedPreferenceChanged结果回调。
            notifyListeners(mcr);
           //返回数据是否写入成功
            return mcr.writeToDiskResult;
        }

commitToMemory方法

We optimistically don't make a deep copy until a memory commit comes in when we're already writing to disk.
翻译:在数据写入物理媒介前,代码不会直接进行复制,先将数据提交到内存。

该方法的主要功能:

把EditorImpl数据更新到SharedPreferencesImpl,这样说可能有点不好理解,在通俗一点说commitToMemory()将Editor编辑的数据同步到内存中。代码中注释比价详细,捋一捋主要的东西。

  • if (mDiskWritesInFlight > 0) {} 说明:HashMap本身不是一个线程安全的集合,因此会对全局的mMap进行一次拷贝,让其他线程可以正常的查询数据
  • mapToWriteToDisk将要写入物理磁盘的数据。mMap是全局变量,用来记录之前存过的数据,第一次执行commitToMemory()时候mapToWriteToDisk和mMap都是0,等到writeToFile()执行完后,会在指定路径生成xxx.xml, 再次调用,mMap就是从存储xxx.xml数据中解析存过的数据。
  • mModified.entrySet()是来自Editor内部实现(EditorImpl),存储Editor put过的key和value,然后mapToWriteToDisk接收来自Editor(编辑)数据。mModified清空掉数据,为下一次存储数据做准备。(感觉Editor就是一个载体,临时文件中转)
  private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            boolean keysCleared = false; 
            List keysModified = null;
            Set listeners = null;
            Map mapToWriteToDisk;
            synchronized (SharedPreferencesImpl.this.mLock) {
               //如果mDiskWritesInFlight>0,说明其他线程在进行写入操作。
                if (mDiskWritesInFlight > 0) {
                    //复制mMap,对新数据进行操作
                    mMap = new HashMap(mMap);
                }
               //正常情况下会走到这里赋值,
                mapToWriteToDisk = mMap;
               //默认值是0 ,自增+1
                mDiskWritesInFlight++;

                boolean hasListeners = mListeners.size() > 0;
               //是否设置了OnSharedPreferenceChangeListener监听
                if (hasListeners) {
                    keysModified = new ArrayList();
                    listeners = new HashSet(mListeners.keySet());
                }
                synchronized (mEditorLock) {
                    //内存数据有没有修改
                    boolean changesMade = false;
                    //如果mClear等于true,说明调用过Editor.clear()
                    if (mClear) {
                        //如果mClear为true,直接清空mapToWriteToDisk(mMap)数据
                        if (!mapToWriteToDisk.isEmpty()) {
                            changesMade = true;
                            mapToWriteToDisk.clear();
                        }
                        keysCleared = true;
                        mClear = false;
                    }
                    // mModified在Editor.putxxx操作已经存放数据了
                    for (Map.Entry e : mModified.entrySet()) {
                        String k = e.getKey();
                        Object v = e.getValue();
                        //EditorImpl存入的value 值等于自身对象或者null,移除对应的key
                        if (v == this || v == null) {
                            //如果之前就不存在该key, 跳过本次操作,没有任何意义。
                            if (!mapToWriteToDisk.containsKey(k)) {
                                continue;
                            }
                            mapToWriteToDisk.remove(k);
                        } else {
                            if (mapToWriteToDisk.containsKey(k)) {
                                Object existingValue = mapToWriteToDisk.get(k);
                                //判断value有没有做修改,没有做过修改,跳过本次操作
                                if (existingValue != null && existingValue.equals(v)) {
                                    continue;
                                }
                            }
                          //将新添加的k,v添加到磁盘中,准备写入物理媒介
                            mapToWriteToDisk.put(k, v);
                        }
                        changesMade = true;

                      //设置了OnSharedPreferenceChangeListener监听
                        if (hasListeners) {
                         //记录发生改变的key
                            keysModified.add(k);
                        }
                    }
                    //清空EditorImpl中的mModified数据.
                    mModified.clear();
                    //数据已经改变,内存变换次数自增
                    if (changesMade) {
                        mCurrentMemoryStateGeneration++;
                    }

                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
             //将MemoryCommitResult对象返回
            return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
                    listeners, mapToWriteToDisk);
        }

MemoryCommitResult

MemoryCommitResult类是SharedPreferencesImpl类的静态内部类。用来保存对内存整理的结果。 这部分没什么好说的,不贴代码了。略微说一下重要的。

  • CountDownLatch一种同步类,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

  • CountDownLatch默认值是1 ,调用await()线程被挂起,代表着阻塞(等待)。调用setDiskWriteResult()计数器会-1 ,值变成0,代表解除阻塞。

enqueueDiskWrite

将已提交到内存的结果排入队列以写入磁盘。会按照它们入队的顺序一次一个地写入磁盘。

enqueueDiskWrite方法可分,Runnable对象为null(apply),和不为null两种情况(commit)。

  • 等于null(commit),isFromSyncCommit为true。然后调用writeToFile,mDiskWritesInFlight计数自减。 postWriteRunnable.run不执行

  • 不等于null(apply), QueuedWork.queue()。然后调用writeToFile,mDiskWritesInFlightj计数自减。 postWriteRunnable.run 执行

 private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        // 区分同步写入物理媒介,还是异步写入物理媒介
        //commit()中传的是null,可以将isFromSyncCommit变更为true;
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        //真正执行写入文件的操作
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                  // postWriteRunnable不为空,针对的是apply()
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

        //同步写入物理媒介
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
               //执行 commitToMemory()过程会加1,上面有说过,wasEmpty为true
                wasEmpty = mDiskWritesInFlight == 1;
            }
            
            if (wasEmpty) {
              // 执行 上面的 writeToDiskRunnable()
                writeToDiskRunnable.run();
                return;
            }
        }
          //异步写入
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

writeToFile

这部分就是将内存中的数据写入到物理磁盘中

  • 第一次执行,mFile文件并不存在(还没有创建),直接走到FileOutputStream,将内存中的数据写入到具体物理路径下xxx.xml,这部分方法流程结束(没全细说,有注释方便理解)。

  • 第二次要调用writeToFile(),fileExists已经存在。mDiskStateGeneration < mcr.memoryStateGeneration 不成立说明数据相同,没有任何改变。setDiskWriteResult把结果回调过去,解除阻塞状态,return 结束不往下执行。反之有新的数据该走到mBackupFile.exists(),然后走到FileOutputStream,将新的数据全量写入到xxx.xml

private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        long startTime = 0;
        long existsTime = 0;
        long backupExistsTime = 0;
        long outputStreamCreateTime = 0;
        long writeTime = 0;
        long fsyncTime = 0;
        long setPermTime = 0;
        long fstatTime = 0;
        long deleteTime = 0;

        if (DEBUG) {
            startTime = System.currentTimeMillis();
        }
        //文件是否存在,mFile路径(我的测试机)/data/user/0/packageName/shared_prefs/xxx.xml,

        //xxx为 getSharedPreferences("xxx",Context.MODE_PRIVATE)
        boolean fileExists = mFile.exists();
        if (DEBUG) {
            existsTime = System.currentTimeMillis();
            // Might not be set, hence init them to a default value
            backupExistsTime = existsTime;
        }

       // xml存在,重命名当前文件,以便下次读取时用作备份
        if (fileExists) {
            boolean needsWrite = false;

           //当磁盘状态早于本次提交时,才需要写入
            if (mDiskStateGeneration < mcr.memoryStateGeneration) {
                //同步写入commit()
                if (isFromSyncCommit) {
                    needsWrite = true;
                } else { 
                   //异步写入apply()
                    synchronized (mLock) {
                        //如果是最新的数据,需要写入物理媒介。
                        if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                            needsWrite = true;
                        }
                    }
                }
            }
            //不需要写入,设置写入结果。
            if (!needsWrite) {
                mcr.setDiskWriteResult(false, true);
                return;
            }
            //备份文件是否存在
            boolean backupFileExists = mBackupFile.exists();

            if (DEBUG) {
                backupExistsTime = System.currentTimeMillis();
            }
            // 不存在备份文件
            if (!backupFileExists) {
                //将mFile重命名为备份文件(数据做一下备份)
                if (!mFile.renameTo(mBackupFile)) {
                    Log.e(TAG, "Couldn't rename file " + mFile
                          + " to backup file " + mBackupFile);
                    mcr.setDiskWriteResult(false, false);
                    return;
                }
            } else {
                mFile.delete(); //删除mFile文件
            }
        }
          //写入文件,删除备份,并返回true,尽可能保持原子性。
          //如果出现异常,删除新文件,下次从备份文件中恢复数据
        try {
            //创建文件输出流
            FileOutputStream str = createFileOutputStream(mFile);

            if (DEBUG) {
                outputStreamCreateTime = System.currentTimeMillis();
            }
        
            if (str == null) {
                mcr.setDiskWriteResult(false, false);
                return;
            }
            //将内存中的数据写入 到物理xml文件里
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
            //记录写入时间
            writeTime = System.currentTimeMillis();
            //同步流数据
            FileUtils.sync(str);
            //记录同步时间
            fsyncTime = System.currentTimeMillis();

            str.close();
            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
            if (DEBUG) {
                setPermTime = System.currentTimeMillis();
            }
            try {
                //获取文件信息
                final StructStat stat = Os.stat(mFile.getPath());
                synchronized (mLock) {
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                }
            } catch (ErrnoException e) {
                // Do nothing
            }

            if (DEBUG) {
                fstatTime = System.currentTimeMillis();
            }

            //有备份文件删除
            mBackupFile.delete();

            if (DEBUG) {
                deleteTime = System.currentTimeMillis();
            }

            mDiskStateGeneration = mcr.memoryStateGeneration;

            //设置物理磁盘写入结果,到这里返回的肯定是写入成功。
            mcr.setDiskWriteResult(true, true);

            if (DEBUG) {
                Log.d(TAG, "write: " + (existsTime - startTime) + "/"
                        + (backupExistsTime - startTime) + "/"
                        + (outputStreamCreateTime - startTime) + "/"
                        + (writeTime - startTime) + "/"
                        + (fsyncTime - startTime) + "/"
                        + (setPermTime - startTime) + "/"
                        + (fstatTime - startTime) + "/"
                        + (deleteTime - startTime));
            }

            long fsyncDuration = fsyncTime - writeTime;
            mSyncTimes.add((int) fsyncDuration);
            mNumSync++;

            if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
                mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
            }

            return;
        } catch (XmlPullParserException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        } catch (IOException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        }

        // 清理没有成功写入的文件
        if (mFile.exists()) {
              //无法删除的文件
            if (!mFile.delete()) {
                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
            }
        }
        mcr.setDiskWriteResult(false, false);
    }

apply()

commitToMemory在上面已经说过,不再复述。apply 方法,创建了一个 awaitCommit 的 Runnable,然后加入到 QueuedWork 中,awaitCommit 中包含了一个等待锁,需要在其它地方释放。

通过 SharedPreferencesImpl.this.enqueueDiskWrite 创建了一个任务来执行真正的 SP 持久化。

        public void apply() {
            final long startTime = System.currentTimeMillis();
          // 将数据提交到内存中
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                @Override
                public void run() {
                    try {
                        //等待写入数据完成,阻塞状态
                        mcr.writtenToDiskLatch.await();
                    } catch (InterruptedException ignored) {
                    }

                    if (DEBUG && mcr.wasWritten) {
                        Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                                + " applied after " + (System.currentTimeMillis() - startTime)
                                + " ms");
                    }
                }
            };
            // QueuedWork.addFinisher:添加一个异步处理工作,用于等待。
            //实际上并没有开始运行,只是为了执行异步工作调用着准备临时集。
      
            QueuedWork.addFinisher(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                @Override
                public void run() {
                    awaitCommit.run();
                  //将awaitCommit对象从QueuedWork移除
                    QueuedWork.removeFinisher(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);
        }

QueuedWork.queue

/**
* Queue a work-runnable for processing asynchronously.
* @param work The new runnable to process
* @param shouldDelay If the message should be delayed
*/
翻译 :
QueuedWork :异步处理可运行的工作排入队列(系统)。
work : 新的可运行文件
shouldDelay : 是否需要延迟。

public static void queue(Runnable work, boolean shouldDelay) {
    //在单独的线程创建处理程序
      Handler handler = getHandler();
      synchronized (sLock) {
            //添加任务队列
          sWork.add(work);
          if (shouldDelay && sCanDelay) {
            //延迟 0.1s发送消息
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
          } else {
            //实时发消息
              handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
          }
      }
  }

getHandler()

这部分内容就是开辟一个单独的线程执行工作任务。sHandler为QueuedWorkHandler对象。当调用sHandler发送消息,消息会在QueuedWorkHandler类的handlerMessage方法进行处理。 收到消息后,调用processPendingWork方法进行处理。

private static Handler getHandler() {
        synchronized (sLock) {
            //第一次调用的默认 null, if条件成立。
            //不等null直接返回sHandler对象
            if (sHandler == null) {
                //创建一个名为queued-work-looper实例对象
                HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                        Process.THREAD_PRIORITY_FOREGROUND);
                //开启线程
                handlerThread.start();
                 //根据HandlerThread线程的looper创建QueuedWorkHandler对象
                sHandler = new QueuedWorkHandler(handlerThread.getLooper());
            }
            return sHandler;
        }
    }

 private static class QueuedWorkHandler extends Handler {
        static final int MSG_RUN = 1;

        QueuedWorkHandler(Looper looper) {
            super(looper);
        }

        public void handleMessage(Message msg) {
            if (msg.what == MSG_RUN) {
                processPendingWork();
            }
        }
    }

processPendingWork

处理待处理的工作,执行写入磁盘任务,至此apply流程走完 。

 private static void processPendingWork() {
        long startTime = 0;

        if (DEBUG) {
            startTime = System.currentTimeMillis();
        }

        synchronized (sProcessingWork) {
            LinkedList work;

            synchronized (sLock) {
              //将sWork待执行队列复制 work中
                work = (LinkedList) sWork.clone();
                //清空全局变量
                sWork.clear();

                // 移除消息队列中的消息
                getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
            }
            //如果执行任务数量大于0
            if (work.size() > 0) {
                for (Runnable w : work) {
                    //执行写入磁盘任务
                    w.run();
                }

                if (DEBUG) {
                    Log.d(LOG_TAG, "processing " + work.size() + " items took " +
                            +(System.currentTimeMillis() - startTime) + " ms");
                }
            }
        }
    }

结论

  • commit

    建议在子线程中调用,commit()在主线程写入文件,会造成UI卡顿。在多个并发 操作commit的时候,它们会等待正在处理的commit保存到磁盘后,有结果在解除阻塞,从而降低了效率。

  • apply

    建议在主线程中调用。执行效率相对于commit是高的,官方也是非常建议,毕竟从效率上讲异步总是比同步要快。But apply()在子线程写入文件,也有可能卡UI。因为主线程有调用的代码,需要等写入任务执行完了才会继续往下执行

  • 对比一下两者回调的时机

    1. commit是在内存和硬盘操作均结束时回调

    2. apply是内存操作结束时就进行回调

  • 小建议
    官方给 SharedPreferences 定位就是轻量级的数据存储类,存一些简单的数据没问题,存一些大数据暴露出的问题和隐患还是很多,例如明显的卡顿或ANR。


参考文章

  • Java并发-理论基础 (文中原子理论出处)

你可能感兴趣的:(SharedPreferences源码角度分析 用commit 还是apply ?)