UI场景及列表播放优化
背景
为了实现跨平台及保持各平台端的渲染一致性,PAG 的主体渲染是在 C++ 层,而不是依赖于平台侧的渲染接口。同时 PAG 是纯 GPU 渲染的方案,任何一个 GPU 渲染的方案都会有一个初始的屏幕缓冲区开销,如果测试的时候是全屏的,每个像素 都需要占用 4 字节的内存,加上双缓冲整体还要乘以 2,已经占用了几十 M 内存了。这个本身不是 PAG 动效占用的内存, 而是任何 GPU 渲染方案的基本占用。如果一个页面中含有多个 PAGView,其内存占用和 CPU 占用就会大一些。
在 PAG 3.0 的版本中,我们的推荐处理方案是使用 PAG 的组合模式,将多个 pag 文件添加同一个 PAGComposition 中,渲染到同一个 PAGView 中,实现多个 pag 文件共享同一个 PAGView,可以实现很少的内存占用就完美支持巨大数量的动效渲染。 这种处理方式虽然解决了多 pag 文件同时渲染时由于 GPU 渲染环境产生的的内存占用和 CPU 占用增加问题,但是对于 UI 列表 场景,却很难将多个 渲染的 cell 放在通过一个 PAGView 中,更多的需要每一个 cell 中包含一个 PAG 的渲染 View。另外,如果 pag 文件中含有 BMP 预合成,一个 BMP 相当于一个视频,而视频的解码又是极其耗费资源的,渲染过程中由于视频解码而产生的内存和 CPU 增加是采用组合模式无法解决的。 于是我们推出了 PAGImageView 的解决方案。
实现原理
针对性能的瓶颈点为每一个 PAGView 中 GPU 渲染的基础开销,我们思考如何减少多 pag 文件渲染时的 GPU 渲染环境。于是我们引入了磁盘缓存,针对每一个 PAGImageView,在渲染当前帧的同时,也将当前帧的渲染数据缓存到本地,当下次渲染到该帧时,直接读取磁盘缓存而不是继续依赖于 GPU 渲染,当磁盘中缓存的数据的总帧数等于 pag 渲染的总帧数时,将会销毁 PAGImageView 持有的 GPU 渲染环境,后续的渲染将由之前的 GPU 渲染演变成磁盘数据的读取上屏,从而降低整个渲染过程中因为实时渲染而引入的 GPU 基础开销,同时 pag 文件中的 BMP 预合成渲染也不会再触发视频解码。为了进一步提升性能,我们做了以下两个层次优化:
- 缓存渲染数据的尺寸和渲染尺寸保持一致,从而解决了部分场景下 pag 文件的尺寸大于渲染尺寸时选用文件尺寸渲染内存占用多余的情况,同时缓存渲染尺寸的大小可以保证渲染的清晰度,如果某些场景用户对于性能要求更高清晰度要求适中,我们同样提供了相关接口可以调整缓存渲染数据的尺寸。
- 针对渲染过程中使用的 BGRA 数据较大的情况,我们选择了 LZ4 压缩,和其它压缩方式相比,LZ4 压缩和解压缩的速度足够快,数据压缩比也较好,平衡了渲染速度和缓存文件大小的要求。
文件缓存是和 pag 文件相对应的,如果 pag 文件的渲染尺寸和上次渲染没有发生变化,且不涉及文本和占位图替换,当下次加载 pag 文件的时候是可以直接去读取缓存的,不需要再次渲染,此时的内存占用和 CPU 占用相对较小,且可以实现渲染性能和 pag 素材的无关性。
适用场景
PAGImageView 的实现原理是将每帧渲染的内容通过 LZ4 压缩缓存到本地磁盘,如果 PAGImageView 渲染的尺寸较大且 pag 文件时长较长,如手机全屏这种情况,会占用较大的磁盘空间做缓存,可能一个 pag 文件缓存下来就要几百 MB,目前磁盘缓存默认占用最大磁盘空间为 1GB,如果这样的情况较多,会不断触发清理缓存逻辑,不但不会提升性能,反而使性能变差。
PAGImageView 适用于 UI 列表 或一个页面中含有多个小尺寸 View 的场景,这种场景下渲染的尺寸较小,一个 pag 文件缓存完占用的磁盘空间也就几十 MB,渲染使用的时候会不断命中缓存,从而提升性能。其余场景我们还是推荐使用 PAGView。
另外,对于一些类似 WebP、APNG 等图片序列使用的场景,由于文件尺寸和图片序列的帧数一般不会太大, 图片序列的内容一般全部缓存至内存,渲染的过程中直接读取内存缓存,基本上不占用 CPU。PAGImageView 提供了全内存缓存模式兼顾该场景,此时模式下内存占用较大,CPU 占用较小。
具体用法
普通 View 中使用
Android
MultiplePAGImageViewActivity 的这个例子演示了在 xml 中作为普通 View 的使用场景。
像普通 View 一样在 xml 中声明
<org.libpag.PAGImageView
android:id="@+id/pagView1"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
在代码中给 PAGImageView 设置相应参数
void fireImageView(int id, String path) {
// 从xml查找View
PAGImageView view = findViewById(id);
// 设置PAG文件路径,和PAGView一样,接受来自assets和sdcard的两种路径
view.setPath(path);
// 设置动画的循环次数,-1为无限循环
view.setRepeatCount(-1);
// 播放动画
view.play();
}
iOS
PAGImageView* pagImageView = [[PAGImageView alloc] initWithFrame:CGRectMake(x, y, w, h)];
[pagImageView setPath:[[NSBundle mainBundle] pathForResource:pagPath];
[pagImageView setCacheAllFramesInMemory:NO];
[pagImageView setRepeatCount:-1];
[pagImageView play];
ListView 中使用
Android
PAGImageViewListActivity 的这个例子演示了在 ListView/GridView 中的使用场景
在 List 的 item 中像普通 view 一样去申明
<org.libpag.PAGImageView
android:id="@+id/pagView"
android:layout_width="wrap_content"
android:layout_height="match_parent"/>
在RecyclerView.Adapter 中初始化并播放 View
@Override
public void onBindViewHolder(ViewHolder viewHolder, final int position) {
viewHolder.getView().setPath(mDataSet[position]);
viewHolder.getView().setRepeatCount(-1);
viewHolder.getView().play();
}
iOS
#define WIDTH 100
@interface PAGCell : UITableViewCell
@property (nonatomic,strong) PAGImageView* pagImageView;
@property (nonatomic,strong) NSString* filePath;
@end
@implementation PAGCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
self.pagImageView = [[PAGImageView alloc] initWithFrame:CGRectMake(20, 0, WIDTH, WIDTH)];
[self.contentView addSubview:self.pagImageView];
}
return self;
}
- (void)setFilePath:(NSString *)filePath {
if ([filePath length] > 0) {
[self.pagImageView setPath:filePath];
[self.pagImageView setCacheAllFramesInMemory:NO];
[self.pagImageView setRepeatCount:-1];
[self.pagImageView play];
}
}
@end
其他配置
memoryCache
PAGImageView默认是会开启磁盘缓存的,但是对于内存缓存考虑内存限制,默认是关闭的。
如果需要开启可以调用 API 去打开
// Android
public void setCacheAllFramesInMemory(boolean enable);
// iOS
- (void)setCacheAllFramesInMemory:(BOOL)enable;
这个接口会显著的提高内存占用,使用时候要评估好具体场景是否适合。
renderScale
对于低端机型可能性能有限制,不太需要绘制太高清晰度时,可以去考虑设置 renderScale,这里可以同时降低 CPU 和内存占用。
在渲染或者缓存但是时候,输出尺寸会叠加上这个比率,比如原始尺寸 100100,但是设置了 0.5 的 renderScale,那尺寸就会变成 50*50.
// Android
public void setRenderScale(float renderScale);
// iOS
- (void)setRenderScale:(float)scale;
maxDisk
对于缓存到磁盘的文件,当时间久的时候,可能会慢慢增加,我们增加了一个设置 maxDisk 的接口。
当每次缓存达到这个阈值的时候,我们会按文件的使用时间排序,一次性删除40%的缓存文件。
// Android
public static void SetMaxDiskCache(long maxDiskCache);
// iOS
+ (void)SetMaxDiskSize:(NSUInteger)size;
资源设置
我们有两种设置资源的方式,setPath 和 setComposition。
这里如果不需要修改原始 PAG 文件的时候,我们强烈推荐使用 setPath 来送入数据。
通过 setPath 方式来设置资源的,我们仅仅在第一次第一遍绘制的时候需要 GPU 和 PAG 相关的资源,用完以后马上就会释放。这种情况可以带来比较显著的内存优化。
当通过 setComposition 设置资源的时候,当 Composition 对象不是直接通过 PAFile.load 生成的,或者 Composition 对象有任何修改的时候。 在 PAGImageView detach 的时候,我们会自动清理掉磁盘缓存。
比如 PAGImageView 对应的 PAG,每次都是仅仅修改某个图层的文字,哪怕每次都是一样的文字,这种情况也不会做跨越View生命周期的磁盘缓存。因为我们无法把这些修改用唯一固定的 key 做映射,所以我们在 View detach 的时候会做磁盘清理