探索dealloc真諦

動機由來

最近在封裝一個 UITextField 分類的時候遇到了一個問題胰蝠,大致需求是封裝 UITextField 的若干功能塞茅,方便業(yè)務(wù)方這樣使用:

// 限制輸入長度
[_tf ltv_limitLength:5];
// 限制輸入字符
[_tf ltv_limitContent:[NSCharacterSet characterSetWithCharactersInString:@"-+*"]];
// 匹配輸入條件觸發(fā)action
[_tf ltv_matchCondition:^BOOL(NSString *text) {
    return [text isEqualToString:@"asd"];
} action:^(NSString *text) {
    NSLog(@"matched asd");
}];

基本實現(xiàn)思路是借助一個全局單例亩码,作為UITextField內(nèi)容變化時通知的觀察者,其中object參數(shù)指定了需要監(jiān)聽的 UITextField 實例野瘦,這樣一來描沟,當(dāng)輸入內(nèi)容發(fā)生變化,就能觸發(fā)對應(yīng) UITextField 實例相關(guān)的邏輯處理:

[[NSNotificationCenter defaultCenter] addObserver:[self manager] selector:@selector(textfieldDidChangedTextNotification:) name:UITextFieldTextDidChangeNotification object:target];

這種思路有一個問題需要處理鞭光,就是當(dāng) UITextField 實例釋放的時候吏廉,需要移除對應(yīng)的通知。也就是說衰猛,我需要監(jiān)聽 UITextField 實例的釋放迟蜜。由于是系統(tǒng)控件刹孔,沒法直接復(fù)寫 dealloc 方法啡省,因此需要借助一些運行時魔法。當(dāng)時主要有兩種思路:

  1. 借助hook髓霞,替換 dealloc 方法卦睹。但是 dealloc 是NSObjec的方法,若要hook該方法方库,會對所有的cocoa實例產(chǎn)生影響结序,而我的實際目標(biāo)只有UITextField,顯然這種方式不太妙纵潦。而且事實上徐鹤,ARC下是無法直接hook dealloc 方法的(通過運行時可以實現(xiàn)),會產(chǎn)生編譯報錯:ARC forbids use of 'dealloc' in a @selector邀层。因此返敬,這種方案Pass!

  2. 借助AssociatedObject寥院。我們知道劲赠,ARC下,一個實例釋放后,同時會解除對其實例變量的強引用凛澎。這樣一來霹肝,我就可以通過AssociatedObject動態(tài)給UITextField實例綁定一個自定義的輔助對象,并且監(jiān)聽該輔助對象的 dealloc 方法調(diào)用塑煎。因為按照我的理論沫换,當(dāng)UITextField實例被釋放后,輔助對象唯一的強引用被解除最铁,必然將觸發(fā) dealloc 的調(diào)用苗沧。這樣一來,我就能夠間接監(jiān)聽宿主UITextField實例的釋放了炭晒。

    然而待逞,想法很美好,現(xiàn)實略骨感网严。我確實能夠監(jiān)聽UITextField實例的釋放了识樱,然而似乎忘記了我真正的意圖——真正要做的是在UITextField實例被釋放之前拿到實例本身,調(diào)用方法移除對應(yīng)的通知:

    [[NSNotificationCenter defaultCenter] removeObserver:[self manager] name:UITextFieldTextDidChangeNotification object:target]
    

    我忽略了一個很重要的問題:當(dāng)實例變量的 dealloc 方法調(diào)用的時候震束,其宿主對象已經(jīng)被釋放了怜庸,也就是說在實例變量的 dealloc 方法中已經(jīng)拿不到宿主對象了。因此我還是拿不到UITextField實例9复濉墓怀!PassT绿恕!

這個問題似乎沒有很好的解決方案,最終換了一種思路:不再為每個UITextField實例綁定觀察者監(jiān)聽通知服协,而是注冊一個全局的通知:

[[NSNotificationCenter defaultCenter] addObserver:[self manager] selector:@selector(textfieldDidChangedTextNotification:) name:UITextFieldTextDidChangeNotification object:nil];

在監(jiān)聽通知的回調(diào)方法中判斷觸發(fā)通知的UITextField實例是否是需要處理的實例兆览,僅在命中的時候進行邏輯處理勃黍。

