Android源码阅读 SharedPreferences - 2

前言

由于笔者目前水平限制,表达能力有限,尽请见谅。

SharedPreferences提供了一种轻量级的数据存储方式,允许保存和获取简单的键值对。它适用于保存少量的数据,如用户设置或应用程序的配置信息。

ContextImplContext抽象类的一个具体实现。在Android中,Context是一个抽象类,它提供了访问应用资源、启动活动、发送广播、接收意图等一系列操作的接口。Context是一个场景描述符,它提供了与操作系统交互的接口。

本文将继续探索Framework层ContextImpl类对于getSharedPreferences的具体实现,以及SharedPreferencesImpl类的实现等。

正文

Part1

具体实现源码如下

@Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        // At least one application in the world actually passes in a null
        // name.  This happened to work because when we generated the file name
        // we would stringify it to "null.xml".  Nice.
        if (mPackageInfo.getApplicationInfo().targetSdkVersion <
                Build.VERSION_CODES.KITKAT) {
            if (name == null) {
                name = "null";
            }
        }

        File file;
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }

对于null名称的处理: 方法的开始部分处理了一个应用传入null作为SharedPreferences的名称特殊情况。

有趣的:)注释说明:在Android KitKat(API级别19)之前,如果传入的名称为null,系统会将其转换为字符串"null.xml"作为文件名。为了保持向后兼容性,这段代码在目标SDK版本小于KitKat时,如果传入的namenull,会将其改为字符串"null",来保证应用的兼容性。

线程安全的实现: 方法中使用了synchronized (ContextImpl.class)来确保线程安全。这意味着在同一时刻,只有一个线程可以执行这个同步代码块内的代码。这对于防止并发问题是必要的,因为SharedPreferences的文件是被多个线程共享的资源。

synchronized是Java中的一个关键字,用于控制对共享资源的并发访问,确保在同一时刻只有一个线程可以执行特定代码段。它是Java语言内置的线程同步机制的一部分。synchronized可以用于方法或代码块,以确保线程安全,即防止多线程同时修改同一资源导致的数据不一致问题。

当一个线程进入一个同步方法或同步代码块时,它会自动获取锁。如果其他线程试图进入被锁定对象的另一个同步方法或代码块,它们将被阻塞,直到锁被释放。当当前线程离开同步区域时,锁会被自动释放,此时其他线程可以尝试获取锁。

(使用synchronized时需要谨慎,因为不当的使用可能会导致死锁或降低系统的性能。)

另外需要注意锁定的是ContextImpl.class这个对象,而不仅仅是它内部处理SharedPreferences部分的代码。

缓存SharedPreferences文件路径: mSharedPrefsPaths用于缓存每个SharedPreferences文件的路径。如果请求的SharedPreferences文件路径已经在mSharedPrefsPaths中,就直接使用缓存的路径;如果不在,就生成新的文件路径,并将其添加到mSharedPrefsPaths中。

Part2

可以发现实际上,通过name和mode获取sharedPreferences最后调用了通过file和mode获取sharedPreferecnes。

缓存: 首先,方法使用了一个名为cacheArrayMap,这个映射着维护文件路径到SharedPreferencesImpl对象的映射。这个缓存的目的是为了避免重复创建SharedPreferences实例,提高性能。

线程安全: 使用synchronized (ContextImpl.class)来确保操作的线程安全。这保证了在同一时间内,只有一个线程可以执行这段代码。

查找或创建实例: 方法首先尝试从缓存中获取指定文件的SharedPreferencesImpl实例。如果实例不存在,它会进行几个检查,包括验证模式(checkMode(mode)),并针对Android O及以上版本,检查是否在加密存储中并且用户已解锁(如果SharedPreferences存储在加密存储中,并且用户尚未解锁,那么会抛出一个IllegalStateException异常,提示SharedPreferences在用户解锁之前不可用)。通过这些检查后,它会创建一个新的SharedPreferencesImpl实例,将其添加到缓存中,并返回这个新实例。

MODE_MULTI_PROCESS : 在返回SharedPreferencesImpl实例之前,如果指定了Context.MODE_MULTI_PROCESS模式或应用的目标SDK版本低于Android Honeycomb(3.0),则会调用sp.startReloadIfChangedUnexpectedly()。这是为了处理多进程访问和兼容旧版本Android的历史行为,确保如果SharedPreferences文件在外部被修改,应用能够重新加载这些变化。MODE_MULTI_PROCESS在新版本中已被废弃,因为SharedPreferences默认不支持跨进程同步。

Part2 -0

 这个方法获取了维护文件路径到SharedPreferencesImpl对象的映射

@GuardedBy("ContextImpl.class")用于告诉开发者,任何访问getSharedPreferencesCacheLocked方法内资源的操作都应该在synchronized (ContextImpl.class)块内进行,确保线程安全。

sSharedPrefsCache是一个全局缓存,用于存储所有应用程序包的SharedPreferences缓存。

使用应用程序包名从sSharedPrefsCache中获取对应的缓存。如果找不到,说明这是该应用程序第一次请求SharedPreferences,需要为这个包名创建一个新的缓存并添加到sSharedPrefsCache中。

Part3

对于SharedPreferencesImpl类,代码较多,不全部展示。

