DataBinding學(xué)習(xí)筆記(一)源碼分析

DataBinding整體使用流程

整體流程圖.png
整體流程圖.png

開發(fā)階段

UserModel.java
public class UserModel {
    public String name;
    public String nickName;
    public int age;

    public UserModel(String name, String nickName, int age) {
        this.name = name;
        this.age = age;
        this.nickName = nickName;
    }

    public boolean isAge18() {
        return age >= 18;
    }
}
activity_main.xml

在xml中使用"@{}"標識符

<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable name="user" type="com.listen.test_databinding.UserModel"/>
        <variable name="testClick" type="android.view.View.OnClickListener"/>
        <import type="android.view.View"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context="com.listen.test_databinding.MainActivity">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text='@{"名字" + user.name}'/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text='@{user.nickName}'
            android:visibility="@{null == user.nickName ? View.VISIBLE : View.GONE}"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text='@{user.isAge18() ? "man" : "boy"}'/>

        <Button
            android:id="@+id/btn_test"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:onClick="@{testClick}" android:text="測試"/>
    </LinearLayout>
</layout>
MainActivity.java
public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding mBinding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        final UserModel user = new UserModel("listen", "ls", 18);
        mBinding.setUser(user);
        mBinding.setTestClick(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this, "testClick", Toast.LENGTH_SHORT).show();
            }
        });
    }
}

編譯階段

1.Databinding會自動解析識別xml中的"@{}"標識符谷婆,并在以下目錄生成2個xml文件

1.build/intermediates/data-binding-layout-out/activity_main.xml
2.build/intermediates/data-binding-info/debug/activity_main-layout.xml

activity_main.xml

帶“@{}”的xml文件是android系統(tǒng)無法識別的慨蛙,為了向后兼容辽聊,需要在編譯期統(tǒng)一轉(zhuǎn)換成系統(tǒng)能識別的標準xml布局,而原先在布局中添加的"@{}"期贫,"@{三目運算符}"等信息跟匆,則會存儲在activity_main-layout.xml中。

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"    
    android:orientation="vertical"
    android:tag="layout/activity_main_0"
    tools:context="com.listen.test_databinding.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:tag="binding_1"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:tag="binding_2"
    />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:tag="binding_3"/>

    <Button
        android:id="@+id/btn_test"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:tag="binding_4" android:text="測試"/>
</LinearLayout>
activity_main-layout.xml(xml描述文件)

1.任何view只要用到了"@{}"標識通砍,就會在activity_main-layout.xml中生成target描述玛臂,并根據(jù)該view在parent中的位置生成"binding_[index]"標識,并設(shè)置在tag中封孙。
2.如果一個view即沒有設(shè)置"android:id"迹冤,也沒有使用"@{}"標識,則不會在activity_main-layout.xml中生成這個view的target描述虎忌。
3.LinearLayout比較特殊泡徙,并沒有設(shè)置"android:id",也沒有使用"@{}"膜蠢,但還是會生成一個默認的tag="layout/activity_main_0"堪藐,表示它是根布局,在ViewDataBinding.java實例化時狡蝶,需要判斷根布局的tag庶橱,后面源碼會分析到。

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout absoluteFilePath="/Users/lisong/Documents/AndroidStudioWorkSpace/Test_Databinding/app/src/main/res/layout/activity_main.xml" directory="layout"
        isMerge="false"
        layout="activity_main" modulePackage="com.listen.test_databinding">
    <Variables name="user" declared="true" type="com.listen.test_databinding.UserModel">
        ...
    </Variables>
    <Variables name="testClick" declared="true" type="android.view.View.OnClickListener">
        ...
    </Variables>
    <Imports name="View" type="android.view.View">
        ...
    </Imports>
    <Targets>
        <Target tag="layout/activity_main_0" view="LinearLayout">
            <Expressions/>
            ...
        </Target>
        <Target tag="binding_1" view="TextView">
            <Expressions>
                <Expression attribute="android:text" text=""名字" + user.name">
                    ...
                </Expression>
            </Expressions>
        </Target>
        <Target tag="binding_2" view="TextView">
            <Expressions>
                <Expression attribute="android:text" text="user.nickName">
                    ...
                </Expression>
                <Expression attribute="android:visibility"
                            text="null == user.nickName ? View.VISIBLE : View.GONE">
                    ...
                </Expression>
            </Expressions>
        </Target>
        <Target tag="binding_3" view="TextView">
            <Expressions>
                <Expression attribute="android:text"
                            text="user.isAge18() ? "man" : "boy"">
                    ...
                </Expression>
            </Expressions>
        </Target>
        <Target id="@+id/btn_test" tag="binding_4" view="Button">
            <Expressions>
                <Expression attribute="android:onClick" text="testClick">
                    ...
                </Expression>
            </Expressions>
        </Target>
    </Targets>
