IGListKit中的diff算法詳解

近期浮毯,我們項(xiàng)目里面引入了IGListKit的第三方庫拳球,它是對(duì)collectionView的一層封裝倦西,主要用于feed流的實(shí)現(xiàn),它的其中一個(gè)優(yōu)勢(shì)就是刷新視圖的時(shí)候并不是刷新的整個(gè)collectionView旅急,而是通過diff算法算出新老數(shù)組的差異逢勾,根據(jù)這個(gè)差異collectionView進(jìn)行部分更新牡整,這個(gè)更新的邏輯在UICollectionView+IGListBatchUpdateData.m這個(gè)分類中藐吮,函數(shù)如下:

- (void)ig_applyBatchUpdateData:(IGListBatchUpdateData *)updateData {
    [self deleteItemsAtIndexPaths:updateData.deleteIndexPaths];
    [self insertItemsAtIndexPaths:updateData.insertIndexPaths];

    for (IGListMoveIndexPath *move in updateData.moveIndexPaths) {
        [self moveItemAtIndexPath:move.from toIndexPath:move.to];
    }

    for (IGListMoveIndex *move in updateData.moveSections) {
        [self moveSection:move.from toSection:move.to];
    }

    [self deleteSections:updateData.deleteSections];
    [self insertSections:updateData.insertSections];
}

這個(gè)函數(shù)會(huì)在-performBatchUpdates:completion:batchUpdatesBlock中被調(diào)用√颖矗可以看出谣辞,每次更新只會(huì)涉及到部分視圖的插入、刪除沐扳、移動(dòng)泥从,非常高效。下面分析這個(gè)diff算法是如何將這類差異算出來的沪摄。

前置工作

diff函數(shù)簡(jiǎn)化

整個(gè)diff算法相關(guān)的流程都放在IGListDiff.mm這個(gè)類里了躯嫉,其核心的函數(shù)的聲明如下:

static id IGListDiffing(BOOL returnIndexPaths,
                        NSInteger fromSection,
                        NSInteger toSection,
                        NSArray<id<IGListDiffable>> *oldArray,
                        NSArray<id<IGListDiffable>> *newArray,
                        IGListDiffOption option,
                        IGListExperiment experiments)

這個(gè)函數(shù)參數(shù)有點(diǎn)多,而實(shí)際上核心的兩個(gè)參數(shù)是oldArraynewArray杨拐,returnIndexPaths在一般情況下傳NO祈餐,可以用NO代替,而fromSectiontoSection在分析算法中可以刪掉(默認(rèn)在同一個(gè)section上操作)option一般傳IGListDiffEquality,因此可以用IGListDiffEquality代替哄陶,而experiments整個(gè)流程都沒用到因此可以直接刪除帆阳,經(jīng)過一番代碼替換/刪除之后,這個(gè)函數(shù)的聲明就簡(jiǎn)化成了

static id IGListDiffing(NSArray<id<IGListDiffable>> *oldArray,
                        NSArray<id<IGListDiffable>> *newArray)

相關(guān)函數(shù)/結(jié)構(gòu)體/方法介紹

IGListIndexSetResult

這是函數(shù)的返回值(returnIndexPathsNO的時(shí)候)其結(jié)構(gòu)如下

NS_SWIFT_NAME(ListIndexSetResult)
@interface IGListIndexSetResult : NSObject
///插入索引的集合(新數(shù)組的索引)
@property (nonatomic, strong, readonly) NSIndexSet *inserts;
///刪除索引的集合(舊數(shù)組的索引)
@property (nonatomic, strong, readonly) NSIndexSet *deletes;
///更新索引的集合(舊數(shù)組的索引)
@property (nonatomic, strong, readonly) NSIndexSet *updates;
///移動(dòng)索引的集合(from是舊數(shù)組的索引屋吨,to是新數(shù)組的索引)
@property (nonatomic, copy, readonly) NSArray<IGListMoveIndex *> *moves;
///是否改變過
@property (nonatomic, assign, readonly) BOOL hasChanges;

@end

NS_ASSUME_NONNULL_END

最后返回的結(jié)果需要給inserts蜒谤、deletesupdates至扰、moves賦值返回(初始化方法在IGListIndexSetResultInternal.h里面)

IGListMoveIndex

封裝一個(gè)移動(dòng)的操作鳍徽,其結(jié)構(gòu)如下:

NS_ASSUME_NONNULL_BEGIN

