UITableView的Cell復(fù)用原理和源碼分析

簡介

在我們的日常開發(fā)中,絕大多數(shù)情況下只要詳細閱讀類頭文件里的注釋柬批,組合UIKit框架里的大量控件就能很好的滿足工作的需求赴魁。但僅僅會使用UIKit里的控件還遠遠不夠吝镣,假如現(xiàn)在產(chǎn)品需要一個類似 Excel 樣式的控件來呈現(xiàn)數(shù)據(jù),需要這個控件能上下左右滑動泉蝌,這時候你會發(fā)現(xiàn)UIKit里就沒有現(xiàn)成的控件可用了歇万。UITableView 可以看做一個只可以上下滾動的 Excel揩晴,所以我們的直覺是應(yīng)該仿寫 UITableView 來實現(xiàn)這個自定義的控件。這篇文章我將會通過開源項目 Chameleon 來分析UITableView的 hacking 源碼贪磺,閱讀完這篇文章后你將會了解 UITableView 的繪制過程和 UITableViewCell 的復(fù)用原理硫兰。 并且我會在下一篇文章中實現(xiàn)一個類似 Excel 的自定義控件。

Chameleon

Chameleon 是一個移植 iOS 的 UIKit 框架到 Mac OS X 下的開源項目寒锚。該項目的目的在于盡可能給出 UIKit 的可替代方案劫映,并且讓 Mac OS 的開發(fā)者盡可能的開發(fā)出類似 iOS 的 UI 界面。

UITableView的簡單使用

//創(chuàng)建UITableView對象刹前,并設(shè)置代代理和數(shù)據(jù)源為包含該視圖的視圖控制器
UITableView *tableView = [[UITableView alloc] initWithFrame:frame style:UITableViewStyleGrouped];   
tableView.delegate = self;
tableView.dataSource = self;
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kReuseCellIdentifier];
[self.view addSubview:tableView];

//實現(xiàn)代理和數(shù)據(jù)源協(xié)議中的方法
#pragma mark - UITableViewDelegate

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return kDefaultCellHeight;
}

#pragma mark - UITableViewDataSource

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kReuseCellIdentifier];
    return cell;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.dataArray.count;
}

創(chuàng)建UITableView實例對象

UITableView *tableView = [[UITableView alloc] initWithFrame:frame style:UITableViewStyleGrouped]; 

initWithFrame: style: 方法源碼如下:

- (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)theStyle
{
    if ((self=[super initWithFrame:frame])) {
        _style = theStyle;
        
        //_cachedCells 用于保存正在顯示的Cell對象的引用
        _cachedCells = [[NSMutableDictionary alloc] init];
        
        //在計算完每個 section 包含的 section 頭部泳赋,尾部視圖的高度,和包含的每個 row 的整體高度后喇喉,
        //使用 UITableViewSection 對象對這些高度值進行保存摹蘑,并將該 UITableViewSection 對象的引用
        //保存到 _sections中。在指定完 dataSource 后轧飞,至下一次數(shù)據(jù)源變化調(diào)用 reloadData 方法衅鹿,
        //由于數(shù)據(jù)源沒有變化,section 相關(guān)的高度值是不會變化过咬,只需計算一次大渤,所以需要緩存起來。
        _sections = [[NSMutableArray alloc] init];
        
        //_reusableCells用于保存存在但未顯示在界面上的可復(fù)用的Cell
        _reusableCells = [[NSMutableSet alloc] init];
        
        self.separatorColor = [UIColor colorWithRed:.88f green:.88f blue:.88f alpha:1];
        self.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
        self.showsHorizontalScrollIndicator = NO;
        self.allowsSelection = YES;
        self.allowsSelectionDuringEditing = NO;
        self.sectionHeaderHeight = self.sectionFooterHeight = 22;
        self.alwaysBounceVertical = YES;
        
        if (_style == UITableViewStylePlain) {
            self.backgroundColor = [UIColor whiteColor];
        }
        [self _setNeedsReload];
    }
    return self;
}

我將需要關(guān)注的地方做了詳細的注釋掸绞,這里我們需要關(guān)注_cachedCells, _sections, _reusableCells 這三個變量的作用泵三。

設(shè)置數(shù)據(jù)源

tableView.dataSource = self;

下面是 dataSrouce 的 setter 方法源碼:

