Android 源码分析

SharedPreferences 源码分析

2017-11-23  本文已影响0人  _夜

SharedPreferences 的使用隐藏了很多陷阱.

建议:

  1. 第一次获取 Sp 时不要直接对 Sp 进行操作,避免阻塞当前线程;
  2. Sp 被加载到内存后不会被移除,需要注意 Sp 对内存的占用;
  3. Sp 的每次写入都是全量写入,注意写入的数据量大小及耗时;
  4. commit() 是同步写入,当只有一个写入任务时,会在当前线程同步写入;当前方有多个写入任务时(包括 apply() 提交的写入任务),会进入单线程池排队等待写入,当前线程进入 wait() 状态,需要注意对线程的阻塞;
  5. apply() 是异步写入,但会阻塞 activity 的生命周期,需要注意 apply() 的并发对 activity 生命周期的影响.(API<11 时,会阻塞 Activity onPause(); API>=11 时,会阻塞 Activity onStop();会阻塞 service 的 onStop());
  6. 多线程对同一个 Sp 进行操作时,需要注意数据被覆盖的问题;

一、从获取 Sp 的引用开始看起

Context.getSharedPreferences(name, mode);
|
public abstract SharedPreferences getSharedPreferences(String name, int mode);
|
ContextImpl.getSharedPreferences(name, mode);
|
public SharedPreferences getSharedPreferences(String name, int mode){
    ...
    File file;
    synchronized (ContextImpl.class) {
        if (mSharedPrefsPaths == null) { // 该变量为进程内全局变量,key 为 SP File 类型,value 为 SP 完整路径
            mSharedPrefsPaths = new ArrayMap<>();
        }
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            file = getSharedPreferencesPath(name); // 拼接 SP 文件的完整路径
            mSharedPrefsPaths.put(name, file);
        }
    }
    return getSharedPreferences(file, mode);
}
|
public SharedPreferences getSharedPreferences(File file, int mode) {
    checkMode(mode); // Android N 及以上不允许 MODE_WORLD_READABLE,MODE_WORLD_WRITEABLE
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        // 取出当前进程中的所有的 SP 集合
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            // 第一次获取 Sp 时为 null,从磁盘加载,之后再次获取则直接读内存
            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;
}

二、第一次获取 Sp 的情景

sp = new SharedPreferencesImpl(file, mode);
|
SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file); // 命名备份文件:.bak
    mMode = mode;
    mLoaded = false;
    mMap = null;
    startLoadFromDisk(); // 从磁盘加载 Sp 文件
}
|
private void startLoadFromDisk() {
    synchronized (this) {
        mLoaded = false;
    }
    // 虽然是开启了子线程加载,但第一次加载 Sp 时并不会出现并发的情况
    new Thread("SharedPreferencesImpl-load") {
        public void run() { 
            loadFromDisk();
        }
    }.start();
}

/**
 * Sp 文件的加载是在子线程,但是若在没加载完成前从 main 线程调用 getXXX 会阻塞 main 线程
 */
private void loadFromDisk() {
    synchronized (SharedPreferencesImpl.this) {
        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;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
                map = XmlUtils.readMapXml(str); // 解析 Sp 文件,保存到 map 中
            } catch (XmlPullParserException | IOException e) {
                Log.w(TAG, "getSharedPreferences", e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        /* ignore */
    }

    synchronized (SharedPreferencesImpl.this) {
        mLoaded = true;
        if (map != null) {
            mMap = map;
            mStatTimestamp = stat.st_mtime;
            mStatSize = stat.st_size;
        } else {
            mMap = new HashMap<>();
        }
        notifyAll(); // 或许等待的线程,如调用 getXXX() 所在的线程
    }
}

三、从 Sp 获取数据的情景

sp.getString(key, default);
|
String getString(String key, @Nullable String defValue);
|
SharedPreferencesImpl.getString(key, default);
|
public String getString(String key, @Nullable String defValue) {
    synchronized (this) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);// 直接从内存读取,类型强转失败时会抛出异常
        return v != null ? v : defValue;
    }
}
|
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 {
            wait(); // Sp 未加载完成时线程等待,此处可能造成 main 线程阻塞
        } catch (InterruptedException unused) {
        }
    }
}

