Android多進(jìn)程線程安全SharedPreferences

https://github.com/jovezhougang/mpsp

原理

使用ContentProvider 來(lái)保證進(jìn)程訪問(wèn)數(shù)據(jù)安全,使用讀寫(xiě)鎖來(lái)保證線程訪問(wèn)安全

測(cè)試

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        MPSharedPreferences mpSharedPreferences = new MPSharedPreferences(getApplicationContext(),
                getPackageName());
        mpSharedPreferences.registerOnSharedPreferenceChangeListener(new MPSharedPreferences.OnSharedPreferenceChangeListener() {
            @Override
            public void onSharedPreferenceChanged(MPSharedPreferences sharedPreferences,
                                                  String key) {
                System.out.println(key);
            }
        });
        mpSharedPreferences.putBoolean("boolean",true);
        mpSharedPreferences.putFloat("float",9.9f);
        mpSharedPreferences.putInt("int",10);
        mpSharedPreferences.putLong("long",System.currentTimeMillis());
        mpSharedPreferences.putString("string","hello world");

        Set<String> sets = new HashSet<>();
        sets.add("8888");
        sets.add("9999");
        sets.add("0000");
        mpSharedPreferences.putStringSet("stringSet",sets);
        System.out.println(mpSharedPreferences.getString("string",""));
        System.out.println(mpSharedPreferences.getBoolean("boolean",false));
        System.out.println(mpSharedPreferences.getFloat("float",0));
        System.out.println(mpSharedPreferences.getInt("int",0));
        System.out.println(mpSharedPreferences.getLong("long",0));
        System.out.println(mpSharedPreferences.getStringSet("stringSet",null));

        for (int i = 0;i < 10000;i++) {
            System.out.println("i == " + i);
            System.out.println(mpSharedPreferences.getAll());
        }
    }

實(shí)現(xiàn)

package com.jove.mpsp;

