本文已收录到 AndroidFamily,技能和职场问题,请关注公众号 [彭旭锐] 发问。

前言

咱们持续上一篇文章的分析:

  • Android 初代 K-V 存储结构 SharedPreferences,旧时代的余晖?(上)
  • Android 初代 K-V 存储结构 SharedPreferences,旧时代的余晖?(下)

6. 两种写回战略

在获得业务目标后,咱们持续分析 Editor 接口中的 commit 同步写回战略和 apply 异步写回战略。

6.1 commit 同步写回战略

Editor#commit 同步写回相对简单,中心进程分为 4 步:

  • 1、调用 commitToMemory() 创立 MemoryCommitResult 业务目标;
  • 2、调用 enqueueDiskWrite(mrc, null) 提交磁盘写回使命(在其时线程履行);
  • 3、调用 CountDownLatch#await() 堵塞等候磁盘写回完结;
  • 4、调用 notifyListeners() 触发回调监听。

commit 同步写回示意图

Android 初代 K-V 存储框架 SharedPreferences,旧时代的余晖?(下)

其实严格来说,commit 同步写回也不肯定是在其时线程同步写回,也有或许在后台 HandlerThread 线程写回。但不管怎么样,关于 commit 同步写回来说,都会调用 CountDownLatch#await() 堵塞等候磁盘写回完结,所以在逻辑上也等价于在其时线程同步写回。

SharedPreferencesImpl.java

public final class EditorImpl implements Editor {
    @Override
    public boolean commit() {
        // 1、获取业务目标(前文已分析)
        MemoryCommitResult mcr = commitToMemory();
        // 2、提交磁盘写回使命
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* 写回成功回调 */);
        // 3、堵塞等候写回完结
        mcr.writtenToDiskLatch.await();
        // 4、触发回调监听器
        notifyListeners(mcr);
        return mcr.writeToDiskResult;
    }
}

6.2 apply 异步写回战略

Editor#apply 异步写回相对复杂,中心进程分为 5 步:

  • 1、调用 commitToMemory() 创立 MemoryCommitResult 业务目标;
  • 2、创立 awaitCommit Ruunnable 并提交到 QueuedWork 中。awaitCommit 中会调用 CountDownLatch#await() 堵塞等候磁盘写回完结;
  • 3、创立 postWriteRunnable Runnable,在 run() 中会履行 awaitCommit 使命并将其从 QueuedWork 中移除;
  • 4、调用 enqueueDiskWrite(mcr, postWriteRunnable) 提交磁盘写回使命(在子线程履行);
  • 5、调用 notifyListeners() 触发回调监听。

能够看到不管是调用 commit 仍是 apply,终究都会调用 SharedPreferencesImpl#enqueueDiskWrite() 提交磁盘写回使命。

差异在于:

  • 在 commit 中 enqueueDiskWrite() 的第 2 个参数是 null;
  • 在 apply 中 enqueueDiskWrite() 的第 2 个参数是一个 postWriteRunnable 写回完毕的回调目标,enqueueDiskWrite() 内部就是依据第 2 个参数来差异 commit 和 apply 战略。

apply 异步写回示意图

Android 初代 K-V 存储框架 SharedPreferences,旧时代的余晖?(下)

SharedPreferencesImpl.java

@Override
public void apply() {
    // 1、获取业务目标(前文已分析)
    final MemoryCommitResult mcr = commitToMemory();
    // 2、提交 aWait 使命
    // 疑问:postWriteRunnable 能够了解,awaitCommit 是什么?
    final Runnable awaitCommit = new Runnable() {
        @Override
        public void run() {
            // 堵塞线程直到磁盘使命履行完毕
            mcr.writtenToDiskLatch.await();
        }
    };
    QueuedWork.addFinisher(awaitCommit);
    // 3、创立写回成功回调
    Runnable postWriteRunnable = new Runnable() {
        @Override
        public void run() {
            // 履行 aWait 使命
            awaitCommit.run();
            // 移除 aWait 使命
            QueuedWork.removeFinisher(awaitCommit);
        }
    };
    // 4、提交磁盘写回使命,并绑定写回成功回调
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable /* 写回成功回调 */);
    // 5、触发回调监听器
    notifyListeners(mcr);
}

QueuedWork.java