四、存储数据到 Sp 的情景

Editor editor = sp.edit();
|
SharedPreferencesImpl.edit();
|
public SharedPreferences.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 (this) {
        awaitLoadedLocked();// 若 Sp 未加载完成,同样会阻塞在这里
    }

    return new EditorImpl();
}

1、EditorImpl 是 Sp 的精华部分

editor.putInt(key, value);
|
Editor putInt(String key, int value);
|
EditorImpl.putInt(key, value);
|
public SharedPreferences.Editor putInt(String key, int value) {
    // 只是暂存到 EditorImpl 的成员变量 mModified 中,尚未真正写入磁盘文件
    synchronized (this) {
        mModified.put(key, value);
        return this;
    }
}
|
private final Map<String, Object> mModified = Maps.newHashMap();

2、调用 commit() 方法写入数据

触发写入磁盘文件及内存 mMap 中的操作是调用 commit() 或 apply() 方法

editor.commit();
|
EditorImpl.commit();
|
public boolean commit() {
    SharedPreferencesImpl.MemoryCommitResult mcr = commitToMemory();
    // the second parm null means: sync write on this thread okay
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null );
    try {
        // 写入任务完成前会阻塞在这里,问题很严重
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    }
    notifyListeners(mcr);// 调用注册的 listener
    return mcr.writeToDiskResult;
}
private SharedPreferencesImpl.MemoryCommitResult commitToMemory() {
    SharedPreferencesImpl.MemoryCommitResult mcr = new SharedPreferencesImpl.MemoryCommitResult();
    // 此处加锁,多次调用 commit() 或 apply() 均会阻塞在这里
    synchronized (SharedPreferencesImpl.this) {
        // We optimistically don't make a deep copy until
        // a memory commit comes in when we're already
        // writing to disk.
        // mDiskWritesInFlight 是 SharedPreferencesImpl 的成员变量
        if (mDiskWritesInFlight > 0) {
            // We can't modify our mMap as a currently
            // in-flight write owns it.  Clone it before
            // modifying it.
            // noinspection unchecked
            mMap = new HashMap<String, Object>(mMap);
        }
        // 注意此处具有迷糊性,每个写入任务的 mcr.mapToWriteToDisk 是不同的
        mcr.mapToWriteToDisk = mMap;
        mDiskWritesInFlight++; // 每次有写入任务时自增,写入完成后自减

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

        synchronized (this) {
            // 此处需要注意 clear() 的作用时机,会影响并发写入的情况
            if (mClear) {
                if (!mMap.isEmpty()) {
                    mcr.changesMade = true;
                    mMap.clear();
                }
                mClear = false;
            }

            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                // "this" is the magic value for a removal mutation. In addition,
                // setting a value to "null" for a given key is specified to be
                // equivalent to calling remove on that key.
                if (v == this || v == null) {
                    // value 为 null 时等效于删除该 key
                    if (!mMap.containsKey(k)) {
                        continue;
                    }
                    mMap.remove(k);
                } else {
                    if (mMap.containsKey(k)) {
                        Object existingValue = mMap.get(k);
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    mMap.put(k, v);
                }

                // 若 value 为 null,有可能导致没有改动的情况被认为有改动
                mcr.changesMade = true;
                if (hasListeners) {
                    // 在有 listener 的情况下,才会为 mcr.keysModified 填充数据
                    mcr.keysModified.add(k);
                }
            }

            mModified.clear();
        }
    }
    return mcr;
}

/**
 * mcr 内部类,重点在于有加锁部分:
 */
private static class MemoryCommitResult {
    public boolean changesMade;  // any keys different?
    public List<String> keysModified; // 在有 listener 的情况下,才会为 mcr.keysModified 填充数据
    public Set<OnSharedPreferenceChangeListener> listeners;  // may be null
    public Map<?, ?> mapToWriteToDisk; // 引用了 Sp 的整个 map
    public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
    public volatile boolean writeToDiskResult = false;