NS_SWIFT_NAME(ListMoveIndex)
@interface IGListMoveIndex : NSObject

@property (nonatomic, assign, readonly) NSInteger from;

@property (nonatomic, assign, readonly) NSInteger to;

@end

專門的初始化方法在IGListMoveIndexInternal.h里面

IGListDiffable

一個(gè)協(xié)議,數(shù)組里的對(duì)象都需要遵循這個(gè)協(xié)議才能有效地使用diff函數(shù)

NS_SWIFT_NAME(ListDiffable)
@protocol IGListDiffable

//返回對(duì)象唯一id敢课,在diff算法中以它作為元素存入哈希表的key
- (nonnull id<NSObject>)diffIdentifier;

//判斷兩個(gè)對(duì)象是否相等旬盯,在diff算法用這個(gè)方法判斷兩個(gè)對(duì)象是否是同一個(gè)對(duì)象
- (BOOL)isEqualToDiffableObject:(nullable id<IGListDiffable>)object;

@end

在IGListKit中,NSStringNSNumber默認(rèn)遵循了這個(gè)協(xié)議

IGListEntry

diff算法中用于標(biāo)記元素狀態(tài)的結(jié)構(gòu)體

struct IGListEntry {
    該元素在舊數(shù)組中出現(xiàn)的次數(shù)
    NSInteger oldCounter = 0;
    該元素在新數(shù)組中出現(xiàn)的次數(shù)
    NSInteger newCounter = 0;
    存放元素在舊數(shù)組中的索引,在算法中胖翰,可以保證棧頂是較小的索引
    stack<NSInteger> oldIndexes;
    這個(gè)元素是否需要更新
    BOOL updated = NO;
};

IGListRecord

封裝entry和它所在的索引接剩,主要用于插入和刪除(如果index有值,則代表該元素需要插入或者刪除萨咳,否則為NSNotFound

struct IGListRecord {
    IGListEntry *entry;
    mutable NSInteger index;
    
    IGListRecord() {
        entry = NULL;
        index = NSNotFound;
    }
};

其它工具函數(shù)

還有其它的一些函數(shù)懊缺,在section/useIndexPath這些參數(shù)去掉之后,變得沒那么復(fù)雜了培他,下面統(tǒng)一列出來

///取元素在哈希表中的key鹃两,這里取的就是diffIdentifier
static id<NSObject> IGListTableKey(__unsafe_unretained id<IGListDiffable> object) {
    id<NSObject> key = [object diffIdentifier];
    NSCAssert(key != nil, @"Cannot use a nil key for the diffIdentifier of object %@", object);
    return key;
}

///判斷兩個(gè)值是否相等,這個(gè)函數(shù)在建無序哈希表的時(shí)候用到
struct IGListEqualID {
    bool operator()(const id a, const id b) const {
        return (a == b) || [a isEqual: b];
    }
};

///求元素的哈希值舀凛,這個(gè)函數(shù)在建無序哈希表的時(shí)候用到
struct IGListHashID {
    size_t operator()(const id o) const {
        return (size_t)[o hash];
    }
};

///給集合增加索引
static void addIndexToCollection( __unsafe_unretained id collection,NSInteger index) {
    [collection addIndex:index];
};

///向哈希表增加索引
static void addIndexToMap( NSInteger index, __unsafe_unretained id<IGListDiffable> object, __unsafe_unretained NSMapTable *map) {
    id value;
    value = @(index);

    [map setObject:value forKey:[object diffIdentifier]];
}

IGListDiffing函數(shù)的算法流程

下面開始逐步剖析IGListDiffing這個(gè)函數(shù)

變量的聲明

    const NSInteger newCount = newArray.count;
    const NSInteger oldCount = oldArray.count;
    
    NSMapTable *oldMap = [NSMapTable strongToStrongObjectsMapTable];
    NSMapTable *newMap = [NSMapTable strongToStrongObjectsMapTable];
    
    unordered_map<id<NSObject>, IGListEntry, IGListHashID, IGListEqualID> table;

newCount俊扳,oldCount方便后面使用,table是后面初始化的哈希表猛遍,為了方便講解把它挪到前面來馋记,它以diffIdentifier為鍵,entry為值懊烤,其查找復(fù)雜度為o(1)梯醒。而oldMapnewMap并不參與這個(gè)diff算法,它們到最后就是已數(shù)組的index為key,數(shù)組的元素為值的哈希表而已腌紧。不過因?yàn)閮?yōu)化算法(減少循環(huán)的次數(shù))而把它的初始化操作寫到diff算法的循環(huán)里面茸习。把初始化操作拎出來就是

     for (NSInteger i = 0; i < oldCount; i++) {
        addIndexToMap(i, oldArray[i], oldMap);
    }
    for (NSInteger i = 0; i < newCount; i++) {
        addIndexToMap( i, newArray[i], newMap);
    }