- (void)textfieldDidChangedTextNotification:(NSNotification *)notification
{
    UITextField *textField = (UITextField *)notification.object;
    if ([_targetTable containsObject:textField]) {
        [textField.operations enumerateObjectsUsingBlock:^(LTVTFOperation * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            obj.action();
        }];
    }
}

這種方案雖然有個顯而易見的缺陷(會監(jiān)聽所有的UITextField實例)踱蛀,但是個人認為比hook dealloc方法要好,首先受眾對象只限定在UITextField馋辈,其次多余的邏輯處理較為簡單抚芦,不會產(chǎn)生較大的性能影響。另外迈螟,想了想IQKeyBoard也是全局監(jiān)聽UITextField叉抡,問題應(yīng)該不大吧~ 如果你有更好的方案,歡迎來撩~

雖然眼前問題是解決了答毫,但是此時內(nèi)心已經(jīng)暗戳戳萌芽了一個更大的困惑:dealloc方法到底干了啥褥民?

進入正題

首先,我們都知道當(dāng)一個對象的引用計數(shù)為0的時候烙常,就會調(diào)用 dealloc 方法進行析構(gòu)轴捎。在MRC時代鹤盒,內(nèi)存需要手動管理,解除對象引用需要手動調(diào) release 侦副,通常也會這樣寫 dealloc

- (void)dealloc {
    self.instance1 = nil;
    self.instance2 = nil;
    // ...
    // 非cocoa對象內(nèi)存的釋放侦锯,如CF對象
    // ...
    [super dealloc];
}
  • 移除對相關(guān)實例的引用
  • 非cocoa對象的釋放
  • 調(diào)用 [super dealloc] 來釋放父類中的對象

而到了ARC時代,dealloc 基本變成了這樣:

- (void)dealloc {
    // ...
    // 非cocoa對象內(nèi)存的釋放秦驯,如CF對象
    // ...
}

除了非cocoa對象還需要手動釋放尺碰,實例變量釋放和 [super dealloc] 都不見了身影。這也就是我們要探索的兩個ARC下 dealloc 的問題:

  1. 對象的實例變量如何釋放译隘?
  2. 父類中的對象析構(gòu)如何實現(xiàn)亲桥?

初探dealloc的調(diào)用

當(dāng)探索一個方法無從下手時,最好的方法就是查看調(diào)用棧固耘,說不定就能從中窺見一二题篷。測試代碼如下:

// 父類Animal
@interface Animal : NSObject
@property (nonatomic, strong) Skill *skill;
@end
@implementation Animal
- (void)dealloc{
    NSLog(@"%s",__func__);
}
@end

// 子類Dog
@interface Dog : Animal
@end
@implementation Dog
- (void)dealloc{
    NSLog(@"%s",__func__);
}
@end

// 實例變量類型Skill
@interface Skill : NSObject
@end
@implementation Skill
- (void)dealloc{
    NSLog(@"%s",__func__);
}
@end
    
int main(int argc, const char * argv[]) {
    Dog *dog = [Dog new];
    dog.skill = [Skill new];
    return 0;
}

運行工程,由于dog實例很快過了作用域厅目,因此會觸發(fā)實例的釋放番枚。打印的日志如下:

2018-11-01 17:09:43.986073+0800 DeallocExporeDemo[5674:1072191] -[Dog dealloc]
2018-11-01 17:09:43.986302+0800 DeallocExporeDemo[5674:1072191] -[Animal dealloc]
2018-11-01 17:09:45.751398+0800 DeallocExporeDemo[5674:1072191] -[Skill dealloc]

可見雖然dealloc方法中盡管沒調(diào)用 [super dealloc] ,也沒有手動釋放對實例變量skill的引用损敷,父類Animal的 dealloc 和實例變量skill的 dealloc 方法最終都調(diào)用了葫笼。

由于觸發(fā)對象調(diào)用dealloc的直接原因是對象引用計數(shù)為0,而實例變量實際上是被 dog.skill 這個變量所持有拗馒,因此可以通過 Watchpoint 來監(jiān)聽skill變量的內(nèi)存變化路星。在main函數(shù)的 return 0; 語句上打個斷點,然后通過 watchpoint set variable dog->_skill 設(shè)置監(jiān)聽:

image

繼續(xù)執(zhí)行诱桂,隨后就能監(jiān)聽到skill內(nèi)存的變化:

image

可見dog的skill實例變量的內(nèi)存地址從 0x00000001007661b0 變成了 0x0000000000000000洋丐,也就是說這個時間節(jié)點skill對象被釋放了(其實嚴格來說這么說是不正確的,此時堆上的skill對象并沒有被釋放访诱,我們監(jiān)聽到的只是棧上的skill變量值被清掉了垫挨,因此也就無法再通過變量訪問該對象了)韩肝。

此時調(diào)用棧如下:

image

可見子類的 dealloc 調(diào)用之后触菜,父類的跟著調(diào)用。隨后通過一系列運行時方法哀峻,最終在一個名為 .cxx_destruct 的方法中調(diào)用了 objc_storeStrong 來完成釋放工作涡相。另外可以看到這個 .cxx_destruct 是Animal的方法,怎么來的呢剩蟀?運行時都做了些什么事催蝗?帶著這些疑問繼續(xù)往下看。

NSObject的dealloc實現(xiàn)

是時候來看一下runtime中相關(guān)的實現(xiàn)了育特,runtime源碼可以在 Source Browser 下載丙号。

經(jīng)過定位和調(diào)用追蹤先朦,發(fā)現(xiàn)經(jīng)過了如下函數(shù):

dealloc -> _objc_rootDealloc -> object_dispose -> objc_destructInstance

前面都是些簡單的判斷和跳轉(zhuǎn),重要的是 objc_destructInstance 函數(shù):

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }
    return obj;
}

可以看到這個函數(shù)主要做了3件事:

  • object_cxxDestruct

    這個函數(shù)有點眼熟犬缨,跟剛才調(diào)用棧中看到的 .cxx_destruct 長得很像喳魏,猜測實例變量釋放以及調(diào)用父類的dealloc都是在這里面進行的。

  • _object_remove_assocations

    顧名思義怀薛,用來釋放動態(tài)綁定的對象刺彩。

  • clearDeallocating

    該函數(shù)實現(xiàn)如下:

    inline void objc_object::clearDeallocating()
    {
        if (slowpath(!isa.nonpointer)) {
            // Slow path for raw pointer isa.
            sidetable_clearDeallocating();
        }
        else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
            // Slow path for non-pointer isa with weak refs and/or side table data.
            clearDeallocating_slow();
        }
    
        assert(!sidetable_present());
    }
    
    NEVER_INLINE void objc_object::clearDeallocating_slow()
    {
        assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));
        SideTable& table = SideTables()[this];
        table.lock();
        if (isa.weakly_referenced) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        if (isa.has_sidetable_rc) {
            table.refcnts.erase(this);
        }
        table.unlock();
    }
    

    可以看到做了兩件事:

    1. 將對象弱引用表清空,即將弱引用該對象的指針置為nil
    2. 清空引用計數(shù)表(當(dāng)一個對象的引用計數(shù)值過大(超過255)時枝恋,引用計數(shù)會存儲在一個叫 SideTable 的屬性中创倔,此時isa的 has_sidetable_rc 值為1)

接下來,要探索的就是 object_cxxDestruct 函數(shù)了焚碌,實現(xiàn)如下:

void object_cxxDestruct(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    object_cxxDestructFromClass(obj, obj->ISA());
}

object_cxxDestructFromClass 這個函數(shù)之前在調(diào)用棧里看到過畦攘,再往里看:

static void object_cxxDestructFromClass(id obj, Class cls)
{
    void (*dtor)(id);

    // Call cls's dtor first, then superclasses's dtors.

    for ( ; cls; cls = cls->superclass) {
        if (!cls->hasCxxDtor()) return; 
        dtor = (void(*)(id))
            lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
        if (dtor != (void(*)(id))_objc_msgForward_impcache) {
            if (PrintCxxCtors) {
                _objc_inform("CXX: calling C++ destructors for class %s", 
                             cls->nameForLogging());
            }
            (*dtor)(obj);
        }
    }
}

