SKCanvas高效绘制方案:仅绘制可视区域内的点和线段解决卡顿内存不足问题
问题根因
你当前代码存在两个核心性能问题:
- 每次绘制时都重新创建
SKPaint对象,该对象属于重资源,频繁创建销毁会带来不必要的内存和CPU开销 - 全量遍历所有历史点位绘制线段,运行数小时后点位数量可达数万甚至数十万级别,每次绘制都全量执行绘制逻辑,GPU/CPU负载持续走高,最终导致内存不足、卡顿。
核心优化思路
要实现仅绘制可视区域内的轨迹,核心逻辑是先判断线段是否与当前视口相交,仅绘制相交的线段,同时配合前置优化减少不必要的资源消耗。
具体实现步骤
- 第一步:将
SKPaint提取为类成员变量,仅在参数变化时更新,避免每次绘制重复创建 - 第二步:获取当前地图的可视区域边界(根据你自定义地图的平移、缩放参数计算,返回地图坐标体系下的视口矩形)
- 第三步:遍历点位时,先判断前后两点组成的线段是否与视口相交,仅对相交的线段执行绘制操作
修改后的代码示例
// 类成员变量,仅初始化或线宽、颜色变化时更新 private SKPaint _linePaint = new SKPaint { Style = SKPaintStyle.Stroke, Color = SKColors.White, StrokeJoin = SKStrokeJoin.Round, StrokeCap = SKStrokeCap.Round, IsAntialias = true }; // 绘制方法内的逻辑 // 1. 先获取当前可视区域(你需要根据自己的地图缩放平移逻辑实现该方法,返回地图坐标下的视口矩形) SKRect viewport = GetCurrentMapViewport(); _linePaint.StrokeWidth = lineWidth * 2f; if (localPoints.Count < 2) return; Point previousPoint = localPoints[0]; for (int i = 1; i < localPoints.Count; i++) { Point currentPoint = localPoints[i]; // 判断当前线段是否与可视区域相交 if (SegmentIntersectsRect(previousPoint, currentPoint, viewport)) { canvas.DrawLine(previousPoint.X, previousPoint.Y, currentPoint.X, currentPoint.Y, _linePaint); } previousPoint = currentPoint; } // 线段与矩形相交判断的辅助方法,也可以直接用SkiaSharp内置的SKRect.Intersects方法扩展实现 private bool SegmentIntersectsRect(Point p1, Point p2, SKRect rect) { // 先判断两个端点是否有任意一个在矩形内 if (rect.Contains(p1.X, p1.Y) || rect.Contains(p2.X, p2.Y)) { return true; } // 再判断线段是否和矩形四条边相交 if (LineIntersectsLine(p1, p2, new Point(rect.Left, rect.Top), new Point(rect.Right, rect.Top))) return true; if (LineIntersectsLine(p1, p2, new Point(rect.Right, rect.Top), new Point(rect.Right, rect.Bottom))) return true; if (LineIntersectsLine(p1, p2, new Point(rect.Right, rect.Bottom), new Point(rect.Left, rect.Bottom))) return true; if (LineIntersectsLine(p1, p2, new Point(rect.Left, rect.Bottom), new Point(rect.Left, rect.Top))) return true; return false; } // 两条线段相交判断的辅助方法 private bool LineIntersectsLine(Point a1, Point a2, Point b1, Point b2) { float ccw(Point p1, Point p2, Point p3) { return (p2.X - p1.X) * (p3.Y - p1.Y) - (p2.Y - p1.Y) * (p3.X - p1.X); } return ccw(a1, a2, b1) * ccw(a1, a2, b2) < 0 && ccw(b1, b2, a1) * ccw(b1, b2, a2) < 0; }
额外高阶优化(针对点位特别多的场景)
- 点位预索引:将历史点位按网格或R树建立空间索引,绘制前直接筛选出视口覆盖范围内的点位集合,不用每次全量遍历所有历史点位
- 轨迹抽稀:根据当前缩放等级,用道格拉斯-普克算法对轨迹进行抽稀,减少需要绘制的线段数量
- 离屏缓存:将已经绘制过的、不随视口频繁变化的轨迹区域缓存为位图,绘制时直接贴位图,避免重复绘制大量线段
内容的提问来源于stack exchange,提问作者Dan
相关产品推荐
相关产品推荐