// 提交 aWait 使命(后文详细分析)
private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
public static void addFinisher(Runnable finisher) {
    synchronized (sLock) {
        sFinishers.add(finisher);
    }
}
public static void removeFinisher(Runnable finisher) {
    synchronized (sLock) {
        sFinishers.remove(finisher);
    }
}

这儿有一个疑问:

在 apply() 办法中,在履行 enqueueDiskWrite() 前创立了 awaitCommit 使命并加入到 QueudWork 等候行列,直到磁盘写回完毕才将 awaitCommit 移除。这个 awaitCommit 使命是做什么的呢?

咱们略微再答复,先持续往下走。

6.3 enqueueDiskWrite() 提交磁盘写回业务

能够看到,不管是 commit 仍是 apply,终究都会调用 SharedPreferencesImpl#enqueueDiskWrite() 提交写回磁盘使命。虽然 enqueueDiskWrite() 还没到实在调用磁盘写回操作的当地,但确实创立了与磁盘 IO 相关的 Runnable 使命,中心进程分为 4 步:

  • 进程 1:依据是否有 postWriteRunnable 回调差异是 commit 和 apply;
  • 进程 2:创立磁盘写回使命(实在履行磁盘 IO 的当地):
    • 2.1 调用 writeToFile() 履行写回磁盘 IO 操作;
    • 2.2 在写回完毕后对前文说到的 mDiskWritesInFlight 计数自减 1;
    • 2.3 履行 postWriteRunnable 写回成功回调;
  • 进程 3:假如是异步写回,则提交到 QueuedWork 使命行列;
  • 进程 4:假如是同步写回,则查看 mDiskWritesInFlight 变量。假如存在并发写回的业务,则也要提交到 QueuedWork 使命行列,否则就直接在其时线程履行。

其中进程 2 是实在履行磁盘 IO 的当地,逻辑也很好了解。欠好了解的是,咱们发现除了 “同步写回并且不存在并发写回业务” 这种特殊状况,其他状况都会交给 QueuedWork 再调度一次。

在通过 QueuedWork#queue 提交使命时,会将 writeToDiskRunnable 使命追加到 sWork 使命行列中。假如是首次提交使命,QueuedWork 内部还会创立一个 HandlerThread 线程,通过这个子线程完结异步的写回使命。这说明 SharedPreference 的异步写回相当于运用了一个单线程的线程池,事实上在 Android 8.0 曾经的版别中就是运用一个 singleThreadExecutor 线程池完结的。

提交使命示意图

Android 初代 K-V 存储框架 SharedPreferences,旧时代的余晖?(下)

SharedPreferencesImpl.java

private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
    // 1、依据是否有 postWriteRunnable 回调差异是 commit 和 apply
    final boolean isFromSyncCommit = (postWriteRunnable == null);
    // 2、创立磁盘写回使命
    final Runnable writeToDiskRunnable = new Runnable() {
        @Override
        public void run() {
            synchronized (mWritingToDiskLock) {
                // 2.1 写入磁盘文件
                writeToFile(mcr, isFromSyncCommit);
            }
            synchronized (mLock) {
                // 2.2 mDiskWritesInFlight:进行中业务自减 1
                mDiskWritesInFlight--;
            }
            if (postWriteRunnable != null) {
                // 2.3 触发写回成功回调
                postWriteRunnable.run();
            }
        }
    };
    // 3、同步写回且不存在并发写回,则直接在其时线程
    // 这就是前文说到 “commit 也不是肯定在其时线程同步写回” 的源码出处
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            // 假如存在并发写回的业务,则此处 wasEmpty = false
            wasEmpty = mDiskWritesInFlight == 1;
        }
        // wasEmpty 为 true 说明其时只要一个线程在履行提交操作,那么就直接在此线程上完结使命
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
    // 4、交给 QueuedWork 调度(同步使命不能够延迟)
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit /*是否能够延迟*/ );
}
@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    // 稍后分析
}

QueuedWork 调度:

QueuedWork.java

