Android SharedPreferences 详解 源码解析

1.实现类

SharedPreferences 只是一个接口,其实现类是SharedPreferencesImpl。

工作流程分析:
创建sp 的时候,会去查看是否有bak文件,如果有的话,把bak文件,重命名成file的真正文件名,读取到内存。

    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        startLoadFromDisk();
    }

    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }

如果在读取的过程中,你调用了getString,那么该方法会等到io 读取到map 完成,返回结果。

2.getString 会直接从磁盘里面直接读取吗?

不会,会从内存里面读取。

    @Override
    @Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

3.apply 和 commit 的 区别。

apply 不会立马写在磁盘里面,commit 会的。
关键位置是SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

我们看下这个方法:

    /**
     * Enqueue an already-committed-to-memory result to be written
     * to disk.
     *
     * They will be written to disk one-at-a-time in the order
     * that they're enqueued.
     *
     * @param postWriteRunnable if non-null, we're being called
     *   from apply() and this is the runnable to run after
     *   the write proceeds.  if null (from a regular commit()),
     *   then we're allowed to do this disk write on the main
     *   thread (which in addition to reducing allocations and
     *   creating a background thread, this has the advantage that
     *   we catch them in userdebug StrictMode reports to convert
     *   them where possible to apply() ...)
     */
    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

        // Typical #commit() path with fewer allocations, doing a write on
        // the current thread.
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }

        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

如果是commit 的话,writeToDiskRunnable.run(); 写文件的这个操作直接进行。如果不是的话,会放到一个队列里面去执行。QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);

疑问:

如果我apply 是异步进行的,那么为什么我putString (“aaa”,“111”)之后调用apply ,立马就getString(“aaa”,"")能够返回正确的结果呢?

因为apply 和commit 一样,都会先把改动保存到内存,然后写到文件里面。

@Override
        public void apply() {
            final long startTime = System.currentTimeMillis();

            final MemoryCommitResult mcr = commitToMemory();
        // Returns true if any changes were made
        private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            List<String> keysModified = null;
            Set<OnSharedPreferenceChangeListener> listeners = null;
            Map<String, Object> mapToWriteToDisk;

            synchronized (SharedPreferencesImpl.this.mLock) {
                // 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
                    mMap = new HashMap<String, Object>(mMap);
                }
                mapToWriteToDisk = mMap;
                mDiskWritesInFlight++;

                boolean hasListeners = mListeners.size() > 0;
                if (hasListeners) {
                    keysModified = new ArrayList<String>();
                    listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
                }

                synchronized (mEditorLock) {
                    boolean changesMade = false;

                    if (mClear) {
                        if (!mapToWriteToDisk.isEmpty()) {
                            changesMade = true;
                            mapToWriteToDisk.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 (!mapToWriteToDisk.containsKey(k)) {
                                continue;
                            }
                            mapToWriteToDisk.remove(k);
                        } else {
                            if (mapToWriteToDisk.containsKey(k)) {
                                Object existingValue = mapToWriteToDisk.get(k);
                                if (existingValue != null && existingValue.equals(v)) {
                                    continue;
                                }
                            }
                            mapToWriteToDisk.put(k, v);
                        }

                        changesMade = true;
                        if (hasListeners) {
                            keysModified.add(k);
                        }
                    }

                    mModified.clear();

                    if (changesMade) {
                        mCurrentMemoryStateGeneration++;
                    }

                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
        }

mapToWriteToDisk = mMap; 这一句话就把mMap 赋值给mapToWriteToDisk 。而所有的改动都会在这个mapToWriteToDisk 上去修改。其实最终修改的就是mMap。所以不需要等到写到文件里面,你就可以拿到正确的结果。

4.apply 的实现方法

比如当我们调用sp.setString().apply 的时候,首先会把你设置的String 提交到内存里面,也就是map 里面。

然后会调用QueuedWork.addFinisher(awaitCommit); 把这个等待的runnable 添加到QueueWork 的finish 队列里。
等待的Runnable 代码如下:

            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }
                    }
                };

然后把写文件的runnable 放入QueueWork 的队列里面。QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);

在QueueWork 里面会发送一个延迟100ms 的消息,在消息里面会去处理写文件的Runnable。

写文件成功之后,会把等待的Runnable 从QueueWork 的 finisher 里面移除。QueuedWork.removeFinisher(awaitCommit);

在ActivityThread hanlderStopActivity 的时候,会调用QueueWork 的waitTofinish() 方法,等待所有的apply 的写文件完成。

commit 的实现

commit 的实现比较简单,当我们commit 的时候,会把写文件的runnable 发送到QueueWork 的 队列里,所以就算是commit 写文件也不是在主线程写的。但是commit 方法会调用mcr.writtenToDiskLatch.await(); 去等待QueueWork 写文件完成。

5.设计优缺点

6.SP设计里面的备份文件

SP会涉及两个文件,一个真正的文件,一个备份文件。
如果sp 改变了,是要写文件的话,如果当前的sp 文件file存在,首先把文件真正的file重命名为file.bak.
如果重命名不成功的话,整个操作以失败告终。

