Android 從觀察者模式到 DataBinding

前言

做過 iOS 的同學(xué)應(yīng)該都了解過 KVO呜象,是觀察者模式在 Objective-C 中的應(yīng)用膳凝。使用 KVO,能很方便的實現(xiàn)對對象屬性的監(jiān)聽恭陡。雖然 iOS 提供了對對象屬性的觀察者模式機制,但想想很多 Android 同學(xué)們應(yīng)該不會在意上煤。這不是很容易么休玩,我分分鐘也能寫一個:

public class User {
    String mName;
    Observable mObservable;

    public User(String name) {
        mName = name;
    }

    public String getName() {
        return mName;
    }

    public void setName(String name) {
        boolean isSame = TextUtils.equals(this.mName, name);
        this.mName = name;
        if (!isSame && mObservable != null) {
            mObservable.onNameChanged(name);
        }
    }

    public void setObservable(Observable observer) {
        this.mObservable = observer;
    }

    public interface Observable {
        void onNameChanged(String newName);
    }
}
User user = new User("我叫王尼瑪");
user.setObservable(new User.Observable() {
    @Override
    public void onNameChanged(String newName) {
        Log.i("user newName = ", newName);
    }
});
user.setName("呵呵,這你都信");

但是冷靜下來想想劫狠,如果一個大的工程中有很多這種需求呢拴疤?是不是 User1User2独泞,...... 都要寫這些機械的代碼了呐矾?那回過頭想想,如果不想自己寫這些代碼的話懦砂,那么我們大 Android 真的就沒有這種機制么蜒犯?想想不服氣,于是翻了翻資料荞膘,果然我們還是有的: ObservableField

ObservableField

1. 使用方式

使用還是很簡單的罚随,我們直接看代碼吧

ObservableField<String> name = new ObservableField<>();
name.addOnPropertyChangedCallback(
    new android.databinding.Observable.OnPropertyChangedCallback() {
        @Override
        public void onPropertyChanged(android.databinding.Observable observable, int i) {
            Log.d("name = ", "name = " + observable.toString() + "; i= " + i);
        }
    });
name.set("我叫張三");

這下舒服多了,不用自己實現(xiàn) Observable 接口和 setObservable 方法羽资,同時對于其他類型的變量淘菩,如 int,float 或者 自定義的類型屠升,也不用重新實現(xiàn)了潮改,直接定義 ObservableField<Integer>ObservableField<Float> 就行了腹暖,好開心_

2. 原理實現(xiàn)

還是直接看源碼吧汇在,反正代碼量也不多 O(∩_∩)O~

public class ObservableField<T> extends BaseObservable implements Serializable {
    static final long serialVersionUID = 1L;
    private T mValue;
    
    ......

    public T get() {
        return mValue;
    }

    public void set(T value) {
        if (value != mValue) {
            mValue = value;
            notifyChange();
        }
    }
}
public class BaseObservable implements Observable {
    private transient PropertyChangeRegistry mCallbacks;

    ......
    
    @Override
    public synchronized void addOnPropertyChangedCallback(OnPropertyChangedCallback callback) {
        ......
        
        mCallbacks.add(callback);
    }

    @Override
    public synchronized void removeOnPropertyChangedCallback(OnPropertyChangedCallback callback) {
        if (mCallbacks != null) {
            mCallbacks.remove(callback);
        }
    }

    public synchronized void notifyChange() {
        if (mCallbacks != null) {
            mCallbacks.notifyCallbacks(this, 0, null);
        }
    }

    ......
}

注:mCallbacks.notifyCallbacks(this, 0, null); 方法中,0 表示的是 fieldID微服,在 dataBinding 中表示數(shù)據(jù)資源 id趾疚。因此這里并沒有關(guān)聯(lián)視圖資源,所以這里設(shè)置為 0

可以看到以蕴,ObservableField 是一個泛型糙麦,所以支持多種類型的觀察者模式。BaseObservable 是其父類丛肮,實現(xiàn)了觀察者模式的核心代碼赡磅,可以看到 addOnPropertyChangedCallbackaddOnPropertyChangedCallback 2個添加和移除監(jiān)聽的方法,真正的監(jiān)聽者都被保存到 PropertyChangeRegistry.mCallbacks (類型是 List) 里面宝与。

