SharedPreferences存取数据流程分析

SharedPreferences存取数据流程分析

SharedPreferencesImpl

今天研究一下SharedPreferences存取数据的实现,在Android中SharedPreferences是一个接口,真正的实现类是SharedPreferencesImpl,下面就开始分析SharedPreferencesImpl,首先查看相关的属性

private final File mFile;// 存放数据的文件
private final File mBackupFile;// 更新数据时的备份文件
private final int mMode; //sharedPreferences的模式
private final Object mLock = new Object();// 锁
private final Object mWritingToDiskLock = new Object(); // 写入磁盘的锁
private Map mMap; // 存放的数据映射
private int mDiskWritesInFlight = 0; // 排队写入磁盘的任务数
private long mDiskStateGeneration; // 最后一次提交写入磁盘的state,自增的
private long mCurrentMemoryStateGeneration; // 当前内存数据的state, 自增的
private boolean mLoaded = false; // 是否已经在加载文件的数据

下面在看下构造函数

SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file); //构造出备份文件
    mMode = mode;
    mLoaded = false;//置为false
    mMap = null; 
    mThrowable = null;
    startLoadFromDisk(); //开始加载文件数据
}

//开个线程做加载数据的操作
private void startLoadFromDisk() {
    synchronized (mLock) {
    // mLoaded置为false
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

//构造个备份文件
static File makeBackupFile(File prefsFile) {
    return new File(prefsFile.getPath() + ".bak");
}

下面再看下真正加载数据的方法,loadFromDisk()

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 map = null;
    StructStat stat = null;
    Throwable thrown = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
            // 构造存放文件的数据流,从mFile读数据
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                        // 读取数据生成map
                map = (Map) 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
        mLoaded = true;
        mThrowable = thrown;

        // It's important that we always signal waiters, even if we'll make
        // them fail with an exception. The try-finally is pretty wide, but
        // better safe than sorry.
        try {
        // 如果正常加载完成,没有异常
            if (thrown == null) {
            // 如果数据不为空
                if (map != null) {
                    mMap = map;//把加载的数据赋值给mMap
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                } else {
                // 数据为空则初始化一个mMap
                    mMap = new HashMap<>();
                }
            }
            // In case of a thrown exception, we retain the old map. That allows
            // any open editors to commit and store updates.
        } catch (Throwable t) {
            mThrowable = t;
        } finally {
        // 加载完成,唤醒等待mLock的线程
            mLock.notifyAll();
        }
    }
}
  1. 从mFile中加载数据,这个过程只加载一次,如果已经加载过了,则不再加载
  2. 如果加载的过程中,发现备份文件还存在,那么就说明上次 新建/更新 数据时发生了异常,此时mFile文件的数据得不到保障,所以继续采用备份文件的数据,放弃上次的操作
  3. 如果mFile里面没有数据,则重新初始化mMap,如果有数据,则把读取出来的数据赋值给mMap
  4. 数据读取成功后,唤醒等待加载数据的线程

因为SharedPreferences里面只有读取数据的方法,下面看下getInt()

@Override
public int getInt(String key, int defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();//判断数据是否加载完成,没有加载完成,则阻塞等待
        Integer v = (Integer)mMap.get(key);//从mMap中去数据
        return v != null ? v : defValue;
    }
}

@GuardedBy("mLock")
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 {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}
  1. 首先判断数据是否加载完成,如果没有加载完成,则阻塞当前线程。加载完成后会被加载数据线程唤醒
  2. 数据加载完成后,从mMap中去数据
  3. 如果mMap中有数据则返回,没有的话返回默认值

其他的读取数据的方法也是类似的,还有另外一个方法需要注意下

@Override
public Map getAll() {
    synchronized (mLock) {
        awaitLoadedLocked();
        //noinspection unchecked
        return new HashMap(mMap);
    }
}

调用这个方法会返回当前shardePreferences的所有数据,但是一定不要更改里面的内容,因为如果更改了内容,那么SharedPreferences的数据一致性就得不到保证了。

下面再分析下ShardePreferences的新增,更新和删除的方法,这些方法就需要EditorImpl 这个类来做

EditorImpl

private final Object mEditorLock = new Object();// 锁
private final Map mModified = new HashMap<>(); // 更改的数据
private boolean mClear = false; //是否清空数据

