【显示问题定位法分享】以修复腾讯视频弹幕的display frame尺寸错误问题为例
1 问题现象描述:
在OpenFDE 14版本上使用腾讯视频播放视频时,弹幕高度拉伸异常,在自由窗口模式切换到最大化窗口模式时,视频显示大小异常,仍显示的是自由小窗时的尺寸大小。如下图所示:

弹幕拉伸异常

最大化时视频显示大小异常
2 显示问题定位步骤
2.1 dumpsys SurfaceFlinger

因为surfaceflinger负责合成图像,所以它掌握了所有图像Layer的信息。因此dump它,可以找到一些关键数据。如上图是surfaceflinger dump出来的信息。此处简单介绍几个字段的含义,
1. Disp Frame指的是按照屏幕坐标系,给当前layer分配的起始点和大小尺寸。
2. Source Crop表示的是从buffer的原始坐标系中,从起始点截取的大小尺寸用于显示。
如果两者不相等,那么surfaceflinger就会做缩放,使得souce crop尺寸能贴合disp frame尺寸。
如下两图中标号为#546的两组是腾讯视频弹幕尺寸正常和异常的数据。

腾讯视频正常情况抓取的layer信息
用上图的disp frame 尺寸的292-176= source crop的116 说明这里不需要缩放。