當(dāng)調(diào)用 ObservableFieldset 方法時焚廊,會執(zhí)行 ObservableField.notifiyCallbacks 方法冶匹,如下:

public class BaseObservable implements Observable {
    public synchronized void notifyChange() {
        if (mCallbacks != null) {
            mCallbacks.notifyCallbacks(this, 0, null);
        }
    }
    
    ......
}

最終會執(zhí)行到 callback.onPropertyChanged(sender, arg);,如下代碼所示:

public class CallbackRegistry<C, T, A> implements Cloneable {
    private void notifyCallbacks(T sender, int arg, A arg2, final int startIndex,
            final int endIndex, final long bits) {
        ......
        for (int i = startIndex; i < endIndex; i++) {
            ......
            mNotifier.onNotifyCallback(mCallbacks.get(i), sender, arg, arg2);
            .....
        }
    }

    ......
}
private static final CallbackRegistry.NotifierCallback<Observable.OnPropertyChangedCallback,
        Observable, Void> NOTIFIER_CALLBACK = 
        new CallbackRegistry.NotifierCallback<Observable.OnPropertyChangedCallback, Observable, Void>() {
        @Override
        public void onNotifyCallback(Observable.OnPropertyChangedCallback callback, Observable sender,
                int arg, Void notUsed) {
            callback.onPropertyChanged(sender, arg);
        }
    };

這里咆瘟,我們可以看到嚼隘,通過遍歷的方式,去執(zhí)行 callback 方法袒餐,將前面通過 BaseObservable.addOnPropertyChangedCallback 添加的全部觀察者都響應(yīng)了一邊

3. 小結(jié)

到這里為止飞蛹,雖然把 ObservableField 的觀察者模式給講清楚了,但還是感覺有些失望灸眼,內(nèi)容很少很簡單卧檐。不過還沒完呢,Google 大神們就是基于此焰宣,玩出了 DataBinding

DataBinding

使用步驟

  1. IDE 配置

    • Android SDK API 版本 7 以上

    • 使用 Gradle 1.5.0-alpha1 及以上

    • 使用 Android Studio 1.3 及以上

  2. 配置開啟 dataBinding

    在主工程的 build.gradle 中霉囚,添加代碼:

    android {
        ......
        
        dataBinding {
            enabled = true
        }
    }
    
  3. 定義數(shù)據(jù)層 Model

    定義的 ObservableUser 類,繼承自 BaseObservable (同前面的 ObservableField)匕积。在 set 接口里添加 notifyPropertyChanged 調(diào)用盈罐,通知視圖更新

    public class ObservableUser extends BaseObservable {
        private String firstName;
        private String lastName;
    
        public ObservableUser(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }
    
        @Bindable
        public String getFirstName() {
            return firstName;
        }
    
        @Bindable
        public String getLastName() {
            return lastName;
        }
    
        public void setFirstName(String firstName) {
            this.firstName = firstName;
            notifyPropertyChanged(com.netease.mvvmsample.BR.firstName);
        }
    
        public void setLastName(String lastName) {
            this.lastName = lastName;
            notifyPropertyChanged(com.netease.mvvmsample.BR.lastName);
        }
    }
    
  4. 定義事件響應(yīng) Handler

    public class Handler {
        private ObservableUser mObservableUser;
    
        public Handler(ObservableUser user) {
            mObservableUser = user;
        }
    
        public void onClickButton(View view) {
            mObservableUser.setLastName("呵呵呵,我變了 " + mCount++ + " 次");
        }
    }
    
  5. 定義布局代碼

    在 data 標(biāo)簽下面闸天,定義 model 數(shù)據(jù)和事件響應(yīng) handler暖呕。使用 @{} 分別將 Button 的文本信息和 user.lastNameButton 的點擊響應(yīng)和 handler.onClickButton 綁定在一起

    <?xml version="1.0" encoding="utf-8"?>
    <layout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">
    
        <data>
            <variable
                name="user"
                type="com.netease.mvvmsample.ObservableUser">
            </variable>
            <variable
                name="handler"
                type="com.netease.mvvmsample.Handler">
            </variable>
        </data>
        <LinearLayout
            <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{user.lastName}"
                android:onClick="@{handler.onClickButton}"/>
        </LinearLayout  
    </layout>
    
  6. 設(shè)置布局和綁定數(shù)據(jù)和事件

    在定義了上面的布局 xml 文件之后苞氮,Android Studio 會自動生成 ViewModel 類湾揽。假設(shè)文件名是 activity_main.xml,那么程序編譯后笼吟,會生成 ActivityMainBinding 類库物。代替原來的 setContentView(R.layout.activity_main); 方法,使用 ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main); 來設(shè)置布局資源贷帮,并返回 binding 對象戚揭。新建數(shù)據(jù)和事件處理對象,并設(shè)置給 binding 對象

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
            ObservableUser user = new ObservableUser("Zhang", "San");
            Handler handler = new Handler(user);
            binding.setUser(user);
            binding.setHandler(handler);
        }
    }
    
  7. 效果展示

    image

