WXRecycleListComponent在iOS中的實現(xiàn)

WXRecycleListComponent在iOS中的實現(xiàn)

概述

WXRecycleListComponent是阿里Weex團隊在17年下半年為了對超長列表能更好地進行展示,而提供的一個新的解決方案监婶,在Weex的0.18.0版本正式對外發(fā)布Release版本摆舟,號稱在內存使用上進行了較大的優(yōu)化改進罩驻,且具有較好的FPS铃将,內存和滑動流暢度當然是每個Weex使用者最為關心的話題剑令。我們知道之前Weex提供的WXListComponent組件透傳了Native的UITableView,在內存的控制上較普通的Scroller提升明顯赏寇,引入了只渲染可見區(qū)域捻撑,View內存回收復用等機制保證了內存使用率磨隘。但是WXListComponent對超長列表的顯示還是存在著內存和FPS問題,這篇文章就讓我們一起來看看WXRecycleListComponent這個新組件在iOS中的具體實現(xiàn)顾患,如何在這些方面提出了新的解決方案番捂。

整體架構

我們先從架構角度來對比傳統(tǒng)的WXRecycleListComponent組件

傳統(tǒng)WXComponent架構

與WXRecycleListComponent組件有哪些區(qū)別?

WXRecycleListComponent架構

最左邊的業(yè)務代碼角度來看江解,兩種架構是相同的結構设预,都是M個模塊+N條數據。而傳統(tǒng)的組件會在JSFramework層將N條數據解析生成為N個虛擬節(jié)點Virtual DOM犁河,再通過call native方法調用生成Native中的組件Component并渲染出具體的視圖鳖枕。WXListComponent中做的一個優(yōu)化點就是將不可見區(qū)域的view及時的回收,需要的時候再渲染桨螺,這里要特別注意宾符,這個操作級別是視圖(View)級別。這個優(yōu)化必然會比全部view都渲染好很多灭翔,但是這個流程本質并不是復用的概念魏烫,當數據量達到一定級別后,這個方案還是會存在內存上的問題,同時View的回收和渲染也同樣會降低幀率(FPS)哄褒。

對比下圖的WXListComponent架構中的JSFramwork部分稀蟋,這里做了較大的改變。不在根據數據來生成元素dom呐赡,而是根據模板來生成模板dom退客,然后再通過call native方法調用生成Native中可復用的Component,再把數據當成數據源链嘀,分別對應加載復用的Component萌狂。這與iOS列表(UITableView/UICollectionView)中真正的復用原理一致,生成多個復用的cell管闷,通過數據源來決定使用哪個cell粥脚,完成cell級別的復用窃肠。從全局方案的角度來開包个,這必然對整體內存和滑動流暢度都具有較大的提高。但是這打破了傳統(tǒng)的weex組件解析方案和流程冤留,必然會產生一定的工作量和不穩(wěn)定性碧囊,我們下文具體分析其內部實現(xiàn)。

WXRecycleListComponent在SDK的Component目錄中單獨有一個RecycleList目錄纤怒,目前共包含16個文件糯而。

WXRecycleListComponent目錄

我們從直觀上可以感受到WXRecycleListComponent整個實現(xiàn)方案還是具有一定的復雜性。在SDK中注冊Component的是WXRecycleListComponent類泊窘,我們先看下其interface中要實現(xiàn)的代理和屬性熄驼,大致了解下整個組件的一個框架。

@interface WXRecycleListComponent : WXScrollerComponent
@property(nonatomic, strong) WXRecycleListDataManager *dataManager;
@property(nonatomic, strong) WXRecycleListTemplateManager *templateManager;
@property(nonatomic, strong) WXRecycleListUpdateManager *updateManager;
@end

@interface WXRecycleListComponent () <WXRecycleListLayoutDelegate, WXRecycleListUpdateDelegate, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource>
@end