</Layout>

2.生成ActivityMainBinding.java和BR.java

DataBinding根據(jù)解析后的activity_main-layout.xml贪惹,和layout下的activity_main.xml文件苏章,生成build/intermediates/classes/debug/[項目路徑]/databinding/
ActivityMainBinding.java和BR.java

ActivityMainBinding目錄.png
ActivityMainBinding目錄.png
ActivityMainBinding主要具備以下功能

1.作為view和model的連接器,持有需要展示的數(shù)據(jù)和views的成員變量
2.將數(shù)據(jù)映射到view(就是setText奏瞬,setOnClick等)
3.在UI線程更新數(shù)據(jù)

BR.java就是一個常量類

可以通過binding.setVariable(BRuser, new User())進行數(shù)據(jù)更新

public class BR {
    public static final int _all = 0;
    public static final int testClick = 1;
    public static final int user = 2;

    public BR() {
    }
}
public boolean setVariable(int variableId, Object variable) {
    switch(variableId) {
        case BR.testClick :
            setTestClick((android.view.View.OnClickListener) variable);
            return true;
        case BR.user :
            setUser((com.listen.test_databinding.UserModel) variable);
            return true;
    }
    return false;
}

運行階段

Databinding框架最主要做的事枫绅,就是以上2步,接下來就是在代碼中調(diào)用生成的ViewDataBinding硼端,并進行數(shù)據(jù)綁定操作并淋。

DataBindingUtil是一切的入口
ActivityMainBinding mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
public static <T extends ViewDataBinding> T setContentView(Activity activity, int layoutId, DataBindingComponent bindingComponent) {
    activity.setContentView(layoutId);// 最終調(diào)用的還是activity.setContentView(),不過這里的layoutId是已經(jīng)去掉"@{}"的標準xml布局
    View decorView = activity.getWindow().getDecorView();
    ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);//獲取根頂級容器view
    return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}
取出布局的rootView珍昨,調(diào)用ActivityMainBinding.bind()
private static <T extends ViewDataBinding> T bindToAddedViews(android.databinding.DataBindingComponent component, ViewGroup parent, int startChildren, int layoutId) {
    final int endChildren = parent.getChildCount();
    final int childrenAdded = endChildren - startChildren;
    if (childrenAdded == 1) {
        // 從頂級容器view中獲取當前布局的rootView县耽,調(diào)用bind方法
        final View childView = parent.getChildAt(endChildren - 1);
        return bind(component, childView, layoutId);
    } else {
        ...
    }
}

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

/**  DataBinderMapper.java */
public android.databinding.ViewDataBinding getDataBinder(android.databinding.DataBindingComponent bindingComponent, android.view.View view, int layoutId) {
    switch(layoutId) {
        case com.listen.test_databinding.R.layout.activity_main:
            // 將rootView傳遞給ActivityMainBinding.bind()
            return com.listen.test_databinding.databinding.ActivityMainBinding.bind(view, bindingComponent);
    }
    return null;
}

此處做了rootView的判斷,如果傳遞過來的不是當前ViewDataBinding綁定的布局镣典,則拋異常兔毙。所以即使rootView沒有設(shè)置id,及"@{}"兄春,在info-layout.xml中也會生成相應(yīng)的target描述澎剥。

