設(shè)計模式之四:訪問者模式

目錄介紹

1.訪問者模式介紹
2.訪問者模式定義
3.訪問者模式UML圖
4.訪問者模式簡單案例
5.訪問者模式之Android源碼分析
5.1 注解簡單介紹
5.2 注解與訪問者模式關(guān)系
5.3 注解與性能的關(guān)系
6.訪問者模式之實踐
6.1 介紹
6.2 編譯期注解之ButterKnife
6.3 編譯期注解之Dagger2
6.4 自己寫個簡單支持View的ID注入的工具
6.4.0 基本邏輯思路
7.注解之ButterKnife源碼分析
7.0 簡單工作流程
7.1 首先看看Bind注解
7.2 看看支持的類型
7.3 編譯時注解之ButterKnifeProcessor
7.4 接下來看看findAndParseTargets(env)代碼
7.5 然后看看parseBind方法
7.6 看看parseBind中的parseBindMany方法
7.7 回到bindingClass.brewJava()方法中
7.8 接下來看注解過程,bind方法
7.9 看注解過程中bind中的findViewBinderForClass 方法

好消息

  • 博客筆記大匯總【16年3月到至今】翎蹈,包括Java基礎(chǔ)及深入知識點论笔,Android技術(shù)博客递瑰,Python學(xué)習(xí)筆記等等,還包括平時開發(fā)中遇到的bug匯總奏寨,當(dāng)然也在工作之余收集了大量的面試題,長期更新維護(hù)并且修正鹰服,持續(xù)完善……開源的文件是markdown格式的病瞳!同時也開源了生活博客,從12年起悲酷,積累共計47篇[近20萬字]套菜,轉(zhuǎn)載請注明出處,謝謝设易!
  • 鏈接地址:https://github.com/yangchong211/YCBlogs
  • 如果覺得好逗柴,可以star一下,謝謝顿肺!當(dāng)然也歡迎提出建議戏溺,萬事起于忽微,量變引起質(zhì)變屠尊!

1.訪問者模式介紹

訪問者模式,是一種將數(shù)據(jù)操作和數(shù)據(jù)結(jié)構(gòu)分離的設(shè)計模式.
大多數(shù)情況下,你并不需要使用訪問者模式,但是當(dāng)你一旦需要使用它時,那你就是真地需要它了.

2.訪問者模式定義

定義:封裝一些作用于某種數(shù)據(jù)結(jié)構(gòu)中的各元素的操作,它可以在不改變這個數(shù)據(jù)結(jié)構(gòu)的前提下定義作用于這些元素的新的操作.
優(yōu)點:
1.各角色職責(zé)分離,符合單一職責(zé)的原則
2.具有優(yōu)秀的擴(kuò)展性
3.使得數(shù)據(jù)結(jié)構(gòu)和作用于結(jié)構(gòu)上的操作解耦,使得操作集合可以獨立變化
4.靈活性
缺點:
1.具體元素對訪問者公布細(xì)節(jié),違反了迪米特原則
2.具體元素變更時導(dǎo)致修改成本變大
3.違反了依賴導(dǎo)致原則,為了達(dá)到”區(qū)別對待”而依賴了具體類,沒有依賴抽象.
使用場景:
1.對象結(jié)構(gòu)比較穩(wěn)定,但經(jīng)常需要在此對象結(jié)構(gòu)上定義新的操作.
2.需要對一個對象結(jié)構(gòu)中的對象進(jìn)行很多不同的并且不相關(guān)的操作,它可以在不改變這個數(shù)據(jù)結(jié)構(gòu)的前提下定義作用于這些元素的新的操作

3.訪問者模式UML圖

Visitor:接口或者抽象類,它定義了對每一個元素的訪問行為,它的參數(shù)就是可以訪問的元素
ConcreteVisitor:具體訪問者,對每一個元素類訪問的具體行為
Element:元素接口或者抽象類,定義了一個accept方法,每一個月uansu都要可以被訪問者訪問
ElementA:具體元素類,它提供了接受訪問的具體方法實現(xiàn)
ObjectStructure:對象結(jié)構(gòu),內(nèi)部管理元素集合,并且可以迭代這些元素供訪問者訪問


Image.png

4.訪問者模式簡單案例

