美文网首页
SharedPreferences分析

SharedPreferences分析

作者: 风月寒 | 来源:发表于2021-02-02 11:58 被阅读0次

SharedPreference 是一个轻量级的数据存储方式,使用起来也非常方便,以键值对的形式存储在本地,在本地磁盘以想XML文件保存,因此会带来以下问题:

1、通过 getXXX() 方法获取数据,可能会导致主线程阻塞

2、SharedPreference 加载的数据会一直留在内存中,浪费内存

3、apply() 方法虽然是异步的,可能会发生 ANR

4、apply() 方法无法获取到操作成功或者失败的结果

5、多进程不安全,可能导致文件缺失

通过 getXXX() 方法获取数据,可能会导致主线程阻塞
public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }
    
private void awaitLoadedLocked() {
        if (!mLoaded) {
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }

在getXXX(),会先调用awaitLoadedLocked(),在该方法内调用了wait() 方法,当mLoaded为true时,会notifyAll。从mMap根据key查询到值返回。那有一个问题,mLoaded什么时候为true?mMap什么时候初始化并赋值?

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();
    }
    
private void loadFromDisk() {
        synchronized (mLock) {
            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<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);
                    map = (Map<String, Object>) XmlUtils.readMapXml(str);
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {

        } catch (Throwable t) {
            thrown = t;
        }

        synchronized (mLock) {
            mLoaded = true;
            mThrowable = thrown;

            try {
                if (thrown == null) {
                    if (map != null) {
                        mMap = map;
                        mStatTimestamp = stat.st_mtim;
                        mStatSize = stat.st_size;
                    } else {
                        mMap = new HashMap<>();
                    }
                }
            } catch (Throwable t) {
                mThrowable = t;
            } finally {
                mLock.notifyAll();
            }
        }
    }

从上面可以看出,在SharedPreferencesImpl的构造方法中会去从磁盘中加载文件,然后保存在内存中,然后加载完成之后,就会将mLoaded置为true,并赋值给mMap。如果读取几 KB 的数据还好,假设读取一个大的文件,势必会造成主线程阻塞。

apply提交
public void apply() {
            final long startTime = System.currentTimeMillis();

            final MemoryCommitResult mcr = commitToMemory();//1
            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(awaitCommit);//2

            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };

            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);//3

            notifyListeners(mcr);
        }

在1处,将在内存修改的map的数据转移到写入磁盘的map中,并且清空mModified自己,然后返回一个MemoryCommitResult对象。

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;
                //这个变量是在后面用来判断commit提交数据方式是在当前线程立马执行还是放入队列中,而且每次不管是apply提交还是commit提交,这个变量都会增加。
                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();
                       
                        if (v == this || v == null) {
                            if (!mapToWriteToDisk.containsKey(k)) {
                                continue;
                            }
                            mapToWriteToDisk.remove(k);
                        } else {
                        //mapToWriteToDisk保存的是最开始初始化从磁盘加载的那些数据,mModified则是在内存直接修改的或者添加的数据,如果mapToWriteToDisk中含有该数据,则直接跳过。否则是直接添加到mapToWriteToDisk中。
                            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) {
                        //用于版本判断的,判断当前版本与原来版本时候一样大,不一样大,如果是commit方式,则需要有返回结果,apply则不需要。
                        mCurrentMemoryStateGeneration++;
                    }

                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
            //最后封装成MemoryCommitResult,后面得所有操作时针对这个实例
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
        }

在注释2处可以看到将一个runnable加入到QueuedWork,我们来看看QueuedWork是什么东西。

QueuedWork{
    private static final Object sLock = new Object();

    private static Object sProcessingWork = new Object();

    //拿到锁才能处理任务
    @GuardedBy("sLock")
    @UnsupportedAppUsage
    private static final LinkedList<Runnable> sFinishers = new LinkedList<>();

    //全局锁
    @GuardedBy("sLock")
    private static final LinkedList<Runnable> sWork = new LinkedList<>();
}

在QueuedWork中,有两个队列,sWork队列存储实际要执行的任务,sFinishers队列也是存储检测任务,用来检测sWork中的工作任务是否执行完成。由前面可知,通过queue进行任务添加。