public static ActivityMainBinding bind(View view, DataBindingComponent bindingComponent) {
    if(!"layout/activity_main_0".equals(view.getTag())) {
        throw new RuntimeException("view tag isn\'t correct on view:" + view.getTag());
    } else {
        return new ActivityMainBinding(bindingComponent, view); // ActivityMainBinding在此處初始化
    }
}

這里需要特別注意的是在編譯期自動生成的activity_main.xml文件中自動添加了tag="binding_1","binding_2"等赶舆,其實在初始化完這些view后哑姚,都已經(jīng)清空祭饭,是不影響我們在代碼中設(shè)置tag的;不過rootView并沒有清除tag(就是xml布局最外層的layout)叙量,如果>=14以上版本倡蝙,在代碼里設(shè)置setTag(R.id.databinding,"anything"),或宛乃,<14版本悠咱,在代碼里設(shè)置setTag("anything"),則會報錯征炼,so析既,這個tag是由DataBinding占著的,使用上得小心谆奥。

public ActivityMainBinding(android.databinding.DataBindingComponent bindingComponent, View root) {
    super(bindingComponent, root, 0);
    // 遍歷布局眼坏,找到所有views,并存儲在bindings[]中酸些,5表示布局一共有5個view宰译,sIncludes存儲被include進     // 來的布局,sViewsWithIds存儲設(shè)置了"android:id"魄懂,但是沒有用到"@{}"的view
    Object[] bindings = mapBindings(bindingComponent, root, 5, sIncludes, sViewsWithIds);

    // 將bindings[]中的view取出沿侈,賦值給當前各個view的成員變量,并清除tag市栗,避免沖突
    this.btnTest = (Button)bindings[4];
    this.btnTest.setTag((Object)null);
    this.mboundView0 = (LinearLayout)bindings[0];
    this.mboundView0.setTag((Object)null);
    this.mboundView1 = (TextView)bindings[1];
    this.mboundView1.setTag((Object)null);
    this.mboundView2 = (TextView)bindings[2];
    this.mboundView2.setTag((Object)null);
    this.mboundView3 = (TextView)bindings[3];
    this.mboundView3.setTag((Object)null);
    this.setRootTag(root);
    /**
    ViewDatabBinding.java
    protected void setRootTag(View view) {
        //private static final boolean USE_TAG_ID = DataBinderMapper.TARGET_MIN_SDK >= 14;
        if (USE_TAG_ID) {
        view.setTag(R.id.dataBinding, this);
        } else {
        view.setTag(this);
        }
    }
    */

    
    // 請求刷新缀拭,實現(xiàn)數(shù)據(jù)與view的綁定
    this.invalidateAll();
}

mapBindings(),其實就是遞歸遍歷view樹的過程填帽,不過不是byId蛛淋,而是byTag,尋找以"binding_"開頭的view篡腌,并取出"binding_[索引]"中的索引褐荷,賦值給binding[]數(shù)組。所有的view只在一次遍歷中獲得嘹悼,而如果是用findViewById的方式叛甫,每次調(diào)用都需要遍歷一次view樹[性能對比]。需要特別注意的是binding數(shù)組的元素不一定都是view或viewGroup杨伙,如果有include布局的時候binding數(shù)組存儲的可能是include布局的viewDataBinding對象合溺。

