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/

原创粉丝点击