注:這里僅僅講了 DataBinding 的基本使用撵枢,并沒有打算深入講述 DataBinding 的進一步使用民晒,如果有同學(xué)想要了解高級使用的話,如類方法锄禽,類型別名等潜必,可以查看官方文檔 Data Binding Library

數(shù)據(jù)和事件關(guān)聯(lián)原理

知道了 DataBinding 如何使用之后,就很好奇沃但,這個DataBinding 機制是如何實現(xiàn)的了磁滚。但是在 Android Studio 里面怎么也沒有找到 ActivityMainBinding 的代碼,在 ObservableUsergetLastName 方法中設(shè)置斷點,查看調(diào)用棧垂攘。我們可以發(fā)現(xiàn)维雇,其實是 ActivityMainBinding.executeBindings 方法調(diào)用了 model 的 get 方法。

image

然而晒他,悲劇的是吱型,我想點擊查看 executeBindings 調(diào)用情況,Android Studio 是跳到了 activity_main.xml 里去了仪芒。

image

好吧唁影,還是想看源碼,那就用 dex2jarjd-gui 工具查看了 class.dex 文件掂名,果然看到了源碼

image

當(dāng)然,其他 Android Studio 上沒能看到的代碼哟沫,如 DataBinderMapper 等的代碼也都能找到了饺蔑。

