【android-源码浅析】SharedPreferences

679 阅读5分钟

如果我的文章对您有所帮助,欢迎 👍 , 我的Github.


1. 前言

SharedPreferences(以下简称sp)在android中比较常用的工具类,在使用过程中还是有一些需要注意的坑,我们来浅析下源码。

本文sp基于anrdroid-28.

2. 浅析

2.1 sp对象创建

-> android.app.ContextImpl line:408

@Override
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);
            ...
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
    }
    ...
    return sp;
}

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    ...
    // 重载方法,使用默认路径 /package/shared_prefs/$name.xml
    return getSharedPreferences(file, mode);
}

-> android.app.Activity#getPreferences
// getLocalClassName() = $activityClassName.xml
public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {
    return getSharedPreferences(getLocalClassName(), mode);
}

先从缓存中看是否存在spImpl,没有就会重新创建一个。这个地方加了synchronized (ContextImpl.class)防止多线程中多次创建实例。

2.2 mode

// sp文件权限
// android.content.Context#MODE_PRIVATE 私有(用户组内)
// android.content.Context#MODE_WORLD_READABLE 公开读
// android.content.Context#MODE_WORLD_WRITEABLE 公开写
// android.content.Context#MODE_APPEND 同 MODE_PRIVATE
// android.content.Context#MODE_MULTI_PROCESS 同 MODE_PRIVATE,会判断其他进程是否需要同步文件(不可靠)
int mode

// getSharedPreferences时检查
// MODE_WORLD_READABLE & MODE_WORLD_WRITEABLE在Build.VERSION_CODES.N以后已被废弃
private void checkMode(int mode) {
    if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
        if ((mode & MODE_WORLD_READABLE) != 0) {
            throw new SecurityException("MODE_WORLD_READABLE no longer supported");
        }
        if ((mode & MODE_WORLD_WRITEABLE) != 0) {
            throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
        }
    }
}

// sp在写入文件的时候根据mode设置了文件权限
-> android.app.SharedPreferencesImpl#writeToFile line:782
-> android.app.ContextImpl#setFilePermissionsFromMode

static void setFilePermissionsFromMode(String name, int mode,
        int extraPermissions) {
    // S_xxx 这些是linux中文件权限flag
    // 默认 同用户&用户组可读可写
    int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR
        |FileUtils.S_IRGRP|FileUtils.S_IWGRP
        |extraPermissions;
    // 给与其他可读权限,Build.VERSION_CODES.N后不支持
    if ((mode&MODE_WORLD_READABLE) != 0) {
        perms |= FileUtils.S_IROTH;
    }
    // 给与其他可写权限,Build.VERSION_CODES.N后不支持
    if ((mode&MODE_WORLD_WRITEABLE) != 0) {
        perms |= FileUtils.S_IWOTH;
    }
    ...
    FileUtils.setPermissions(name, perms, -1, -1);
}

通过mode控制文件的访问权限

2.3 sp缓存更新

-> android.app.ContextImpl line:408

public SharedPreferences getSharedPreferences(File file, int mode) {
    ...
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
        getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        // If somebody else (some other process) changed the prefs
        // file behind our back, we reload it.  This has been the
        // historical (if undocumented) behavior.
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}

-> android.app.ContextImpl line:454

@Override
public void reloadSharedPreferences() {
    // Build the list of all per-context impls (i.e. caches) we know about
    ArrayList<SharedPreferencesImpl> spImpls = new ArrayList<>();
    ...
    // Issue the reload outside the cache lock
    for (int i = 0; i < spImpls.size(); i++) {
        spImpls.get(i).startReloadIfChangedUnexpectedly();
    }
}

-> android.app.SharedPreferencesImpl line:198

void startReloadIfChangedUnexpectedly() {
    synchronized (mLock) {
        // TODO: wait for any pending writes to disk?
        if (!hasFileChangedUnexpectedly()) {
            return;
        }
        startLoadFromDisk();
    }
}

如果mode包含MODE_MULTI_PROCESS,sp会在初始化时判断文件是否需要更新,但这不是可靠的。 如果sp文件发生变化时,会通过 reloadSharedPreferences进行更新缓存

2.4 sp文件加载解析

// 构造函数和startReloadIfChangedUnexpectedly中会加载解析sp文件
SharedPreferencesImpl(File file, int mode) {
    ...
    startLoadFromDisk();
}

void startReloadIfChangedUnexpectedly(){
    ...
    startLoadFromDisk();
}

// 锁住,并开启一个线程
private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

private void loadFromDisk() {
    ...
    // 如果备份文件存在,说明上次写入动作失败,会让备份文件转正
    if (mBackupFile.exists()) {
        mFile.delete();
        mBackupFile.renameTo(mFile);
    }
   
    ...
    
    // 解析xml,其中value可能是set or list or map
    map = (Map<String, Object>) XmlUtils.readMapXml(str);

    ...

    // 同步文件信息,以便文件变化时判断
    mStatTimestamp = stat.st_mtim;
    mStatSize = stat.st_size;

    ...
    // 释放锁
    mLock.notifyAll();
}