@GuardedBy("sLock")
private static LinkedList<Runnable> sWork = new LinkedList<>();
// 提交使命
// shouldDelay:是否延迟
public static void queue(Runnable work, boolean shouldDelay) {
    Handler handler = getHandler();
    synchronized (sLock) {
        // 入队
        sWork.add(work);
        // 发送 Handler 音讯,触发 HandlerThread 履行使命
        if (shouldDelay && sCanDelay) {
            handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY /* 100ms */);
        } else {
            handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
        }
    }
}
private static Handler getHandler() {
    synchronized (sLock) {
        if (sHandler == null) {
            // 创立 HandlerThread 后台线程
            HandlerThread handlerThread = new HandlerThread("queued-work-looper", Process.THREAD_PRIORITY_FOREGROUND);
            handlerThread.start();
            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() {
    synchronized (sProcessingWork) {
        LinkedList<Runnable> work;
        synchronized (sLock) {
            // 创立新的使命行列
            // 这一步是有必要的,否则会与 enqueueDiskWrite 抵触
            work = sWork;
            sWork = new LinkedList<>();
            // Remove all msg-s as all work will be processed now
            getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
        }
        // 遍历 ,按顺序履行 sWork 使命行列
        if (work.size() > 0) {
            for (Runnable w : work) {
                w.run();
            }
        }
    }
}

比较不了解的是:

同一个文件的屡次写回串行化能够了解,关于多个文件的写回串行化意义是什么,是不是能够用多线程来写回多个不同的文件?或许这也是 SharedPreferences 是轻量级结构的原因之一,你觉得呢?

6.4 自动等候写回使命完毕

现在咱们能够答复 6.1 中遗留的问题:

在 apply() 办法中,在履行 enqueueDiskWrite() 前创立了 awaitCommit 使命并加入到 QueudWork 等候行列,直到磁盘写回完毕才将 awaitCommit 移除。这个 awaitCommit 使命是做什么的呢?

要了解这个问题需求管理分析到 ActivityThread 中的主线程音讯循环:

能够看到,在主线程的 Activity#onPause、Activity#onStop、Service#onStop、Service#onStartCommand 等生命周期状况改变时,会调用 QueudeWork.waitToFinish():

ActivityThread.java

@Override
public void handlePauseActivity(...) {
    performPauseActivity(r, finished, reason, pendingActions);
    // Make sure any pending writes are now committed.
    if (r.isPreHoneycomb()) {
        QueuedWork.waitToFinish();
    }
    ...
}
private void handleStopService(IBinder token) {
    ...
    QueuedWork.waitToFinish();
    ActivityManager.getService().serviceDoneExecuting(token, SERVICE_DONE_EXECUTING_STOP, 0, 0);
    ...
}

waitToFinish() 会履行一切 sFinishers 等候行列中的 aWaitCommit 使命,自动等候一切磁盘写回使命完毕。在写回使命完毕之前,主线程会堵塞在等候锁上,这儿也有或许发生 ANR。

自动等候示意图

Android 初代 K-V 存储框架 SharedPreferences,旧时代的余晖?(下)

至于为什么 Google 要在 ActivityThread 中部分生命周期中自动等候一切磁盘写回使命完毕呢?官方并没有明确表示,结合头条和抖音技能团队的文章,我比较倾向于这 2 点解说:

  • 解说 1 – 跨进程同步(首要): 为了保证跨进程的数据同步,要求在组件跳转前,保证其时组件的写回使命有必要在其时生命周期内完结;
  • 解说 2 – 数据完整性: 为了避免在组件跳转的进程中或许发生的 Crash 造成未写回的数据丢掉,要求其时组件的写回使命有必要在其时生命周期内完结。

当然这两个解说并不全面,因为就算要求自动等候,也不能保证跨进程实时同步,也不能保证不发生 Crash。

抖音技能团队观念

Android 初代 K-V 存储框架 SharedPreferences,旧时代的余晖?(下)

QueuedWork.java

@GuardedBy("sLock")
private static Handler sHandler = null;
public static void waitToFinish() {
    boolean hadMessages = false;
    Handler handler = getHandler();
    synchronized (sLock) {
        if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
            // Delayed work will be processed at processPendingWork() below
            handler.removeMessages(QueuedWorkHandler.MSG_RUN);
        }
        // We should not delay any work as this might delay the finishers
        sCanDelay = false;
    }
    // Android 8.0 优化:帮助子线程履行磁盘写回
    // 作用有限,因为 QueuedWork 运用了 sProcessingWork 锁保证同一时刻最多只要一个线程在履行磁盘写回
    // 所以这儿应该是尝试在主线程履行,能够提升线程优先级
    processPendingWork();
    // 履行 sFinshers 等候行列,等候一切写回使命完毕
    try {
        while (true) {
            Runnable finisher;
            synchronized (sLock) {
                finisher = sFinishers.poll();
            }
            if (finisher == null) {
                break;
            }
            // 履行 mcr.writtenToDiskLatch.await();
            // 堵塞线程直到磁盘使命履行完毕
            finisher.run();
        }
    } finally {
        sCanDelay = true;
    }
}

Android 7.1 QueuedWork 源码对比:

public static boolean hasPendingWork() {
    return !sPendingWorkFinishers.isEmpty();
}

7. writeToFile() 姗姗来迟

终究走到详细调用磁盘 IO 操作的当地了!

7.1 写回进程

writeToFile() 的逻辑相对复杂一些了。通过简化后,剩余的中心进程只要 4 大进程:

  • 进程 1:过滤无效写回业务:

    • 1.1 业务的 memoryStateGeneration 内存版别小于 mDiskStateGeneration 磁盘版别,跳过;
    • 1.2 同步写回有必要写回;
    • 1.3 异步写回业务的 memoryStateGeneration 内存版别版别小于 mCurrentMemoryStateGeneration 最新内存版别,跳过。
  • 进程 2:文件备份:

    • 2.1 假如不存在备份文件,则将旧文件重命名为备份文件;
    • 2.2 假如存在备份文件,则删去无效的旧文件(上一次写回出并且后处理没有成功删去的状况)。
  • 进程 3:全量掩盖写回磁盘:

    • 3.1 翻开文件输出流;
    • 3.2 将 mapToWriteToDisk 映射表全量写出;
    • 3.3 调用 FileUtils.sync() 强制操作系统页缓存写回磁盘;
    • 3.4 写入成功,则删去被封文件(假如没有走到这一步,在将来读取文件时,会从头康复备份文件);
    • 3.5 将磁盘版别记载为其时内存版别;
    • 3.6 写回完毕(成功)。
  • 进程 4:后处理: 删去写至半途的无效文件。

7.2 写回优化

持续分析发现,SharedPreference 的写回操作并不是简单的调用磁盘 IO,在保证 “可用性” 方面也做了一些优化规划:

  • 优化 1 – 过滤无效的写回业务:

如前文所述,commit 和 apply 都或许呈现并发修正同一个文件的状况,此刻在连续修正同一个文件的业务序列中,旧的业务是没有意义的。为了过滤这些无意义的业务,在创立 MemoryCommitResult 业务目标时会记载其时的 memoryStateGeneration 内存版别,而在 writeToFile() 中就会依据这个字段过滤无效业务,避免了无效的 I/O 操作。

  • 优化 2 – 备份旧文件:

因为写回文件的进程存在不确定的异常(比如内核溃散或许机器断电),为了保证文件的完整性,SharedPreferences 采用了文件备份机制。在履行写回操作之前,会先将旧文件重命名为 .bak 备份文件,在全量掩盖写入新文件后再删去备份文件。

假如写回文件失利,那么在后处理进程中会删去写至半途的无效文件。此刻磁盘中只要一个备份文件,而实在文件需求比及下次触发写回业务时再写回。

假如直到运用退出都没有触发下次写回,或许写回的进程中 Crash,那么在前文说到的创立 SharedPreferencesImpl 目标的结构办法中调用 loadFromDisk() 读取并解析文件数据时,会从备份文件康复数据。

  • 优化 3 – 强制页缓存写回:

在写回文件成功后,SharedPreference 会调用 FileUtils.sync() 强制操作系统将页缓存写回磁盘。

写回示意图

Android 初代 K-V 存储框架 SharedPreferences,旧时代的余晖?(下)

SharedPreferencesImpl.java

// 内存版别
@GuardedBy("this")
private long mCurrentMemoryStateGeneration;
// 磁盘版别
@GuardedBy("mWritingToDiskLock")
private long mDiskStateGeneration;
// 写回业务
private static class MemoryCommitResult {
    // 内存版别
    final long memoryStateGeneration;
    // 需求全量掩盖写回磁盘的数据
    final Map<String, Object> mapToWriteToDisk;
    // 同步计数器
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
    // 后文写回完毕后调用
    // wasWritten:是否有履行写回
    // result:是否成功
    void setDiskWriteResult(boolean wasWritten, boolean result) {
        this.wasWritten = wasWritten;
        writeToDiskResult = result;
        // 唤醒等候锁
        writtenToDiskLatch.countDown();
    }
}
// 提交写回业务
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
    ...
    // 创立磁盘写回使命
    final Runnable writeToDiskRunnable = new Runnable() {
        @Override
        public void run() {
            synchronized (mWritingToDiskLock) {
                // 2.1 写入磁盘文件
                writeToFile(mcr, isFromSyncCommit);
            }
            synchronized (mLock) {
                // 2.2 mDiskWritesInFlight:进行中业务自减 1
                mDiskWritesInFlight--;
            }
            if (postWriteRunnable != null) {
                // 2.3 触发写回成功回调
                postWriteRunnable.run();
            }
        }
    };
    ...
}
// 写回文件
// isFromSyncCommit:是否同步写回
@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    boolean fileExists = mFile.exists();
    // 假如旧文件存在
    if (fileExists) { 
        // 1. 过滤无效写回业务
        // 是否需求履行写回
        boolean needsWrite = false;
        // 1.1 磁盘版别小于内存版别,才有或许需求写回
        // (只要旧文件存在才会走到这个分支,但是旧文件不存在的时候也或许存在无意义的写回,
        // 猜想官方是希望首次创立文件的写回能够及时尽快履行,毕竟只要一个后台线程)
        if (mDiskStateGeneration < mcr.memoryStateGeneration) {
            if (isFromSyncCommit) {
                // 1.2 同步写回有必要写回
                needsWrite = true;
            } else {
                // 1.3 异步写回需求判断业务目标的内存版别,只要最新的内存版别才有必要履行写回
                synchronized (mLock) {
                    if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                        needsWrite = true;
                    }
                }
            }
        }
        if (!needsWrite) {
            // 1.4 无效的异步写回,直接完毕
            mcr.setDiskWriteResult(false, true);
            return;
        }
        // 2. 文件备份
        boolean backupFileExists = mBackupFile.exists();
        if (!backupFileExists) {
            // 2.1 假如不存在备份文件,则将旧文件重命名为备份文件
            if (!mFile.renameTo(mBackupFile)) {
                // 备份失利
                mcr.setDiskWriteResult(false, false);
                return;
            }
        } else {
            // 2.2 假如存在备份文件,则删去无效的旧文件(上一次写回出并且后处理没有成功删去的状况)
            mFile.delete();
        }
    }
    try {
        // 3、全量掩盖写回磁盘
        // 3.1 翻开文件输出流
        FileOutputStream str = createFileOutputStream(mFile);
        if (str == null) {
            // 翻开输出流失利
            mcr.setDiskWriteResult(false, false);
            return;
        }
        // 3.2 将 mapToWriteToDisk 映射表全量写出
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        // 3.3 FileUtils.sync:强制操作系统将页缓存写回磁盘
        FileUtils.sync(str);
        // 封闭输出流
        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
        // 3.4 写入成功,则删去被封文件(假如没有走到这一步,在将来读取文件时,会从头康复备份文件)
        mBackupFile.delete();
        // 3.5 将磁盘版别记载为其时内存版别
        mDiskStateGeneration = mcr.memoryStateGeneration;
        // 3.6 写回完毕(成功)
        mcr.setDiskWriteResult(true, true);
        return;
    } catch (XmlPullParserException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    } catch (IOException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    }
    // 在 try 块中抛出异常,会走到这儿
    // 4、后处理:删去写至半途的无效文件
    if (mFile.exists()) {
        if (!mFile.delete()) {
            Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
        }
    }
    // 写回完毕(失利)
    mcr.setDiskWriteResult(false, false);
}
// -> 读取并解析文件数据
private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        // 1、假如存在备份文件,则康复备份数据(后文详细分析)
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }
    ...
}