- (void)setDataSource:(id<UITableViewDataSource>)newSource
{
    _dataSource = newSource;

    _dataSourceHas.numberOfSectionsInTableView = [_dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)];
    _dataSourceHas.titleForHeaderInSection = [_dataSource respondsToSelector:@selector(tableView:titleForHeaderInSection:)];
    _dataSourceHas.titleForFooterInSection = [_dataSource respondsToSelector:@selector(tableView:titleForFooterInSection:)];
    _dataSourceHas.commitEditingStyle = [_dataSource respondsToSelector:@selector(tableView:commitEditingStyle:forRowAtIndexPath:)];
    _dataSourceHas.canEditRowAtIndexPath = [_dataSource respondsToSelector:@selector(tableView:canEditRowAtIndexPath:)];
    
    [self _setNeedsReload];
}

_dataSourceHas 是用于記錄該數(shù)據(jù)源實現(xiàn)了哪些協(xié)議方法的結(jié)構(gòu)體,該結(jié)構(gòu)體源碼如下:

struct {
        unsigned numberOfSectionsInTableView : 1;
        unsigned titleForHeaderInSection : 1;
        unsigned titleForFooterInSection : 1;
        unsigned commitEditingStyle : 1;
        unsigned canEditRowAtIndexPath : 1;
    } _dataSourceHas;

記錄是否實現(xiàn)了某協(xié)議可以使用布爾值來表示衔掸,布爾變量占用的內(nèi)存大小一般為一個字節(jié)烫幕,即8比特。但該結(jié)構(gòu)體使用了 bitfields 用一個比特(0或1)來記錄是否實現(xiàn)了某協(xié)議敞映,大大縮小了占用的內(nèi)存较曼。
在設(shè)置好了數(shù)據(jù)源后需要打一個標(biāo)記,告訴NSRunLoop數(shù)據(jù)源已經(jīng)設(shè)置好了振愿,需要在下一次循環(huán)中使用數(shù)據(jù)源進行布局捷犹。下面看看 _setNeedReload 的源碼:

- (void)_setNeedsReload
{
    _needsReload = YES;
    [self setNeedsLayout];
}

在調(diào)用了 setNeedsLayout 方法后,NSRunloop 會在下一次循環(huán)中自動調(diào)用 layoutSubViews 方法冕末。

  • 視圖的內(nèi)容需要重繪時可以調(diào)用 setNeedsDisplay 方法萍歉,該方法會設(shè)置該視圖的 displayIfNeeded 變量為 YES ,NSRunLoop 在下一次循環(huán)檢中測到該值為 YES 則會自動調(diào)用 drawRect 進行重繪档桃。
  • 視圖的內(nèi)容沒有變化枪孩,但在父視圖中位置變化了可以調(diào)用 setNeedsLayout,該方法會設(shè)置該視圖的 layoutIfNeeded 變量為YES,NSRunLoop 在下一次循環(huán)檢中測到該值為 YES 則會自動調(diào)用 layoutSubViews 進行重繪。
  • 更詳細的內(nèi)容可參考 When is layoutSubviews called?

設(shè)置代理

tableView.delegate = self;

下面是 delegate 的 setter 方法源碼:

- (void)setDelegate:(id<UITableViewDelegate>)newDelegate
{
    [super setDelegate:newDelegate];
    _delegateHas.heightForRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)];
    _delegateHas.heightForHeaderInSection = [newDelegate respondsToSelector:@selector(tableView:heightForHeaderInSection:)];
    _delegateHas.heightForFooterInSection = [newDelegate respondsToSelector:@selector(tableView:heightForFooterInSection:)];
    _delegateHas.viewForHeaderInSection = [newDelegate respondsToSelector:@selector(tableView:viewForHeaderInSection:)];
    _delegateHas.viewForFooterInSection = [newDelegate respondsToSelector:@selector(tableView:viewForFooterInSection:)];
    _delegateHas.willSelectRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:willSelectRowAtIndexPath:)];
    _delegateHas.didSelectRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)];
    _delegateHas.willDeselectRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:willDeselectRowAtIndexPath:)];
    _delegateHas.didDeselectRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:didDeselectRowAtIndexPath:)];
    _delegateHas.willBeginEditingRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:willBeginEditingRowAtIndexPath:)];
    _delegateHas.didEndEditingRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:didEndEditingRowAtIndexPath:)];
    _delegateHas.titleForDeleteConfirmationButtonForRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:titleForDeleteConfirmationButtonForRowAtIndexPath:)];
}

與設(shè)置數(shù)據(jù)源一樣蔑舞,這里使用了類似的結(jié)構(gòu)體來記錄代理實現(xiàn)了哪些協(xié)議方法拒担。