直接查看 DataBindingUtil.setContentView 里面的源碼吧:

  1. MainActivity.onCreate

    ActivityMainBinding binding = 
            DataBindingUtil.setContentView(this, R.layout.activity_main);
    
  2. 省略部分代碼,直接來到 DataBindingUtil.bind

    static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent,
            View root, int layoutId) {
        return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
    }
    

    其中嗜诀,bindingComponentnull猾警;rootlayoutId 都對應(yīng) activity_main.xml 中定義的 LinearLayout

  3. DataBinderMapper.getDataBinder

    public ViewDataBinding getDataBinder(DataBindingComponent paramDataBindingComponent,
        View paramView, int paramInt) {
        switch (paramInt) {
            default:
              return null;
            case 2130968601:
        }
        return ActivityMainBinding.bind(paramView, paramDataBindingComponent);
    }
    
  4. ActivityMainBinding.bind

    public static ActivityMainBinding bind(View paramView, DataBindingComponent paramDataBindingComponent) {
        if (!"layout/activity_main_0".equals(paramView.getTag()))
            throw new RuntimeException("view tag isn't correct on view:" + paramView.getTag());
        return new ActivityMainBinding(paramDataBindingComponent, paramView);
    }
    

    這里可以看到,返回的 ActivityMainBinding 對象是在這里被創(chuàng)建的了

  5. ActivityMainBinding 構(gòu)造函數(shù)

    public ActivityMainBinding(DataBindingComponent paramDataBindingComponent, View paramView) {
        super(paramDataBindingComponent, paramView, 1);
        paramDataBindingComponent = mapBindings(paramDataBindingComponent, paramView, 2, sIncludes, sViewsWithIds);
        this.mboundView0 = ((LinearLayout)paramDataBindingComponent[0]);
        this.mboundView0.setTag(null);
        this.mboundView1 = ((Button)paramDataBindingComponent[1]);
        this.mboundView1.setTag(null);
        setRootTag(paramView);
        invalidateAll();
    }
    

    這里還有好多疑問隆敢,mboundView0 和 mboundView1 分別對應(yīng)什么控件呢发皿?setRootTag 和 invalidateAll 都是干啥的呢?

  6. ViewDataBinding.mapBindings

    protected static Object[] mapBindings(DataBindingComponent bindingComponent, View root,
            int numBindings, IncludedLayouts includes, SparseIntArray viewsWithIds) {
        Object[] bindings = new Object[numBindings];
        mapBindings(bindingComponent, root, bindings, includes, viewsWithIds, true);
        return bindings;
    }
    
    private static void mapBindings(DataBindingComponent bindingComponent, View view,
            Object[] bindings, IncludedLayouts includes, SparseIntArray viewsWithIds,
            boolean isRoot) {
        ......
        if (view instanceof ViewGroup) {
            ......
            for (int i = 0; i < count; i++) {
                ......
                if (bindings[index] == null) {
                    bindings[index] = view;
                }
                ......
                {   
                    bindings[index] = DataBindingUtil.bind(bindingComponent, included,
                                        layoutId);
                }
                ......
                
                if (!isInclude) {
                    mapBindings(bindingComponent, child, bindings, includes, viewsWithIds, false);
                }
            }
        }
    }
    

    注:這里會遞歸的執(zhí)行 mapBindings 將傳入的 bindings 數(shù)據(jù)給填充好拂蝎。
    binding 數(shù)組里面的數(shù)據(jù)穴墅,可能是 view 也可能是 ViewDataBinding
    在當(dāng)期的示例程序中,bindings[0]LinearLayout温自,bindings[1]Button玄货;所以,ActivityMainBinding.mboundView0 就是 layout 中定義的 LinearLayout悼泌;ActivityMainBinding.mboundView1 就是 layout 中定義的 Button松捉。

  7. ViewDataBinding.setRootTag

    protected void setRootTag(View view) {
        ......
        view.setTag(R.id.dataBinding, this);
        ......
    }
    

    ActivityMainBinding 和布局文件中的 LinearLayout 關(guān)聯(lián)起來了。

  8. ActivityMainBinding 構(gòu)造函數(shù)

    public ActivityMainBinding(DataBindingComponent paramDataBindingComponent, View paramView) {
        ......
        invalidateAll();
    }
    
  9. 跳過部分代碼馆里,調(diào)用到 ActivityMainBinding.requestRebind

    protected void requestRebind() {
        ......
        if (SDK_INT >= 16) {
            mChoreographer.postFrameCallback(mFrameCallback);
        } else {
            mUIThreadHandler.post(mRebindRunnable);
        }
    }
    

    假設(shè)當(dāng)前 SDK_INT == 23隘世,直接查看 mFrameCallback 的定義。則在下一幀的時候鸠踪,調(diào)用 mRebindRunnable.run();

    mFrameCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            mRebindRunnable.run();
        }
    };
    
  10. 最終執(zhí)行 ActivityMainBinding.executeBindings 方法

    protected void executeBindings()
    {
        ......  
    
        Handler localHandler = this.mHandler;
        ObservableUser localObservableUser = this.mUser;
            
        ......
        while (true)
        {
            ......
            
            localObject1 = new OnClickListenerImpl();
            this.mAndroidViewViewOnCl = ((OnClickListenerImpl)localObject1);
            localObject1 = ((OnClickListenerImpl)localObject1).setValue(localHandler);
    
            localObject3 = localObject4;
            
            ......
            
            localObject3 = localObservableUser.getLastName();
            TextViewBindingAdapter.setText(this.mboundView1, (CharSequence)localObject3);
            
            this.mboundView1.setOnClickListener((View.OnClickListener)localObject1);
            return;
            ......
        }
    }
    

    注:這里 mUsermHandler 是 MainActivity.onCreate 中的設(shè)置的:

    binding.setUser(user);
    binding.setHandler(handler);
    

    這里清楚的看到調(diào)用 localObservableUser.getLastName 獲取 model 中的數(shù)據(jù)丙者,然后設(shè)置給 mboundView1 (Button)

    新建 OnClickListenerImpl 對象,處理 mboundView1 (Button)的點擊事件慢哈,而最終也還是會調(diào)用到 Handler.onClickButton 方法上

    public static class OnClickListenerImpl implements View.OnClickListener {
        private Handler value;
    
        public void onClick(View paramView) {
            this.value.onClickButton(paramView);
        }
        
        ......
    }
    

數(shù)據(jù)變化驅(qū)動視圖改變

