教程技术博客文档【免费下载链接】YCBlogs技术博客笔记大汇总包括Java基础线程并发数据结构Android技术博客等等常用设计模式常见的算法网络协议知识点部分flutter笔记还包括平时开发中遇到的bug汇总当然也在工作之余收集了大量的面试题长期更新维护并且修正持续完善……开源的文件是markdown格式的转载请注明出处谢谢项目地址https://gitcode.com/gh_mirrors/yc/YCBlogs点击查看免费下载导读SurfaceView 是 Android 中承载游戏、视频、直播等高频复杂绘制的关键视图它允许开发者在独立子线程中更新画面从而避免阻塞 UI 主线程、降低卡顿与 ANR 风险。本文以 YCBlogs 仓库中的 question/android/14.Android之音视频.md 为核心骨架结合仓库内 SurfaceView 学习笔记 与 SurfaceView 源码分析 等资料系统讲解 SurfaceView 与 View 的本质区别、双缓冲机制的由来与工作流程并通过完整可运行的代码模板说明在子线程中通过lockCanvas/unlockCanvasAndPost更新画面的正确姿势最后解释子线程中不能操作 UI与SurfaceView 在子线程中绘制两者为何并不矛盾。读完本文你将掌握 SurfaceView 的核心原理、适用场景、标准使用流程及其底层实现依据。1. SurfaceView 是什么与 View 的本质区别1.1 SurfaceView 的作用与定位SurfaceView 是 Android 中一种比较特殊的视图View。它与视图容器并不在同一个视图层上绘制工作在一个独立的线程中完成不需要及时响应用户的输入也不会因为绘制耗时造成响应上的 ANR 问题。SurfaceView 一般用在游戏、视频、摄影等一些复杂 UI 且要求高效图像显示的场景这类图像处理通常都需要开单独的线程来处理。从源码结构看SurfaceView继承自View但它拥有独立的绘图表面Surface在 WMS 中有对应的 WindowState在 SurfaceFlinger 中有对应的 Layer。普通 Activity 中的多个 View 组成 View Hierarchy 树形结构只有最顶层的 DecorView根结点视图对 WMS 可见而 SurfaceView 自带的 Surface 虽然在 App 端仍处于 View Hierarchy 中但在 Server 端WMS 和 SurfaceFlinger与宿主窗口是分离的因此对 Surface 的渲染可以放到单独线程执行渲染时甚至可以拥有自己的 GL context这对游戏、视频等性能敏感场景非常有益见 flutter/04.混合开发/19.SurfaceView学习.md。1.2 SurfaceView 与 View 的最本质区别更新线程不同SurfaceView 可以在新起的单独线程子线程中重新绘制画面而 View 必须在 UI 主线程中更新画面。阻塞风险不同在 UI 主线程中更新画面可能引发问题——如果单帧更新时间过长主 UI 线程会被正在执行的绘制函数阻塞进而无法响应按键、触屏等消息SurfaceView 由于是在新的线程中更新画面能够在非 UI 线程中绘制所以不会阻塞 UI 主线程导致卡顿甚至 ANR。双缓冲机制不同View 在绘图时没有实现双缓冲机制而 SurfaceView 在底层机制中就已经实现了双缓冲机制详见第 2 节。仓库中另一份面试问答也印证了上述结论View 需要在 UI 线程对画面进行刷新适用于主动更新SurfaceView 可在子线程进行页面刷新适用于被动更新与频繁刷新场景SurfaceView 在底层已实现双缓冲机制而 View 没有见 question/android/08.Android之View事件问题.md 与 android/05.事件分发/08.AndroidView事件问题.md。1.3 子线程绘制带来的新问题事件同步SurfaceView 在子线程中绘制虽然解决了主线程阻塞问题但也带来了事件同步的复杂性例如用户触屏一次需要交由 SurfaceView 的绘制线程处理通常需要设计一个 event queue 来保存 TouchEvent。这比普通 View 稍显复杂因为其中涉及线程安全问题队列的读写需要同步处理。1.4 SurfaceView 的优缺点优点可以在一个独立的线程中进行绘制不会影响主线程使用双缓冲机制播放视频时画面更流畅。缺点Surface 不在 View Hierarchy 中其显示也不受 View 的属性控制所以不能进行平移、缩放等变换也不能放在其它 ViewGroup 中SurfaceView 不能嵌套使用即不具备普通 View 的完整布局变换能力。从 flutter/04.混合开发/20.SurfaceView源码.md 还可补充SurfaceView 在构造函数中调用了setWillNotDraw(true)这会导致其自身重写的draw()、onDraw()都不执行——即 SurfaceView 的绘制不走普通 View 的绘制流程而是直接面向自己独立的 Surface 绘图表面。1.5 典型应用场景视频播放SurfaceView MediaPlayerSurfaceView 用于显示 MediaPlayer 播放的视频流画面渲染详见 flutter/04.混合开发/20.SurfaceView源码.md 中的播放流程。直播软件中不停点赞的动效、天气软件的全屏雨雪动效、游戏中流水与云等高频变化效果。短视频滑动分页播放仓库 android/20.工具库Lib/26.仿抖音滑动分页视频.md 展示了在 ViewPager/RecyclerView 中初始化、播放与销毁 SurfaceView 视频的实践同时指出直接管理 SurfaceView MediaPlayer 存在生命周期不易控制、内存泄漏、来回滑动出现上一视频最后一帧画面等问题可作为选型与优化时的参考。2. SurfaceView 如何保证 UI 流畅性双缓冲机制详解2.1 双缓冲机制由来问题的由来CPU 访问内存的速度要远远快于访问屏幕的速度。如果需要绘制大量复杂的图像每次都一个个从内存中读取图形然后绘制到屏幕就会造成多次访问屏幕导致效率很低——这就如同 CPU 与内存之间还需要多级缓存一样本质都是为了提升效率。第一层缓冲绘制图像时不用绘制一点显示一点的方案而是先在内存中将所有图像都绘制到一个 Bitmap 对象上然后一次性将内存中的 Bitmap 绘制到屏幕从而提高绘制效率。Android 中 View 的onDraw()方法已经实现了这一层缓冲——onDraw()中不是绘制一点显示一点而是全部绘制完成后一次性显示到屏幕。第二层缓冲onDraw()方法中的 Canvas 对象与屏幕直接关联而onDraw()运行在 UI 线程中如果要绘制的图像过于复杂仍可能导致应用程序卡顿甚至 ANR。因此可以先创建一个临时的 Canvas 对象将图像都绘制到这个临时 Canvas 中绘制完成后再将这个临时 Canvas 中的内容即一个 Bitmap通过drawBitmap()方法绘制到onDraw()方法中的 Canvas 对象上。这就相当于一次 Bitmap 的拷贝过程比直接绘制效率更高可以减少对 UI 线程的阻塞。2.2 如何理解 SurfaceView 的双缓冲双缓冲在运用时可以这样理解SurfaceView 在更新视图时用到了两张 Canvas——一张frontCanvas和一张backCanvas每次实际显示的是frontCanvasbackCanvas存储的是上一次更改前的视图当使用lockCanvas()获取画布时得到的实际上是backCanvas而不是正在显示的frontCanvas在获取到的backCanvas上绘制新视图后调用unlockCanvasAndPost(canvas)提交这张上传的 Canvas 将替换原来的frontCanvas成为新的frontCanvas原来的frontCanvas则切换到后台作为backCanvas。举例说明如果已经先后两次绘制了视图 A 和 B那么再调用lockCanvas()获取视图获得的将是 A而不是正在显示的 B之后将重绘的 C 视图上传C 将取代 B 成为新的frontCanvas显示在 SurfaceView 上原来的 B 则转换为backCanvas。从游戏开发的角度看双缓冲技术正是为了解决反复局部刷屏带来的闪烁把要处理的图片先在内存中处理好再整体一次性显示到屏幕上从而避免前面还没显示完、程序又请求重新绘制导致的屏幕闪烁见 flutter/04.混合开发/20.SurfaceView源码.md。3. 为什么在子线程更新画面不会阻塞 UI 主线程与子线程不能操作 UI矛盾吗3.1 标准使用流程代码模板使用 SurfaceView 的标准流程如下实现SurfaceHolder.Callback接口并重写其中的三个方法在surfaceCreated中创建或准备好用于绘制的线程在surfaceChanged中开启线程进行图像的绘制在surfaceDestroyed中结束绘制线程并调用SurfaceHolder.removeCallback()移除回调绘制线程每帧开始之前调用lockCanvas()锁住画布进行绘图绘制完一帧数据之后调用unlockCanvasAndPost()提交数据来显示图像用于控制子线程绘制的标记参数如mIsDrawing需要用volatile关键字修饰以保证多线程可见性与安全。以下为仓库 question/android/14.Android之音视频.md 中给出的完整伪代码模板/** * 必须实现SurfaceHolder.Callback接口和Runnable接口 */ public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable { // 是否绘制 private volatile boolean mIsDrawing; // SurfaceView 控制器 private SurfaceHolder mSurfaceHolder; // 画笔 private Paint mPaint; // 画布 private Canvas mCanvas; // 独立的线程 private Thread mThread; public MySurfaceView(Context context) { super(context); init(); } public MySurfaceView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public MySurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mSurfaceHolder getHolder(); // 注册回调事件 mSurfaceHolder.addCallback(this); mPaint new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setStyle(Paint.Style.STROKE); } Override public void surfaceCreated(SurfaceHolder holder) { VideoLogUtil.d(onSurfaceCreated); mThread new Thread(this, yc); } Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { VideoLogUtil.d(onSurfaceChanged format ---- width ---- height); // 并开启线程 mIsDrawing true; mThread.start(); } Override public void surfaceDestroyed(SurfaceHolder holder) { VideoLogUtil.d(onSurfaceDestroyed); // 不再绘制移除回调线程终止 mIsDrawing false; mSurfaceHolder.removeCallback(this); mThread.interrupt(); } Override public void run() { while (mIsDrawing) { VideoLogUtil.d(draw canvas); // 锁定画布获得画布对象 mCanvas mSurfaceHolder.lockCanvas(); try { // 使用画布做具体的绘制 draw(); // 线程休眠 100 ms Thread.sleep(100); } catch (Exception e) { VideoLogUtil.d(e.getMessage()); } finally { // 解锁画布提交绘制显示内容 if (mCanvas ! null) { mSurfaceHolder.unlockCanvasAndPost(mCanvas); } } } } private void draw() { // 开始绘制 } }由于 SurfaceView 常被用于游戏、视频等场景绘制操作相对复杂通常都需要开启子线程执行绘制以免阻塞 UI 线程。在子线程中通过SurfaceHolder的lockCanvas()方法获取 Canvas 对象来进行具体的绘制操作此时该 Canvas 对象被当前线程锁定绘制完成后通过unlockCanvasAndPost()提交绘制结果并释放 Canvas 对象。3.2 源码层面的依据为什么这套流程能够做到子线程绘制不阻塞主线程从 flutter/04.混合开发/20.SurfaceView源码.md 中的源码片段可以找到底层依据SurfaceView内部通过mSurfaceHolder一个内部SurfaceHolder实现对外提供服务其lockCanvas()实际调用internalLockCanvas()最终调用的是Surface.lockCanvas()并返回 Surface 中的 CanvaslockCanvas()与unlockCanvasAndPost()的调用过程都加了一把可重入锁mSurfaceLockReentrantLock保证绘制过程中只有绘制完一帧内容并提交更改后才会释放 Canvas才能继续下一帧的绘制操作Surface实现了Parcelable接口可在进程间及本地方法间传输其内部持有一个CompatibleCanvas对象lockCanvas()最终通过 JNI 调用nativeLockCanvas在 Native 层锁定画布unlockCanvasAndPost()则通过nativeUnlockCanvasAndPost提交内容锁与释放的实际过程均在 JNI 层完成。3.3 SurfaceHolder 的三个回调方法SurfaceHolder.Callback接口是使用 SurfaceView 必须实现的它包含三个方法作用与 Activity 生命周期回调类似详见 flutter/04.混合开发/19.SurfaceView学习.md 与 flutter/04.混合开发/20.SurfaceView源码.md回调方法触发时机典型用途surfaceCreated(SurfaceHolder holder)Surface 第一次创建时如 SurfaceView 从不可见变为可见初始化动作设置绘制线程标记位、创建用于绘制的子线程等。从该方法被调用到surfaceDestroyed被调用之前Surface 对象都可以被操作surfaceChanged(SurfaceHolder holder, int format, int width, int height)Surface 大小或格式改变时如横竖屏切换处理画面大小变化该方法在surfaceCreated之后至少会被调用一次surfaceDestroyed(SurfaceHolder holder)Surface 被销毁时如 SurfaceView 从可见变为不可见停止绘制线程并移除回调该方法被调用之后不能再对 Surface 对象进行任何操作否则会报错SurfaceHolder接口的源码结构来自 flutter/04.混合开发/20.SurfaceView源码.md可以分为两类Callback 类接口Callback及Callback2后者额外提供surfaceRedrawNeeded/surfaceRedrawNeededAsync用于监听 Surface 状态以进行相应处理创建绘制线程、停止绘制等交互类方法addCallback/removeCallback、lockCanvas()/lockCanvas(Rect dirty)/lockHardwareCanvas()、unlockCanvasAndPost(Canvas)、getSurfaceFrame()、getSurface()等用于与 Surface 以及 SurfaceView 交互、获取 Canvas 与提交绘制结果。3.4 是否与子线程不能操作 UI矛盾不矛盾。为什么系统不允许在子线程中访问 UI核心原因是 Android 的 UI 控件不是线程安全的如果在多线程中并发访问可能导致 UI 控件处于不可预期的状态。那么为什么系统不对 UI 控件的访问加锁呢缺点有两个加上锁机制会让 UI 访问的逻辑变得复杂锁机制会降低 UI 访问的效率因为锁机制会阻塞某些线程的执行。所以最简单且高效的方法就是采用单线程模型来处理 UI 操作上述分析参考自《Android 艺术探索》转述于 question/android/14.Android之音视频.md。而 SurfaceView 之所以特立独行是因为它不共享普通 View 的那套 UI 线程绘制通道它拥有自己独立的 Surface 绘图表面绘制操作在子线程中通过lockCanvas()获得属于该 Surface 的 Canvas 来完成与主线程中的 View 绘制互不干扰。因此它并不违反UI 单线程模型的原则而是绕开了这条约束——SurfaceView 本身并不直接参与主线程的 View 树绘制流程这也是其draw()/onDraw()不会执行的原因之一。4. 结合源码SurfaceView 为什么频繁刷新也不卡顿综合仓库 flutter/04.混合开发/20.SurfaceView源码.md 的总结可以归纳出使用 SurfaceView 的两大理由减少掉帧如果屏幕刷新频繁onDraw()会被频繁调用执行时间过长会导致掉帧、页面卡顿而 SurfaceView 采用了双缓冲技术提高了绘制速度可以缓解这一现象。避免主线程阻塞普通 View 的onDraw()运行在主线程中若绘制操作耗时会阻塞主线程、降低用户事件响应速度而 SurfaceView 可以在子线程中更新画面不会阻塞主线程提高了响应速度。从 Android 的绘制模型看flutter/04.混合开发/20.SurfaceView源码.md软件绘制模型由 CPU 主导先让视图结构失效invalidate再绘制整个视图结构Android 系统会绘制所有与脏区dirty region相交的区域其缺点是绘制了不需要重绘的视图且可能掩盖一些应用 bug。硬件加速绘制模型由 GPU 主导让视图结构失效、记录和更新显示列表Display List、绘制显示列表没有失效的 View 可重用先前记录的显示列表指令进行重绘提高了显示速度。但硬件加速并非万能存在兼容性部分绘制函数不支持或不完全加速、内存消耗OpenGL API 调用本身占用内存与电量消耗GPU 耗电三个缺陷。SurfaceView 正是为了在高频、复杂、被动刷新的绘制场景下利用独立 Surface 子线程 双缓冲的组合拳将绘制压力从主线程中剥离出去。5. 使用注意事项与选型建议线程标记位必须用volatile控制绘制线程运行的布尔变量如mIsDrawing、flag必须使用volatile修饰确保主线程修改后对绘制子线程立即可见避免线程退出不及时造成资源泄漏或绘制崩溃。lockCanvas()与unlockCanvasAndPost()必须成对出现每一帧绘制都应在try/finally中保证 Canvas 被解锁提交防止画布被长期锁定导致后续帧无法获取 Canvas。surfaceDestroyed中必须停止线程Surface 销毁后不能再对其做任何操作绘图线程必须在surfaceDestroyed中停止否则会报错。缺点评估SurfaceView 不支持平移、缩放等 View 属性变换不能嵌套使用。在需要上述能力的场景如弹窗内播放、需要缩放旋转的视频画面可考虑 TextureView 等替代方案YCBlogs 面试汇总中也提到了SurfaceView 替换方案这一考察点见 question/android/00.精品技术问题.md。视频场景的生命周期管理参考 android/20.工具库Lib/26.仿抖音滑动分页视频.md 的实践直接在页面容器中手动管理 SurfaceView MediaPlayer 容易出现生命周期失控、内存泄漏与上一视频最后一帧残留等问题在分页列表场景中建议结合 Fragment 生命周期或列表项复用onViewRecycled 等来统一管理视频初始化、播放与销毁必要时用首帧图片覆盖初始化阶段的空白或错帧。6. 小结回到原文档的三个核心问题question/android/14.Android之音视频.mdSurfaceView 是做什么的一种拥有独立 Surface 的特殊视图可在子线程中完成复杂高效的图像绘制广泛应用于游戏、视频、摄影等场景。SurfaceView 与 View 的本质区别SurfaceView 可在子线程更新画面且底层实现了双缓冲机制View 只能在 UI 主线程更新画面且无双缓冲由此带来 SurfaceView 不阻塞主线程、画面更流畅的优势以及不支持 View 属性变换、不能嵌套等劣势。为何不阻塞主线程且不矛盾因为绘制发生在独立线程、面向独立的 Surface 绘图表面通过lockCanvas()/unlockCanvasAndPost()成对操作完成每帧提交这与UI 控件非线程安全、采用单线程模型处理 UI的原则并不冲突SurfaceView 是绕开主线程 View 绘制通道的特例。掌握上述原理与代码模板后即可在实际项目中安全、高效地使用 SurfaceView 完成视频播放、动态特效等高频绘制需求。赞分享教程技术博客文档【免费下载链接】YCBlogs技术博客笔记大汇总包括Java基础线程并发数据结构Android技术博客等等常用设计模式常见的算法网络协议知识点部分flutter笔记还包括平时开发中遇到的bug汇总当然也在工作之余收集了大量的面试题长期更新维护并且修正持续完善……开源的文件是markdown格式的转载请注明出处谢谢项目地址https://gitcode.com/gh_mirrors/yc/YCBlogs点击查看免费下载相关推荐YCBlogs 技术笔记Android SurfaceView 全面解析——独立绘图表面、双缓冲原理与 Flutter 混合开发实践YCBlogs 技术笔记Android SurfaceView 全面解析——独立绘图表面、双缓冲原理与 Flutter 混合开发实践 SurfaceView教程技术博客文档YCBlogs 深入解读 Android View 的 onDraw 绘制机制与自定义绘制实践YCBlogs 深入解读 Android View 的 onDraw 绘制机制与自定义绘制实践 导读 本文以 YCBlogs 仓库中 android/04.Vi教程技术博客文档代码能力实测DeepSeek-R1-Distill-Llama-8B如何在LiveCodeBench实现39.6%通过率代码能力实测DeepSeek R1 Distill Llama 8B如何在LiveCodeBench实现39.6%通过率 DeepSeek R1 Disti创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考