通過上述代碼烘豹,我們看到組件內部實現(xiàn)了UICollectionViewDataSource和UICollectionViewDelegateFlowLayout的代理方法瓜贾,那么WXRecycleListComponent的列表展示所對應的Native組件為UICollectionView。其他3個Manager對象和2個自定義delegate的具體功能携悯,我們繼續(xù)展開介紹祭芦。

3個Manager對象:DataManager、TemplateManager憔鬼、UpdateManager

WXRecycleListDataManager

由于WXRecycleListComponent是基于UICollectionView的龟劲,頁面的顯示必然要使用到數據源,所以WXRecycleListDataManager的作用就是管理整個列表的數據源轴或,也就是架構圖中紫色的部分昌跌。列表數據的每次更新都要更新這個類中的數據源,這個類提供最基本的數據初始化照雁、數據更新蚕愤、查詢數量等功能。

@interface WXRecycleListDataManager : NSObject
- (instancetype)initWithData:(NSArray *)data;
- (void)updateData:(NSArray *)data;
- (NSArray *)data;
- (NSDictionary *)dataAtIndex:(NSInteger)index;
- (NSInteger)numberOfItems;
- (NSInteger)numberOfVirtualComponent;
- (NSDictionary*)virtualComponentDataWithId:(NSString*)componentId;
- (void)updateVirtualComponentData:(NSString*)componentId data:(NSDictionary*)data;
- (NSDictionary*)virtualComponentDataWithIndexPath:(NSIndexPath*)indexPath;
- (NSString*)virtualComponentIdWithIndexPath:(NSIndexPath*)indexPath;
- (void)deleteVirtualComponentAtIndexPaths:(NSArray<NSIndexPath*>*)indexPaths;
@end
WXRecycleListTemplateManager

WXRecycleListComponent的展示、回收與恢復是基于一群模板Cell的审胸,WXRecycleListTemplateManager的作用就是負責如何管理這些可回收的模板Cell。

- (void)addTemplate:(WXCellSlotComponent *)component;
- (WXCellSlotComponent *)dequeueCellSlotWithType:(NSString *)type forIndexPath:(NSIndexPath *)indexPath;
- (WXCellSlotComponent *)templateWithType:(NSString *)type;

WXRecycleListComponent會在_insertSubcomponent方法中烫扼,調用[addTemplate:]來添加模板類型,一個WXRecycleListComponent中的所有模板會存在一張Map表中,key為WXCellSlotComponent對象的templateCaseType屬性(對應Vue中<cell-slot>case 或者 default 屬性)双絮,object就是具體的模板Cell組件WXCellSlotComponent。同時會為這個cell注冊一個ReuseId焚挠。后續(xù)只要通過templateCaseType就可以取到對應的模板信息了。

- (void)addTemplate:(WXCellSlotComponent *)component
{
    NSString *templateType = component.templateCaseType;
    [_templateTypeMap setObject:component forKey:templateType];
    if (_collectionView) {
        [self _registerCellClassForReuseID:templateType];
    }
}
WXRecycleListUpdateManager

這個類主要負責處理UICollectionView視圖的更新管理。根據暴露出的方法可以清晰地看到,拿到newData硅急、appendingData與oldData就可以去更新UICollectionView視圖中的Cell。

- (void)updateWithNewData:(NSArray *)newData
                  oldData:(NSArray *)oldData
               completion:(WXRecycleListUpdateCompletion)completion
                animation:(BOOL)isAnimated;

- (void)updateWithAppendingData:(NSArray *)appendingData
                        oldData:(NSArray *)oldData
                     completion:(WXRecycleListUpdateCompletion)completion
                      animation:(BOOL)isAnimated;

那么凤壁,newData和appendingData有什么不同?我們具體看下這個類的實現(xiàn)。核心代碼在方法-performBatchUpdates里,以下代碼對源碼做了部分精簡徒仓。