處理特殊情況

如果newCountoldCount為0,則可以判斷為刪除所有舊元素或者增加所有新元素,就不需要走diff算法了

    if (newCount == 0) {
            [oldArray enumerateObjectsUsingBlock:^(id<IGListDiffable> obj, NSUInteger idx, BOOL *stop) {
                addIndexToMap( idx, obj, oldMap);
            }];
            return [[IGListIndexSetResult alloc] initWithInserts:[NSIndexSet new]
                                                         deletes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, oldCount)]
                                                         updates:[NSIndexSet new]
                                                           moves:[NSArray new]
                                                     oldIndexMap:oldMap
                                                     newIndexMap:newMap];
        
    }
    
    if (oldCount == 0) {
            [newArray enumerateObjectsUsingBlock:^(id<IGListDiffable> obj, NSUInteger idx, BOOL *stop) {
                addIndexToMap(idx, obj, newMap);
            }];
            return [[IGListIndexSetResult alloc] initWithInserts:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, newCount)]
                                                         deletes:[NSIndexSet new]
                                                         updates:[NSIndexSet new]
                                                           moves:[NSArray new]
                                                     oldIndexMap:oldMap
                                                     newIndexMap:newMap];
        
    }

diff算法Step1

遍歷新數(shù)組,為每個(gè)新數(shù)組的元素創(chuàng)建一個(gè)entry壁肋,并增加entrynewCounter

    vector<IGListRecord> newResultsArray(newCount);
    for (NSInteger i = 0; i < newCount; i++) {
        id<NSObject> key = IGListTableKey(newArray[i]);
        IGListEntry &entry = table[key];
        entry.newCounter++;
        
        //增加NSNotFound是為了防止oldIndexed為空号胚,NSNotFound相當(dāng)于棧底的標(biāo)志位
        entry.oldIndexes.push(NSNotFound);
        
        
        newResultsArray[i].entry = &entry;
    }

需要注意的是IGListEntry &entry = table[key]這句代碼返回的是entry的地址(如果沒有table里沒有這個(gè)key就創(chuàng)建),如果數(shù)組中有相同的key的時(shí)候,newResultsArray存放的索引中的entry會(huì)指向同一個(gè)地址浸遗。

這一步過后猫胁,會(huì)建立一個(gè)用于存放IGListRecordnewResultsArray,每個(gè)IGListRecord的index仍未NSNotFound乙帮,entry為新創(chuàng)建的IGListEntry杜漠,其newCounter都是大于0的。

diff算法Step2

遍歷舊數(shù)組察净,為每個(gè)舊數(shù)組的元素創(chuàng)建entry驾茴,并增加它們的oldCounter,將對(duì)應(yīng)的索引壓入oldIndexes棧中氢卡。

    vector<IGListRecord> oldResultsArray(oldCount);
    for (NSInteger i = oldCount - 1; i >= 0; i--) {
        id<NSObject> key = IGListTableKey(oldArray[i]);
        IGListEntry &entry = table[key];
        entry.oldCounter++;
        
        // 將i入棧
        entry.oldIndexes.push(i);
        
        oldResultsArray[i].entry = &entry;
    }

這里的循環(huán)采用倒序的方式锈至,在多個(gè)key相同的時(shí)候,oldIndexes會(huì)有一系列的索引壓棧译秦,倒序就會(huì)確保棧頂?shù)乃饕亲钚〉摹?/p>

這一步過后峡捡,會(huì)建立一個(gè)用于存放IGListRecordoldResultsArray击碗,每個(gè)IGListRecord的index仍未NSNotFound,對(duì)于oldResultsArraynewResultsArray其中的entry们拙,分三種情況:

  • 該元素只有新數(shù)組有稍途,則entrynewCounter>0,oldCounter=0,oldIndexes棧頂為NSNotFound
  • 該元素只有舊數(shù)組有砚婆,則entrynewCounter=0械拍,oldCounter>0,oldIndexes棧頂不為NSNotFound,而是元素在舊數(shù)組中的最小索引
  • 該元素新舊數(shù)組有装盯,則entrynewCounter>0坷虑,oldCounter>0,oldIndexes棧頂不為NSNotFound,而是元素在舊數(shù)組中的最小索引,而oldResultsArraynewResultsArray都指向同一個(gè)entry