至此,SharedPreferences 中心源码分析完毕。


8. SharedPreferences 的其他细节

SharedPreferences 还有其他细节值得学习。

8.1 SharedPreferences 锁总结

SharedPreferences 是线程安全的,但它的线程安全并不是直接运用一个大局的锁目标,而是采用多种颗粒度的锁目标完结 “锁细化” ,并且还贴心地运用了 @GuardedBy 注解符号字段或办法所述的锁等级。

运用 @GuardedBy 注解符号锁等级

@GuardedBy("mLock")
private Map<String, Object> mMap;
目标锁 功用呢 描述
1、SharedPreferenceImpl#mLock SharedPreferenceImpl 目标的大局锁 大局运用
2、EditorImpl#mEditorLock EditorImpl 修正器的写锁 保证多线程拜访 Editor 的竞赛安全
3、SharedPreferenceImpl#mWritingToDiskLock SharedPreferenceImpl#writeToFile() 的互斥锁 writeToFile() 中会修正内存状况,需求保证多线程竞赛安全
4、QueuedWork.sLock QueuedWork 的互斥锁 保证 sFinishers 和 sWork 的多线程资源竞赛安全
5、QueuedWork.sProcessingWork QueuedWork#processPendingWork() 的互斥锁 保证同一时刻最多只要一个线程履行磁盘写回使命

