0%

视频列表异步获取视频大小的优化方案

iOS 的 Photo Framework 框架对系统相册中的所有照片、视频以及相册资源均采用抽象对象予以封装,用 PHAsset 统一表示一个多媒体资源实体,其中包含有该资源实体的多种属性,如下所示

  • mediaType:多媒体类型,照片、视频还是音频
  • mediaSubtypes:子类型,live、连拍等等
  • pixelWidth、pixelHeight:资源实体的宽高
  • creationDate:创建时间
  • duration:视频时长,对于照片始终返回 0

但是对于视频实体而言,不能直接从其 PHAsset 对象中获取到视频大小信息,需要采用下面的异步接口来获取

1
2
3
4
5
6
7
8
9
[[PHImageManager defaultManager] requestAVAssetForVideo:targetAsset options:options resultHandler:^(AVAsset * _Nullable asset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) {
@strongify(self)
dispatch_async(dispatch_get_main_queue(), ^{
@strongify(self)
AVURLAsset *urlAsset = (AVURLAsset *)asset;
NSURL *url = urlAsset.URL;
NSNumber *fileSizeValue = nil;
[url getResourceValue:&fileSizeValue forKey:NSURLFileSizeKey error:nil];
});

当前需要实现一个效果:能够在一个展示手机相册所有视频的 UICollectionView 列表中,标识每一个视频的大小。

但是上述的异步方法涉及到异步线程与主线程之间的切换,其性能并不可靠,在滑动列表过程中,列表元素 cell 会触发多次异步请求,通过实验统计得知,针对 2000 条视频发起上述请求需要耗时大约 7s,而获取 2000 条视频的 duration 值时,由于是直接从 PHAsset 对象获取,因此只需要 0.1s,二者差别显著。

因此,如果不加处理直接在列表每一个 cell 中发起请求,由于 PHAsset 内部对请求做了排队处理,快速滑动过程中会发生延时现象,滑动速度越快,视频资源越多,请求队列延迟现象越严重。因为无法及时获取到当前展示的视频,势必会影响到用户选取视频的流程。

所以针对这一问题,采取以下方式进行优化

优化1:进行视频大小缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 存缓存
[[PHImageManager defaultManager] requestAVAssetForVideo:targetAsset options:options resultHandler:^(AVAsset * _Nullable asset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) {
@strongify(self)
dispatch_async(dispatch_get_main_queue(), ^{
@strongify(self)
AVURLAsset *urlAsset = (AVURLAsset *)asset;
NSURL *url = urlAsset.URL;
NSNumber *fileSizeValue = nil;
[url getResourceValue:&fileSizeValue forKey:NSURLFileSizeKey error:nil];
if (targetAsset && self.mutipleImageVC) {
[self.mutipleImageVC.fileSizeDict setObject:fileSizeValue forKey:targetAsset.localIdentifier];
}
});
}];

// 取缓存
if (fileSizeDict[targetAsset.localIdentifier]) {
return fileSizeDict[targetAsset.localIdentifier];
}

这样可以在滑动中仅获取一遍视频大小,之后获取视频大小时先进行缓存命中,减少发起多余的异步请求。但是第一次滑动列表时仍然存在延时情况。

优化2:快速滑动时不进行异步请求

考虑到用户快速滑动列表时才会产生明显的延时效果,同时快速滑动时用户并不会关注每一个 cell 上的视频大小文案,因此当 collectionview 滑动速度较快时可以禁止 cell 进行视频大小的异步请求。

1. 获取滑动速度

首要问题是获取到 collectionview 的滑动速度,实现很简单,在 scrollViewDidScroll 方法中获取当前的 contentOffset,与前一次检测到的 contentOffset 对比,同时除去两次检测之间的时间间隔,得到滑动速度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (!self.startDate) {
self.started = scrollView.contentOffset.y;
self.startDate = [NSDate new];
} else {
CGFloat end = scrollView.contentOffset.y;
NSDate *endDate = [NSDate new];
self.velocity = sqrt((end - self.started) * (end - self.started))/([endDate timeIntervalSinceDate:self.startDate] * 1000.0);
self.started = end;
self.startDate = endDate;
self.needRequest = (self.velocity < 0.5);
}
}

经过试验发现 velocity 大于 0.5 即可认为进入到高速滑动状态,不需要进行异步请求。

2. 通知 cell 进行异步操作

在上一步,我们用 needRequest 属性来标识当前是否可以进行异步请求,接下来在 cell 中对此属性进行监听,当 needRequest 从 NO 变为 YES 时表示滑动速度已经变慢,可以进行请求了。

1
2
3
4
5
6
7
8
9
[[RACObserve(_mutipleImageVC, needRequest) distinctUntilChanged] subscribeNext:^(id x) {
@strongify(self)
PHAsset *targetAsset = self.targetAsset;
// 取缓存
if (targetAsset && x) {
// 缓存未命中,发起请求
[self requestVideoSize:targetAsset fromDelegateFunction:NO];
}
}];

要注意由于 needRequest 会在滑动中触发多次,通过 distinctUntilChanged 可以保证只在 needRequest 发生变化时才发起请求,避免冗余请求。

3. 首屏发起异步请求

此时有一个问题,cell 在初始化过程中会建立对 needRequest 的监听,但是此时 cell 还未被赋值相应的 PHAsset,所以必须等到赋值 PHAsset 后再进行异步请求,但是这样就又会在 cell 复用中发起多次请求。因此需要对请求进行控制,对 cell 加入一个标志位 requested,在复用时对其进行判断

1
2
3
4
5
6
7
{//复用赋值
if (self.requested) {
return;
}
self.requested = YES;
[self requestVideoSize:targetAsset fromDelegateFunction:YES];
}

cell 只在未发生滑动之前才在复用时发起请求,滑动后就通过监听 needRequest 来发起请求。

这样会引入新的问题,即当 collectionview 执行 reloadData 时,cell 会被复用,在复用赋值中不会发起请求,但同时由于没有滑动,needRequest 不会发出信号,cell 就不能根据监听来发起请求,导致除非用户滑动列表否则永远不会展示视频大小的 bug。

解决方案是在 collectionview 执行 reloadData 时更新一个 beginReload 标志位为 YES,cell 监听到 beginReload 为 YES 时,将其 requested 标志位复位为 NO,这样 cell 复用时就可以发起请求了。同时当用户开始滑动时,再将 beginReload 置为 NO,重新变为监听 needRequest 发起请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
[self.imageCollectionView reloadData];
self.beginReload = YES;
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
self.beginReload = NO;
}

{
[RACObserve(_mutipleImageVC, beginReload) subscribeNext:^(id x) {
@strongify(self)
self.requested = !(self.mutipleImageVC.beginReload);
}];
}

具体流程如下

4. 手势问题

这样处理后基本可以将一段时间内的异步请求造成的延时控制在用户无法察觉的范围内,但是实际测试发现,当高速滑动中用手指急停列表时,needRequest 可能仍然为 NO,但是不会再调用 scrollViewDidScroll,因而不能恢复为 YES,所以采取下面方式进行防护

1
2
3
4
5
[[RACObserve(self, velocity) throttle:0.3] subscribeNext:^(id x) {
@strongify(self);
self.velocity = 0;
self.needRequest = (self.velocity < 0.5);
}];

0.3 秒内如果 velocity 的值不发生变化,则将其置为 YES,保证急停后可以感知到速度降低。

总结

  • 列表中大量 cell 需要尽量避免耗时的异步请求,否则容易影响性能
  • 对于异步请求返回的数据尽可能缓存到内存
  • 高速滑动时可以不进行异步请求,防止过多无用的请求产生