private static void mapBindings(DataBindingComponent bindingComponent, View view, Object[] bindings, ViewDataBinding.IncludedLayouts includes, SparseIntArray viewsWithIds, boolean isRoot) {
    ViewDataBinding existingBinding = getBinding(view);
    if(existingBinding == null) {
        Object objTag = view.getTag();
        String tag = objTag instanceof String?(String)objTag:null;
        
        if(isRoot && tag != null && tag.startsWith("layout")) {
            // 如果是rootView,則從"layout/activity_main_0"中取出索引"0"缀台,設(shè)置到bindings[0]中
            viewGroup = tag.lastIndexOf(95);
            count = parseTagInt(tag, viewGroup + 1);
            if(bindings[count] == null) {
                bindings[count] = view;
            }
            ...
        } else if(tag != null && tag.startsWith("binding_")) {
            // 同樣判斷tag,取出"binding_1"哮奇,"bingding_2"中的索引膛腐,賦值到bindings[]中
            viewGroup = parseTagInt(tag, BINDING_NUMBER_START);
            if(bindings[viewGroup] == null) {
                bindings[viewGroup] = view;
            }
            ...
        }
        
        // isBound=false睛约,說明當前的view既不是根布局,也沒有用到"@{}"(如果有用到就會生成"binding_"
        // 的tag)哲身;則通過id獲取該view辩涝,并設(shè)置到bingding[]
        // 如果存在設(shè)置了id,但是沒有“@{}”的view會被添加到sViewsWithIds中勘天,如果
        // "binding_[index]"的index最大為3怔揩,則view的起始index設(shè)置為4。
        // static {
        //    sIncludes = null;
        //    sViewsWithIds = new android.util.SparseIntArray();
        //    sViewsWithIds.put(R.id.btn_test, 4);
        //}
        if(!isBound) {
            viewGroup = view.getId();
            if(viewGroup > 0 && viewsWithIds != null && (count = viewsWithIds.get(viewGroup, -1)) >= 0 && bindings[count] == null) {
                bindings[count] = view;
            }
        }

        if(view instanceof ViewGroup) {
            // 如果是view是個viewGroup脯丝,則遍歷子view
            ViewGroup var25 = (ViewGroup)view;
            count = var25.getChildCount();
            ...

            for(int i = 0; i < count; ++i) {
                View child = var25.getChildAt(i);
                boolean isInclude = false;
                if(indexInIncludes >= 0 && child.getTag() instanceof String) {
                    String childTag = (String)child.getTag();
                    if(childTag.endsWith("_0") && childTag.startsWith("layout") && childTag.indexOf(47) > 0) {
                        // 如果當前view也是一個rootView商膊,則判斷tag中是否有include標識信息
                        // 如果包含include標簽,生成的info-layout文件應(yīng)該是以下樣式:
                        // <Target include="include_main" tag="layout/activity_main_0">
                        // </Target>
                        int includeIndex = findIncludeIndex(childTag, minInclude, includes, indexInIncludes);
                        if(includeIndex >= 0) {
                            isInclude = true;
                            ...
                            // 如果包含include信息宠进,則重新調(diào)用DataBindingUtil.bind()生成ViewDataBinding晕拆,重復(fù)當前流程,
                            // 不過當前的bindings[index]就不是一個view材蹬,而是一個viewDataBinding
                            bindings[index] = DataBindingUtil.bind(bindingComponent, child, layoutId);
                            ...
                        }
                    }
                }

                if(!isInclude) {
                    // 如果只是一個viewGroup实幕,不是include進來的布局,則重新調(diào)用mapBindings堤器,只是isRoot=false昆庇,則會上面進入"binding_"的判斷邏輯
                    mapBindings(bindingComponent, child, bindings, includes, viewsWithIds, false);
                }
            }
        }

    }
}

view遍歷流程圖
view遍歷流程.png
view遍歷流程.png

View都找到了,現(xiàn)在該是時候設(shè)置listener闸溃,data的時候了整吆。這時候會通過invalidateAll()請求數(shù)據(jù)更新,層層調(diào)用后圈暗,還是回到了ActivityMainBinding的executeBindings()掂为,在這個方法里將更新后的model數(shù)據(jù),onclick等重新設(shè)置到Textview员串,Button上勇哗,完成了model->view的單向綁定。


// 子類:xxxViewDataBinding extends ViewDataBinding
public void invalidateAll() {
    synchronized(this) {
        this.mDirtyFlags = 4L;
    }

    this.requestRebind();
}

// 父類:ViewDataBinding.java
// 通過handler.post()執(zhí)行mRebindRunnable
protected void requestRebind() {
    ...
    mUIThreadHandler.post(mRebindRunnable);
}

// mRebindRunnable調(diào)用了executePendingBindings()
private final Runnable mRebindRunnable = new Runnable() {
    @Override
    public void run() {
        ...
        executePendingBindings();
    }
};

// executePendingBindings調(diào)用了executeBindings()
public void executePendingBindings() {
    ...
    executeBindings();
    ...
}

