Skip to content

SharedPreferences

zhangpan edited this page Aug 18, 2021 · 7 revisions

SharedPreferences是Android中轻量级的数据存储方式,适用于保存简单的数据类型。其内部是以xml结构保存在/data/data/{包名}/shared_prefs文件夹下的。数据以键值对的形式保存,如下:

<map>
    <float name="isFloat" value="1.5" />
    <string name="isString">Android</string>
    <int name="isInt" value="1" />
    <long name="isLong" value="1000" />
    <boolean name="isBoolean" value="true" />
    <set name="isStringSet">
        <string>element 1</string>
        <string>element 2</string>
        <string>element 3</string>
    </set>
</map>

1.简单使用

// 通过Context获取SharedPreferencesImpl实例
SharedPreferences sp = Context.getSharedPreferences("my_sp", Context.MODE_PRIVATE);
// 通过SP获取EditorImpl实例
SharedPreferences.Editor edit = sp.edit();
// 存入key为value,值为text的数据。
edit.putString("value","test");
// 提交
edit.apply(); // 或者edit.commit();
// 从SP中读取value对应的值
sp.getString("value","");

2.获取SharedPreferences

通过ContextImpl的getSharedPreferences获取

// 缓存了name到文件的映射
private ArrayMap<String, File> mSharedPrefsPaths;
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
		// 查找name对应的SP文件
    File file;
    synchronized (ContextImpl.class) {
        if (mSharedPrefsPaths == null) {
            mSharedPrefsPaths = new ArrayMap<>();
        }
        // 根据name从缓存中取出SP文件
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            // 没有在缓存中则根据name创建xml文件
            file = getSharedPreferencesPath(name);
            // 保存name与创建的文件
            mSharedPrefsPaths.put(name, file);
        }
    }
  	// 真正获取SP
    return getSharedPreferences(file, mode);
}

// 调用makeFilename创建xml文件:
@Override
public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}
// xml 与 SharedPreferencesImpl的缓存,注意这里static修饰的
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        // 获取缓存集合ArrayMap
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        // 从缓存中读取SP
        sp = cache.get(file);
        if (sp == null) {
						// ...
          	// 实例化SP
            sp = new SharedPreferencesImpl(file, mode);
            // 放入缓存
            cache.put(file, sp);
            return sp;
        }
    }
		// ...
    return sp;
}

可以看到ContextImpl的getSharedPreferences最终获取到的是SharedPreferencesImpl实例对象。SharedPreferencesImpl的构造方法如下:

SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    // 创建备份文件
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    // 开启从磁盘加载
    startLoadFromDisk();
}
@UnsupportedAppUsage
private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    // 开启子线程从磁盘加载数据
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}
private void loadFromDisk() {
    // ...

    Map<String, Object> map = null;
    StructStat stat = null;
    Throwable thrown = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                // 读取xml文件流
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                // 将xml文件解析成map集合
                map = (Map<String, Object>) XmlUtils.readMapXml(str);
            } catch (Exception e) {
                Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    }  catch (Throwable t) {
        thrown = t;
    }

    synchronized (mLock) {
        mLoaded = true;
        mThrowable = thrown;
        try {
            if (thrown == null) {
                if (map != null) {
                    // 赋值给mMap
                    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();
        }
    }
}

SharedPreferencesImpl的构造方法中会从磁盘上读取xml文件,并将该文件解析成一个Map集合,并赋值mMap这个成员变量。

接着调用了SharedPreferencesImpl的edit:

// SharedPreferencesImpl
@Override
public Editor edit() {
		
    synchronized (mLock) {
        awaitLoadedLocked();
    }

    return new EditorImpl();
}

在synchronized中调用awaitLoadedLocked方法,注意这里synchronized的加锁对象是mLock:

@GuardedBy("mLock")
private void awaitLoadedLocked() {
    if (!mLoaded) {
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }
    while (!mLoaded) {
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}

3.从SP中取数据

取数据时应保证SP的初始化已经完成,以getString为例:

// SharedPreferencesImpl
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

private void awaitLoadedLocked() {
    if (!mLoaded) {
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }
    while (!mLoaded) {
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}

使用awaitLoadedLocked()方法检测,是否已经加载完成,如果没有加载完成,就等待堵塞。等加载完成之后,继续执行;

4.编辑数据

调用sharedPreferences.edit()返回一个EditorImpl对象,操作数据之后调用apply()或者commit()。

1.commit流程

// SharedPreferencesImpl
@Override
public boolean commit() {

    MemoryCommitResult mcr = commitToMemory();// 提交到内存

    SharedPreferencesImpl.this.enqueueDiskWrite( // 写入磁盘
        mcr, null /* sync write on this thread okay */);
    try {
        mcr.writtenToDiskLatch.await(); // 等待写入磁盘完毕
    } catch (InterruptedException e) {
        return false;
    } finally {}
    notifyListeners(mcr);//通知监听
    return mcr.writeToDiskResult;
}

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    final boolean isFromSyncCommit = (postWriteRunnable == null);
    //如果postWriteRunnable为空表示来自commit()方法调用
    final Runnable writeToDiskRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr, isFromSyncCommit);//蒋数据写入磁盘
                }
                synchronized (mLock) {
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };

   //如果是commit提交,且mDiskWritesInFlight为1的时候,直接在当前所在线程执行写入磁盘操作
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }

    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