腾讯视频异常情况抓取的layer信息
上图是弹幕异常时的数据,用408-176=272 大于 source crop的116. 此处说明该layer会被surfaceflinger放大。到此我们至少确认了问题的原因是disp frame和source crop的尺寸不匹配。接下来我们定位为什么数据不匹配。
2.2 定位disp frame和source crop数据不匹配的原因
2.2.1 在hwc层确定数据问题
首先,通过理论分析,我们知道layer的buffer、displayframe和 source crop数据肯定在hwcomposer中是存在的,所以我们通过在hwcomposer中图像合成 预处理方法 (hwc_prepare)中添加了如下的日志打印。
td::string layer_name = pdev->display->layer_names[i];
int height = fb_layer->displayFrame.bottom - fb_layer->displayFrame.top;
hwc_rect_t sourceCrop = fb_layer->sourceCropi;
int sourceCropHeight = fmax(1, sourceCrop.bottom - sourceCrop.top);
ALOGE("hwc_prepare layer_name: %s buffer width: %d, buffer height: %d, displayframe height: %d sourcecrop height %d",
layer_name.c_str(), pdev->display->layer_handles_ext[i].width, pdev->display->layer_handles_ext[i].height, height, sourceCropHeight);
我们看到腾讯视频播放视频时从最大化点还原按钮,发生异常弹幕后,日志如下:
05-07 18:18:47.691 120 190 E hwcomposer: hwc_prepare layer_name: SurfaceView[com.tencent.qqlive/com.tencent.qqlive.kmm.VideoDetailKmmActivityBk](BLAST)#162 buffer width: 464, buffer height: 261, displayframe height: 702 sourcecrop height 261
05-07 18:18:50.691 120 190 E hwcomposer: hwc_prepare layer_name: SurfaceView[com.tencent.qqlive/com.tencent.qqlive.kmm.VideoDetailKmmActivityBk](BLAST)#162 buffer width: 752, buffer height: 702, displayframe height: 261 sourcecrop height 702
第一行可以看出buffer和source crop height的值都是261 ,但是display frame的height却为702。第二行是过一段时间后,再点击最大化的日志。 这时display frame height是261,261是之前小窗状态下的高度。 这说明display frame 比buffer或crop的数据慢了一帧,而且从数据可以跨时间的现象看,计算display frame的依赖数据是被 缓存 下来了,当下一次窗口发生变化的时候 ,才计算出来上一帧的display frame,用作了当前帧的值。
2.2.2 在SurfaceView层中确认数据问题
通过理论分析,我们在core/java/android/view/SurfaceView.java中找到positionChanged()方法,该方法和我们的操作是对应的(改变了surfaceView的位置和大小)。这里简单说明一下Transaction机制是Android为了保证窗口属性变更的原子性、一致性和高效性而提出的。我们在postionChanged方法中看到调用了和transaction有关的applyOrMergeTransaction(mPositionChangedTransaction, frameNumber)方法,applyOrMergeTransaction方法根据viewRoot是否为空,走了不同的路径。
private void applyOrMergeTransaction(Transaction t, long frameNumber) {
final ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null) {
// If we are using BLAST, merge the transaction with the viewroot buffer transaction.
viewRoot.mergeWithNextTransaction(t, frameNumber);
} else {
t.apply(); //直接走这里解决不了问题,直接调用这个,会导致buffer原始尺寸未能更新
}
}
在viewRoot != null 的分支里,最终代码会调用到BLASTBufferQueue::mergeWithNextTransaction(SurfaceComposerClient::Transaction* t, uint64_t frameNumber)。在这个mergeWithNextTransaction方法中我们看到一个关键比较:
if (mLastAcquiredFrameNumber >= frameNumber) {
ALOGE("blastbufferqueue mLastAcquiredFrameNumber %d, frameNumber %d",mLastAcquiredFrameNumber,frameNumber);
// Apply the transaction since we have already acquired the desired frame.
t->apply();
} else {
mPendingTransactions.emplace_back(frameNumber, *t); //暂存未到期的事物
// Clear the transaction so it can't be applied elsewhere.
t->clear();
}
结合上面看到的数据被缓存起来的现象,我们有理由推测,某一次数据更新的transaction由于这个frameNumber比较,frameNumber大于lastAcquiredFrameNumber,导致未能及时apply,所以才产生这个问题。 所以我们主动将frameNumber减1按如下修改做测试:
String packageName = getContext().getPackageName();
boolean isTencentVideo = "com.tencent.qqlive".equals(packageName);
if (isTencentVideo) {
Log.w(TAG," origin frame Number "+frameNumber);
applyOrMergeTransaction(mPositionChangedTransaction, frameNumber-1);
}else{
applyOrMergeTransaction(mPositionChangedTransaction, frameNumber);
Log.w(TAG," normal origin frameNumber "+frameNumber);
}
结果问题就被解决了。日志如下:我们可以看到 origin frame Number是1604,由于我们减1后,才传给后续方法,所以在blastbufferqueue中比较的时候,mLastAcquiredFrameNumber 和 frameNumber刚好都是1603,所以apply得到立即执行。
05-11 17:46:39.987 3970 4037 E SurfaceView: 52417823 updateSurfacePosition RenderWorker, frameNr = 1604, position = [-28, 81, 507, 308] surfaceSize = 535x227
05-11 17:46:39.987 3970 4037 E SurfaceView: postion changed postionleft -28 postiontop 81 postion.width 535 postion.height227 mSurfaceWidth 535 mSurfaceHeight 227
05-11 17:46:39.987 3970 4037 W SurfaceView: origin frame Number 1604
05-11 17:46:39.987 3970 4037 E BLASTBufferQueue:blastbufferqueue mLastAcquiredFrameNumber 1603, frameNumber 1603
但是这种解决方法有点粗暴,所以我们改成在positionChanged回调中,延迟调用requestUpdateSurfacePositionAndScale()进行二次刷新操作,其实就是不带frameNumber的apply一次transaction。
3 问题说明
腾讯视频弹幕拉伸现象的根源在于Android12起引入的BLAST图形系统,它将SurfaceView的位置更新操作从主线程转移到了RenderThread 上异步执行,然后依赖framenumber和transaction来做异步中的同步匹配。但是这里的framenumber和mLastAcquiredFrameNumber始终差1,导致apply不能及时运行。所以产生了弹幕和视频缩放的问题。这里为什么从renderThread中下发的framenumber为什么引发这个问题,我们就没有深究了。因为我们测试aosp16已经没这个问题了

浙公网安备 33010602011771号