通過分析,最終 (*dtor)(obj); 執(zhí)行的其實是 SEL_cxx_destruct 這個SEL標(biāo)記的函數(shù)十电,通過全局搜索 SEL_cxx_destruct 念搬,不難發(fā)現(xiàn)該SEL對應(yīng)的正是之前看到的 .cxx_destruct 方法,也就是說摆出,最終是 .cxx_destruct 方法被調(diào)用了朗徊。

探索.cxx_destruct方法

之前在調(diào)用棧中看到該方法是Animal類中的方法,而我們并沒有申明該方法偎漫,也沒有動態(tài)插入該方法的相關(guān)代碼爷恳。并且這個方法是析構(gòu)對象相關(guān)的,具有很強的通用性象踊,那么猜測是在編譯的時候由前端編譯器(clang)自動插入的温亲。

我們可以通過 DLIntrospection 來查看Animal類中是否真的存在這個方法,該工具可以方便在lldb中打印類中所有的實例變量杯矩、方法栈虚、對象遵守的協(xié)議等信息,是一個NSObject的分類文件史隆,直接拉到工程中即可使用魂务。

在main函數(shù)中打個斷點,然后在lldb中打印Animal類的實例方法:

po [[Animal class] instanceMethods]
image

可以看到確實是有 .css_destruct 這個方法泌射。隨后粘姜,通過查閱相關(guān)資料,驗證了我之前的猜測熔酷。在clang源碼里孤紧,找到了相關(guān)的代碼:

void CodeGenModule::EmitObjCIvarInitializations(ObjCImplementationDecl *D) {
    IdentifierInfo *II = &getContext().Idents.get(".cxx_destruct");
    Selector cxxSelector = getContext().Selectors.getSelector(0, &II);
    ObjCMethodDecl *DTORMethod =
    ObjCMethodDecl::Create(getContext(), D->getLocation(), D->getLocation(),
                          cxxSelector, getContext().VoidTy, nullptr, D,
                          /isInstance=/true, /isVariadic=/false,
                       /isPropertyAccessor=/true, /isImplicitlyDeclared=/true,
                          /isDefined=/false, ObjCMethodDecl::Required);
    D->addInstanceMethod(DTORMethod);
    CodeGenFunction(*this).GenerateObjCCtorDtorMethod(D, DTORMethod, false);
}

在clang的CodeGenModule模塊中看到了上面代碼(只摘錄了相關(guān)代碼),經(jīng)過分析大概是clang通過CodeGen為具體類插入了 .cxx_destruct 方法拒秘。 GenerateObjCCtorDtorMethod 函數(shù)實現(xiàn)在 CGObjC.cpp 文件中号显,其中聲明了 .cxx_destruct 的具體實現(xiàn)臭猜。最終對象釋放時,會調(diào)用到 emitCXXDestructMethod 函數(shù):

 static void emitCXXDestructMethod(CodeGenFunction &CGF,
                                   ObjCImplementationDecl *impl) {
   CodeGenFunction::RunCleanupsScope scope(CGF);
   llvm::Value *self = CGF.LoadObjCSelf();
   const ObjCInterfaceDecl *iface = impl->getClassInterface();
   for (const ObjCIvarDecl *ivar = iface->all_declared_ivar_begin();
        ivar; ivar = ivar->getNextIvar()) {
     QualType type = ivar->getType();
     // Check whether the ivar is a destructible type.
     QualType::DestructionKind dtorKind = type.isDestructedType();
     if (!dtorKind) continue;
     CodeGenFunction::Destroyer *destroyer = nullptr;
     // Use a call to objc_storeStrong to destroy strong ivars, for the
     // general benefit of the tools.
     if (dtorKind == QualType::DK_objc_strong_lifetime) {
       destroyer = destroyARCStrongWithStore;
     // Otherwise use the default for the destruction kind.
     } else {
       destroyer = CGF.getDestroyer(dtorKind);
     }
     CleanupKind cleanupKind = CGF.getCleanupKind(dtorKind);
     CGF.EHStack.pushCleanup<DestroyIvar>(cleanupKind, self, ivar, destroyer,
                                          cleanupKind & EHCleanup);
   }
   assert(scope.requiresCleanups() && "nothing to do in .cxx_destruct?");
 }