image

  • 首先将编辑的结果同步到内存中。

  • enqueueDiskWrite()将这个结果同步到磁盘中,enqueueDiskWrite()的第二个参数postWriteRunnable传入空。通常情况下也就是mDiskWritesInFlight(正在执行的写入磁盘操作的数量)为1的时候,直接在当前所在线程执行写入磁盘操作。否则还是异步到QueuedWork中去执行。commit()时,写入磁盘操作会发生在当前线程的说法是不准确的

  • 执行mcr.writtenToDiskLatch.await(); MemoryCommitResult 中有个一个CountDownLatch 成员变量,他的具体作用可以查阅其他资料。总的来说,当前线程执行会堵塞在这,直到mcr.writtenToDiskLatch满足了条件。也就是当写入磁盘成功之后,会继续执行下面的操作。

所以,commit提交之后会有返回结果,同步堵塞直到有返回结果

2. apply()流程

// SharedPreferencesImpl
@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);

    notifyListeners(mcr);
}
  • 加入到QueuedWork中,是一个单线程的操作。
  • 没有返回结果。
  • 默认会有100ms的延迟

4.3 QueuedWork

// QueuedWork
public static void queue(Runnable work, boolean shouldDelay) {
    Handler handler = getHandler();

    synchronized (sLock) {
        sWork.add(work);

        if (shouldDelay && sCanDelay) {
            handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
        } else {
            handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
        }
    }
}
  • 当apply()方式提交的时候,默认消息会延迟发送100毫秒,避免频繁的磁盘写入操作。
  • 当commit()方式,调用QueuedWork的queue()时,会立即向handler()发送Message。

主线程ANR问题

public static void waitToFinish() {
    // ...
    processPendingWork();
    // ...
}

private static void processPendingWork() {
    long startTime = 0;

    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }

    synchronized (sProcessingWork) {
        LinkedList<Runnable> work;

        synchronized (sLock) {
            work = (LinkedList<Runnable>) sWork.clone();
            sWork.clear();

            // Remove all msg-s as all work will be processed now
            getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
        }

        if (work.size() > 0) {
            for (Runnable w : work) {
                w.run();
            }

            if (DEBUG) {
                Log.d(LOG_TAG, "processing " + work.size() + " items took " +
                        +(System.currentTimeMillis() - startTime) + " ms");
            }
        }
    }
}

waitToFinish()会将储存在QueuedWork的操作一并处理掉。什么时候呢?在Activiy的 onPause()、BroadcastReceiver的onReceive()以及Service的onStartCommand()方法之前都会调用waitToFinish()。大家知道这些方法都是执行在主线程中,一旦waitToFinish()执行超时,就会跑出ANR。

5.常见问题

commit()方法和apply()方法的区别?

commit()方法是同步的有返回结果,同步保证使用Countdownlatch,即使同步但不保证往磁盘的写入是发生在当前线程的。apply()方法是异步的具体发生在QueuedWork中,里面维护了一个单线程去执行磁盘写入操作。

commit()和apply()方法其实都是Block主线程。commit()只要在主线程调用就会堵塞主线程;apply()方法磁盘写入操作虽然是异步的,但是当组件(Activity Service BroadCastReceiver)这些系统组件特定状态转换的时候,会把QueuedWork中未完成的那些磁盘写入操作放在主线程执行,且如果比较耗时会产生ANR,手动可怕。

https://www.jianshu.com/p/40e42da910e2

https://juejin.cn/post/6844904022063710221

公众号:玩转安卓Dev

Java基础

面向对象与Java基础知识

Java集合框架

JVM

多线程与并发

设计模式

Kotlin

Android

Android基础知识

Android消息机制

Framework

View事件分发机制

Android屏幕刷新机制

View的绘制流程

Activity启动

性能优化

Jetpack&系统View

第三方框架实现原理

计算机网络

算法

其它

Clone this wiki locally