import android.content.ContentValues;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class MPSharedPreferences {

    private HashSet<OnSharedPreferenceChangeListener> listeners = new HashSet<>();

    public interface OnSharedPreferenceChangeListener {
        void onSharedPreferenceChanged(final MPSharedPreferences sharedPreferences,
                                       final String key);
    }

    private String name;
    private Context context;

    private ContentObserver mContentObserver =
            new ContentObserver(new Handler(Looper.getMainLooper())) {
                @Override
                public void onChange(boolean selfChange, Uri uri) {
                    synchronized (MPSharedPreferences.this) {
                        for (final OnSharedPreferenceChangeListener listener : listeners) {
                            listener.onSharedPreferenceChanged(MPSharedPreferences.this
                                    , uri.getPathSegments().get(1));
                        }
                    }
                }
            };

    public MPSharedPreferences(@NonNull Context context, @NonNull final String name) {
        this.name = name;
        this.context = context;
        this.context.getContentResolver()
                .registerContentObserver(Uri.parse(String.format("content://com.jove.mpsp" +
                                ".provider/%s"
                        , name)), true, mContentObserver);
    }

    public Map<String, ?> getAll() {
        Cursor cursor = null;
        try {
            cursor = context.getContentResolver()
                    .query(Uri.parse(String.format("content://com.jove.mpsp.provider/%s/all/0"
                            , name)), null, null, null, null);
            if (null != cursor && cursor.moveToNext()) {
                final Map<String, Object> map = new HashMap<>(cursor.getColumnCount());
                for (int i = 0; i < cursor.getColumnCount(); i++) {
                    switch (cursor.getType(i)) {
                        case Cursor.FIELD_TYPE_BLOB:
                            map.put(cursor.getColumnName(i), cursor.getBlob(i));
                            break;
                        case Cursor.FIELD_TYPE_STRING:
                        case Cursor.FIELD_TYPE_NULL:
                            map.put(cursor.getColumnName(i), cursor.getString(i));
                            break;
                        case Cursor.FIELD_TYPE_INTEGER:
                            map.put(cursor.getColumnName(i), cursor.getInt(i));
                            if (Long.parseLong(cursor.getString(i)) != cursor.getInt(i)) {
                                map.put(cursor.getColumnName(i),
                                        Long.parseLong(cursor.getString(i)));
                            }
                            break;
                        case Cursor.FIELD_TYPE_FLOAT:
                            map.put(cursor.getColumnName(i), cursor.getFloat(i));
                            break;
                    }
                }
                return map;
            }
        } finally {
            if (null != cursor) {
                cursor.close();
            }
        }
        return null;
    }

    @Nullable
    public String getString(String key, @Nullable String defValue) {
        Cursor cursor = null;
        try {
            cursor = context.getContentResolver()
                    .query(Uri.parse(String.format("content://com.jove.mpsp.provider/%s/%s/1"
                            , name, key)), null, null, null, null);
            if (null != cursor && cursor.moveToNext()) {
                return cursor.getString(0);
            }
        } finally {
            if (null != cursor) {
                cursor.close();
            }
        }
        return defValue;
    }

    @Nullable
    public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
        Cursor cursor = null;
        try {
            cursor = context.getContentResolver()
                    .query(Uri.parse(String.format("content://com.jove.mpsp.provider/%s/%s/6"
                            , name, key)), null, null, null, null);
            if (null != cursor && cursor.moveToNext()) {
                final Set<String> sets = new HashSet<>();
                for (int i = 0; i < cursor.getColumnCount(); i++) {
                    sets.add(cursor.getString(i));
                }
                return sets;
            }
        } finally {
            if (null != cursor) {
                cursor.close();
            }
        }
        return defValues;
    }

    public int getInt(String key, int defValue) {
        Cursor cursor = null;
        try {
            cursor = context.getContentResolver()
                    .query(Uri.parse(String.format("content://com.jove.mpsp.provider/%s/%s/4"
                            , name, key)), null, null, null, null);
            if (null != cursor && cursor.moveToNext()) {
                return cursor.getInt(0);
            }
        } finally {
            if (null != cursor) {
                cursor.close();
            }
        }
        return defValue;
    }

    public long getLong(String key, long defValue) {
        Cursor cursor = null;
        try {
            cursor = context.getContentResolver()
                    .query(Uri.parse(String.format("content://com.jove.mpsp.provider/%s/%s/5"
                            , name, key)), null, null, null, null);
            if (null != cursor && cursor.moveToNext()) {
                return Long.parseLong(cursor.getString(0));
            }
        } finally {
            if (null != cursor) {
                cursor.close();
            }
        }
        return defValue;
    }

    public float getFloat(final String key, final float defValue) {
        Cursor cursor = null;
        try {
            cursor = context.getContentResolver()
                    .query(Uri.parse(String.format("content://com.jove.mpsp.provider/%s/%s/3"
                            , name, key)), null, null, null, null);
            if (null != cursor && cursor.moveToNext()) {
                return cursor.getFloat(0);
            }
        } finally {
            if (null != cursor) {
                cursor.close();
            }
        }
        return defValue;
    }

    public boolean getBoolean(final String key, final boolean defValue) {
        Cursor cursor = null;
        try {
            cursor = context.getContentResolver()
                    .query(Uri.parse(String.format("content://com.jove.mpsp.provider/%s/%s/2"
                            , name, key)), null, null, null, null);
            if (null != cursor && cursor.moveToNext()) {
                return 0 == cursor.getInt(0) ? false : true;
            }
        } finally {
            if (null != cursor) {
                cursor.close();
            }
        }
        return defValue;
    }

    public boolean contains(final String key) {
        Cursor cursor = null;
        try {
            cursor = context.getContentResolver()
                    .query(Uri.parse(String.format("content://com.jove.mpsp.provider/%s/%s/7"
                            , name, key)), null, null, null, null);
            if (null != cursor && cursor.moveToNext()) {
                return cursor.getInt(0) > 0;
            }
        } finally {
            if (null != cursor) {
                cursor.close();
            }
        }
        return false;
    }

    public synchronized void registerOnSharedPreferenceChangeListener(final @NonNull OnSharedPreferenceChangeListener listener) {
        listeners.add(listener);
    }

    public synchronized void unregisterOnSharedPreferenceChangeListener(final @NonNull OnSharedPreferenceChangeListener listener) {
        listeners.remove(listener);
    }

    public boolean putString(String key, @Nullable String value) {
        final ContentValues values = new ContentValues();
        values.put(key, value);
        final Uri uri = context.getContentResolver()
                .insert(Uri.parse(String.format("content://com.jove.mpsp.provider/%s/%s/1"
                        , name, key)), values);
        return null != uri;
    }

    public boolean putStringSet(final String key, final @Nullable Set<String> values) {
        final ContentValues contentValues = new ContentValues();
        int index = 0;
        for (final String value : values) {
            contentValues.put("k" + index, value);
            index++;
        }
        final Uri uri = context.getContentResolver()
                .insert(Uri.parse(String.format("content://com.jove.mpsp.provider/%s/%s/6"
                        , name, key)), contentValues);
        return null != uri;
    }

    public boolean putInt(String key, int value) {
        final ContentValues values = new ContentValues();
        values.put(key, value);
        final Uri uri = context.getContentResolver()
                .insert(Uri.parse(String.format("content://com.jove.mpsp.provider/%s/%s/4"
                        , name, key)), values);
        return null != uri;
    }

    public boolean putLong(String key, long value) {
        final ContentValues values = new ContentValues();
        values.put(key, value);
        final Uri uri = context.getContentResolver()
                .insert(Uri.parse(String.format("content://com.jove.mpsp.provider/%s/%s/5"
                        , name, key)), values);
        return null != uri;
    }

    public boolean putFloat(String key, float value) {
        final ContentValues values = new ContentValues();
        values.put(key, value);
        final Uri uri = context.getContentResolver()
                .insert(Uri.parse(String.format("content://com.jove.mpsp.provider/%s/%s/3"
                        , name, key)), values);
        return null != uri;
    }

    public boolean putBoolean(String key, boolean value) {
        final ContentValues values = new ContentValues();
        values.put(key, value);
        final Uri uri = context.getContentResolver()
                .insert(Uri.parse(String.format("content://com.jove.mpsp.provider/%s/%s/2"
                        , name, key)), values);
        return null != uri;
    }

    public boolean remove(final String key) {
        final int nums = context.getContentResolver()
                .delete(Uri.parse(String.format("content://com.jove.mpsp.provider/remove/%s/%s"
                        , name, key)), null, null);
        return nums > 0;
    }

    public boolean clear() {
        final int nums = context.getContentResolver()
                .delete(Uri.parse(String.format("content://com.jove.mpsp.provider/clear/%s"
                        , name)), null, null);
        return nums > 0;
    }
}
package com.jove.mpsp;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;