經(jīng)過分析押蚤,該函數(shù)做的事情是:遍歷所有實例變量获讳,調(diào)用 destroyARCStrongWithStore 。而 destroyARCStrongWithStore 最終調(diào)用的就是之前調(diào)用棧中看到的 objc_storeStrong 函數(shù)活喊,可以在runtime源碼中看到其實現(xiàn):

void objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

該函數(shù)作用是將obj對象賦值給location變量丐膝,因此只要執(zhí)行 objc_storeStrong(&ivar, null) 就能釋放ivar實例變量。至此钾菊,dealloc 方法如何釋放實例變量這個問題就探索完畢了帅矗。

至于如何調(diào)用 [super dealloc] ,在clang源碼中同樣能找到貓膩煞烫。同樣在 CGObjC.cpp 文件中浑此,存在如下代碼:

 void CodeGenFunction::StartObjCMethod(const ObjCMethodDecl *OMD,
                                       const ObjCContainerDecl *CD) {
   // In ARC, certain methods get an extra cleanup.
   if (CGM.getLangOpts().ObjCAutoRefCount &&
       OMD->isInstanceMethod() &&
       OMD->getSelector().isUnarySelector()) {
     const IdentifierInfo *ident =
       OMD->getSelector().getIdentifierInfoForSlot(0);
     if (ident->isStr("dealloc"))
       EHStack.pushCleanup<FinishARCDealloc>(getARCCleanupKind());
   }
 }

分析可知在 dealloc 方法中插入了代碼,相關(guān)代碼在 FinishARCDealloc 結(jié)構(gòu)中定義:

 namespace {
 struct FinishARCDealloc final : EHScopeStack::Cleanup {
   void Emit(CodeGenFunction &CGF, Flags flags) override {
     const ObjCMethodDecl *method = cast<ObjCMethodDecl>(CGF.CurCodeDecl);
 
     const ObjCImplDecl *impl = cast<ObjCImplDecl>(method->getDeclContext());
     const ObjCInterfaceDecl *iface = impl->getClassInterface();
     if (!iface->getSuperClass()) return;
 
     bool isCategory = isa<ObjCCategoryImplDecl>(impl);
 
     // Call [super dealloc] if we have a superclass.
     llvm::Value *self = CGF.LoadObjCSelf();
 
     CallArgList args;
     CGF.CGM.getObjCRuntime().GenerateMessageSendSuper(CGF, ReturnValueSlot(),
                                                       CGF.getContext().VoidTy,
                                                       method->getSelector(),
                                                       iface,
                                                       isCategory,
                                                       self,
                                                       /*is class msg*/ false,
                                                       args,
                                                       method);
   }
 };
 }

大致意思就是調(diào)用父類的 dealloc 方法滞详。

撥云見日

通過上面的探索分析凛俱,基本搞清楚了ARC下 dealloc 是怎么實現(xiàn)自動釋放實例變量以及調(diào)用父類 dealloc 方法的。這一切要歸功于clang以及運行時庫料饥,在前端編譯過程中CodeGen插入了相關(guān)代碼蒲犬,結(jié)合運行時完成釋放動作。對于ARC下 dealloc 實現(xiàn)原理的摸索就此告終岸啡。

測試demo地址:https://github.com/Lotheve/blogdemo/tree/master/DeallocExporeDemo

最后編輯于
?著作權(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é)果婚禮上,老公的妹妹穿的比我還像新娘陵刹。我一直安慰自己默伍,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布衰琐。 她就那樣靜靜地躺著也糊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪羡宙。 梳的紋絲不亂的頭發(fā)上狸剃,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音狗热,去河邊找鬼钞馁。 笑死,一個胖子當(dāng)著我的面吹牛匿刮,可吹牛的內(nèi)容都是我干的僧凰。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼熟丸,長吁一口氣:“原來是場噩夢啊……” “哼允悦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起虑啤,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤隙弛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后狞山,有當(dāng)?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
  • 正文 我出身青樓章母,卻偏偏與公主長得像母蛛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子乳怎,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

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