getStringForUser原理和线程安全
来源:互联网 发布:unity3d 手游 案例 编辑:程序博客网 时间:2024/06/05 19:14
在电话中有很多的设置项,因为电话的设置项有些需要在全局使用,所以通过Settings.System.putStringForUser 和 Settings.System.getStringForUser,来写入和读取一些设置项。
1.问题描述
getStringForUser有时候拿到了错误的值。通过一些log,我们发现:出现问题时都是在多个线程同时读写的时候。所以它肯定涉及到了线程安全的问题。
2.Settings原理
代码位置:frameworks/base/core/java/android/provider/Settings.java
通过这种方式存储的数据,其实是以非常简单的方式存储在xml文件中
global文件路径:/data/system/users/0/settings_global.xml
格式:
<setting id="28" name="lock_sound" value="/system/media/audio/ui/Lock.ogg" package="android" />
就是以键值对的方式存的,多加了id和包名。
所以看清它的原理后就明白,getInt之类最终就会转换为getString。
读取数据的流程
/** @hide */ public static String getStringForUser(ContentResolver resolver, String name, int userHandle) { android.util.SeempLog.record(android.util.SeempLog.getSeempGetApiIdFromValue(name)); if (MOVED_TO_SECURE.contains(name)) { Log.w(TAG, "Setting " + name + " has moved from android.provider.Settings.System" + " to android.provider.Settings.Secure, returning read-only value."); return Secure.getStringForUser(resolver, name, userHandle); } if (MOVED_TO_GLOBAL.contains(name) || MOVED_TO_SECURE_THEN_GLOBAL.contains(name)) { Log.w(TAG, "Setting " + name + " has moved from android.provider.Settings.System" + " to android.provider.Settings.Global, returning read-only value."); return Global.getStringForUser(resolver, name, userHandle); } return sNameValueCache.getStringForUser(resolver, name, userHandle); }
可以关注到,最后都是从sNameValueCache读取的数据。sNameValueCache其实就是一个缓存,如果每次读取都需要跨进程,读写文件的话,速度上肯定是不能接受的。原理也是非常简单维护了一个Hashmap
// Must synchronize on 'this' to access mValues and mValuesVersion. private final HashMap<String, String> mValues = new HashMap<String, String>();
在读取数据时:
public String getStringForUser(ContentResolver cr, String name, final int userHandle) { final boolean isSelf = (userHandle == UserHandle.myUserId()); if (isSelf) { synchronized (NameValueCache.this) { if (mGenerationTracker != null) { if (mGenerationTracker.isGenerationChanged()) { if (DEBUG) { Log.i(TAG, "Generation changed for type:" + mUri.getPath() + " in package:" + cr.getPackageName() +" and user:" + userHandle); } mValues.clear(); } else if (mValues.containsKey(name)) { // 如果Generation没有变的话,直接从mValues读取数据 return mValues.get(name); } } } } else { if (LOCAL_LOGV) Log.v(TAG, "get setting for user " + userHandle + " by user " + UserHandle.myUserId() + " so skipping cache"); }.....
可以看到如果Generation没有变,会直接从mValues中读取数据,那mGenerationTracker是什么呢?
private static final class GenerationTracker { private final MemoryIntArray mArray; private final Runnable mErrorHandler; private final int mIndex; private int mCurrentGeneration; public GenerationTracker(@NonNull MemoryIntArray array, int index, int generation, Runnable errorHandler) { mArray = array; mIndex = index; mErrorHandler = errorHandler; mCurrentGeneration = generation; } public boolean isGenerationChanged() { final int currentGeneration = readCurrentGeneration(); if (currentGeneration >= 0) { if (currentGeneration == mCurrentGeneration) { return false; } mCurrentGeneration = currentGeneration; } return true; } private int readCurrentGeneration() { try { return mArray.get(mIndex); } catch (IOException e) { Log.e(TAG, "Error getting current generation", e); if (mErrorHandler != null) { mErrorHandler.run(); } } return -1; } public void destroy() { try { mArray.close(); } catch (IOException e) { Log.e(TAG, "Error closing backing array", e); if (mErrorHandler != null) { mErrorHandler.run(); } } } }
可以看到整个类都非常简单,维护了一个MemoryIntArray,一个共享内存的int数组,mCurrentGeneration就从这个共享内存中读出来,其实能想到,这个mCurrentGeneration其实就是类似数据库版本,其实在Android M上,不没有通过共享内存,而是读文件,所以这里明显快多了。
这个Generation怎么更新呢?
synchronized (NameValueCache.this) { if (isSelf && mGenerationTracker == null) { needsGenerationTracker = true; if (args == null) { args = new Bundle(); } args.putString(CALL_METHOD_TRACK_GENERATION_KEY, null); if (DEBUG) { Log.i(TAG, "Requested generation tracker for type: "+ mUri.getPath() + " in package:" + cr.getPackageName() +" and user:" + userHandle); } } } Bundle b = cp.call(cr.getPackageName(), mCallGetCommand, name, args);
r如果mGenerationTracker为空的话,我们会在call时带上args,
@Override public Bundle call(String method, String name, Bundle args) { final int requestingUserId = getRequestingUserId(args); switch (method) { case Settings.CALL_METHOD_GET_GLOBAL: { Setting setting = getGlobalSetting(name); return packageValueForCallResult(setting, isTrackingGeneration(args)); }.......... return null; } private Bundle packageValueForCallResult(Setting setting, boolean trackingGeneration) { if (!trackingGeneration) { if (setting.isNull()) { return NULL_SETTING_BUNDLE; } return Bundle.forPair(Settings.NameValueTable.VALUE, setting.getValue()); } Bundle result = new Bundle(); result.putString(Settings.NameValueTable.VALUE, !setting.isNull() ? setting.getValue() : null); mSettingsRegistry.mGenerationRegistry.addGenerationData(result, setting.getkey()); return result; } private boolean isTrackingGeneration(Bundle args) { return args != null && args.containsKey(Settings.CALL_METHOD_TRACK_GENERATION_KEY); }
j就是说如果有CALL_METHOD_TRACK_GENERATION_KEY这个key,就将会addGenerationData
public void addGenerationData(Bundle bundle, int key) { synchronized (mLock) { MemoryIntArray backingStore = getBackingStoreLocked(); try { if (backingStore != null) { final int index = getKeyIndexLocked(key, mKeyToIndexMap, backingStore); if (index >= 0) { bundle.putParcelable(Settings.CALL_METHOD_TRACK_GENERATION_KEY, backingStore); bundle.putInt(Settings.CALL_METHOD_GENERATION_INDEX_KEY, index); bundle.putInt(Settings.CALL_METHOD_GENERATION_KEY, backingStore.get(index)); if (DEBUG) { Slog.i(LOG_TAG, "Exported index:" + index + " for key:" + SettingsProvider.keyToString(key)); } } } } catch (IOException e) { Slog.e(LOG_TAG, "Error adding generation data", e); destroyBackingStore(); } } }
拿到共享内存的数组,并将对应的index加入都bundle中。index的key其实对应了三张表,global,secure和system。从对应的index读取出的数字就是对应表的generation。
其实正常的逻辑去想,一定是在写数据时更新generation,事实也是如此
private void notifyForSettingsChange(int key, String name) { final int userId = getUserIdFromKey(key); Uri uri = getNotificationUriFor(key, name); mGenerationRegistry.incrementGeneration(key); mHandler.obtainMessage(MyHandler.MSG_NOTIFY_URI_CHANGED, userId, 0, uri).sendToTarget(); if (isSecureSettingsKey(key)) { maybeNotifyProfiles(getTypeFromKey(key), userId, uri, name, sSecureCloneToManagedSettings); } else if (isSystemSettingsKey(key)) { maybeNotifyProfiles(getTypeFromKey(key), userId, uri, name, sSystemCloneToManagedSettings); } mHandler.obtainMessage(MyHandler.MSG_NOTIFY_DATA_CHANGED).sendToTarget(); }
在insert数据后会notifyForSettingsChange,其中一行mGenerationRegistry.incrementGeneration(key);再明显不过了
public void incrementGeneration(int key) { synchronized (mLock) { MemoryIntArray backingStore = getBackingStoreLocked(); if (backingStore != null) { try { final int index = getKeyIndexLocked(key, mKeyToIndexMap, backingStore); if (index >= 0) { final int generation = backingStore.get(index) + 1; backingStore.set(index, generation); } } catch (IOException e) { Slog.e(LOG_TAG, "Error updating generation id", e); destroyBackingStore(); } } } }
读出来,加1,写回去,其实就可以认为,每次写数据后,generation就会自增1。
总结
整个过程就是这样,在调用的进程维护一个缓存,根据generation是否变化来更新缓存,在SettingsProvider中真正操作文件,读写数据。
线程安全问题
public String getStringForUser(ContentResolver cr, String name, final int userHandle) { final boolean isSelf = (userHandle == UserHandle.myUserId()); if (isSelf) { synchronized (NameValueCache.this) { if (mGenerationTracker != null) { if (mGenerationTracker.isGenerationChanged()) { if (DEBUG) { Log.i(TAG, "Generation changed for type:" + mUri.getPath() + " in package:" + cr.getPackageName() +" and user:" + userHandle); } mValues.clear(); } else if (mValues.containsKey(name)) { return mValues.get(name); } } } } else { if (LOCAL_LOGV) Log.v(TAG, "get setting for user " + userHandle + " by user " + UserHandle.myUserId() + " so skipping cache"); } ....... Bundle b = cp.call(cr.getPackageName(), mCallGetCommand, name, args); if (b != null) { String value = b.getString(Settings.NameValueTable.VALUE); // Don't update our cache for reads of other users' data if (isSelf) { synchronized (NameValueCache.this) { ........ mValues.put(name, value); } } else { if (LOCAL_LOGV) Log.i(TAG, "call-query of user " + userHandle + " by " + UserHandle.myUserId() + " so not updating cache"); } return value; } // If the response Bundle is null, we fall through // to the query interface below. } catch (RemoteException e) { // Not supported by the remote side? Fall through // to query(). } }.......
可以看到所有读写mValues的地方都加了所,所以对已多线程同时写这个操作时安全的,问题实际上是加锁是分段的。
出现的问题就是
例:
线程1在call “aaaaa” 完成后,在获取NameValueCache.this锁之前,线程2更新了 “aaaaa” 值,此时线程3继续调用getStringForUser “bbbbb”,并比线程1先获取到NameValueCache.this,更新了Generation,并清空缓存,最后线程1获取到NameValueCache.this并”aaaaa”加入缓存。
所以缓存的”aaaaa”对应value是一个错误的值,但是generation已经更新到最新,之后再来获取”aaaaa”都会获取到这个错误的值
修复:在写入缓存前再检查一下Generation是否变化
https://android-review.googlesource.com/#/c/platform/frameworks/base/+/466977/
- getStringForUser原理和线程安全
- 线程安全原理
- Java线程安全和非线程安全
- Java线程安全和非线程安全
- Java线程安全和非线程安全
- 线程安全和非线程安全
- Java线程安全和非线程安全
- Java线程安全和非线程安全
- Java线程安全和非线程安全
- Java线程安全和非线程安全
- Java线程安全和非线程安全
- Java线程安全和非线程安全
- Java线程安全和非线程安全
- Java线程安全和非线程安全
- 线程安全和非线程安全
- Java线程安全和非线程安全
- Java线程安全和非线程安全
- Java线程安全和非线程安全
- 公共基础知识之软件工程基础
- HDU 2032 杨辉三角
- c++继承经典例子
- MFC自定义消息
- 虚拟机安装spark2.2+hadoop2.7.3
- getStringForUser原理和线程安全
- 对于多进程的复习
- 分页实现方式
- 4、ARP地址解析协议
- 插入排序算法
- PLSQL Developer 配置,在
- java中常见异常(1)
- 技术探讨
- 建造者模式