import java.io.File;
import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class MPContentProvider extends ContentProvider implements SharedPreferences.OnSharedPreferenceChangeListener {
    private static UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    static {
        uriMatcher.addURI("com.jove.mpsp.provider", "remove/*/*", 0x666);
        uriMatcher.addURI("com.jove.mpsp.provider", "clear/*", 0x999);
        uriMatcher.addURI("com.jove.mpsp.provider", "*/*/#", 0x888);
    }

    @Override
    public boolean onCreate() {
        return true;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection,
                        @Nullable String selection, @Nullable String[] selectionArgs,
                        @Nullable String sortOrder) {
        try {
            lock.readLock().lock();
            switch (uriMatcher.match(uri)) {
                case 0x888:
                    final List<String> paths = uri.getPathSegments();
                    final SharedPreferences sp = getContext()
                            .getSharedPreferences(paths.get(0), Context.MODE_PRIVATE);
                    final int action = Integer.parseInt(paths.get(2));
                    if (action == 0) {
                        final Map<String, ?> map = sp.getAll();
                        if (null != map && 0 != map.size()) {
                            final Object[] columnValues = new Object[map.size()];
                            final String[] columnNames = new String[map.size()];
                            int index = 0;
                            for (final String key : map.keySet()) {
                                columnNames[index] = key;
                                columnValues[index] = map.get(key);
                                index++;
                            }
                            final MatrixCursor cursor = new MatrixCursor(columnNames
                                    , 1);
                            cursor.addRow(columnValues);
                            return cursor;
                        }
                        return null;
                    } else if (action == 1) {
                        final String key = paths.get(1);
                        if (sp.contains(key)) {
                            final MatrixCursor cursor = new MatrixCursor(new String[]{"v"}
                                    , 1);
                            cursor.addRow(new Object[]{sp.getString(paths.get(1), "")});
                            return cursor;
                        }
                        return null;
                    } else if (action == 2) {
                        final String key = paths.get(1);
                        if (sp.contains(key)) {
                            final MatrixCursor cursor = new MatrixCursor(new String[]{"v"}
                                    , 1);
                            cursor.addRow(new Object[]{sp.getBoolean(paths.get(1), false) ? 1 : 0});
                            return cursor;
                        }
                        return null;
                    } else if (action == 3) {
                        final String key = paths.get(1);
                        if (sp.contains(key)) {
                            final MatrixCursor cursor = new MatrixCursor(new String[]{"v"}
                                    , 1);
                            cursor.addRow(new Object[]{sp.getFloat(paths.get(1), 0)});
                            return cursor;
                        }
                        return null;
                    } else if (action == 4) {
                        final String key = paths.get(1);
                        if (sp.contains(key)) {
                            final MatrixCursor cursor = new MatrixCursor(new String[]{"v"}
                                    , 1);
                            cursor.addRow(new Object[]{sp.getInt(paths.get(1), 0)});
                            return cursor;
                        }
                        return null;
                    } else if (action == 5) {
                        final String key = paths.get(1);
                        if (sp.contains(key)) {
                            final MatrixCursor cursor = new MatrixCursor(new String[]{"v"}
                                    , 1);
                            cursor.addRow(new Object[]{sp.getLong(paths.get(1), 0)});
                            return cursor;
                        }
                        return null;
                    } else if (action == 6) {
                        final String key = paths.get(1);
                        if (sp.contains(key)) {
                            final Set<String> sets = sp.getStringSet(key, null);
                            if (null != sets && 0 != sets.size()) {
                                final String[] columnNames = new String[sets.size()];
                                final Object[] columnValues = new Object[sets.size()];
                                int index = 0;
                                for (final String value : sets) {
                                    columnNames[index] = "v" + index;
                                    columnValues[index] = value;
                                    index++;
                                }
                                final MatrixCursor cursor = new MatrixCursor(columnNames
                                        , 1);
                                cursor.addRow(columnValues);
                                return cursor;
                            }
                        }
                        return null;
                    } else if (action == 7) {
                        final String key = paths.get(1);
                        if (sp.contains(key)) {
                            final MatrixCursor cursor = new MatrixCursor(new String[]{"v"}
                                    , 1);
                            cursor.addRow(new Object[]{1});
                            return cursor;
                        }
                        return null;
                    }
                    break;
            }
            return null;
        } finally {
            lock.readLock().unlock();
        }
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        try {
            lock.writeLock().lock();
            switch (uriMatcher.match(uri)) {
                case 0x888:
                    final List<String> paths = uri.getPathSegments();
                    final SharedPreferences sp = getContext()
                            .getSharedPreferences(paths.get(0), Context.MODE_PRIVATE);
                    sp.unregisterOnSharedPreferenceChangeListener(this);
                    sp.registerOnSharedPreferenceChangeListener(this);
                    final int action = Integer.parseInt(paths.get(2));
                    if (action == 1) {
                        final String key = paths.get(1);
                        if (sp.edit().putString(key, values.getAsString(key)).commit()) {
                            return uri;
                        }
                        return null;
                    } else if (action == 2) {
                        final String key = paths.get(1);
                        if (sp.edit().putBoolean(key, values.getAsBoolean(key)).commit()) {
                            return uri;
                        }
                        return null;
                    } else if (action == 3) {
                        final String key = paths.get(1);
                        if (sp.edit().putFloat(key, values.getAsFloat(key)).commit()) {
                            return uri;
                        }
                        return null;
                    } else if (action == 4) {
                        final String key = paths.get(1);
                        if (sp.edit().putInt(key, values.getAsInteger(key)).commit()) {
                            return uri;
                        }
                        return null;
                    } else if (action == 5) {
                        final String key = paths.get(1);
                        if (sp.edit().putLong(key, values.getAsLong(key)).commit()) {
                            return uri;
                        }
                        return null;
                    } else if (action == 6) {
                        final Set<String> sets = new HashSet<>();
                        for (final String key : values.keySet()) {
                            sets.add(values.getAsString(key));
                        }
                        final String key = paths.get(1);
                        if (sp.edit().putStringSet(key, sets).commit()) {
                            return uri;
                        }
                        return null;
                    }
                    break;
            }
            return null;
        } finally {
            lock.writeLock().unlock();
        }
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection,
                      @Nullable String[] selectionArgs) {
        try {
            lock.writeLock().lock();
            final List<String> paths = uri.getPathSegments();
            final SharedPreferences sp = getContext()
                    .getSharedPreferences(paths.get(1), Context.MODE_PRIVATE);
            sp.unregisterOnSharedPreferenceChangeListener(this);
            sp.registerOnSharedPreferenceChangeListener(this);
            switch (uriMatcher.match(uri)) {
                case 0x666:
                    return sp.edit().remove(paths.get(2)).commit() ? 1 : 0;
                case 0x999:
                    return sp.edit().clear().commit() ? 1 : 0;
            }
        } finally {
            lock.writeLock().unlock();
        }
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values
            , @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }

    @Override
    public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
                                          final String key) {
        try {
            final Field field = sharedPreferences.getClass().getDeclaredField("mFile");
            field.setAccessible(true);
            final File file = (File) field.get(sharedPreferences);
            getContext().getContentResolver()
                    .notifyChange(Uri.parse(String.format("content://com.jove.mpsp.provider/%s/%s"
                            , file.getName().replaceAll(".xml", ""), key)), null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末楞抡,一起剝皮案震驚了整個(gè)濱河市村怪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌盛末,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件否淤,死亡現(xiàn)場(chǎng)離奇詭異悄但,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)石抡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)檐嚣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人啰扛,你說(shuō)我怎么就攤上這事嚎京。” “怎么了隐解?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵鞍帝,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我煞茫,道長(zhǎng)帕涌,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任续徽,我火速辦了婚禮蚓曼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘钦扭。我一直安慰自己纫版,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布客情。 她就那樣靜靜地躺著捎琐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪裹匙。 梳的紋絲不亂的頭發(fā)上瑞凑,一...
    開(kāi)封第一講書(shū)人閱讀 51,482評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音概页,去河邊找鬼籽御。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的技掏。 我是一名探鬼主播铃将,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼哑梳!你這毒婦竟也來(lái)了劲阎?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤鸠真,失蹤者是張志新(化名)和其女友劉穎悯仙,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體吠卷,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡锡垄,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了祭隔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片货岭。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖疾渴,靈堂內(nèi)的尸體忽然破棺而出千贯,到底是詐尸還是另有隱情,我是刑警寧澤搞坝,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布丈牢,位于F島的核電站,受9級(jí)特大地震影響瞄沙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜慌核,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一距境、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧垮卓,春花似錦垫桂、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至灭将,卻和暖如春疼鸟,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背庙曙。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工空镜, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓吴攒,卻偏偏與公主長(zhǎng)得像张抄,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子洼怔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354