- (void)performBatchUpdates
{
    NSArray *newData = [self.newerData copy];
    NSArray *oldData = [self.olderData copy];
    NSArray *appendingData = [self.appendingData copy];

    WXDiffResult *diffResult;
    if (appendingData) {
        newData = [oldData arrayByAddingObjectsFromArray:appendingData];
        NSIndexSet *inserts = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(oldData.count, appendingData.count)];
        // 對于appendingData精簡一次diff操作
        diffResult = [[WXDiffResult alloc] initWithInserts:inserts deletes:nil updates:nil];
    } else if (newData){
        diffResult = [WXDiffUtil diffWithMinimumDistance:newData oldArray:oldData];
    }
    
    // 計算UICollectionView需要delete彤枢、insert缴啡、reload的indexPahts
    WXRecycleListDiffResult *recycleListDiffResult = [self recycleListUpdatesByDiffResult:diffResult];
    
    if (![diffResult hasChanges] && self.reloadIndexPaths.count == 0) {
        return;
    }
    
    void (^updates)(void) = [^{
        // WXRecycleListUpdateDelegate
        [self.delegate updateManager:self willUpdateData:newData];
        // 具體更新UICollectionView
        [self applyUpdateWithDiffResult:recycleListDiffResult];
    } copy];
    
    void (^completion)(BOOL) = [^(BOOL finished) {
        // WXRecycleListUpdateDelegate
        [self.delegate updateManager:self didUpdateData:newData withSuccess:finished];
    } copy];
    
    // 最后批處理
    [collectionView performBatchUpdates:updates completion:completion];
}

我們可以看到WXRecycleListUpdateManager類的本質作用就是,當Vue傳過來新的數據碘裕,通過這個類來diff新舊數據,然后更新視圖文兢。如果是appdening的Data,則直接insert到最后面兼呵,就不用走diff的算法了维苔,這里對性能上做了一點優(yōu)化,如果單純從功能上來說的話潮尝,個人認為其實這2個方法沒有本質上的區(qū)別。

對于diff新舊數據的處理乱凿,Weex實現(xiàn)中用到了萊文斯坦距離算法(Levenshtein Distance)來比較新舊數據的編輯距離,計算出如何以最少操作次數來更新UICollectionView,具體算法實現(xiàn)中使用了包含動態(tài)規(guī)劃思想的全矩陣迭代法绷落,這里不做具體展開姥闪,可以閱讀WXDiffUtil類中的方法。

// Using the levenshtein algorithm
+ (WXDiffResult *)diffWithMinimumDistance:(NSArray<id<WXDiffable>> *)newArray oldArray:(NSArray<id<WXDiffable>> *)oldArray;

2個代理:LayoutDelegate砌烁、UpdateDelegate

WXRecycleListLayoutDelegate

WXRecycleListLayoutDelegate定義在WXRecycleListLayout類中筐喳,WXRecycleListLayout繼承于UICollectionViewFlowLayout,主要是管理UICollectionView的視圖布局函喉。WXRecycleListLayoutDelegate在UICollectionViewFlowLayout中的layoutAttributesForElementsInRect方法中觸發(fā)避归,如果實現(xiàn)了該代理,那么在布局該Cell時會固定(stick)在頂部函似。

@protocol WXRecycleListLayoutDelegate
- (BOOL)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout isNeedStickyForIndexPath:(NSIndexPath *)indexPath;
@end

@interface WXRecycleListLayout : UICollectionViewFlowLayout
@property (nonatomic, weak) id<WXRecycleListLayoutDelegate> delegate;
@end
WXRecycleListUpdateDelegate

WXRecycleListUpdateDelegate定義在WXRecycleListUpdateManager.h類中槐脏,提供2個方法。分別在UICollectionView更新視圖前后時機觸發(fā)回調撇寞。

@protocol WXRecycleListUpdateDelegate
// 在UICollectionView更新視圖前產生回調
- (void)updateManager:(WXRecycleListUpdateManager *)manager willUpdateData:(id)newData;

// 在UICollectionView更新視圖完成后產生回調
- (void)updateManager:(WXRecycleListUpdateManager *)manager didUpdateData:(id)newData withSuccess:(BOOL)finished;

@end

WXRecycleListComponent.m中的實現(xiàn)