public static void queue(Runnable work, boolean shouldDelay) {
        //过去一个handler
        Handler handler = getHandler();

        synchronized (sLock) {
            //  添加到实际要执行的任务队列中
            sWork.add(work);

            if (shouldDelay && sCanDelay) {
                //这是按照apply方式提交的,需要延时100ms
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
            //这是commit方式提交的
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

private static Handler getHandler() {
        synchronized (sLock) {
            if (sHandler == null) {
                //如果handler没有创建,则启动一个HandlerThread
                HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                        Process.THREAD_PRIORITY_FOREGROUND);
                handlerThread.start();
                //根据HandlerThread的looper创建一个handler,发送的消息则在这个handler里面执行。
                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();
            }
        }
    }
    
private static void processPendingWork() {
        long startTime = 0;

        if (DEBUG) {
            startTime = System.currentTimeMillis();
        }
        //这里添加锁得原因是正在执行得工作线程和添加数据得线程可能同时在操作
        synchronized (sProcessingWork) {
            LinkedList<Runnable> work;

            synchronized (sLock) {
                work = (LinkedList<Runnable>) sWork.clone();
                sWork.clear();

                // Remove all msg-s as all work will be processed now
                getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
            }

            if (work.size() > 0) {
                for (Runnable w : work) {
                    w.run();
                }
            }
        }
    }

在3处将写数据任务和写完数据后需要执行的任务放到队列中

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

    
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                //这个判断是否只有一个需要提交得内容,如果是,则用commit得方式直接在该线程执行,如果不是则添加到队列中,二如果是apply得方式提交则直接添加到队列中,先使用commit提交一个需要耗时10ms的任务,记为任务1,立即再使用apply提交一个需要耗时5ms的任务,记为任务2。那么任务2一定会在任务1前执行完的结论,这个结论是错误。是根据这个变量来判断得。如果超过1个,则会提交到队列中。
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }

        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }
commit提交

commit提交与apply提交得不同就是该方法得第二个参数传入为null。

SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
Context.MODE_MULTI_PROCESS 到底做了什么
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);
                //创建一个SharedPreferencesImpl对象
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);
                return sp;
            }
        }
        //模式为MODE_MULTI_PROCESS
        if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
        
void startReloadIfChangedUnexpectedly() {
        synchronized (mLock) {
            if (!hasFileChangedUnexpectedly()) {
                return;
            }
            startLoadFromDisk();
        }
    }
    
private boolean hasFileChangedUnexpectedly() {
        synchronized (mLock) {
            if (mDiskWritesInFlight > 0) {
                //这个变量在前面解释过,当commitToMemory的时候,这个变量会增加,而当提交的任务执行完之后,这个变量就会减少。
                return false;
            }
        }

        final StructStat stat;
        try {
            BlockGuard.getThreadPolicy().onReadFromDisk();
            stat = Os.stat(mFile.getPath());
        } catch (ErrnoException e) {
            return true;
        }

        synchronized (mLock) {
            return !stat.st_mtim.equals(mStatTimestamp) || mStatSize != stat.st_size;
        }
    }

SharedPreferences 中会记录最后修改时间以及文件大小,当使用 Context.MODE_MULTI_PROCESS 时,此时会通过 StructStat(Os.stat() 返回) 计算得到,然后与当前最后同步时间和文件大小进行比较,如果不匹配就会触发 startLoadFromDisk 方法执行,既重新加载文件内容到内存 mMap 中。

文件损坏 & 备份机制

由于不可预知的原因(比如内核崩溃或者系统突然断电),xml文件的 写操作 异常中止,Android系统本身的文件系统虽然有很多保护措施,但依然会有数据丢失或者文件损坏的情况。

SharedPreferences采用文件备份操作来规避这个问题。

// 尝试写入文件
private void writeToFile(...) {
  if (!backupFileExists) {
      !mFile.renameTo(mBackupFile);
  }
}

这之后,尝试对文件进行写入操作,写入成功时,则将备份文件删除:

// 写入成功,立即删除存在的备份文件
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();

反之,若因异常情况(比如进程被杀)导致写入失败,进程再次启动后,若发现存在备份文件,则将备份文件重名为源文件,原本未完成写入的文件就直接丢弃:

// 从磁盘初始化加载时执行
private void loadFromDisk() {
    synchronized (mLock) {
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }
  }

通过文件备份机制,我们能够保证数据只会丢失最后的更新,而之前成功保存的数据依然能够有效。

相关文章

网友评论

      本文标题:SharedPreferences分析

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