5.訪問者模式之Android源碼分析

5.1 注解簡單介紹
注解分類:運行時注解旷祸,編譯期注解
通俗的分析如下所示:
1、標(biāo)記一些信息知染,這么說可能太抽象肋僧,那么我說,你見過@Override控淡、@SuppressWarnings等嫌吠,這類注解就是用于標(biāo)識,可以用作一些檢驗
2掺炭、運行時動態(tài)處理辫诅,這個大家見得應(yīng)該最多,在運行時拿到類的Class對象涧狮,然后遍歷其方法炕矮、變量,判斷有無注解聲明者冤,然后做一些事情肤视。類似上述三篇博文中的做法。
3涉枫、編譯時動態(tài)處理邢滑,,一般這類注解會在編譯的時候愿汰,根據(jù)注解標(biāo)識困后,動態(tài)生成一些類或者生成一些xml都可以乐纸,在運行時期,這類注解是沒有的會依靠動態(tài)生成的類做一些操作摇予,因為沒有反射汽绢,效率和直接調(diào)用方法沒什么區(qū)別
運行時注解
編譯期注解,編譯注解的核心原理依賴APT侧戴,比如ButterKnife宁昭,Dagger,Retrofit等都是基于APT的
編譯時救鲤,Annotation解析的原理:
原理是在某些代碼元素上(如類型久窟、函數(shù)、字段等)添加注解本缠,在編譯時編譯器會檢查AbstractProcessor的子類斥扛,并且調(diào)用該類型的process函數(shù),然后將添加了注解的所有元素都傳遞到process函數(shù)中丹锹,使得開發(fā)人員可以在編譯器進(jìn)行相應(yīng)的處理稀颁,例如,根據(jù)注解生成新的Java類楣黍,這也就是EventBus匾灶,Retrofit,Dragger等開源庫的基本原理租漂。

5.2 注解與訪問者模式關(guān)系

5.3 注解與性能的關(guān)系
只要解析出來是注解和反射阶女,必然的一個問題就是:這樣會不會影響性能呀?會有性能的損耗

6.訪問者模式之實踐

6.1 介紹
實際開發(fā)中該模式用的少哩治,具有實戰(zhàn)意義的是編譯期注解秃踩,就是APT示例。
6.2 編譯期注解之ButterKnife
ButterKnife簡單介紹
編譯期注解的庫【基于編譯時注解业筏,然后通過APT生成輔助類憔杨,在運行時通過bind函數(shù)調(diào)用那些生成的輔助類來完成功能】
針對View,資源ID等進(jìn)行注解
6.3 編譯期注解之Dagger2
Dagger2簡單介紹
編譯期注解的庫
針對對象進(jìn)行注解
6.4 自己寫個簡單案例
6.4.0 基本邏輯思路
通過ViewInject注解標(biāo)識一些View成員變量;
通過ViewInjecyProcessor捕獲添加了ViewInject注解的元素蒜胖,并且按照宿主類進(jìn)行分類消别;
為每個含有ViewInject注解的宿主類生成一個InjectAdapter輔助類,并且在它的inject函數(shù)中生成初始化View的代碼台谢;
在SimpleDagger的inject函數(shù)中構(gòu)建生成的輔助類寻狂,此時內(nèi)部會它這個InjectAdapter輔助類的inject函數(shù),這個函數(shù)中又會初始化宿主類中的View成員變量朋沮,至此荆虱,View就已經(jīng)被初始化了。

7.注解之ButterKnife源碼分析