大致有如下重要变量:

  • mFile & mBackupFile:表示SharedPreferences数据存储的文件及其备份文件。
  • mMode:访问模式,如私有模式。
  • mLock & mWritingToDiskLock:用于同步控制的锁对象。
  • mMap:内存中保存的键值对数据,它在文件加载后存储SharedPreferences的当前状态。
  • mLoaded:表示数据是否已从磁盘加载到内存中。
  • mListeners:监听SharedPreferences变更的监听器集合。

数据加载

startLoadFromDisk() & loadFromDisk()在构造函数中调用startLoadFromDisk()方法启动一个异步任务,从磁盘加载SharedPreferences数据到内存中。loadFromDisk()方法实际完成加载逻辑,包括读取文件、解析XML并更新内存中的mMap

数据访问

@Override
    public int getInt(String key, int defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            Integer v = (Integer)mMap.get(key);
            return v != null ? v : defValue;
        }
    }
    @Override
    public long getLong(String key, long defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            Long v = (Long)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

实现了SharedPreferences接口的各种数据访问方法(如getStringgetInt等),这些方法首先确保数据已加载(调用awaitLoadedLocked()),然后从mMap中读取相应的值。

数据修改和异步提交

  • edit() & EditorImpledit()方法返回一个Editor对象,用于修改SharedPreferences的值。EditorImpl实现了Editor接口,提供了修改数据和提交更改的方法(apply()commit())。

  • apply() & commit()apply()方法异步提交更改到磁盘,而commit()方法同步提交。它们都使用内部的MemoryCommitResult对象来表示提交结果,并处理磁盘写入逻辑。

磁盘写入

  • enqueueDiskWrite() & writeToFile():这些方法处理将更改写入磁盘的逻辑。enqueueDiskWrite()方法将写入任务加入队列,而writeToFile()实际执行磁盘写入操作,包括文件备份、写入新数据、同步文件系统等。

监听器通知

  • notifyListeners():在数据成功提交到磁盘后,此方法通知所有注册的监听器数据已更改。
Part3 -0

SharedPreferences允许注册监听器来监听偏好设置的更改的实现方式:

维护一个监听器的WeakHashMap存储所有注册的OnSharedPreferenceChangeListener监听器。当SharedPreferences的编辑器(Editor)提交更改时(无论是通过apply()还是commit()方法),会检查这个映射表,并通知所有监听器偏好设置的更改。

  • 注册监听器:调用SharedPreferences.registerOnSharedPreferenceChangeListener()方法
  • 通知监听器:在EditorImplnotifyListeners()方法中,当apply()commit()成功将更改提交到内存和磁盘后,会遍历mListeners映射表,为每个监听器调用onSharedPreferenceChanged()方法,通知它们偏好设置已更改。
Part3 -1

apply()与commit()的区别和使用场景:

  • apply()

    • 异步执行,立即返回。
    • 将更改提交到内存,然后异步写入磁盘,不会告知写入成功与否。
    • 如果有异常,不会中断应用,因为它不抛出异常。
    • 使用场景:当需要修改SharedPreferences并希望这个操作立即返回,不需要确认数据是否成功写入磁盘时使用apply()
  • commit()

    • 同步执行,直到数据完全写入磁盘后返回。
    • 如果写入成功,返回true;如果有异常,返回false
    • 可能会阻塞调用线程直到操作完成,特别是在磁盘操作较慢的设备上。
    • 使用场景:当需要确保更改被立即写入磁盘,并且关心操作的成功与否时使用commit()。适用于对数据完整性要求较高的情况。
Part3 -2

数据的读取和存储

SharedPreferencesImpl通过mMap字段来缓存SharedPreferences的数据。当应用读取偏好设置时(如getStringgetInt等方法),SharedPreferencesImpl会首先确保数据已从磁盘加载到内存中(通过调用awaitLoadedLocked()方法),然后直接从mMap中读取相应的值。

  • 加载数据:通过startLoadFromDisk()方法启动后台线程从磁盘读取XML文件,解析后存储到mMap中。
  • 修改数据edit()方法返回一个EditorImpl实例,允许修改mMap中的数据。修改操作首先在内存中进行,然后通过apply()commit()方法将更改异步或同步地写入磁盘。
  • 写入磁盘:更改提交后,SharedPreferencesImpl使用enqueueDiskWrite()方法安排磁盘写入。实际写入是通过writeToFile()方法完成的,它将内存中的数据写入到一个XML文件中。
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);
    }

MemoryCommitResult mcr封装了待写入磁盘数据以及其他相关信息的对象,例如哪些键被修改了,是否有监听器需要通知等。

Runnable postWriteRunnable是一个可选的Runnable,仅在apply()方法中提供。它在数据成功写入磁盘后执行,主要用于异步操作的场景。如果这个参数为null,表示当前操作是一个commit()调用,即同步写入磁盘。

核心逻辑

writeToDiskRunnable封装了将更改写入磁盘的逻辑。首先获取mWritingToDiskLock锁以保证写入操作的线程安全,然后调用writeToFile方法实际写入磁盘,完成后递减mDiskWritesInFlight计数器并可能执行postWriteRunnable

  • 如果是同步提交,并且当前没有其他写入任务在执行(mDiskWritesInFlight == 1),则立即在当前线程执行writeToDiskRunnable,不需要排队。
  • 如果是异步提交或者存在其他写入任务,将writeToDiskRunnable任务排队到QueuedWork,它会在一个后台线程上异步执行。
  • 30
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏目艾拉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值