diff算法Step3

遍歷新數(shù)組埂奈,新舊數(shù)組都出現(xiàn)的元素迄损,其IGListRecord的index會(huì)賦上其在新/舊數(shù)組的索引

    for (NSInteger i = 0; i < newCount; i++) {
        IGListEntry *entry = newResultsArray[i].entry;
        NSCAssert(!entry->oldIndexes.empty(), @"Old indexes is empty while iterating new item %li. Should have NSNotFound", (long)i);
        ///拿到oldIndexes的棧頂,也就是拿到改元素在oldArray的第一個(gè)索引账磺,然后pop出來
        const NSInteger originalIndex = entry->oldIndexes.top();
        entry->oldIndexes.pop();
        
        if (originalIndex < oldCount) {
            const id<IGListDiffable> n = newArray[i];
            const id<IGListDiffable> o = oldArray[originalIndex];
            if (n != o && ![n isEqualToDiffableObject:o]) {
            //標(biāo)記為update的條件芹敌,只有在key相同且n和o不一樣且isEqualToDiffableObject不相同的時(shí)候
            //才會(huì)走進(jìn)這個(gè)條件
              entry->updated = YES;
            }
        }
        //給兩邊的index賦上對(duì)應(yīng)的索引,如果originalIndex是NSNotFound绑谣,則不會(huì)走到這個(gè)條件
        if (originalIndex != NSNotFound
            && entry->newCounter > 0
            && entry->oldCounter > 0) {
            
            newResultsArray[i].index = originalIndex;
            oldResultsArray[originalIndex].index = i;
        }
    }

PS: entry->updated = YES這個(gè)條件很難觸發(fā)党窜,而且觸發(fā)了也沒看出什么作用拗引,在前面的- (void)ig_applyBatchUpdateData:(IGListBatchUpdateData *)updateData中借宵,是沒有reload這個(gè)操作的,究其原因矾削,在前面_flushCollectionView的方法里面為了規(guī)避一個(gè)bug而將update的操作統(tǒng)一換成delete和insert了壤玫。

這一步主要的作用在于最后,這一步過后哼凯,如果一個(gè)元素兩邊的數(shù)組都存在欲间,newResultsArray中對(duì)應(yīng)的元素的index就會(huì)指向該元素在oldArray中的索引,oldResultsArray對(duì)應(yīng)的元素的index就會(huì)指向該元素在newArray中的索引断部。這個(gè)賦值主要是用于統(tǒng)計(jì)移動(dòng)元素的操作猎贴。

如果newArrayoldArray中又相同的元素,且出現(xiàn)了數(shù)次會(huì)怎么樣呢蝴光?在實(shí)際的IGListKit的使用中一般會(huì)規(guī)避這種情況她渴。如果真的發(fā)生了,分析這一步的算法不難發(fā)現(xiàn):該元素在oldArray中的第i次出現(xiàn)的索引會(huì)跟在newArray中的第i次出現(xiàn)的索引相匹配蔑祟,這種算法得出來的結(jié)果并不是最佳的趁耗,這個(gè)在后面講。

diff算法Step4

接下來就是增刪改查數(shù)組的生成了疆虚,為了優(yōu)化算法苛败,IGListKit把這些算法都放在兩個(gè)循環(huán)里满葛,這里為了方便理解將其拆開。

首先罢屈,定義對(duì)應(yīng)的數(shù)組

    id mInserts, mMoves, mUpdates, mDeletes;

    mInserts = [NSMutableIndexSet new];
    mUpdates = [NSMutableIndexSet new];
    mDeletes = [NSMutableIndexSet new];
    mMoves = [NSMutableArray<IGListMoveIndex *> new];

delete數(shù)組的生成

    for (NSInteger i = 0; i < oldCount; i++) {
        const IGListRecord record = oldResultsArray[i];
        if (record.index == NSNotFound) {
            addIndexToCollection( mDeletes, i);
        }
    }

很好理解嘀韧,通過上面的操作,如果oldResultsArrayindex還是NSNotFound缠捌,則說明newArray中沒有這個(gè)元素乳蛾,就代表需要?jiǎng)h除。