UITableView繪制

由于在設(shè)置數(shù)據(jù)源中調(diào)用了 setNeedsLayout 方法打上了需要布局的 flag,所以會在 1/60 秒(NSRunLoop的循環(huán)周期)后自動調(diào)用 layoutSubViews斗幼。layoutSubViews 的源碼如下:

- (void)layoutSubviews
{
    //對子視圖進行布局澎蛛,該方法會在第一次設(shè)置數(shù)據(jù)源調(diào)用 setNeedsLayout 方法后自動調(diào)用。
    //并且 UITableView 是繼承自 UIScrollview 蜕窿,當(dāng)滾動時也會觸發(fā)該方法的調(diào)用
    _backgroundView.frame = self.bounds;
    
    //在進行布局前必須確保 section 已經(jīng)緩存了所有高度相關(guān)的信息
    [self _reloadDataIfNeeded]; 
    
    //對 UITableView 的 section 進行布局谋逻,包含 section 的頭部,尾部桐经,每一行 Cell
    [self _layoutTableView];
    
    //對 UITableView 的頭視圖毁兆,尾視圖進行布局
    [super layoutSubviews];
}

需要注意的是由于 UITableView 是繼承于 UIScrollView,所以在 UITableView 滾動時會自動調(diào)用該方法阴挣,詳細內(nèi)容可以參考 When is layoutSubviews called?

下面依次來看三個主要方法的實現(xiàn)气堕。
_reloadDataIfNeeded 的源碼如下

- (void)_reloadDataIfNeeded
{
    if (_needsReload) {
        [self reloadData];
    }
}

- (void)reloadData
{
    //當(dāng)數(shù)據(jù)源更新后,需要將所有顯示的UITableViewCell和未顯示可復(fù)用的UITableViewCell全部從父視圖移除畔咧,
    //重新創(chuàng)建
    [[_cachedCells allValues] makeObjectsPerformSelector:@selector(removeFromSuperview)];
    [_reusableCells makeObjectsPerformSelector:@selector(removeFromSuperview)];
    [_reusableCells removeAllObjects];
    [_cachedCells removeAllObjects];

    _selectedRow = nil;
    _highlightedRow = nil;
    
    // 重新計算 section 相關(guān)的高度值茎芭,并緩存起來
    [self _updateSectionsCache];
    [self _setContentSize];
    
    _needsReload = NO;
}

其中 _updateSectionsCashe 方法是最重要的,該方法在數(shù)據(jù)源更新后至下一次數(shù)據(jù)源更新期間只能調(diào)用一次誓沸,該方法的源碼如下:

- (void)_updateSectionsCache
{
    //該逆向源碼只復(fù)用了 section 中的每個 UITableViewCell梅桩,并沒有復(fù)用每個 section 的頭視圖和尾視圖,
    //UIKit肯定是實現(xiàn)了所有視圖的復(fù)用
    // remove all previous section header/footer views
    for (UITableViewSection *previousSectionRecord in _sections) {
        [previousSectionRecord.headerView removeFromSuperview];
        [previousSectionRecord.footerView removeFromSuperview];
    }
    
    // clear the previous cache
    [_sections removeAllObjects];
    
    //如果數(shù)據(jù)源為空拜隧,不做任何處理
    if (_dataSource) {
        // compute the heights/offsets of everything
        const CGFloat defaultRowHeight = _rowHeight ?: _UITableViewDefaultRowHeight;
        const NSInteger numberOfSections = [self numberOfSections];
        for (NSInteger section=0; section<numberOfSections; section++) {
            const NSInteger numberOfRowsInSection = [self numberOfRowsInSection:section];
            
            UITableViewSection *sectionRecord = [[UITableViewSection alloc] init];
            sectionRecord.headerTitle = _dataSourceHas.titleForHeaderInSection? [self.dataSource tableView:self titleForHeaderInSection:section] : nil;
            sectionRecord.footerTitle = _dataSourceHas.titleForFooterInSection? [self.dataSource tableView:self titleForFooterInSection:section] : nil;
            
            sectionRecord.headerHeight = _delegateHas.heightForHeaderInSection? [self.delegate tableView:self heightForHeaderInSection:section] : _sectionHeaderHeight;
            sectionRecord.footerHeight = _delegateHas.heightForFooterInSection ? [self.delegate tableView:self heightForFooterInSection:section] : _sectionFooterHeight;

            sectionRecord.headerView = (sectionRecord.headerHeight > 0 && _delegateHas.viewForHeaderInSection)? [self.delegate tableView:self viewForHeaderInSection:section] : nil;
            sectionRecord.footerView = (sectionRecord.footerHeight > 0 && _delegateHas.viewForFooterInSection)? [self.delegate tableView:self viewForFooterInSection:section] : nil;

            // make a default section header view if there's a title for it and no overriding view
            if (!sectionRecord.headerView && sectionRecord.headerHeight > 0 && sectionRecord.headerTitle) {
                sectionRecord.headerView = [UITableViewSectionLabel sectionLabelWithTitle:sectionRecord.headerTitle];
            }
            
            // make a default section footer view if there's a title for it and no overriding view
            if (!sectionRecord.footerView && sectionRecord.footerHeight > 0 && sectionRecord.footerTitle) {
                sectionRecord.footerView = [UITableViewSectionLabel sectionLabelWithTitle:sectionRecord.footerTitle];
            }

            if (sectionRecord.headerView) {
                [self addSubview:sectionRecord.headerView];
            } else {
                sectionRecord.headerHeight = 0;
            }
            
            if (sectionRecord.footerView) {
                [self addSubview:sectionRecord.footerView];
            } else {
                sectionRecord.footerHeight = 0;
            }
            
            //section 中每個 row 的高度使用了數(shù)組指針來保存
            CGFloat *rowHeights = malloc(numberOfRowsInSection * sizeof(CGFloat));
            CGFloat totalRowsHeight = 0;
            
            //每行 row 的高度通過數(shù)據(jù)源實現(xiàn)的協(xié)議方法 heightForRowAtIndexPath: 返回宿百,
            //若數(shù)據(jù)源沒有實現(xiàn)該協(xié)議方法則使用默認的高度
            for (NSInteger row=0; row<numberOfRowsInSection; row++) {
                const CGFloat rowHeight = _delegateHas.heightForRowAtIndexPath? [self.delegate tableView:self heightForRowAtIndexPath:[NSIndexPath indexPathForRow:row inSection:section]] : defaultRowHeight;
                rowHeights[row] = rowHeight;
                totalRowsHeight += rowHeight;
            }
            
            sectionRecord.rowsHeight = totalRowsHeight;
            [sectionRecord setNumberOfRows:numberOfRowsInSection withHeights:rowHeights];
            free(rowHeights);
            
            //將所有高度信息緩存起來
            [_sections addObject:sectionRecord];
        }
    }
}

我在需要注意的地方加了注釋,上面方法主要是記錄每個 Cell 的高度和整個 section 的高度洪添,并把結(jié)果同過 UITableViewSection 對象緩存起來垦页。

_layoutTableView 的源碼實現(xiàn)如下:

- (void)_layoutTableView
{
    //這里實現(xiàn)了 UITableViewCell 的復(fù)用
    const CGSize boundsSize = self.bounds.size;
    const CGFloat contentOffset = self.contentOffset.y;
    
    //由于 UITableView 繼承于 UIScrollview,所以通過滾動偏移量得到當(dāng)前可視的 bounds
    const CGRect visibleBounds = CGRectMake(0,contentOffset,boundsSize.width,boundsSize.height);
    CGFloat tableHeight = 0;
    
    //若有頭部視圖干奢,則計算頭部視圖在父視圖中的 frame
    if (_tableHeaderView) {
        CGRect tableHeaderFrame = _tableHeaderView.frame;
        tableHeaderFrame.origin = CGPointZero;
        tableHeaderFrame.size.width = boundsSize.width;
        _tableHeaderView.frame = tableHeaderFrame;
        tableHeight += tableHeaderFrame.size.height;
    }
    
    //_cashedCells 用于記錄正在顯示的 UITableViewCell 的引用
    //avaliableCells 用于記錄當(dāng)前正在顯示但在滾動后不再顯示的 UITableViewCell(該 Cell 可以復(fù)用)
    //在滾動后將該字典中的所有數(shù)據(jù)都添加到 _reusableCells 中,
    //記錄下所有當(dāng)前在可視但由于滾動而變得不再可視的 Cell 的引用
    NSMutableDictionary *availableCells = [_cachedCells mutableCopy];
    const NSInteger numberOfSections = [_sections count];
    [_cachedCells removeAllObjects];
    
    for (NSInteger section=0; section<numberOfSections; section++) {
        CGRect sectionRect = [self rectForSection:section];
        tableHeight += sectionRect.size.height;
        //CGRectIntersectsRect 方法用于判斷兩個 rect 是否有相交痊焊,只處理在當(dāng)前可視 bounds 內(nèi)的 section
        if (CGRectIntersectsRect(sectionRect, visibleBounds)) {
            const CGRect headerRect = [self rectForHeaderInSection:section];
            const CGRect footerRect = [self rectForFooterInSection:section];
            UITableViewSection *sectionRecord = [_sections objectAtIndex:section];
            const NSInteger numberOfRows = sectionRecord.numberOfRows;
            
            if (sectionRecord.headerView) {
                sectionRecord.headerView.frame = headerRect;
            }
            
            if (sectionRecord.footerView) {
                sectionRecord.footerView.frame = footerRect;
            }
            
            for (NSInteger row=0; row<numberOfRows; row++) {
                NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
                CGRect rowRect = [self rectForRowAtIndexPath:indexPath];
                //只處理在當(dāng)前可視 bounds 內(nèi)的 row
                if (CGRectIntersectsRect(rowRect,visibleBounds) && rowRect.size.height > 0) {
                //在滾動時,如果向上滾動律胀,除去頂部要隱藏的 Cell 和底部要顯示的 Cell宋光,中部的 Cell 都可以
                //根據(jù) indexPath 直接獲取
                    UITableViewCell *cell = [availableCells objectForKey:indexPath] ?: [self.dataSource tableView:self cellForRowAtIndexPath:indexPath];
                    if (cell) {
                        [_cachedCells setObject:cell forKey:indexPath];
                        
                        //將當(dāng)前仍留在可視區(qū)域的 Cell 從 availableCells 中移除,
                        //availableCells 中剩下的即為頂部已經(jīng)隱藏的 Cell
                        //后面會將該 Cell 加入 _reusableCells 中以便下次取出進行復(fù)用炭菌。
                        [availableCells removeObjectForKey:indexPath];
                        
                        cell.highlighted = [_highlightedRow isEqual:indexPath];
                        cell.selected = [_selectedRow isEqual:indexPath];
                        cell.frame = rowRect;
                        cell.backgroundColor = self.backgroundColor;
                        [cell _setSeparatorStyle:_separatorStyle color:_separatorColor];
                        [self addSubview:cell];
                    }
                }
            }
        }
    }

    //把所有因滾動而不再可視的 Cell 從父視圖移除并加入 _reusableCells 中,以便下次取出復(fù)用
    for (UITableViewCell *cell in [availableCells allValues]) {
        if (cell.reuseIdentifier) {
            [_reusableCells addObject:cell];
        } else {
            [cell removeFromSuperview];
        }
    }

    //把仍在可視區(qū)域的 Cell(但不應(yīng)該在父視圖上顯示) 但已經(jīng)被回收至可復(fù)用的 _reusableCells 中的 Cell從父視圖移除
    NSArray* allCachedCells = [_cachedCells allValues];
    for (UITableViewCell *cell in _reusableCells) {
        if (CGRectIntersectsRect(cell.frame,visibleBounds) && ![allCachedCells containsObject: cell]) {
            [cell removeFromSuperview];
        }
    }
    
    if (_tableFooterView) {
        CGRect tableFooterFrame = _tableFooterView.frame;
        tableFooterFrame.origin = CGPointMake(0,tableHeight);
        tableFooterFrame.size.width = boundsSize.width;
        _tableFooterView.frame = tableFooterFrame;
    }
}

關(guān)于 UIView 的 frame 和bounds 的區(qū)別可以參考 What's the difference between the frame and the bounds?