經過上述Manager對象和一些Delegate的介紹顿天,我們已經將WXRecycleListComponent分解為多個小模塊堂氯,而主的WXRecycleListComponent.m類職責就是如何將上述這些小模塊組合在一起使用。

在WXRecycleListComponent.m類中牌废,除了完成一些必要的WXComponent初始化行為和Load More事件處理之外咽白,主要處理一些組件暴露給前端的一些export method。

WX_EXPORT_METHOD(@selector(appendData:))
WX_EXPORT_METHOD(@selector(appendRange:))
WX_EXPORT_METHOD(@selector(insertData:data:))
WX_EXPORT_METHOD(@selector(updateData:data:))
WX_EXPORT_METHOD(@selector(removeData:count:))
WX_EXPORT_METHOD(@selector(moveData:toIndex:))
WX_EXPORT_METHOD(@selector(scrollTo:options:))
WX_EXPORT_METHOD(@selector(insertRange:range:))
WX_EXPORT_METHOD(@selector(setListData:))

另外的一大部分的代碼是實現(xiàn)UICollectionViewDataSource和UICollectionViewDelegateFlowLayout鸟缕。其中晶框,比較重要的部分在[collectionView:cellForItemAtIndexPath:]方法中,如何根據data和template生成(獲榷印)一個可以復用的cell并且綁定數據和渲染授段。以下對此方法做了一些精簡。

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    // 1. get the data relating to the cell
    id data = [_dataManager dataAtIndex:indexPath.row];
    
    // 2. get the template type specified by data
    NSString * templateType = [self templateType:indexPath];
    _templateManager.collectionView = collectionView;
    
    // 3. dequeue a cell component by template type
    UICollectionViewCell *cellView = [_collectionView dequeueReusableCellWithReuseIdentifier:templateType forIndexPath:indexPath];
    WXCellSlotComponent *cellComponent = (WXCellSlotComponent *)cellView.wx_component;
    if (!cellComponent) {
        cellComponent = [_templateManager dequeueCellSlotWithType:templateType forIndexPath:indexPath];
        cellView.wx_component = cellComponent;
        WXPerformBlockOnComponentThread(^{
            [super _insertSubcomponent:cellComponent atIndex:self.subcomponents.count];
        });
    }
    
    // 4. binding the data to the cell component
    [self _updateBindingData:data forCell:cellComponent atIndexPath:indexPath];

    // 5. Add cell component's view to content view.
    UIView *contentView = cellComponent.view;
    if (contentView.superview == cellView.contentView) {
        return cellView;
    }
    
    for (UIView *view in cellView.contentView.subviews) {
        [view removeFromSuperview];
    }
    [cellView.contentView addSubview:contentView];
    
    [self handleAppear];
    
    return cellView;
}

1番甩、獲取當前行Cell所對應的數據源對象侵贵,由于WXRecycleListComponent的數據管理都是由WXRecycleListDataManager來負責,所以這里很直接的根據index來拿到數據源即可缘薛。

2窍育、獲取當前行的cell所屬模板類型。

3宴胧、所以這里我們可以根據模塊的名字來獲取到cell的對象漱抓,由于UICollectionView注冊的Cell必須為UICollectionViewCell或其子類,而WXCellSlotComponent->WXComponent->NSObject恕齐,所以這里有一個動態(tài)綁定的過程乞娄。又因為Cell隨著頁面的滑動,是會被回收的檐迟,那么所綁定的Component也會被清理掉补胚。所以,這里同樣需要對模板類WXCellSlotComponent進行回收管理追迟,模板的注冊和獲取都是通過WXRecycleListTemplateManager對象來負責處理的。

 WXCellSlotComponent *cellComponent = (WXCellSlotComponent *)cellView.wx_component;
 if (!cellComponent) {
        cellComponent = [_templateManager dequeueCellSlotWithType:templateType forIndexPath:indexPath];
    cellView.wx_component = cellComponent;
    WXPerformBlockOnComponentThread(^{
        //TODO: How can we avoid this?
        [super _insertSubcomponent:cellComponent atIndex:self.subcomponents.count];
    });
}