接着创建fileoutputstream.把map 写进去,改文件权限。
如果整个操作成功,那么把备份文件删除掉。如果失败,把file 文件删除掉。

 // Note: must hold mWritingToDiskLock
    private void writeToFile(MemoryCommitResult mcr) {
        // Rename the current file so it may be used as a backup during the next read
        if (mFile.exists()) {
            if (!mcr.changesMade) {
                // If the file already exists, but no changes were
                // made to the underlying map, it's wasteful to
                // re-write the file.  Return as if we wrote it
                // out.
                mcr.setDiskWriteResult(true);
                return;
            }
            if (!mBackupFile.exists()) {
                if (!mFile.renameTo(mBackupFile)) {
                    Log.e(TAG, "Couldn't rename file " + mFile
                          + " to backup file " + mBackupFile);
                    mcr.setDiskWriteResult(false);
                    return;
                }
            } else {
                mFile.delete();
            }
        }

        // Attempt to write the file, delete the backup and return true as atomically as
        // possible.  If any exception occurs, delete the new file; next time we will restore
        // from the backup.
        try {
            FileOutputStream str = createFileOutputStream(mFile);
            if (str == null) {
                mcr.setDiskWriteResult(false);
                return;
            }
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
            FileUtils.sync(str);
            str.close();
            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
            try {
                final StructStat stat = Os.stat(mFile.getPath());
                synchronized (this) {
                    mStatTimestamp = stat.st_mtime;
                    mStatSize = stat.st_size;
                }
            } catch (ErrnoException e) {
                // Do nothing
            }
            // Writing was successful, delete the backup file if there is one.
            mBackupFile.delete();
            mcr.setDiskWriteResult(true);
            return;
        } catch (XmlPullParserException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        } catch (IOException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        }
        // Clean up an unsuccessfully written file
        if (mFile.exists()) {
            if (!mFile.delete()) {
                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
            }
        }
        mcr.setDiskWriteResult(false);
    }

下次进来去读文件的时候,如果back 文件存在,直接把file 文件删除掉,并且把back 文件,重新命名成file.

private void loadFromDiskLocked() {
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }

总之,所有正确的都以bak 文件为准。

7.wait notify 的使用

8.SharedPreferences 支持多进程吗?

不知道,如果是多进程,可能在一个进程里面写的值,被另外一个进程都给冲掉了。

9.apply 是完全异步的吗?会不会导致ANR?

"main@10722" prio=5 tid=0x2 nid=NA waiting
  java.lang.Thread.State: WAITING
	  at sun.misc.Unsafe.park(Unsafe.java:-1)
	  at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
	  at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:868)
	  at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1023)
	  at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1334)
	  at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:232)
	  at android.app.SharedPreferencesImpl$EditorImpl$1.run(SharedPreferencesImpl.java:466)
	  at android.app.QueuedWork.waitToFinish(QueuedWork.java:194)
	  at android.app.ActivityThread.handleStopActivity(ActivityThread.java:4318)
	  at android.app.servertransaction.StopActivityItem.execute(StopActivityItem.java:41)
	  at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:145)
	  at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
	  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1872)
	  at android.os.Handler.dispatchMessage(Handler.java:106)
	  at android.os.Looper.loop(Looper.java:193)
	  at android.app.ActivityThread.main(ActivityThread.java:6743)
	  at java.lang.reflect.Method.invoke(Method.java:-1)
	  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:486)
	  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:882)

我们看这段堆栈,发现当activity onStop 的时候,会执行QueuedWork.waitToFinish()方法。这个QueuedWork 类,是供SP apply 异步写文件的一个类,里面会有HandlerThread去负责写文件。
waitToFinish 方法会把没有执行的所有runnable,放到主线程执行。所以handleStopActivity 会等待所有的apply 没有完成的runnable 去执行完成。所以,apply 并不是说完全异步的。也有可能导致ANR。但是,apply 这种只会在调用waitToFinish() 的场景才会触发ANR. 如果一个点击事件,如果里面处理的很多的业务逻辑,最后调用了commit 方法,那么有可能因为commit 产生ANR,但是不会因为apply 产生ANR.

我们看下QueueWork 所有waitToFinish() 方法调用的地方:
在这里插入图片描述
我们发现基本上都在ActivityThread 这个类里面。

模拟apply 产生ANR:

Class<?> aClass = null;
                        try {
                            aClass = Class.forName("android.app.QueuedWork");
                            Method addFinisher = aClass.getMethod("addFinisher", Runnable.class);
                            if (addFinisher != null) {
                                addFinisher.invoke(null, new Runnable() {
                                    @Override
                                    public void run() {
                                        try {
                                            Thread.sleep(30000);
                                        } catch (InterruptedException e) {
                                            e.printStackTrace();
                                        }
                                    }
                                });
                            }

                        } catch (ClassNotFoundException e) {
                            e.printStackTrace();
                        } catch (NoSuchMethodException e) {
                            e.printStackTrace();
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        } catch (InvocationTargetException e) {
                            e.printStackTrace();
                        }

我们可以通过以上代码,在QueueWork 里面添加一个finisher,然后点击手机back键,会发现会卡在onStop 方法那里。

总结:

1.面试不会面试业务代码,他们根本不熟悉,只会面android 源码 相关的问题。而sp 从来没有看过。所以很多问题不知道。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值