从LCS到IGListKit框架中的Diff算法(下)

写在前面

在上一篇文章从LCS到IGListKit框架中的Diff算法(上)中我们详细讲了 是什么、怎么使用动态规划来求解 ,最后我们也抛出了 还存在的两个问题,本篇文章我们就主要来分析IGListKit框架中的Diff是如何来解决这两个问题的。

温馨提示:本文主要剖析IGListKit框架中的Diff算法,不会讲IGListKit框架的具体使用。

存在的问题带来的思考

上一篇文章我们提出了 存在的两个问题:

  • 但是当n特别大的时候, 这个时间复杂度依然比较可怕。
  • 希望对新、旧数据都存在的元素的Move进行一些优化,而不是简单的Delete、Insert操作。
    Instagram团队的IGListKit框架结合Paul Heckel’s Diff(1978年)的一篇论文做了进一步的优化,使用一些额外的内存空间,把时间复杂度降低到了 ,并且能够准确获取数据元素的Move/Insert等操作。
    这里,我们还以上一篇文章的两个序列为例:


    求 和 的
    2022060906.png

    我们首先需要处理的是避免使用二维数组,我们结合上图会发现重要的只是我们标记的这条线路,其他位置我们并没有用到,那么需要走一遍的距离就是:
    所以,肯定要走过所有去重之后的元素,仔细思考一下,对于每个元素,我们需要的是什么?
    我们需要的是元素在新数据和旧数据里面的位置。

IGListKit框架的刷新流程

2022061001.png
2022061002.png
2022061003.png
2022061004.png
2022061005.png
2022061006.png

2022061007.png

到这里,我们已经跟踪到IGListKit框架刷新机制的核心部分:IGListDiff类中的IGListDiffing函数,接下来就是本文的重点了:IGListDiff是如何实现的?

IGListDiff算法详解

在理解Diff算法之前我们先来熟悉几个数据结构、函数体:IGListIndexSetResultIGListEntryIGListRecordIGListDiffableIGListMoveIndex

IGListMoveIndex

IGListMoveIndex 封装的是一个移动的操作,from是旧数组的索引,to是新数组的索引:

@interface IGListMoveIndex : NSObject
  
// 旧数组的索引
@property (nonatomic, assign, readonly) NSInteger from;
// 新数组的索引
@property (nonatomic, assign, readonly) NSInteger to;

@end

IGListIndexSetResult

IGListIndexSetResult封装的是一个关于插入、删除等操作的集合:

@interface IGListIndexSetResult : NSObject
  
// 插入索引的集合(新数组的索引)
@property (nonatomic, strong, readonly) NSIndexSet *inserts;
// 删除索引的集合(旧数组的索引)
@property (nonatomic, strong, readonly) NSIndexSet *deletes;
// 更新索引的集合(旧数组的索引)
@property (nonatomic, strong, readonly) NSIndexSet *updates;
// 移动索引的集合
@property (nonatomic, copy, readonly) NSArray *moves;
// 是否发生改变
@property (nonatomic, assign, readonly) BOOL hasChanges;

@end

IGListDiffable

IGListDiffable是一个协议,要求数组里的对象都需要遵循这个协议才能有效地使用diff函数:

@protocol IGListDiffable
 
// 返回对象唯一id,在diff算法中以它作为元素存入哈希表的key
- (nonnull id)diffIdentifier;
// 判断两个对象是否相等,在diff算法用这个方法判断两个对象是否是同一个对象
- (BOOL)isEqualToDiffableObject:(nullable id)object;

@end

IGListEntry

IGListEntry是用于标记元素状态的结构体:

struct IGListEntry {
    // 该元素在旧数组中出现的次数
    NSInteger oldCounter = 0;
    // 该元素在新数组中出现的次数
    NSInteger newCounter = 0;
    // 存放元素在旧数组中的索引,在算法中,可以保证栈顶是较小的索引
    stack oldIndexes;
    // 这个元素是否需要更新
    BOOL updated = NO;
};

IGListRecord

ICListRecord是封装entry结构体和它所在的索引,主要用于插入和删除(如果index有值,则代表该元素需要插入或者更新,否则为NSNotFound,则是需要删除)

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

IGListDiffing函数的算法流程

变量的声明

const NSInteger newCount = newArray.count;
const NSInteger oldCount = oldArray.count;

NSMapTable *oldMap = [NSMapTable strongToStrongObjectsMapTable];
NSMapTable *newMap = [NSMapTable strongToStrongObjectsMapTable];

处理特殊情况:
如果newcount == 0oldcount == 0,即是删除所有旧元素或新增所有新元素,直接返回IGListIndexSetResult集合,不需要走下面的diff算法流程。