8.2 运用 WeakHashMap 存储监听器

SharedPreference 提供了 OnSharedPreferenceChangeListener 回调监听器,能够在主线程监听键值对的改变(包含修正、新增和移除)。

SharedPreferencesImpl.java

@GuardedBy("mLock")
private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
    new WeakHashMap<OnSharedPreferenceChangeListener, Object>();

SharedPreferences.java

public interface SharedPreferences {
    public interface OnSharedPreferenceChangeListener {
        void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
    }
}

比较意外的是: SharedPreference 运用了一个 WeakHashMap 弱键散列表存储监听器,并且将监听器目标作为 Key 目标。这是为什么呢?

这是一种避免内存走漏的考虑,因为 SharedPreferencesImpl 的生命周期是大局的(坐落 ContextImpl 的内存缓存),所以有必要运用弱引证避免内存走漏。想想也对,Java 规范库没有提供相似 WeakArrayList 或 WeakLinkedList 的容器,所以这儿将监听器目标作为 WeakHashMap 的 Key,就很巧妙的复用了 WeakHashMap 自动清理无效数据的能力。

提示: 关于 WeakHashMap 的详细分析,请阅览小彭说 数据结构与算法 专栏文章 《WeakHashMap 和 HashMap 的差异是什么,何时运用?》