這里使用了三個容器 _cachedCells, availableCells, _reusableCells 完成了 Cell 的復(fù)用逛漫,這是 UITableView 最核心的地方黑低。
下面一起看看三個容器在創(chuàng)建到滾動整個過程中所包含的元素的變化情況。
在第一次設(shè)置了數(shù)據(jù)源調(diào)用該方法時,三個容器的內(nèi)容都為空克握,在調(diào)用完該方法后 _cachedCells 包含了當(dāng)前所有可視 Cell 與其對應(yīng)的indexPath 的鍵值對蕾管,availableCells 與 _reusableCells 仍然為空。只有在滾動起來后 _reusableCells 中才會出現(xiàn)多余的未顯示可復(fù)用的 Cell菩暗。

  • 剛創(chuàng)建 UITableView 時的狀態(tài)如下圖(紅色為屏幕內(nèi)容即可視區(qū)域掰曾,藍色為超出屏幕的內(nèi)容,即不可視區(qū)域):


    初始狀態(tài).png

    如圖停团,當(dāng)前 _cachedCells 的元素為當(dāng)前可視的所有 Cell 與其對應(yīng)的 indexPath 的鍵值對旷坦。

  • 向上滾動一個 Cell 的過程中,由于 availableCells 為 _cachedCells 的拷貝佑稠,所以可根據(jù) indexPath 直接取到對應(yīng)的 Cell秒梅,這時從底部滾上來的第7行,由于之前的 _reusableCells 為空舌胶,所以該 Cell 是直接創(chuàng)建的而并非復(fù)用的捆蜀,由于頂部 Cell 滾動出了可視區(qū)域,所以被加入了 _reusableCells 中以便后續(xù)滾動復(fù)用幔嫂。滾動完一行后的狀態(tài)變?yōu)榱?_cachedCells 包含第 2 行到第 7 行 Cell 的引用辆它,_reusableCells 包含第一行 之前滾動出可視區(qū)域的第一行 Cell 的引用。


    向上滾動1個Cell.png
  • 當(dāng)向上滾動兩個 Cell 的過程中履恩,同理第 3 行到第 7 行的 Cell 可以通過對應(yīng)的 indexPath 從 _cachedCells 中獲取锰茉。這時 _reusableCells 中正好有一個可以復(fù)用的 Cell 用來從底部滾動上來的第 8 行。滾動出頂部的第 2 行 Cell 被加入 _reusableCells 中似袁。


    向上滾動2個Cell.png

總結(jié)

到此你已經(jīng)了解了 UITableView 的 Cell 的復(fù)用原理洞辣,可以根據(jù)需要定制出更復(fù)雜的控件。


歡迎關(guān)注我的簡書昙衅,我會定期做一些技術(shù)分享:)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末扬霜,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子而涉,更是在濱河造成了極大的恐慌著瓶,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件啼县,死亡現(xiàn)場離奇詭異材原,居然都是意外死亡,警方通過查閱死者的電腦和手機季眷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門余蟹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人子刮,你說我怎么就攤上這事威酒∫ふ觯” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵葵孤,是天一觀的道長担钮。 經(jīng)常有香客問我,道長尤仍,這世上最難降的妖魔是什么箫津? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮宰啦,結(jié)果婚禮上苏遥,老公的妹妹穿的比我還像新娘。我一直安慰自己绑莺,他們只是感情好暖眼,可當(dāng)我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著纺裁,像睡著了一般诫肠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上欺缘,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天栋豫,我揣著相機與錄音,去河邊找鬼谚殊。 笑死丧鸯,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的嫩絮。 我是一名探鬼主播丛肢,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼剿干!你這毒婦竟也來了蜂怎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤置尔,失蹤者是張志新(化名)和其女友劉穎杠步,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體榜轿,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡幽歼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年阱飘,在試婚紗的時候發(fā)現(xiàn)自己被綠了砸琅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡硝岗,死狀恐怖飞傀,靈堂內(nèi)的尸體忽然破棺而出颠蕴,到底是詐尸還是另有隱情泣刹,我是刑警寧澤助析,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布犀被,位于F島的核電站,受9級特大地震影響外冀,放射性物質(zhì)發(fā)生泄漏寡键。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一雪隧、第九天 我趴在偏房一處隱蔽的房頂上張望西轩。 院中可真熱鬧,春花似錦脑沿、人聲如沸藕畔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽注服。三九已至,卻和暖如春措近,著一層夾襖步出監(jiān)牢的瞬間溶弟,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工瞭郑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留辜御,地道東北人。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓屈张,卻偏偏與公主長得像擒权,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子阁谆,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,802評論 2 345

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

  • 簡介 在一個偶然的機會看到一位大神講解UIKitUITableView Cell復(fù)用原理碳抄。大神zongmumask...
    風(fēng)與鸞閱讀 2,245評論 0 3
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結(jié)起來就是把...
    Dove_iOS閱讀 27,125評論 29 470
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件笛厦、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,024評論 4 62
  • 這一周是本學(xué)期的第八周纳鼎,輪到第八組主持主題班會。按照學(xué)校安排裳凸,本周班會主題是“誠信”贱鄙。第八組結(jié)合下周的期中考試...
    玫蘭妮閱讀 286評論 0 0
  • 我有好幾個堂哥,他們都出門在外開起了飯店姨谷,他們經(jīng)常在微信群里談?wù)撟罱纳夂脡亩耗務(wù)摬藘r和房租,談?wù)撋畹钠D辛和不...
    奶牛炮筒閱讀 3,944評論 8 38