在解析sp文件的时候会通过mLock锁锁住其他操作(解析文件、getXXX)
在回写sp文件的时候会备份一下,如果回写失败了,下次读的时候会直接使用备份文件

2.5 sp文件回写

@Override
public Editor edit() {
    ...
    return new EditorImpl();
}

@Override
public void apply() {
    ...
    // 修改内存中的值
    final MemoryCommitResult mcr = commitToMemory();
    
    // mcr.writtenToDiskLatch 不太明白这给锁的作用,
    // 不管commit还是apply mcr.writtenToDiskLatch都是在执行完writeToFile才await的

    // 回写任务,apply会丢到QueuedWork中的线程执行
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    ...
}

@Override
public boolean commit() {
    // 修改内存中的值
    MemoryCommitResult mcr = commitToMemory();

    ...

    // 与apply的区别在于这个方法是同步执行的(当前线程)
    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
    ...
}

private MemoryCommitResult commitToMemory() {
    //android.app.SharedPreferencesImpl.EditorImpl#mModified putXXX时会记录在这个map中

    // sp数据版本号,如果数据发生改变,这个值会+1
    // 每个sp有一个mDiskStateGeneration版本号,文件回写成功会将其置为memoryStateGeneration
    // 写入文件的时候比mDiskStateGeneration值小的内存数据将遗弃
    long memoryStateGeneration;
    // 遍历android.app.SharedPreferencesImpl.EditorImpl#mModified与当前sp数据的,记录其中有变化的值
    List<String> keysModified = null;
    ...
    // 最终写入文件的完整数据
    Map<String, Object> mapToWriteToDisk;
    ...
    return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
            mapToWriteToDisk);
}


private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    final Runnable writeToDiskRunnable = new Runnable() {
            @Override
            public void run() {
                ...
                writeToFile(mcr, isFromSyncCommit);
                ...
            }
        };
    
    ...

    // commit 和 apply的区别,在于是否同步执行 writeToFile
    if (isFromSyncCommit) {
        ...        
        writeToDiskRunnable.run();
        ...
    }

    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    ...

    boolean fileExists = mFile.exists();
    
    // sp文件存在时判断下是否需要写入(版本号),是否需要备份(防止写入过程错误,导致文件损坏)
    if (fileExists) {
        boolean needsWrite = false;

        // 比较当前要写入的版本与disk的版本,看是否需要写入
        if (mDiskStateGeneration < mcr.memoryStateGeneration) {
            if (isFromSyncCommit) {
                needsWrite = true;
            } else {
                // apply方式存在多线程可能,如果sp内存版本号改变就遗弃当前操作,相当于有一定的节流
                synchronized (mLock) {
                    if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                        needsWrite = true;
                    }
                }
            }
        }
        
        if (!needsWrite) {
            mcr.setDiskWriteResult(false, true);
            return;
        }

        boolean backupFileExists = mBackupFile.exists();

        // 备份文件不存在将当前文件转为备份文件
        if (!backupFileExists) {
            if (!mFile.renameTo(mBackupFile)) {
                Log.e(TAG, "Couldn't rename file " + mFile
                        + " to backup file " + mBackupFile);
                mcr.setDiskWriteResult(false, false);
                return;
            }
        } else {
            mFile.delete();
        }
    }

    FileOutputStream str = createFileOutputStream(mFile);

    ...

    // 将内存数据回写sp文件
    XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

    ...

    // 根据mode,设置文件权限
    ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

    ...
    final StructStat stat = Os.stat(mFile.getPath());
        // 同步文件信息,以便文件变化时判断
    mStatTimestamp = stat.st_mtim;
    mStatSize = stat.st_size;

    ...

    // 干掉备份文件,下次回写开始时重新备份
    mBackupFile.delete();

    ...

    // 同步disk文件版本号
    mDiskStateGeneration = mcr.memoryStateGeneration;

    mcr.setDiskWriteResult(true, true);
    ...
}

edit操作每次都会返回一个新的对象,所以put操作要做合并处理。 apply相比commit会使用QueuedWork的线程写入文件,并且在写入文件的时候会锁住getXXX操作 apply操作可能会失败,并没有回调。

2.6 sp加密

android-23(6.0+)之后可以使用Jitpack提供的implementation "androidx.security:security-crypto:$version"库获取一个加密的sp对象

EncryptedSharedPreferences是对SharedPreferences的一个包装类,getXXX和putXXX时会对key-value进行加解密(加密的密钥会保存在android密钥库中)

String masterKey = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);

SharedPreferences sp = EncryptedSharedPreferences.create(
    "secret_sp",
    masterKey,
    context,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);

2.7 相关class

  • android.content.Context
  • android.content.SharedPreferences
  • (hide) android.app.SharedPreferencesImpl
  • (hide) android.app.ContextImpl
  • (hide) android.os.FileUtils
  • (hide) com.android.internal.util.XmlUtils

3. 总结

  1. 项目中的SpUtils不需要再对sp对象进行缓存,sp自身已经做了缓存。

  2. Sp不适合再多进程中共享,这是不稳定的方案。

  3. sp中读写都会加锁,有大量读写操作的尽量不要使用sp,或者对读写进行文件拆分、edit批量提交。