    public void setDiskWriteResult(boolean result) {
        writeToDiskResult = result;
        writtenToDiskLatch.countDown();
    }
}

private void enqueueDiskWrite(final SharedPreferencesImpl.MemoryCommitResult mcr, final Runnable postWriteRunnable) {
    final Runnable writeToDiskRunnable = new Runnable() {
        public void run() {
            // mWritingToDiskLock 为 SharedPreferencesImpl 的成员变量,仅在此处使用
            // commit() 会阻塞在这里,注意多个写入任务对 main 线程的阻塞
            synchronized (mWritingToDiskLock) {
                writeToFile(mcr);
            }
            synchronized (SharedPreferencesImpl.this) {
                mDiskWritesInFlight--; // 写入步骤完成后,自减
            }
            if (postWriteRunnable != null) {
                // 把会影响 activity 生命周期的 runnable 移除,并避免造成内存泄漏
                postWriteRunnable.run();
            }
        }
    };

    // 第二个参数为 null,则意味着同步写入磁盘文件
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    // Typical #commit() path with fewer allocations, doing a write on the current thread.
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (SharedPreferencesImpl.this) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        // 如果只有一个写入任务,则在调用写入线程同步写入,若有多个写入任务,在进入单线程队列写入
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
    // 当有多个写入任务时,commit() 的写入任何会放入单线程池,注意此处 apply() 对 commit() 的阻塞
    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}


/**
 *写入磁盘文件的过程
 */
 private void writeToFile(SharedPreferencesImpl.MemoryCommitResult mcr) {
    // Rename the current file so it may be used as a backup during the next read
    if (mFile.exists()) {
        if (!mcr.changesMade) {
            // If the file already exists, but no changes were
            // made to the underlying map, it's wasteful to
            // re-write the file.  Return as if we wrote it
            // out.
            mcr.setDiskWriteResult(true);
            return;
        }
        if (!mBackupFile.exists()) {// 备份文件不存在的情况
            if (!mFile.renameTo(mBackupFile)) {// 重命名原文件为备份文件
                Log.e(TAG, "Couldn't rename file " + mFile
                        + " to backup file " + mBackupFile);
                mcr.setDiskWriteResult(false);
                return;
            }
        } else {// 备份文件存在时,把原文件从磁盘删除
            mFile.delete();
        }
    }

    // Attempt to write the file, delete the backup and return true as atomically as
    // possible.  If any exception occurs, delete the new file; next time we will restore
    // from the backup.
    try {
        // 注意此处,不是写入到备份文件
        FileOutputStream str = createFileOutputStream(mFile);
        if (str == null) {
            mcr.setDiskWriteResult(false);
            return;
        }
        // 写入过程不是增量的方式,而是全量写入,此处要注意数据量较大的情况
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        FileUtils.sync(str);
        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
        try {
            final StructStat stat = Os.stat(mFile.getPath());
            synchronized (this) {
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
            }
        } catch (ErrnoException e) {
            // Do nothing
        }
        // Writing was successful, delete the backup file if there is one.
        mBackupFile.delete();
        mcr.setDiskWriteResult(true);
        return;
    } catch (XmlPullParserException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    } catch (IOException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    }
    // Clean up an unsuccessfully written file
    if (mFile.exists()) {
        if (!mFile.delete()) {
            Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
        }
    }
    mcr.setDiskWriteResult(false);
}

3、调用 apply() 方法写入数据

editor.apply();
|
EditorImpl.apply();
|
public void apply() {
    final SharedPreferencesImpl.MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
        public void run() {
            try {
                // 写入任务完成前会阻塞在这里
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException ignored) {
            }
        }
    };

    // 此处很重要,会阻塞 activity 的生命周期
    QueuedWork.add(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
        public void run() {
            awaitCommit.run();
            // 避免造成内存泄漏
            QueuedWork.remove(awaitCommit);
        }
    };
    // apply() 提交的写入任何会放入单线程池执行
    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);
}

上一篇 下一篇

猜你喜欢

热点阅读