下面看下putString(),其他的putXXX()类似

@Override
public Editor putString(String key, @Nullable String value) {
// 加锁,保证线程安全,数据一致性
    synchronized (mEditorLock) {
        mModified.put(key, value);// 把值放入mModified
        return this;
    }
}
  1. 添加或者更新数据时,都会加锁,保证线程安全,数据一致性。然后会把数据放入mModified

添加完数据后还需要更新到文件和内存中,一般会调用commit()和apply()下面先看看apply()

@Override
public void apply() {
    final long startTime = System.currentTimeMillis();
    // 首先更新内存中的数据,
    final MemoryCommitResult mcr = commitToMemory();
    // 这是等待提交到磁盘的任务
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                // 如果写入磁盘任务未完成,则一直等待writtenToDiskLatch是CountDownLatch
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }

                if (DEBUG && mcr.wasWritten) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " applied after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
        };

// 把awaitCommit 添加QueuedWork的finisher的任务队列
    QueuedWork.addFinisher(awaitCommit);

// 磁盘任务写完之后需要执行任务
    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
            // 执行awaitCommit.run()
                awaitCommit.run();
                // 把awaitCommit 移除finisher队列
                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);
}

上面的代码中引入了MemoryCommitResult,MemoryCommitResult就是用来记录更新的数据提交到内存中的结果,比较简单,会放在下面再看

  1. 首先把数据更新到内存中
  2. 构造两个任务,awaitCommit和postWriteRunnable
  3. 调用enqueueDiskWrite(),传入postWriteRunnable

继续追踪enqueueDiskWrite

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
     // commit()方法传入的postWriteRunnable为null,而apply()传入的不为null
    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
                    postWriteRunnable.run();
                }
            }
        };

    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    // 如果isFromSyncCommit为true
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        // 如果提交的写入磁盘数据的任务数为1,那么就直接执行,不再交给QueuedWork
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
    // 默认交给QueuedWork执行
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
  1. 给isFromSyncCommit赋值,commit()方法中postWriteRunnable,及isFromSyncCommit为true,而apply()为false
  2. 构建一个将数据写入磁盘的任务writeToDiskRunnable
  3. isFromSyncCommit为true,并且写入磁盘的任务数为1,那么就会直接在当前线程执行,否则提交给QueuedWork,放在子线程中执行

上面有个关键的方法writeToFile()

private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    ...

    boolean fileExists = mFile.exists();

    ...
    if (fileExists) {
        boolean needsWrite = false;

        // Only need to write if the disk state is older than this commit
        //只有当前磁盘的state小于内存的state,才会执行写入磁盘的任务
        if (mDiskStateGeneration < mcr.memoryStateGeneration) {
            if (isFromSyncCommit) {
                needsWrite = true;
            } else {
                synchronized (mLock) {
                    // 如果不是同步写入的话,那么只有当前内存state等于mcr.memoryStateGeneration才会执行写入
                    if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                        needsWrite = true;
                    }
                }
            }
        }
        
        // 如果不需要写入,则返回
        if (!needsWrite) {
            mcr.setDiskWriteResult(false, true);
            return;
        }

        boolean backupFileExists = mBackupFile.exists();
        
          ....
          
          // 如果备份文件不存在
        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文件删除
            mFile.delete();
        }
    }

    // 尝试写入数据到mFile,如果写入成功则把备份文件删除;如果写入的过程中发生了异常,则把新文件删除,下次会从备份文件读取数据。这里就跟读取数据的时候关联上了
    try {
        FileOutputStream str = createFileOutputStream(mFile);
        
        // 如果数据流为null
        if (str == null) {
            mcr.setDiskWriteResult(false, false);
            return;
        }
        // 把mcr.mapToWriteToDisk数据写入文件,此时所有的数据包括更新/新增的数据都在mcr.mapToWriteToDisk里面,因为在commitToMemory处理,下面会分析commitToMemory
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

        writeTime = System.currentTimeMillis();

        FileUtils.sync(str);

        fsyncTime = System.currentTimeMillis();

        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

        try {
            final StructStat stat = Os.stat(mFile.getPath());
            synchronized (mLock) {
                mStatTimestamp = stat.st_mtim;
                mStatSize = stat.st_size;
            }
        } catch (ErrnoException e) {
            // Do nothing
        }

        // 写入数据成功,删除备份文件
        mBackupFile.delete();

        // 更新磁盘的mDiskStateGeneration为mcr.memoryStateGeneration
        mDiskStateGeneration = mcr.memoryStateGeneration;
        ...

        mcr.setDiskWriteResult(true, true);

        mNumSync++;

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

    // 如果写入数据的过程中发生了异常,那么就删除mFile文件,下次加载时会直接读取备份文件
    if (mFile.exists()) {
        if (!mFile.delete()) {
            Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
        }
    }
    mcr.setDiskWriteResult(false, false);
}