7.0 簡單工作流程
開始它會掃描Java代碼中所有的ButterKnife注解@Bind、@OnClick怀读、@OnItemClicked等。
當(dāng)它發(fā)現(xiàn)一個類中含有任何一個注解時骑脱,ButterKnifeProcessor會幫你生成一個Java類菜枷,名字類似$$ViewBinder,這個新生成的類實現(xiàn)了ViewBinder接口叁丧。
這個ViewBinder類中包含了所有對應(yīng)的代碼啤誊,比如@Bind注解對應(yīng)findViewById(), @OnClick對應(yīng)了view.setOnClickListener()等等。
最后當(dāng)Activity啟動ButterKnife.bind(this)執(zhí)行時拥娄,ButterKnife會去加載對應(yīng)的ViewBinder類調(diào)用它們的bind()方法蚊锹。
7.1 首先看看Bind注解
@Retention保留時間,可選值SOURCE(源碼時)稚瘾,CLASS(編譯時)牡昆,RUNTIME(運行時),默認(rèn)為CLASS摊欠,值為SOURCE大都為MarkAnnotation丢烘,這類Annotation大都用來校驗,比如Override,Deprecated,SuppressWarnings
@Target可以用來修飾哪些程序元素些椒,如TYPE,METHOD,CONSTRUCTOR,FIELD,PARAMETER等播瞳,未標(biāo)注則表示可修飾所有

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.CLASS;
//說明就是編譯時動態(tài)處理的    這個值是一個枚舉:有三個:SOURCE、RUNTIME免糕、CLASS
@Retention(CLASS)
//標(biāo)明這個注解能標(biāo)識哪些東西赢乓,比如類、變量石窑、方法牌芋、甚至是注解本身(元注解)等
@Target(FIELD)
public @interface Bind {
  /** View ID to which the field will be bound. */
  int[] value();
}

7.2 看看支持的類型

Image.png

Image.png

7.3 編譯時注解之ButterKnifeProcessor
Annotation processing是在編譯階段執(zhí)行的,它的原理就是讀入Java源代碼尼斧,解析注解姜贡,然后生成新的Java代碼。新生成的Java代碼最后被編譯成Java字節(jié)碼棺棵,注解解析器(Annotation Processor)不能改變讀入的Java 類楼咳,比如不能加入或刪除Java方法。
這個是編譯過程中很重要的一部分烛恤,對于butterKnife所有的注解母怜,都是在編譯時進(jìn)行注解。其中ButterKnifeProcessor這個類是繼承AbstractProcessor缚柏,并重寫 process方法
來看看Process方法中的源代碼

@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //獲取所有注釋的元素并且解析
    Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);
    //循環(huán)遍歷苹熏,獲取到注解中的鍵值
    for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
        TypeElement typeElement = entry.getKey();
        BindingClass bindingClass = entry.getValue();
        try {
            //寫進(jìn)文件,生成輔助類
            JavaFileObject jfo = filer.createSourceFile(bindingClass.getFqcn(), typeElement);
            Writer writer = jfo.openWriter();
            writer.write(bindingClass.brewJava());
            writer.flush();
            writer.close();
        } catch (IOException e) {
            error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
                    e.getMessage());
        }
    }
    return true;
}

7.4 接下來看看findAndParseTargets(env)代碼
遍歷,然后比較并且解析

private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
    //創(chuàng)建一個map集合轨域,用來存儲所有注釋的元素的鍵值對
    Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<TypeElement, BindingClass>();
    //創(chuàng)建刪除刪除目標(biāo)名稱的set集合
    Set<String> erasedTargetNames = new LinkedHashSet<String>();
    //遍歷
    // Process each @Bind element.
    for (Element element : env.getElementsAnnotatedWith(Bind.class)) {
        try {
            //解析
            parseBind(element, targetClassMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, Bind.class, e);
        }
    }
}

7.5 然后看看parseBind方法

Image.png

重點了解一下幾個方法

isInaccessibleViaGeneratedCode
驗證方法修飾符不能為private和static
驗證包含類型不能為非Class
驗證包含類的可見性并不是private

isBindingInWrongPackage
它判斷了這個類的包名袱耽,包名不能以android.開頭
它判斷了這個類的包名,包名不能以java.開頭

private boolean isInaccessibleViaGeneratedCode(Class<? extends Annotation> annotationClass,
                                              String targetThing, Element element) {
    boolean hasError = false;
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    Set<Modifier> modifiers = element.getModifiers();
    //驗證方法修飾符不能為private和static干发。
    if (modifiers.contains(PRIVATE) || modifiers.contains(STATIC)) {
        error(element, "@%s %s must not be private or static. (%s.%s)",
                annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
                element.getSimpleName());
        hasError = true;
    }

    //驗證包含類型不能為非Class
    if (enclosingElement.getKind() != CLASS) {
        error(enclosingElement, "@%s %s may only be contained in classes. (%s.%s)",
                annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
                element.getSimpleName());
        hasError = true;
    }

    //驗證包含類的可見性并不是private朱巨。
    if (enclosingElement.getModifiers().contains(PRIVATE)) {
        error(enclosingElement, "@%s %s may not be contained in private classes. (%s.%s)",
                annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
                element.getSimpleName());
        hasError = true;
    }
    return hasError;
}