查看下代碼蔓钟,set 函數(shù)中,需要添加一句 notifyPropertyChanged 方法卵贱。其實這里對 lastName 的監(jiān)聽者滥沫,就是 ViewDataBinding$WeakPropertyListener侣集,而內(nèi)部調(diào)用的還是 AcitivityMainBinding.handleFieldChange 方法,最終還是調(diào)用了 AcitivityMainBinding.requestRebind兰绣。這里就已經(jīng)和前面分析的過程一樣世分,也就是說最終視圖發(fā)生改變生效,走的還是消息隊列缀辩。

public class ObservableUser extends BaseObservable {
    
    ......
        
    public void setLastName(String lastName) {
        this.lastName = lastName;
        notifyPropertyChanged(com.netease.mvvmsample.BR.lastName);
    }
}
private static class WeakPropertyListener
        extends Observable.OnPropertyChangedCallback
        implements ObservableReference<Observable> {
    final WeakListener<Observable> mListener;

    @Override
    public void onPropertyChanged(Observable sender, int propertyId) {
        ViewDataBinding binder = mListener.getBinder();
        ......
        binder.handleFieldChange(mListener.mLocalFieldId, sender, propertyId);
    }
}

小結(jié)

由上面的源碼解析臭埋,已經(jīng)知道幾點

  1. 數(shù)據(jù)如何和 view 關(guān)聯(lián)起來的

  2. 事件處理如何和 view 關(guān)聯(lián)起來的

  3. 數(shù)據(jù)和事件處理的關(guān)聯(lián)發(fā)生是扔給消息隊列處理的

    • SDK_INT >= 16: mChoreographer.postFrameCallback(mFrameCallback);
    • SDK_INT < 16: mUIThreadHandler.post(mRebindRunnable);
  4. 當(dāng)數(shù)據(jù)改變,通知視圖改變時臀玄,走的是消息隊列瓢阴。因此一次數(shù)據(jù)改動,并界面可能不會立馬生效

  5. 數(shù)據(jù)和視圖的綁定健无,其實是單向的荣恐,即數(shù)據(jù)發(fā)生改變通知了視圖,而視圖發(fā)生并不能自動通知數(shù)據(jù)

  6. 雖然沒看到 Android Studio 是如何實現(xiàn)代碼生成累贤,但相關(guān)的工具大家可以看下 javapoet

總結(jié)

有了 DataBinding叠穆,后面就有人玩出了 MVVM 模式了。當(dāng)然啦臼膏,這里主要是抱著學(xué)習(xí)的態(tài)度在闡述 Android 里面的 DataBinding硼被,并不是在推崇 DataBindingMVVM。這些概念有人推崇有人貶低渗磅,引用別人的一句話嚷硫,希望大家對新知識都能做到:

我們需要保持的是一個擁抱變化的心,以及理性分析的態(tài)度夺溢。
在新技術(shù)的面前论巍,不盲從,也不守舊风响,一切的決策都應(yīng)該建立在認(rèn)真分析的基礎(chǔ)上嘉汰,這樣才能應(yīng)對技術(shù)的變化
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市状勤,隨后出現(xiàn)的幾起案子鞋怀,更是在濱河造成了極大的恐慌,老刑警劉巖持搜,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件密似,死亡現(xiàn)場離奇詭異,居然都是意外死亡葫盼,警方通過查閱死者的電腦和手機残腌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人抛猫,你說我怎么就攤上這事蟆盹。” “怎么了闺金?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵逾滥,是天一觀的道長。 經(jīng)常有香客問我败匹,道長寨昙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任掀亩,我火速辦了婚禮舔哪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘槽棍。我一直安慰自己尸红,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布刹泄。 她就那樣靜靜地躺著,像睡著了一般怎爵。 火紅的嫁衣襯著肌膚如雪特石。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天鳖链,我揣著相機與錄音姆蘸,去河邊找鬼。 笑死芙委,一個胖子當(dāng)著我的面吹牛逞敷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播灌侣,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼推捐,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了侧啼?” 一聲冷哼從身側(cè)響起牛柒,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎痊乾,沒想到半個月后皮壁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡哪审,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年蛾魄,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡滴须,死狀恐怖舌狗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情描馅,我是刑警寧澤把夸,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站铭污,受9級特大地震影響恋日,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜嘹狞,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一岂膳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧磅网,春花似錦谈截、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至燎潮,卻和暖如春喻鳄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背确封。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工除呵, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人爪喘。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓颜曾,卻偏偏與公主長得像,于是被迫代替她去往敵國和親秉剑。 傳聞我的和親對象是個殘疾皇子泛豪,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容