上面是分析了写入磁盘缓存的方法,下面再看下写入内存缓存的方法commitToMemory()

private MemoryCommitResult commitToMemory() {
    long memoryStateGeneration;
    List keysModified = null;
    Set listeners = null;
    Map mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {
        // 只会在调用了commit和apply方法时,才会执行内存缓存,把数据更新到内存
        if (mDiskWritesInFlight > 0) {
            // 如果当前正在向磁盘写入mMap,那么禁止修改mMap
            // 对当前的mMap进行clone,下面就只会操作这个mMap,因为hashmap不是线程安全,SharedPreference最重要的必须保证的就是数据一致性
            mMap = new HashMap(mMap);
        }
        mapToWriteToDisk = mMap;//把mMap赋值给mapToWriteToDisk
        mDiskWritesInFlight++;// 写入磁盘的任务数 加1

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

        synchronized (mEditorLock) {
            boolean changesMade = false;
            // 如果调用了clear方法,clear标志就会为true
            if (mClear) {
                if (!mapToWriteToDisk.isEmpty()) {
                    changesMade = true;
                    // 清楚数据,此时清楚的只是mMap里面的数据,并没有清除mModified,即更新后的数据没有清除
                    mapToWriteToDisk.clear();
                }
                mClear = false;
            }
    
                // 遍历修改的数据
            for (Map.Entry e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                // 如果v == null 或者v== this,这里this就是EditorImpl对象,就代表需要移除当前k
                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;
                        }
                    }
                    // 把k,v写入mapToWriteToDisk
                    mapToWriteToDisk.put(k, v);
                }

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

             // 修改后的数据集清空,此时所有的数据,原来的数据和更新后的数据全部在mapToWriteToDisk里面
            mModified.clear();
                
                // 如果有更新
            if (changesMade) {
                mCurrentMemoryStateGeneration++;
            }
                // 更新memoryStateGeneration为mCurrentMemoryStateGeneration
            memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
    }
    return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
            mapToWriteToDisk);
}

只有在调用了commit或者apply方法时,才会调用这个方法。这个方法比较简单,就是把mModified中的数据和原来的数据mMap做添加,更新,删除的操作。得到最新的数据mapToWriteToDisk,更新内存状态,最终构造成MemoryCommitResult返回

上面是apply方法,下面再看commit(),跟apply基本一样

@Override
public boolean commit() {
    long startTime = 0;

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

    MemoryCommitResult mcr = commitToMemory();//更新内存的映射

    // 执行硬盘缓存的任务,传入null,表示可以在当前线程执行写入磁盘的操作
    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");
        }
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

分析完apply方法后commit()就很容易了,commit跟apply的区别就在与commit允许在当前线程执行写入磁盘的任务,并且不管写入磁盘是否在当前任务执行,commit都会阻塞当前线程,等待磁盘更新完成。

MemoryCommitResult

这个类比较简单,先看相关的属性

final long memoryStateGeneration;// 当前的内存state
@Nullable final List keysModified;//做了改变的值的列表
@Nullable final Set listeners;
final Map mapToWriteToDisk;// 存放此次内存更新后的数据
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);//用来等待磁盘写入完成

只有一个关键方法

void setDiskWriteResult(boolean wasWritten, boolean result) {
    this.wasWritten = wasWritten;
    writeToDiskResult = result;
    writtenToDiskLatch.countDown();// writtenToDiskLatch计数减一,唤醒等待此任务的线程
}

上面的方法是在设置最终磁盘更新任务的结果,并且将writtenToDiskLatch计数减一,这样等待磁盘更新任务的线程会被唤醒,继续执行。

你可能感兴趣的:(SharedPreferences存取数据流程分析)