private boolean isBindingInWrongPackage(Class<? extends Annotation> annotationClass,
                                        Element element) {
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    String qualifiedName = enclosingElement.getQualifiedName().toString();

    //它判斷了這個類的包名,包名不能以android.開頭
    if (qualifiedName.startsWith(ANDROID_PREFIX)) {
        error(element, "@%s-annotated class incorrectly in Android framework package. (%s)",
                annotationClass.getSimpleName(), qualifiedName);
        return true;
    }
    //它判斷了這個類的包名枉长,包名不能以java.開頭
    if (qualifiedName.startsWith(JAVA_PREFIX)) {
        error(element, "@%s-annotated class incorrectly in Java framework package. (%s)",
                annotationClass.getSimpleName(), qualifiedName);
        return true;
    }
    return false;
}

7.6 看看parseBind中的parseBindMany方法
如下所示

private void parseBindMany(Element element, Map<TypeElement, BindingClass> targetClassMap,
                           Set<String> erasedTargetNames) {
    //是否有錯誤冀续,默認(rèn)是false
    boolean hasError = false;
    //獲取被包含的元素
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    //驗證的類型是一個列表List或數(shù)組array。
    TypeMirror elementType = element.asType();
    //刪除類型
    String erasedType = doubleErasure(elementType);
    TypeMirror viewType = null;
    FieldCollectionViewBinding.Kind kind = null;
    //比較獲取的類型是否是ARRAY類型
    if (elementType.getKind() == TypeKind.ARRAY) {
        ArrayType arrayType = (ArrayType) elementType;
        viewType = arrayType.getComponentType();
        kind = FieldCollectionViewBinding.Kind.ARRAY;
    } else if (LIST_TYPE.equals(erasedType)) {
        DeclaredType declaredType = (DeclaredType) elementType;
        List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
        if (typeArguments.size() != 1) {
            error(element, "@%s List must have a generic component. (%s.%s)",
                    Bind.class.getSimpleName(), enclosingElement.getQualifiedName(),
                    element.getSimpleName());
            hasError = true;
        } else {
            viewType = typeArguments.get(0);
        }
        kind = FieldCollectionViewBinding.Kind.LIST;
    } else {
        //如果都不是list或者array類型必峰,那么直接拋出異常
        throw new AssertionError();
    }
    if (viewType != null && viewType.getKind() == TypeKind.TYPEVAR) {
        TypeVariable typeVariable = (TypeVariable) viewType;
        viewType = typeVariable.getUpperBound();
    }

    //驗證目標(biāo)類型是否從視圖擴(kuò)展.
    if (viewType != null && !isSubtypeOfType(viewType, VIEW_TYPE) && !isInterface(viewType)) {
        error(element, "@%s List or array type must extend from View or be an interface. (%s.%s)",
                Bind.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName());
        hasError = true;
    }

    //如果有錯誤洪唐,執(zhí)行結(jié)束
    if (hasError) {
        return;
    }

    // 收集實地信息.
    String name = element.getSimpleName().toString();
    int[] ids = element.getAnnotation(Bind.class).value();
    if (ids.length == 0) {
        error(element, "@%s must specify at least one ID. (%s.%s)", Bind.class.getSimpleName(),
                enclosingElement.getQualifiedName(), element.getSimpleName());
        return;
    }

    Integer duplicateId = findDuplicate(ids);
    if (duplicateId != null) {
        error(element, "@%s annotation contains duplicate ID %d. (%s.%s)", Bind.class.getSimpleName(),
                duplicateId, enclosingElement.getQualifiedName(), element.getSimpleName());
    }
    assert viewType != null; // Always false as hasError would have been true.
    String type = viewType.toString();
    boolean required = isRequiredBinding(element);
    BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
    FieldCollectionViewBinding binding = new FieldCollectionViewBinding(name, type, kind, required);
    //將生成的BindingClass存入數(shù)組中
    bindingClass.addFieldCollection(ids, binding);
    erasedTargetNames.add(enclosingElement.toString());
}