4骚腥、如何將數據源應用到相應的WXCellSlotComponent上敦间,并以正確的樣式進行展示,這是整個流程最為關鍵和精彩的部分束铭。下一個小節(jié)會具體介紹下這個綁定流程廓块。

5、將WXCellSlotComponent視圖添加到Cell的content視圖中契沫,用于最終的顯示带猴。

由第5步我們可以看到,Cell真實的size就是Component中view的size懈万,而由于第3步的介紹我們知道Cell與Component是可以被回收的拴清,一旦回收后靶病,size就需要重新計算,而計算size本身是比較費時的口予。所以娄周,在UICollectionViewDelegateFlowLayout的

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;

里,對計算出來的size根據indexPath進行cache沪停,進一步提升整體的性能煤辨。

數據解析與綁定

我們通過以下例子來講解數據解析和綁定的過程,也可以通過Weex中的例子來理解整個過程木张。recycle-list標簽的模板語法我們不進行展開介紹众辨,主要介紹整個流程在iOS中的實現(xiàn)。

<template>
  <recycle-list for="(item, i) in labels" switch="type">
    <cell-slot v-if=true case="label">
      <text>{{i}}33333</text>
    </cell-slot>
  </recycle-list>
</template>

<script>
  export default {
    data () {
      return {
        labels: [// 數據源
          { type: 'label' },
        ],
      }
    }
  }
</script>

我們回到UICollectionView數據源方法中的第4步舷礼,其方法經過精簡后的實現(xiàn)如下鹃彻,入參為數據源data、當前的Cell模板對象cellComponent以及位置索引indexPath且轨。

- (void)_updateBindingData:(id)data forCell:(WXCellSlotComponent *)cellComponent atIndexPath:(NSIndexPath *)indexPath
{
    id originalData = data;
    
    if (!data[@"indexPath"] || !data[@"recycleListComponentRef"]) {
        NSMutableDictionary * dataNew = [data mutableCopy];
        dataNew[@"recycleListComponentRef"] = self.ref;
        dataNew[@"indexPath"] = indexPath;
        data = dataNew;
    }
    
    if ([originalData isKindOfClass:[NSDictionary class]] && _aliasKey &&!data[@"phase"]) {
        data = @{_aliasKey:data,@"aliasKey":_aliasKey};
    }
    
    if (_indexKey) {
        NSMutableDictionary *dataNew = [data mutableCopy];
        dataNew[_indexKey] = @(indexPath.item);
        data = dataNew;
    }
    
    WXPerformBlockSyncOnComponentThread(^{
        [cellComponent updateCellData:[data copy]];
    });
}

傳入數據源后浮声,我們會對數據源進行一些包裝,附加上aliasKey旋奢、index泳挥、indexPath、recycleListComponentRef至朗、type等值屉符,這些值都是在渲染真正組件內容時不可或缺的一些信息。包裝后的對象如下锹引。

{
    aliasKey = item;
    i = 0;
    item =     {
        indexPath = "<NSIndexPath: 0xc000000000000016> {length = 2, path = 0 - 0}";
        recycleListComponentRef = "_root";
        type = label;
    };
}

通過這些包裝完成的數據矗钟,我們就能根據已經注入的模板語法獲取到這個Cell具體要顯示的條件、規(guī)則和值嫌变,然后完成數據綁定吨艇,最終完成布局、渲染和顯示腾啥。

- (void)updateCellData:(NSDictionary *)data
{
    [self updateBindingData:data];
    [self triggerLayout];
}

那么东涡,模板語法的注入與解析轉換是在什么時候完成的?

其實在WXComponentManager中通過_buildComponentForData方法生成Component的時候倘待,會根據一些條件判斷是否為模板組件疮跑,如果是,則進行數據解析與綁定的行為凸舵。

