Android SharedPreferences 源码实现

在Android中保存数据的方式之一就是使用 SharedPreferences , 因为会用到这个类,所以想分析下它的源码实现。

1. 获取SharedPreference实例。

val sharedPreference = getSharedPreferences("main", Context.MODE_PRIVATE)
sharedPreference.edit().putBoolean("init", true).apply()

通常使用方式是通过 Context 获取一个 SharedPreferences 实例,下面看下具体的源码。

1.1 通过 ContextImpl 来获取SharedPreferencesImpl

class:: ContextImpl
@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);
}

@Override
public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}
  1. ContextImpl内部全局变量 mSharedPrefsPaths 是一个 ArrayMap<String, File> 的map容器,其中的key对应的是String类型的 name, value 对应的File是SharedPreferences保存数据的地方。
  2. SharedPreferences 中的文件是 xml 文件
@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;
        }
    }
    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;
}

@GuardedBy("ContextImpl.class")
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }
    final String packageName = getPackageName();
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);
    }
    return packagePrefs;
}
  1. sSharedPrefsCache 是一个静态变量,ArrayMap类型,维护了 File 和 SharedPreferencesImpl 的对应关系。因为是静态变量,所以 SharedPreferencesImpl 创建后会一直缓存在 sSharedPrefsCache 这个ArrayMap中。
  2. 如果 sSharedPrefsCache 没有这个File对应的 SharedPreferencesImpl 对象,则直接创建。

name -> file -> SharedPreferences实例。 上面的代码描述了一种转换关系,把name转换成file, 再从缓存中取 SharedPreferences 实例。这比用户传 file 的方式更简便。

1.2 SharedPreferencesImpl 构造方法实现

@UnsupportedAppUsage
SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    startLoadFromDisk();
}

构造方法中的一些参数需要描述一下他们的作用:

  1. mFile: 这个是SharedPreferences保存数据的地方,是一个xml类型的文件
  2. mLoaded: 这个和mMap有关,startLoadFromDisk() 方法会读取 mFile 里面的内容并保存到 mMap 中,加载过程中 mLoaded = false, 加载完之后 mLoaded = true.
@UnsupportedAppUsage
private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

可以看到,调用 startLoadFromDisk() 方法时会先设置 mLoaded = false, 然后创建一个线程去读取 mFile 里面的内容。

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) {
            // An errno exception means the stat failed. Treat as empty/non-existing by
            // ignoring.
        } catch (Throwable t) {
            thrown = t;
        }

        synchronized (mLock) {
            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;
                        mStatTimestamp = stat.st_mtim;
                        mStatSize = stat.st_size;
                    } else {
                        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.notifyAll();
            }
        }
    }
  1. loadFromDisk() 方法会先判断 mLoaded 是否为 true , 如果为 true 则直接返回,代表这个文件是已经加载完了的,因为SharedPreferences是线程共享的,所以存在已经被其他线程加载完的可能。
  2. 通过XmlUtils加载文件流,创建一个 map 对象,加载完之后设置 mLoaded = true. 并把这个 map 对象赋值给 mMap.

总结如下:

  1. SharedPreferences 是一种永久存储数据的方式,通过 xml 文件,保存key-value的对应关系。
  2. 创建SharedPreferencesImpl实例时会调用 startLoadDisk() 读取 mFile 中保存的数据,这个操作是在子线程中实现的.
  3. mMap保存 mFile 中的数据,可以提高 SharedPreferences 获取key对应的value的速率。

2. SharedPreferences 保存和读取数据

val sharedPreference = getSharedPreferences("main", Context.MODE_PRIVATE)
sharedPreference.getBoolean("init", false)
sharedPreference.edit().putBoolean("init", true).apply()

上面代码分别是 get 和 set 的使用方式现在看下源码实现

2.1 getXXX() 实现

@Override
public boolean getBoolean(String key, boolean defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        Boolean v = (Boolean)mMap.get(key);
        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);
    }
}

这里就说下 mLoaded 为false的状态,这个状态下在构造方法中创建的线程还没有完成读取 mFile 的逻辑,这种情况下,mMap 还为 null, 所以是读取不到对应的 value 的。所以主线程会被阻塞,直到 SharedPreferencesImpl-load 线程完成加载逻辑。之后再从 mMap 中取 value.

2.2 setXXX() 实现

@Override
public Editor edit() {
    // TODO: remove the need to call awaitLoadedLocked() when
    // requesting an editor.  will require some work on the
    // Editor, but then we should be able to do:
    //
    //      context.getSharedPreferences(..).edit().putString(..).apply()
    //
    // ... all without blocking.
    synchronized (mLock) {
        awaitLoadedLocked();
    }
    return new EditorImpl();
}

第一步是调用 sharedPreferences.edit() 获取一个 Editor 对象,这个对象的实现类是 EditorImpl , 而且这个过程也要等 SharedPreferencesImpl-load 加载完。

@Override
public Editor putBoolean(String key, boolean value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}

@Override
public void apply() {
    final long startTime = System.currentTimeMillis();
    final MemoryCommitResult mcr = commitToMemory();
    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);
    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                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);
}
  1. putxxx() 方法会把新的一对 key-value 保存到 mModified 中。
  2. commitToMemory() 会获取 SharedPreferencesImpl 中的mMap对象,对比 mModified 中的key, 如果有 mMap 中没有的 key 或者 key相同但 value 不同的情况下,MemoryCommitResult 中的 memoryStateGeneration 就会加1,代表有变动。
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 wri
    // the current thread.
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
  1. 如果 postWriteRunnable == null. 那么 isFromSyncCommit = true, 此时就会在当前线程调用 writeToDiskRunnable.run() 这是一个把 MemoryCommitResult 中的改动同步到 mFile 的过程,一个IO操作,当调用 commit() 时,postWriteRunnable = null.
  2. apply() 方法会传递一个 postWriteRunnable , 所以同步的 Runnable 就会交给 QueueWork 去处理。QueueWork 内部是一个 HandlerThread , 所有任务都会存放到一个 LinkedList 中,然后在handleMessage()方法中循环的取任务执行。
上一篇:springboot mybatis 自动生成代码(maven+IntelliJ IDEA)


下一篇:mybatisGenerator代码生成器