insert數(shù)組的生成

    for (NSInteger i = 0; i < newCount; i++) {
        const IGListRecord record = newResultsArray[i];
        if (record.index == NSNotFound) {
            addIndexToCollection(mInserts, i);
        } 
    }

這個(gè)也很好理解鄙币,通過上面的操作肃叶,如果newResultsArrayindex還是NSNotFound,則說明oldArray中沒有這個(gè)元素十嘿,就代表需要添加因惭。

update數(shù)組的生成

    for (NSInteger i = 0; i < newCount; i++) {
        const IGListRecord record = newResultsArray[i];
        const NSInteger oldIndex = record.index;
        if (record.index == NSNotFound) {
        } else {
            if (record.entry->updated) {
                addIndexToCollection( mUpdates, oldIndex);
            }
        }
    }

之前已經(jīng)標(biāo)記過update的,就表示需要update绩衷。之所以是這個(gè)oldIndex應(yīng)該是跟collectionViewbadgeUpdate的規(guī)則有關(guān)蹦魔,后面會(huì)將update替換成insert和delete。

moves數(shù)組生成

moves數(shù)組的核心實(shí)現(xiàn)如下:

        id move;
        move = [[IGListMoveIndex alloc] initWithFrom:oldIndex to:newIndex];          
        [mMoves addObject:move];

之前的算法中咳燕,oldIndexnewIndex都已經(jīng)得出了勿决,可以直接使用,但是招盲,在一些情況里面低缩,我們是不需要move操作的。比如:

oldArray = @[@"1",@"2",@"3"];
newArray = @[@"2",@"3"];

這個(gè)情況我們只需執(zhí)行一次delete操作就可以從oldArray變到newArray了曹货,同理咆繁,有些情況下只需要insert操作就行了,對(duì)于此顶籽,IGListKit引入了runningOffset,整體算法如下

    vector<NSInteger> deleteOffsets(oldCount), insertOffsets(newCount);
    NSInteger runningOffset = 0;
    for (NSInteger i = 0; i < oldCount; i++) {
        deleteOffsets[i] = runningOffset;
        //如果需要?jiǎng)h除玩般,則runningOffset++
        if (record.index == NSNotFound) {
            runningOffset++;
        }
    }
    runningOffset = 0;
    
    for (NSInteger i = 0; i < newCount; i++) {
        insertOffsets[i] = runningOffset;
        如果需要插入,則runningOffset++
        if (record.index == NSNotFound) {
            runningOffset++;
        }
    }
    for (NSInteger i = 0; i < newCount; i++) {
        const IGListRecord record = newResultsArray[i];
        const NSInteger oldIndex = record.index;
        if (record.index == NSNotFound) {
        } else {
            //對(duì)應(yīng)插入的偏移量
            const NSInteger insertOffset = insertOffsets[i];
          //對(duì)應(yīng)刪除的偏移量
            const NSInteger deleteOffset = deleteOffsets[oldIndex];
            if ((oldIndex - deleteOffset + insertOffset) != i) {
                id move;
                move = [[IGListMoveIndex alloc] initWithFrom:oldIndex to:i];         
                [mMoves addObject:move];
            }
        }
    }

大意就是礼饱,如果前面出現(xiàn)的刪除坏为,則后面元素的位置都是要往左移,如果前面出現(xiàn)了插入镊绪,后面元素的位置都是要往右移匀伏,oldIndex - deleteOffset + insertOffset是執(zhí)行了刪除,插入后元素的最新位置镰吆,如果它與i相等帘撰,則沒必要move了。

函數(shù)返回

    return [[IGListIndexSetResult alloc] initWithInserts:mInserts
                                                     deletes:mDeletes
                                                     updates:mUpdates
                                                       moves:mMoves
                                                 oldIndexMap:oldMap
                                                 newIndexMap:newMap];

算完diff之后万皿,每個(gè)數(shù)組的元素都有值了摧找,便可以封裝IGListIndexSetResult返回了核行。

數(shù)組含有多個(gè)相同元素的情況

前面說過,如果newArrayoldArray中又相同的元素蹬耘,且出現(xiàn)了數(shù)次芝雪,該元素在oldArray中的第i次出現(xiàn)的索引會(huì)跟在newArray中的第i次出現(xiàn)的索引相匹配。這種匹配方式并不是最佳的综苔,舉個(gè)例子:

oldArray = @[@"2",@"3",@"1"];
newArray = @[@"1"@"2",@"1"];