8.3 怎么查看文件被其他进程修正?

在读取和写入文件后记载 mStatTimestamp 时刻戳和 mStatSize 文件巨细,在查看时查看这两个字段是否发生变化

SharedPreferencesImpl.java

// 文件时刻戳
@GuardedBy("mLock")
private StructTimespec mStatTimestamp;
// 文件巨细
@GuardedBy("mLock")
private long mStatSize;
// 读取文件
private void loadFromDisk() {
    ...
    mStatTimestamp = stat.st_mtim;
    mStatSize = stat.st_size;
    ...
}
// 写入文件
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    ...
    mStatTimestamp = stat.st_mtim;
    mStatSize = stat.st_size;
    ...
}
// 查看文件
private boolean hasFileChangedUnexpectedly() {
    synchronized (mLock) {
        if (mDiskWritesInFlight > 0) {
            // If we know we caused it, it's not unexpected.
            if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
            return false;
        }
    }
    // 读取文件 Stat 信息
    final StructStat stat = Os.stat(mFile.getPath());
    synchronized (mLock) {
        // 查看修正时刻和文件巨细
        return !stat.st_mtim.equals(mStatTimestamp) || mStatSize != stat.st_size;
    }
}

至此,SharedPreferences 全部源码分析完毕。


9. 总结

能够看到,虽然 SharedPreferences 是一个轻量级的 K-V 存储结构,但的确是一个完整的存储方案。从源码分析中,咱们能够看到 SharedPreferences 在读写功能、可用性方面都有做一些优化,例如:锁细化、业务化、业务过滤、文件备份等,值得细细品味。

鄙人篇文章里,咱们来盘点 SharedPreferences 中存在的 “缺陷”,为什么 SharedPreferences 没有乘上新时代的船只。请关注。


版权声明

本文为稀土技能社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

参考资料

  • Android SharedPreferences 的了解与运用 —— ghroosk 著
  • 一文读懂 SharedPreferences 的缺陷及一点点思考 —— 业志陈 著
  • 反思|官方也无力回天?Android SharedPreferences 的规划与完结 —— 却把青梅嗅 著
  • 分析 SharedPreference apply 引起的 ANR 问题 —— 字节跳动技能团队
  • 今天头条 ANR 优化实践系列 – 告别 SharedPreference 等候 —— 字节跳动技能团队

Android 初代 K-V 存储框架 SharedPreferences,旧时代的余晖?(下)