/ 今日科技快讯 /
近日,有自称腾讯7年的老员工称,“人到中年,被腾讯暴力裁员。”据他文章披露,2019年3月某天下午,腾讯HR派出大批保安单方面暴力裁员,让很多保安逼迫其收拾东西离开,当场封掉工卡、内网账号、公司邮箱等所有腾讯内部权限和资料,理由是每天工作不足八小时。
对此,腾讯方面回应称,“该名前员工在离职之前的相当长时间内,无论是在岗时段、实际工作成果还是其他相关行为表现,均未能匹配对应岗位要求。
/ 作者简介 /
惊喜不惊喜?才刚上一天班,明天就是又是周末啦,提前祝大家周末愉快!
本篇文章转自Drummor的博客,分享了SharedPreferences的原理以及替代方案。
原文地址:
https://juejin.im/post/5df7af66e51d4557f17fb4f7
/ 前言 /
先来一波灵魂追问:
听说提交要用apply(),为什么?
和commit()什么区别?
跨进程怎么操作?
会堵塞主线程吗?
很着急有替代方案吗?
/ 加载/初始化 /
一切从getSharedPreference(String name,int Mode)这个方法说起;通过这个方法获取到一个SharedPreference实例。SharedPreferences是一个接口(interface),他的具体实现类为SharedPreferencesImpl。SharedPreference的加载的主要过程:
找到对应name的文件。
加载对应文件到内存中SharedPreference。
一个xml文件对应一个ShredPreferences单例。
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
private ArrayMap<String, File> mSharedPrefsPaths;
sSharedPrefsCache存储的是File和SharedPreferencesImpl键值对,当对应File的SharedPreferencesImpl加载之后就会一直存储于sSharedPrefsCache中。类似的mSharedPrefsPaths存储的是name和File的对应关系。
当通过name最终找到对应的File之后,就会实例化一个SharedPreferencesImpl对象。在SharedPreferences构造方法中开启一个子线程加载磁盘中的xml文件。
大家都应该很明确的一点是,SP持久化的本质是在本地磁盘记录了一个xml文件,这个文件所在的文件夹shared_prefs
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
怎么保证使用sp.get(String name)的时候SP的初始化或者说从磁盘中加载到内存中这一过程已经完成了呢?
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) {
// 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);
}
}
使用awaitLoadedLocked()方法检测,是否已经加载完成,如果没有加载完成,就等待堵塞。等加载完成之后,继续执行;
在loadFromDisk()方法中,如果加载成功会把mLoaded标志位置为true,然后 mLock.notifyAll();
最终,就把位于磁盘中的文件,加载到了内存中对应一个SharedPreferces对象,SharedPreferences中mMap。
/ 编辑提交 /
当向SP中存入数据的时候,实例代码如下。
sharedPreferences.edit().putInt("number", 100).puString("age","18").apply();
sharedPreferences.edit().putInt("number", 100).commit();
调用sharedPreferences.edit()返回一个EditorImpl对象,操作数据之后调用apply()或者commit()。
commit()流程
@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) {
//如果postWriteRunnable为空表示来自commit()方法调用
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();
}
}
};
//当commit提交,且mDiskWritesInFlight为1的时候,直接在当前所在线程执行写入磁盘操作
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
//交个QueuedWork,QueuedWork内部维护了一个HandlerThread,一直执行写入磁盘操作。
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
如注释:当调用commit()方法之后
首先将编辑的结果同步到内存中。
enqueueDiskWrite()将这个结果同步到磁盘中,enqueueDiskWrite()的第二个参数postWriteRunnable传入空。通常情况下也就是mDiskWritesInFlight(正在执行的写入磁盘操作的数量)为1的时候,直接在当前所在线程执行写入磁盘操作。否则还是异步到QueuedWork中去执行。commit()时,写入磁盘操作会发生在当前线程的说法是不准确的。
执行mcr.writtenToDiskLatch.await(); MemoryCommitResult 中有个一个CountDownLatch 成员变量,他的具体作用可以查阅其他资料。总的来说,当前线程执行会堵塞在这,直到mcr.writtenToDiskLatch满足了条件。也就是当写入磁盘成功之后,会继续执行下面的操作。
所以,commit提交之后会有返回结果,同步堵塞直到有返回结果。
apply()流程
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {mcr.writtenToDiskLatch.await();}
};
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的延迟
3. QueuedWork
3.1 关于延迟磁盘写入。
/** Delay for delayed runnables, as big as possible but low enough to be barely perceivable */
private static final long DELAY = 100;
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
You don't need to worry about Android component lifecycles and their interaction with apply() writing to disk. The framework makes sure in-flight disk writes from apply() complete before switching states.
官方文档中有这样段话,意思是您不需要担心Android组件生命周期及其对apply()写入磁盘的影响。框层架确保在切换状态之前完成使用apply()方法正在执行磁盘写入的动作。
然而还真是不让人那么省心。
罪魁祸首在这:
//QueuedWork.java
public static void waitToFinish() {
...
processPendingWork();//执行文件写入磁盘操作
....
}
private static void processPendingWork() {
long startTime = 0;
....
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
...
}
waitToFinish()会将,储存在QueuedWork的操作一并处理掉。什么时候呢?在Activiy的 onPause()、BroadcastReceiver的onReceive()以及Service的onStartCommand()方法之前都会调用waitToFinish()。大家知道这些方法都是执行在主线程中,一旦waitToFinish()执行超时,就会跑出ANR。
至于waitToFinish调用具体时机,查看ActivityThread.java类文件。这里只是说本质原理。
/ 跨进程操作 /
\\ContextImpl
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");
}
}
}
Andorid 7.0及以上会抛出异常,Sharepreferences不再支持多进程模式。多进程共享文件会出现问题的本质在于,因为不同进程,所以线程同步会失效。要解决这个问题,可尝试跨进程解决方案,如ContentProvider、AIDL、AIDL、Service。
/ 替代方案 /
有问题,主线程堵塞。
效率低。
一不留神容易产生ANR。
既然SharedPreferences有这么多问题?就没人管管吗?温和的治理方法或者说小建议
温和改良派
低频 尽量保证多次edit一个apply,原因上文讲过,尽量维持低频的写入。
异步 能用apply()方法提交的就用apply()方法提交,原因这个方法是异步的,有延迟的(100s)
小量 尽量维持Sharepreferences的体量小些,方便磁盘快速写入。
合规 如果存JSON数据,就不要使用Sharepreferences了,因为SharedPerences本质是xml文件格式存储的,要存储JSON文件需要转义效率很低。不如直接自己编写代码文件读写在App私有目录中存储。
激进铲除派
腾讯微信团队的MMKV采用内存映射的方式,解决SharedPreferences的各种问题。
原理基于内存映射mmap
/ 小结 /
通过本文我们了解了SharedPreferences的基本原理。再回头看看文章开头的那几个问题,是不是有答案了。
commit()方法和apply()方法的区别:commit()方法是同步的有返回结果,同步保证使用Countdownlatch,即使同步但不保证往磁盘的写入是发生在当前线程的。apply()方法是异步的具体发生在QueuedWork中,里面维护了一个单线程去执行磁盘写入操作。
commit()和apply()方法其实都是Block主线程。commit()只要在主线程调用就会堵塞主线程;apply()方法磁盘写入操作虽然是异步的,但是当组件(Activity Service BroadCastReceiver)这些系统组件特定状态转换的时候,会把QueuedWork中未完成的那些磁盘写入操作放在主线程执行,且如果比较耗时会产生ANR。
跨进程操作,需要借助Android平台常规的IPC手段(如,AIDL ContentProvider等)来完成。
推荐阅读:
网络请求只会用Retrofit?外国人已经在用Graphql了
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注