// 子類:xxxViewDataBinding extends ViewDataBinding
protected void executeBindings() {
    ...
    // 當調(diào)用ViewDataBinding.setUser(new User())時寸齐,就是給成員變量mUser賦值欲诺,在這里獲取this.mUser
    UserModel user = this.mUser;

    if((dirtyFlags & 6L) != 0L) {
        // 獲取并構(gòu)建數(shù)據(jù),所以model中的字段要么為public渺鹦,要么提供一個getter方法扰法,不然這里無法獲取
        // 可以看到,在xml中的@{}表達式毅厚,此時已經(jīng)解析成對應(yīng)的方法isAge18塞颁,user.name等
        if(user != null) {
            userIsAge18User = user.isAge18();
            nameUser = user.name;
            nickNameUser = user.nickName;
        }

        ...
        // 獲取并構(gòu)建數(shù)據(jù)
        userIsAge18UserStrin = userIsAge18User?"man":"boy";
        stringNameUser = "名字" + nameUser;
        ObjectnullNickNameUs1 = null == nickNameUser;
        ...

        objectnullNickNameUs = ObjectnullNickNameUs1?0:8;
    }

    // 設(shè)置listener
    if((dirtyFlags & 5L) != 0L) {
        this.btnTest.setOnClickListener(testClick);
    }

    // 通過TextViewBindingAdapter將數(shù)據(jù)設(shè)置到TextView上
    if((dirtyFlags & 6L) != 0L) {
        TextViewBindingAdapter.setText(this.mboundView1, stringNameUser);
        TextViewBindingAdapter.setText(this.mboundView2, nickNameUser);
        this.mboundView2.setVisibility(objectnullNickNameUs);
        TextViewBindingAdapter.setText(this.mboundView3, userIsAge18UserStrin);
    }

}
以上便是當我們通過DataBindingUtil.setContentView()對Databinding進行初始化,以及當我們獲取到最新數(shù)據(jù),通過Binding.setModel進行數(shù)據(jù)更新時的操作流程祠锣。
數(shù)據(jù)綁定流程.png
數(shù)據(jù)綁定流程.png

參考

英文官方文檔
https://developer.android.com/topic/libraries/data-binding/index.html

Google開發(fā)團隊介紹DataDinding使用
https://realm.io/cn/news/data-binding-android-boyar-mount/?utm_source=tuicool&utm_medium=referral

QQ音樂團隊分享酷窥,比較貼近源碼的介紹
http://gold.xitu.io/entry/57e48e7ba22b9d006139c60b

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市伴网,隨后出現(xiàn)的幾起案子蓬推,更是在濱河造成了極大的恐慌,老刑警劉巖澡腾,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沸伏,死亡現(xiàn)場離奇詭異,居然都是意外死亡动分,警方通過查閱死者的電腦和手機毅糟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來刺啦,“玉大人留特,你說我怎么就攤上這事÷耆常” “怎么了蜕青?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長糊渊。 經(jīng)常有香客問我右核,道長,這世上最難降的妖魔是什么渺绒? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任贺喝,我火速辦了婚禮,結(jié)果婚禮上宗兼,老公的妹妹穿的比我還像新娘躏鱼。我一直安慰自己,他們只是感情好殷绍,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布染苛。 她就那樣靜靜地躺著,像睡著了一般主到。 火紅的嫁衣襯著肌膚如雪茶行。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天登钥,我揣著相機與錄音畔师,去河邊找鬼。 笑死牧牢,一個胖子當著我的面吹牛看锉,可吹牛的內(nèi)容都是我干的姿锭。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼伯铣,長吁一口氣:“原來是場噩夢啊……” “哼艾凯!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起懂傀,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蜡感,沒想到半個月后蹬蚁,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡郑兴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年犀斋,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片情连。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡叽粹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出却舀,到底是詐尸還是另有隱情虫几,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布挽拔,位于F島的核電站辆脸,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏螃诅。R本人自食惡果不足惜啡氢,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望术裸。 院中可真熱鬧倘是,春花似錦、人聲如沸袭艺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽匹表。三九已至门坷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間袍镀,已是汗流浹背默蚌。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留苇羡,地道東北人绸吸。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像绞铃,于是被迫代替她去往敵國和親青自。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

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