WXComponentConfig *config = [WXComponentFactory configWithComponentName:type];
BOOL isTemplate = [config.properties[@"isTemplate"] boolValue] || (supercomponent && supercomponent->_isTemplate);
if (isTemplate) {
    bindingProps = [self _extractBindingProps:&attributes];
    bindingStyles = [self _extractBindings:&styles];
    bindingAttibutes = [self _extractBindings:&attributes];
    bindingEvents = [self _extractBindingEvents:&events];
}

模板語法表達式的解析和綁定感覺是整個方案中比較難懂的部分祖娘,其主要實現(xiàn)在WXComponent+DataBinding這個擴展類中,主要工作就是是對前端與客戶端之間通信的模板語法進行一步一步地解析轉換啊奄,細節(jié)工作比較繁瑣渐苏,其中一部分語法和表達式的解析代碼用C++完成掀潮,主要的相關的實現(xiàn)在WXJSASTParser.mm文件中,有一定的閱讀理解門檻整以。然后就是將相應表達式所要對應的行為定于在WXDataBindingBlock這個block中胧辽,然后存儲在一個bingdingMap中,即完成了整個綁定操作公黑。

總結

整個長列表復用方案在長列表的展示上具有較大的性能突破邑商,整體方案的設計思路上與傳統(tǒng)Weex組件架構有較大不同,充分利用了Native組件的復用機制凡蚜。最后再對整個框架創(chuàng)新之處進行一個總結人断。

  • 前端不對列表結構進行展開

iOS系統(tǒng)的Native組件實現(xiàn)本身就具備了很優(yōu)異的交互效果和內存控制能力,以UITableView和UICollectionView為例朝蜘,其本身有著非常好的滾動流暢度恶迈、內存控制能力、View復用能力谱醇,那么我們就要最大化的去利用這些Native的能力暇仲,而不是花很大的時間和精力再重新造一個輪子去模仿它,盡可能的利用Native提供的優(yōu)良特性才是Weex最大的優(yōu)勢和價值副渴。

  • 前端與客戶端之間的模板語法

復用機制的實現(xiàn)必然需要一套復用的模板奈附,如何將Vue的業(yè)務代碼轉換成一套Native復用的模板也是一項比較大的挑戰(zhàn)。整套語法的制定和解析過程還是有很大的工作量煮剧,有無數的細節(jié)工作需要不斷的實現(xiàn)和完善斥滤。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市勉盅,隨后出現(xiàn)的幾起案子佑颇,更是在濱河造成了極大的恐慌,老刑警劉巖草娜,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挑胸,死亡現(xiàn)場離奇詭異,居然都是意外死亡宰闰,警方通過查閱死者的電腦和手機嗜暴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來议蟆,“玉大人,你說我怎么就攤上這事萎战「廊荩” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵蚂维,是天一觀的道長戳粒。 經常有香客問我路狮,道長,這世上最難降的妖魔是什么蔚约? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任奄妨,我火速辦了婚禮,結果婚禮上苹祟,老公的妹妹穿的比我還像新娘砸抛。我一直安慰自己,他們只是感情好树枫,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布直焙。 她就那樣靜靜地躺著,像睡著了一般砂轻。 火紅的嫁衣襯著肌膚如雪奔誓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天搔涝,我揣著相機與錄音厨喂,去河邊找鬼。 笑死庄呈,一個胖子當著我的面吹牛蜕煌,可吹牛的內容都是我干的。 我是一名探鬼主播抒痒,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼幌绍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了故响?” 一聲冷哼從身側響起傀广,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎彩届,沒想到半個月后伪冰,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡樟蠕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年贮聂,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片寨辩。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡吓懈,死狀恐怖,靈堂內的尸體忽然破棺而出靡狞,到底是詐尸還是另有隱情耻警,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站甘穿,受9級特大地震影響腮恩,放射性物質發(fā)生泄漏。R本人自食惡果不足惜温兼,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一秸滴、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧募判,春花似錦荡含、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至敦腔,卻和暖如春均澳,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背符衔。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工找前, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人判族。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓躺盛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親形帮。 傳聞我的和親對象是個殘疾皇子槽惫,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345