1. 项目概述:从“卡顿”到“流畅”的底层逻辑
作为一名在移动端图形领域摸爬滚打了十多年的老码农,我几乎见证了Android图形栈从蹒跚学步到健步如飞的全过程。早期做应用开发,最头疼的就是列表滑动时的白块、动画渲染时的掉帧,那时候大家调侃“又不是不能用”,但心里都清楚,体验的鸿沟就摆在那里。这一切的转机,很大程度上要归功于“硬件加速”这个概念的彻底落地。今天,我们不聊那些高屋建瓴的架构图,而是直接钻进源码里,把Android硬件加速的启动流程、关键对象以及它们之间如何“勾心斗角”给掰开揉碎了讲清楚。这篇文章适合所有对Android UI性能优化有追求的中高级开发者,尤其是那些不满足于只会使用 android:hardwareAccelerated=”true” ,更想弄明白按下这个开关后,系统到底为你默默做了哪些“脏活累活”的同学。我们会从 ViewRootImpl 这个中枢开始,一路追踪到 ThreadedRenderer 的创建,再到 RenderNode 与 DisplayList 的录制,最后看看这些指令是如何跨进程交给GPU的。相信我,看完之后,你再遇到UI卡顿问题,排查的思路会清晰得多。
2. 硬件加速的整体架构与核心思想
在深入代码之前,我们必须建立一个正确的认知模型。Android的硬件加速,本质上是一个 绘制指令的录制与回放 系统,它改变了传统软件绘制“即时执行”的模式。
2.1 从“即时绘制”到“指令录制”的范式转变
在未开启硬件加速或 Canvas 处于软件渲染模式时,我们调用 canvas.drawXxx() 方法,相当于直接向一个位图( Bitmap )的像素内存进行写入操作。这个操作是同步的、立即生效的。想象成一个画家直接在画布上作画,每一笔下去,颜料立刻附着在画布上,无法撤回。
而硬件加速模式下, Canvas 背后关联的不再是一个简单的像素缓冲区,而是一个 显示列表 。当你调用 drawXxx() 时,你并不是在“画画”,而是在“写剧本”。系统将你的绘制操作(如画一个矩形、应用一个矩阵变换、设置一个颜色)转换成一个一个的绘制指令,并记录(录制)下来。这个“剧本”就是 DisplayList (在更新版本的源码中,其Java层对应物常被称为 RenderNode 的绘制指令集)。只有当整个视图树的绘制指令都录制完毕,这个“剧本”才会被提交给另一个专门的渲染线程(RenderThread),由它来负责指挥GPU这个“超级演员”进行高效地“演出”(渲染)。
这种转变带来了几个根本性优势:
- 避免无效绘制 :视图树中很多子View可能被其他View遮挡,或者其属性(如位置、透明度)未发生变化。在录制模式下,系统可以智能地跳过这些未变化部分的指令重新录制,直接复用上一帧的“剧本”。
- 并行化与异步化 :主线程(UI线程)只负责“写剧本”(录制指令),渲染线程负责“演出”(执行GPU渲染)。两者可以并行工作,主线程在录制完一帧后可以立即开始准备下一帧的逻辑,而不必等待渲染完成,极大地提升了帧率上限。
- GPU友好 :录制下来的指令是GPU能够直接理解或高效处理的抽象命令(如变换矩阵、纹理、路径),GPU擅长并行处理大量此类计算密集型任务。
2.2 核心角色关系图
理解下面几个核心类的关系,是读懂源码的关键:
ViewRootImpl (中枢调度)
|
|— 持有 —> ThreadedRenderer (硬件加速渲染器入口)
| |
| |— 管理 —> RootRenderNode (根渲染节点)
| | |
| | |— 包含 —> 子View对应的RenderNode
| |
| |— 关联 —> RenderProxy (Native层代理,通向RenderThread)
|
|— 触发 —> View.draw(Canvas) —(Canvas是HardwareRenderer包裹的)—> 录制指令到RenderNode
ThreadedRenderer :这是Java层硬件加速的起点和总管家。一个 ViewRootImpl 对应一个 ThreadedRenderer (如果开启了硬件加速)。它负责初始化渲染上下文、创建根 RenderNode 、管理渲染线程的通信。
RenderNode :这是硬件加速世界的“演员”。每个需要独立进行变换、裁剪、透明度合成的 View (或 ViewGroup )在硬件加速模式下都会对应一个 RenderNode 。它内部封装了两样东西:一是描述其自身属性的 RenderProperties (如位置、透明度、旋转角度),二是记录其绘制命令的 DisplayList 。
DisplayList :这是“剧本”本身。它存储在 RenderNode 内部,是一系列绘制操作(Op)的列表。当 View 的 draw(Canvas) 方法被调用时,传入的 Canvas 实际上是一个 DisplayListCanvas ,它负责将 drawLine , drawBitmap 等调用翻译成Op,并添加到 DisplayList 中。
RenderThread :这是一个独立的、高优先级的线程。它从 RenderNode 中取出 DisplayList ,通过OpenGL ES或Vulkan API驱动GPU进行实际的渲染工作。它与UI线程通过 RenderProxy 进行异步通信。
3. 源码启动流程深度追踪
理论铺垫完毕,我们穿上“潜水服”,进入 android.view 包下的源码世界。我们的起点是 ViewRootImpl 。
3.1 启程:ViewRootImpl的初始化与Renderer创建
ViewRootImpl 是连接 WindowManager 和视图树的桥梁。在 ViewRootImpl.setView() 方法中,会调用 enableHardwareAcceleration() 来决策是否启用硬件加速。
// ViewRootImpl.java
private void enableHardwareAcceleration(WindowManager.LayoutParams attrs) {
// 判断条件:应用级别开启、Window级别未显式关闭、系统支持
mAttachInfo.mHardwareAccelerated = false;
if (attrs.flags & WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED != 0) {
// ... 省略一些版本和系统条件检查 ...
try {
// 关键调用:创建ThreadedRenderer
mAttachInfo.mThreadedRenderer = ThreadedRenderer.create(mContext, translucent, attrs.getTitle().toString());
if (mAttachInfo.mThreadedRenderer != null) {
mAttachInfo.mHardwareAccelerated = true;
}
} catch (Exception e) {
// 创建失败,降级为软件渲染
}
}
}
这里的关键是 ThreadedRenderer.create() 。我们跟进去:
// ThreadedRenderer.java
public static ThreadedRenderer create(Context context, boolean translucent, String name) {
// 1. 检查系统是否支持硬件加速
if (!isAvailable()) {
return null;
}
// 2. 调用Native方法创建
return new ThreadedRenderer(context, translucent, name);
}
private ThreadedRenderer(Context context, boolean translucent, String name) {
// 保存参数,初始化一些状态
mTranslucent = translucent;
// 3. 另一个关键Native调用:nCreateRootRenderNode
long rootNodePtr = nCreateRootRenderNode();
mRootNode = RenderNode.adopt(rootNodePtr); // 包装成Java对象
mRootNode.setClipToBounds(false);
// 4. 初始化Native层的渲染代理
mNativeProxy = nCreateProxy(translucent, rootNodePtr);
// ... 其他初始化
}
nCreateRootRenderNode 和 nCreateProxy 都是JNI方法,它们跳转到Native层(通常是 android_view_ThreadedRenderer.cpp ),创建了代表根节点的 RootRenderNode (C++对象)和 RenderProxy 。 RenderProxy 是连接Java层与 RenderThread 的桥梁。
注意 :
RootRenderNode是一个特殊的RenderNode,它代表整个窗口的渲染层。它的DisplayList里包含的是所有子View的RenderNode。
3.2 绘制入口:performTraversals中的硬件加速路径
当需要更新UI时(如调用 invalidate() ), ViewRootImpl 会在其著名的 performTraversals() 方法中协调测量、布局、绘制三大流程。在绘制阶段,硬件加速路径与软件路径分道扬镳。
在 performDraw() -> draw(boolean fullRedrawNeeded) 中:
// ViewRootImpl.java
private void draw(boolean fullRedrawNeeded) {
if (!dirty.isEmpty() || mIsAnimating || ...) {
if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
// !!!硬件加速绘制路径 !!!
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
} else {
// 软件绘制路径
if (!drawSoftware(surface, mAttachInfo, ...)) {
return;
}
}
}
}
我们进入了 ThreadedRenderer.draw() 方法。这是硬件加速绘制的核心调度方法。
3.3 核心调度:ThreadedRenderer.draw() 详解
// ThreadedRenderer.java
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
// 1. 更新显示信息(如Surface尺寸、位置)
updateRootDisplayList(view, callbacks);
// 2. 如果根节点的DisplayList无效,说明没有内容需要绘制,直接返回
if (!mRootNode.isValid()) {
return;
}
// 3. 通知Native层代理,开始一帧的渲染
// 这个方法会阻塞,直到上一帧被RenderThread处理完(通过Fence机制),避免帧堆积。
nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length);
}
updateRootDisplayList 是另一个重中之重,它负责触发整个视图树的绘制指令录制。
// ThreadedRenderer.java
private void updateRootDisplayList(View view, DrawCallbacks callbacks) {
// 记录一个追踪标记,用于性能调试
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Record View#draw");
// 1. 为根RenderNode启动一个DisplayListCanvas
final DisplayListCanvas canvas = mRootNode.start(mSurfaceWidth, mSurfaceHeight);
try {
// 2. 这里会调用一系列回调,例如绘制DecorView的背景等
if (callbacks != null) {
callbacks.onPreDraw(canvas);
}
// 3. !!!最关键的调用:让顶层View(通常是DecorView)将其内容绘制到Canvas上 !!!
// 注意:此时传入的canvas是HardwareCanvas,它连接的是根RenderNode的DisplayList。
int saveCount = canvas.save();
canvas.translate(0, -view.mScrollY); // 处理滚动偏移
view.draw(canvas); // 这会递归触发整个视图树的draw(Canvas)
canvas.restoreToCount(saveCount);
if (callbacks != null) {
callbacks.onPostDraw(canvas);
}
} finally {
// 4. 结束录制,将Canvas中的指令最终提交到根RenderNode的DisplayList中
mRootNode.end(canvas);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
3.4 视图树的递归录制:View.draw(Canvas) 的硬件加速之旅
当 view.draw(canvas) 被调用时,这个 canvas 已经是 DisplayListCanvas 。我们看看 View.draw(Canvas canvas) 方法在硬件加速下的关键逻辑(简化版):
// View.java
public void draw(Canvas canvas) {
// ... 省略背景、滚动条等绘制判断 ...
// 关键步骤:dispatchDraw,用于绘制子View
dispatchDraw(canvas);
// ... 省略前景、滚动条等 ...
}
protected void dispatchDraw(Canvas canvas) {
// 遍历所有子View
for (int i = 0; i < childrenCount; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
// 1. 处理子View的动画和变换
more |= applyLegacyAnimation(parent, child, drawingTime);
// 2. 关键:为子View获取或创建其对应的RenderNode
RenderNode renderNode = child.mRenderNode;
if (renderNode != null && (child.mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0) {
// 子View需要更新其DisplayList
final DisplayListCanvas childCanvas = renderNode.start(child.getWidth(), child.getHeight());
try {
// 变换Canvas的坐标系到子View的位置
childCanvas.translate(child.mLeft, child.mTop);
// !!!递归调用子View的draw方法,录制其指令 !!!
child.draw(childCanvas);
} finally {
renderNode.end(childCanvas);
}
}
// 3. 将子View的RenderNode插入到当前Canvas(父Canvas)的DisplayList中
// 注意:这里插入的不是像素,而是一个“引用”,告诉渲染器:“这里有一个RenderNode,请按照它的属性去渲染它”。
canvas.drawRenderNode(renderNode);
}
}
}
这个过程是递归的:
- 父
View(如DecorView)的draw方法被调用,传入连接根RenderNode的Canvas。 - 父
View在dispatchDraw中遍历子View。 - 对于每个需要绘制的子
View,先获取其专属的RenderNode和DisplayListCanvas,然后递归调用child.draw(childCanvas),让子View将自己的绘制命令录制到自己的DisplayList中。 - 子
View录制完毕后,父View通过canvas.drawRenderNode(renderNode),将子View的RenderNode作为一个绘制操作(DrawRenderNodeOp)插入到 父View自己的DisplayList 中。 - 最终,所有
RenderNode像一棵树一样被组织起来,根RenderNode的DisplayList包含了所有子RenderNode的引用。
实操心得 :理解
drawRenderNode是关键。它意味着在硬件加速下,ViewGroup绘制子View时,并不是立即绘制子View的像素,而是将子View的RenderNode(一个包含属性和绘制指令的封装对象)作为一个“占位符”插入到自己的绘制列表里。真正的合成与渲染发生在后续的RenderThread中。这解释了为什么修改子View的translationX或alpha属性可以非常高效——只需要更新RenderNode的属性,无需重新录制其内部的DisplayList。
3.5 一锤定音:nSyncAndDrawFrame与RenderThread的协作
当 updateRootDisplayList 完成,根 RenderNode 拥有了完整且最新的 DisplayList 后, ThreadedRenderer.draw() 会调用 nSyncAndDrawFrame 。这是一个JNI调用,它将控制权交给Native层的 RenderProxy 。
RenderProxy 的主要工作如下:
- 同步 :等待上一帧的GPU渲染工作完成(通过
Fence机制),确保不会发生帧覆盖,这是保证画面不撕裂的重要机制之一。 - 构建帧 :将当前帧的所有数据(根
RenderNode的DisplayList、窗口的Surface信息、动画时间戳等)打包成一个Frame对象。 - 投递任务 :将这个
Frame对象作为任务,投递到RenderThread的消息队列中。 - 唤醒渲染线程 :
RenderThread被唤醒,从队列中取出Frame任务。
接下来就是 RenderThread 的舞台了:
- 解析DisplayList :
RenderThread遍历RenderNode树,解析每个DisplayList中的绘制操作(Op)。 - 状态管理与批处理 :它将状态变更(如切换着色器、绑定纹理)和绘制命令进行优化和批处理,减少GPU的状态切换开销。
- 调用GPU驱动 :通过OpenGL ES或Vulkan API,将优化后的命令序列提交给GPU。
- 交换缓冲区 :渲染完成后,通知
SurfaceFlinger进行图层合成,最终将画面显示到屏幕上。
至此,一帧的硬件加速绘制流程就走完了。整个过程体现了清晰的职责分离: UI线程负责构建和更新渲染指令集(CPU密集型),RenderThread负责高效执行这些指令(GPU密集型) 。
4. 关键对象与数据结构剖析
理解了流程,我们还需要深入看看几个核心对象内部到底有什么。
4.1 RenderNode:属性与指令的容器
RenderNode 在Java层是一个轻量级的包装,核心数据都在Native层。它主要包含两部分:
-
RenderProperties :存储所有影响渲染结果的属性。
-
mLeft,mTop,mRight,mBottom:位置和尺寸。 -
mTranslationX,mTranslationY,mTranslationZ:位移。 -
mRotationX,mRotationY,mRotationZ:旋转。 -
mScaleX,mScaleY:缩放。 -
mPivotX,mPivotY:变换支点。 -
mAlpha:透明度。 -
mElevation:海拔高度,影响阴影。 -
mClipToBounds:是否裁剪到边界。 - 这些属性可以通过
View的setTranslationX、setAlpha等方法直接映射更新。当属性改变时,RenderNode会标记自己为“属性脏”,在下一帧渲染时,RenderThread会读取新的属性值并应用,而无需重新录制DisplayList。
-
-
DisplayList (在Native层) :一个存储绘制操作(Op)的容器。操作类型繁多,例如:
-
DrawPathOp -
DrawBitmapOp -
DrawTextOp -
DrawRenderNodeOp(用于插入子RenderNode) -
SaveLayerOp,RestoreToCountOp(用于图层保存恢复) - 每个Op都包含了执行该绘制所需的所有参数。
-
4.2 DisplayListCanvas:录制指令的翻译官
DisplayListCanvas 是 Canvas 的一个子类。当调用 renderNode.start() 时,就会获得一个与此 RenderNode 绑定的 DisplayListCanvas 。它的 drawXxx() 方法被重写,行为与软件Canvas截然不同:
// 示例,非精确源码
@Override
public void drawRect(float left, float top, float right, float bottom, Paint paint) {
// 1. 将Paint中的颜色、样式、Shader等参数提取出来
// 2. 创建一个DrawRectOp对象,包含这些参数
DrawRectOp op = new DrawRectOp(left, top, right, bottom, paint.getColor(), paint.getStyle());
// 3. 将这个Op添加到当前RenderNode关联的DisplayList中
nDrawRect(mNativeCanvasWrapper, op);
// 注意:没有任何像素在此刻被改变!
}
4.3 动画与属性更新的高效性来源
为什么硬件加速下属性动画如此高效?结合上面的结构就一目了然了。
假设一个 View 执行位移动画:
- 帧1 :
View的draw()被调用,其RenderNode录制了绘制一个蓝色矩形的DisplayList。此时translationX=0。 - 属性动画开始 :动画引擎(如
ValueAnimator)在每一帧直接更新View的translationX属性(本质上是更新其RenderNode的mTranslationX)。 - 帧2...N :
View的draw()方法 不会被调用 !因为它的内容(蓝色矩形)没有变,只是位置变了。系统检测到只有RenderNode的属性脏了,而DisplayList是干净的。 - 渲染时 :
RenderThread在渲染每一帧时,会读取RenderNode最新的translationX值,并将其作为一个变换矩阵应用到该RenderNode的整个DisplayList上,然后指挥GPU绘制。整个过程完全跳过了UI线程的绘制指令录制,开销极小。
5. 常见问题与实战排查技巧
了解了原理,我们来看看实战中会遇到的问题和排查手段。
5.1 硬件加速的“坑”与规避方案
-
绘制内容不支持 :不是所有
Canvas操作都支持硬件加速。例如,自定义View时使用了Canvas的clipPath(非矩形)、drawTextOnPath,或者某些极端的PathEffect。在硬件加速开启时,这些操作可能被忽略(静默失败)或导致回退到软件渲染层,引发性能问题。- 排查 :在开发者选项中打开“显示硬件层更新”,如果某个区域频繁闪烁(表示硬件层在重建),很可能触发了回退。
- 解决 :对于不支持的操作,有几种选择:
- 方案A :在
View的onDraw中,通过canvas.isHardwareAccelerated()判断,对不支持的操作使用Bitmap离屏缓冲(软件绘制)后再用canvas.drawBitmap绘制,但这会牺牲一些性能。 - 方案B :为该
View单独关闭硬件加速view.setLayerType(View.LAYER_TYPE_SOFTWARE, null),但需谨慎,因为这会强制该View及其子树走软件渲染路径。 - 方案C(推荐) :寻找替代API。例如,用
canvas.clipRect代替复杂的clipPath。
- 方案A :在
-
内存与纹理溢出 :每个
RenderNode和离屏缓冲(如View.setLayerType(LAYER_TYPE_HARDWARE)创建的)都会消耗GPU内存(纹理)。过度使用硬件层或创建巨大尺寸的Bitmap(会被上传为GPU纹理)可能导致OutOfMemoryError或渲染异常。- 排查 :使用Android Studio的Memory Profiler,关注
Graphics部分的内存使用。观察TextureView或硬件层View的数量和尺寸。 - 解决 :及时释放不再需要的
Bitmap(recycle())。对于TextureView,确保其生命周期管理得当。避免为静态内容设置硬件层。
- 排查 :使用Android Studio的Memory Profiler,关注
-
过度绘制(Overdraw)在硬件加速下依然存在 :硬件加速优化的是绘制指令的执行效率,但无法自动消除被完全遮挡的像素绘制。如果UI设计存在大量重叠且不透明的视图,GPU仍然会忠实地绘制所有图层,浪费算力和功耗。
- 排查 :在开发者选项中打开“调试GPU过度绘制”,蓝色、绿色为佳,红色、深红色区域需要优化。
- 解决 :优化布局层级,减少不必要的背景,使用
android:outlineSpotShadowColor和android:outlineAmbientShadowColor替代复杂的视图叠加来制造阴影效果。
5.2 性能问题排查工具箱
当遇到UI卡顿,怀疑是硬件加速相关问题时,可以按以下步骤排查:
-
定位卡顿阶段 :使用Systrace工具。这是最强大的武器。抓取trace后,重点关注:
- UI Thread :如果
Record View#draw或updateRootDisplayList耗时很长,说明录制DisplayList的CPU开销大。可能是onDraw逻辑复杂,或触发了不支持的绘制操作导致回退。 - RenderThread :如果
RenderThread的DrawFrame耗时很长,说明GPU渲染压力大。可能是过度绘制严重,或使用了复杂的Shader效果。 - Alerts :Systrace会直接给出警告,如“Expensive DisplayList operations”。
- UI Thread :如果
-
检查硬件层状态 :在开发者选项中开启“显示硬件层更新”(Show hardware layers updates)。正常交互时,屏幕不应有大面积闪烁。如果某个区域持续闪烁,说明对应的
RenderNode在频繁重建DisplayList,需要检查其内容是否在频繁改变,或者是否被错误地设置了动画。 -
使用Profile GPU Rendering :在开发者选项中开启“GPU渲染模式分析”或“Profile HWUI rendering”。观察屏幕上各条柱状图:
- 紫色(Swap Buffers) :处理帧的时间,通常代表GPU工作的耗时。过高表示GPU负载重。
- 红色(Execute) :
RenderThread执行绘制命令的时间。 - 橙色(Process) :
UI Thread准备DisplayList的时间。 - 通过对比不同页面的柱状图,可以快速定位是CPU录制瓶颈还是GPU渲染瓶颈。
-
代码级检查 :
- 检查自定义
View的onDraw方法,是否包含Paint的setShader、setMaskFilter等复杂操作。 - 检查是否在动画过程中调用了
View.invalidate()而不是通过属性动画直接修改属性。invalidate()会触发DisplayList的重新录制。 - 检查
View的setLayerType使用是否合理。LAYER_TYPE_HARDWARE用于正在执行复杂变换动画的View以提升性能,但动画结束后应及时清除(设置为LAYER_TYPE_NONE)。
- 检查自定义
5.3 一个典型性能问题的分析与解决
场景 :一个自定义的环形进度条 View ,在旋转动画时卡顿。
初步分析 :使用Systrace抓取,发现 UI Thread 的 Record View#draw 峰值很高,且 RenderThread 相对空闲。这表明瓶颈在CPU端的指令录制。
代码审查 :发现 onDraw 中这样绘制圆环:
protected void onDraw(Canvas canvas) {
// 错误示例:在每一帧都新建Path和Paint
Path path = new Path();
path.addArc(ovalRect, startAngle, sweepAngle);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(width);
paint.setColor(color);
canvas.drawPath(path, paint);
}
问题根源 :在硬件加速下, onDraw 每帧都被调用以录制 DisplayList 。而上述代码每帧都创建新的 Path 和 Paint 对象。 Path 对象的创建和 DrawPathOp 的录制本身就有一定开销,更重要的是, Paint 对象的改变(即使是同参数的新对象)可能导致 DisplayList 无法被完全复用,因为系统可能认为绘制状态发生了改变。
优化方案 :
private Path mPath = new Path();
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private RectF mOvalRect = new RectF();
{
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mStrokeWidth);
mPaint.setColor(mColor);
}
protected void onDraw(Canvas canvas) {
// 1. 复用对象,只更新数据
mOvalRect.set(0, 0, getWidth(), getHeight());
mPath.reset();
mPath.addArc(mOvalRect, mStartAngle, mSweepAngle);
// 2. 如果颜色等属性会变,也只需调用setter,而不是创建新对象
// mPaint.setColor(mCurrentColor);
canvas.drawPath(mPath, mPaint);
}
更进一步优化 :如果进度条只是匀速旋转( startAngle 变化),而形状不变。可以考虑使用属性动画直接修改 View 的 rotation 属性,或者修改 RenderNode 的 rotationZ ,这样连 onDraw 和 DisplayList 录制都省了,性能最佳。
硬件加速是一套精密的协作系统,理解其流程和原理,能让我们在追求极致流畅体验的路上,从“玄学调优”走向“精准打击”。下次当你再面对复杂的UI性能问题时,希望这份源码层面的地图,能帮你更快地找到问题的坐标。

341

被折叠的 条评论
为什么被折叠?