7.7 回到bindingClass.brewJava()方法中
繼續(xù)回到process代碼中,思考write是做什么用的呢吼蚁?
這個函數(shù)通過將我們綁定類的信息寫入到文件外還負(fù)責(zé)創(chuàng)建綁定和創(chuàng)建解除綁定凭需。在將綁定函數(shù)寫入到文件后,整個編譯器的注解方法就結(jié)束桂敛。

Image.png

Image.png

7.8 接下來看注解過程功炮,bind方法
如圖所示

Image.png

我們可以看到最終都是調(diào)用這個方法

static void bind(Object target, Object source, ButterKnife.Finder finder) {
    //獲取class
    Class<?> targetClass = target.getClass();
    try {
        if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName());
        //通過findViewBinderForClass生成每個類
        ButterKnife.ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
        if (viewBinder != null) {
            //如果viewBinder不為空,則進(jìn)行綁定
            viewBinder.bind(finder, target, source);
        }
    } catch (Exception e) {
        throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
    }
}

7.9 看注解過程中bind中的findViewBinderForClass 方法

private static ButterKnife.ViewBinder<Object> findViewBinderForClass(Class<?> cls)
        throws IllegalAccessException, InstantiationException {
    //從內(nèi)存中查找
    ButterKnife.ViewBinder<Object> viewBinder = BINDERS.get(cls);
    if (viewBinder != null) {
        if (debug) Log.d(TAG, "HIT: Cached in view binder map.");
        return viewBinder;
    }
    String clsName = cls.getName();
    //檢查是否為framework class
    if (clsName.startsWith(ANDROID_PREFIX) || clsName.startsWith(JAVA_PREFIX)) {
        if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
        return NOP_VIEW_BINDER;
    }
    try {
        //實例化“MainActivity$$ViewBinder”這樣的類
        Class<?> viewBindingClass = Class.forName(clsName + ButterKnifeProcessor.SUFFIX);
        //noinspection unchecked
        viewBinder = (ButterKnife.ViewBinder<Object>) viewBindingClass.newInstance();
        if (debug) Log.d(TAG, "HIT: Loaded view binder class.");
    } catch (ClassNotFoundException e) {
        if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
        //異常术唬,則去父類查找
        viewBinder = findViewBinderForClass(cls.getSuperclass());
    }
    //放入內(nèi)存并返回
    BINDERS.put(cls, viewBinder);
    return viewBinder;
}

其他

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末薪伏,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子粗仓,更是在濱河造成了極大的恐慌嫁怀,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件借浊,死亡現(xiàn)場離奇詭異塘淑,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)蚂斤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進(jìn)店門存捺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人曙蒸,你說我怎么就攤上這事捌治。” “怎么了纽窟?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵肖油,是天一觀的道長。 經(jīng)常有香客問我臂港,道長森枪,這世上最難降的妖魔是什么视搏? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮县袱,結(jié)果婚禮上浑娜,老公的妹妹穿的比我還像新娘。我一直安慰自己式散,他們只是感情好棚愤,可當(dāng)我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著杂数,像睡著了一般。 火紅的嫁衣襯著肌膚如雪瘸洛。 梳的紋絲不亂的頭發(fā)上揍移,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天,我揣著相機(jī)與錄音反肋,去河邊找鬼那伐。 笑死,一個胖子當(dāng)著我的面吹牛石蔗,可吹牛的內(nèi)容都是我干的罕邀。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼养距,長吁一口氣:“原來是場噩夢啊……” “哼诉探!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起棍厌,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤肾胯,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后耘纱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡员寇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年牲览,在試婚紗的時候發(fā)現(xiàn)自己被綠了贡必。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衫樊。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡臀栈,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布娩贷,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏盆昙。R本人自食惡果不足惜诵闭,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一模狭、第九天 我趴在偏房一處隱蔽的房頂上張望踩衩。 院中可真熱鬧,春花似錦贩汉、人聲如沸驱富。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽褐鸥。三九已至,卻和暖如春赐稽,著一層夾襖步出監(jiān)牢的瞬間叫榕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工姊舵, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留晰绎,地道東北人。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓括丁,卻偏偏與公主長得像荞下,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子史飞,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,612評論 2 350

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