if (newCount == 0) {
  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 obj, NSUInteger idx, BOOL *stop) {
        addIndexToMap(returnIndexPaths, toSection, 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算法第一步
先定义一个无序去重unordered_maptable,以diffIdentifier为Key,entry为Value,其查找复杂度为。

unordered_map, IGListEntry, IGListHashID, IGListEqualID> table;

正序遍历新数组:

vector newResultsArray(newCount);
for (NSInteger i = 0; i < newCount; i++) {
  // 1.取每个元素的identifier
  id key = IGListTableKey(newArray[i]);
  // 2.为每个元素创建一个entry,如果table里面不含这个key就创建
  IGListEntry &entry = table[key];
  // 3.entry的newCounter值 +1
  entry.newCounter++;
  // 4.NSNotFound入栈:目的是防止oldIndexes为空,NSNotFound相当于栈底的标志位
  entry.oldIndexes.push(NSNotFound);
  newResultsArray[i].entry = &entry;
}

Diff算法第二步
倒序遍历旧数组:

vector oldResultsArray(oldCount);
for (NSInteger i = oldCount - 1; i >= 0; i--) {
  // 1.取每个元素的identifier
  id key = IGListTableKey(oldArray[i]);
  IGListEntry &entry = table[key];
  // 2.entry的olderCounter值 + 1
  entry.oldCounter++;
  // 4.将索引i入栈
  entry.oldIndexes.push(i);
  oldResultsArray[i].entry = &entry;
}

温馨提示:这里采用倒序遍历是为了保证,当存在多个key相同的时候,oldIndexes会有一系列的索引入栈,保证栈顶的索引是最小的。
分析
在上面两部之后,会建立一个用于存放IGListRecordoldResultsArray,此时每个IGListRecord仍为NSNotFound,对于oldResultsArraynewResultsArray中的entry,会有三种情况:

  • 该元素只有新数组有,则entrynewCounter>0,oldCounter=0,oldIndexes栈顶为NSNotFound
  • 该元素只有旧数组有,则entrynewCounter=0,oldCounter>0,oldIndexes栈顶不为NSNotFound,而是元素在旧数组中的最小索引。
  • 该元素新旧数组都有,则entrynewCounter>0,oldCounter>0,oldIndexes栈顶不为NSNotFound,而是元素在旧数组中的最小索引,而oldResultsArraynewResultsArray都指向同一个entry
    Diff算法第三步
    处理同时出现在新、旧数组里面的都存在的元素(注意:这里所说的存在的元素不是指两个元素的值相,而是我们指定的identifier相等,这一点大家要注意一下),其IGListRecord的index会赋上新、旧数据的索引:
for (NSInteger i = 0; i < newCount; i++) {
    // 1. 取出新数组中元素对应的entry
    IGListEntry *entry = newResultsArray[i].entry;
    NSCAssert(!entry->oldIndexes.empty(), @"Old indexes is empty while iterating new item %li. Should have NSNotFound", (long)i);
  
    // 2.拿到oldIndexes的栈顶,也就是拿到该元素在oldArray的第一个索引,然后pop出来
    const NSInteger originalIndex = entry->oldIndexes.top();
    entry->oldIndexes.pop();
    if (originalIndex < oldCount) {
        const id n = newArray[i];
        const id o = oldArray[originalIndex];
        switch (option) {
            case IGListDiffPointerPersonality:
            if (n != o) {
                entry->updated = YES;
            }
            break;
            case IGListDiffEquality:
            if (n != o && ![n isEqualToDiffableObject:o]) {
                // 3.标记需要更新
                entry->updated = YES;
            }
            break;
        }
    }
    if (originalIndex != NSNotFound && entry->newCounter > 0 && entry->oldCounter > 0) {
        // 4.如果用identifier标记的元素在新旧数据中都存在 那么新数组对应元素的index会指向这个元素在老数据中的索引
        newResultsArray[i].index = originalIndex;
        oldResultsArray[originalIndex].index = i;
    }
}

Diff算法第四步
遍历老数据,处理需要删除的元素:

id mInserts, mMoves, mUpdates, mDeletes;
if (returnIndexPaths) {
    mInserts = [NSMutableArray new];
    mMoves = [NSMutableArray new];
    mUpdates = [NSMutableArray new];
    mDeletes = [NSMutableArray new];
} else {
    mInserts = [NSMutableIndexSet new];
    mMoves = [NSMutableArray new];
    mUpdates = [NSMutableIndexSet new];
    mDeletes = [NSMutableIndexSet new];
}

vector deleteOffsets(oldCount), insertOffsets(newCount);
NSInteger runningOffset = 0;

for (NSInteger i = 0; i < oldCount; i++) {
      deleteOffsets[i] = runningOffset;
      const IGListRecord record = oldResultsArray[i];
        // 1.如果记录每个元素的entey记录record里面index的值不存在,则把该元素标记为删除
      if (record.index == NSNotFound) {
          addIndexToCollection(returnIndexPaths, mDeletes, fromSection, i);
          runningOffset++;
      }
      addIndexToMap(returnIndexPaths, fromSection, i, oldArray[i], oldMap);
  }

Diff算法第五步

runningOffset = 0;
for (NSInteger i = 0; i < newCount; i++) {
  insertOffsets[i] = runningOffset;
  const IGListRecord record = newResultsArray[i];
  const NSInteger oldIndex = record.index;
  // 1.如果记录每个元素的entey记录record里面index的值不存在,则把该元素标记为插入
  if (record.index == NSNotFound) {
      addIndexToCollection(returnIndexPaths, mInserts, toSection, i);
      runningOffset++;
  } else {
      // 2.如果记录每个元素的entey里面updated标记为YES,则标记该元素为更新
      if (record.entry->updated) {
          addIndexToCollection(returnIndexPaths, mUpdates, fromSection, oldIndex);
      }
    // 3.标记该元素为移动
      const NSInteger insertOffset = insertOffsets[i];
      const NSInteger deleteOffset = deleteOffsets[oldIndex];
      if ((oldIndex - deleteOffset + insertOffset) != i) {
          id move;
          if (returnIndexPaths) {
              NSIndexPath *from = [NSIndexPath indexPathForItem:oldIndex inSection:fromSection];
              NSIndexPath *to = [NSIndexPath indexPathForItem:i inSection:toSection];
              move = [[IGListMoveIndexPath alloc] initWithFrom:from to:to];
          } else {
              move = [[IGListMoveIndex alloc] initWithFrom:oldIndex to:i];
          }
          [mMoves addObject:move];
      }
}

Diff算法第六步
处理完插入删除更新移动之后,返回IGListIndexSetResult类型的值,到这里,IGListKit框架中整个Diff 核心部分就结束了,只讲原理肯定很空洞,我们这里举一个实际的例子来走一遍这个Diff的流程,加深印象和理解。

实际刷新举例说明

、现在比如一个UICollectionView列表中的初始数据为oldDatasource,数据如下:

BasicModel *model0 = [[BasicModel alloc] init];
model0.modelId = @"123";
model0.title = @"A";
BasicModel *model1 = [[BasicModel alloc] init];
model1.modelId = @"234";
model1.title = @"B";
BasicModel *model2 = [[BasicModel alloc] init];
model2.modelId = @"345";
model2.title = @"C";
[self.oldDatasource addObjectsFromArray:@[model0,model1,model2]];

、刷新后的数据为freshDatasource,数据如下:

BasicModel *model3 = [[BasicModel alloc] init];
model3.modelId = @"123";
model3.title = @"D";
BasicModel *model4 = [[BasicModel alloc] init];
model4.modelId = @"345";
model4.title = @"C";
BasicModel *model5 = [[BasicModel alloc] init];
model5.modelId = @"456";
model5.title = @"E";
[self.freshDatasource addObjectsFromArray:@[model3,model4,model5]];

、在BasicModel中遵守协议,实现下面两个方法:

@implementation BasicModel

// 返回对象唯一id,在diff算法中以它作为元素存入哈希表的key
- (id)diffIdentifier {
    if (self.modelId) {
        return self.modelId;
    }
    return self;
}

// 判断两个对象是否相等,在diff算法用这个方法判断两个对象是否是同一个对象
- (BOOL)isEqualToDiffableObject:(BasicModel *)object {
    return [self isEqual:object];
}

@end

、经过Diff核心算法第一步之后的结构如下:

newResultsArray里面有三个 IGListRecord 类型的元素,元素里面的entry情况如下:
  
entry[D].newCounter = 1;
entry[D].oldIndexes.pop() = NSNotFound;
entry[D].updated = NO;

entry[C].newCounter = 1;
entry[C].oldIndexes.pop() = NSNotFound;
entry[C].updated = NO;

entry[E].newCounter = 1;
entry[E].oldIndexes.pop() = NSNotFound;
entry[E].updated = NO;

、经过Diff核心算法第二步之后的结构如下:

oldResultsArray里面有三个 IGListRecord 类型的元素,元素里面的entry情况如下:

entry[C].oldCounter = 1;
entry[C].oldIndexes.pop() = 2;
entry[C].updated = NO;

entry[B].oldCounter = 1;
entry[B].oldIndexes.pop() = 1;
entry[B].updated = NO;

entry[A].oldCounter = 1;
entry[A].oldIndexes.pop() = 0;
entry[A].updated = NO;

、经过Diff核心算法第三步之后的结构如下:

entry[D].updated = YES; // 因为A元素 和 D元素的 modelID一样 标记为刷新
entry[C].updated = YES; // 新老数据里面都有C元素 并且modelID一样 标记为刷新

// A 和 D 的record
newResultsArray[0].index = 0;
oldResultsArray[0].index = 0;

// C 在新老数据中的record
newResultsArray[1].index = 2;
oldResultsArray[2].index = 1;

、经过Diff核心算法第四步之后的结构如下:
因为 oldResultsArray[1].index = NSNotFound,所以老数据中这个位置元素需要被删除,即是数据B。
、经过Diff核心算法第五步之后的结构如下:

因为 newResultsArray[2].index = NSNotFound,所以新数据中这个位置元素需要插入,即是数据E。
D元素是更新,C元素是移动。

、最后的结论:

Insert: E
Update: D(A -> D)
Move: C
Delete: B

总结

看到这里,相信大家对IGListKit框架中Diff算法实现原理有了一个更加清晰的了解,这个算法的时间复杂度就变成了,ICListKit框架就是通过这种刷新机制来提升了APP的整体性能。

你可能感兴趣的:(从LCS到IGListKit框架中的Diff算法(下))