肉眼看惩系,oldArray只需delete @"3",insert @"1"到索引為0的位置就變成了newArray了,而這個(gè)diff算法則需要個(gè)操作(@"2"從0移到1如筛,@"1"從索引2移到0堡牡,刪除@"3",插入@"1"到索引2)這是因?yàn)?code>oldArray中的索引2跟newArray中的索引0匹配了,導(dǎo)致了@"1"進(jìn)行不必要的移動(dòng)杨刨。

實(shí)際開發(fā)中晤柄,我們也很少出現(xiàn)這種情況,IGListKit也不鼓勵(lì)這種情況出現(xiàn)(會(huì)作去重且assert掉)

總結(jié)

diff算法是一個(gè)非常高效的算法妖胀,如果不把關(guān)鍵的代碼抽出來芥颈,IGListDiffing只是進(jìn)行了5次for循環(huán)而已,時(shí)間復(fù)雜度和空間復(fù)雜度都是o(n)赚抡。在前面3次循環(huán)中將元素的狀態(tài)都標(biāo)記出來爬坑,后面兩次循環(huán)計(jì)算出數(shù)組從舊到新所需的操作。IGListKit使用它進(jìn)行collectionView的部分更新涂臣,也提升了app的性能盾计。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市肉康,隨后出現(xiàn)的幾起案子闯估,更是在濱河造成了極大的恐慌灼舍,老刑警劉巖吼和,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異骑素,居然都是意外死亡炫乓,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門献丑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來末捣,“玉大人,你說我怎么就攤上這事创橄÷嶙觯” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵妥畏,是天一觀的道長(zhǎng)邦邦。 經(jīng)常有香客問我安吁,道長(zhǎng),這世上最難降的妖魔是什么燃辖? 我笑而不...
    開封第一講書人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任鬼店,我火速辦了婚禮,結(jié)果婚禮上黔龟,老公的妹妹穿的比我還像新娘妇智。我一直安慰自己,他們只是感情好氏身,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開白布巍棱。 她就那樣靜靜地躺著,像睡著了一般蛋欣。 火紅的嫁衣襯著肌膚如雪拉盾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評(píng)論 1 290
  • 那天豁状,我揣著相機(jī)與錄音捉偏,去河邊找鬼。 笑死泻红,一個(gè)胖子當(dāng)著我的面吹牛夭禽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播谊路,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼讹躯,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了缠劝?” 一聲冷哼從身側(cè)響起潮梯,我...
    開封第一講書人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惨恭,沒想到半個(gè)月后秉馏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡脱羡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年萝究,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锉罐。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡帆竹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出脓规,到底是詐尸還是另有隱情栽连,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布侨舆,位于F島的核電站秒紧,受9級(jí)特大地震影響舷暮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜噩茄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一下面、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧绩聘,春花似錦沥割、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至衅谷,卻和暖如春椒拗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背获黔。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來泰國打工蚀苛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人玷氏。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓堵未,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親盏触。 傳聞我的和親對(duì)象是個(gè)殘疾皇子渗蟹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349

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

  • 總結(jié)了一些開發(fā)中常用的函數(shù): usleep() //函數(shù)延遲代碼執(zhí)行若干微秒。 unpack() //函數(shù)從二進(jìn)制...
    ADL2022閱讀 454評(píng)論 0 3
  • PHP常用函數(shù)大全 usleep() 函數(shù)延遲代碼執(zhí)行若干微秒赞辩。 unpack() 函數(shù)從二進(jìn)制字符串對(duì)數(shù)據(jù)進(jìn)行解...
    上街買菜丶迷倒老太閱讀 1,360評(píng)論 0 20
  • 基礎(chǔ)篇NumPy的主要對(duì)象是同種元素的多維數(shù)組雌芽。這是一個(gè)所有的元素都是一種類型、通過一個(gè)正整數(shù)元組索引的元素表格(...
    oyan99閱讀 5,115評(píng)論 0 18
  • 一、基本數(shù)據(jù)類型 注釋 單行注釋:// 區(qū)域注釋:/* */ 文檔注釋:/** */ 數(shù)值 對(duì)于byte類型而言...
    龍貓小爺閱讀 4,257評(píng)論 0 16
  • 蘋果在WWDC2019的session中公開了iOS13一些新的系統(tǒng)API召庞, 其中對(duì)于非常穩(wěn)定的UITableVi...
    A大閱讀 2,706評(píng)論 0 5