☰
Android性能优化:主线程doFrame卡顿ANR排查实战
2026/10/21 6:58:54 网站建设 项目流程

1. 问题现象与本质认知:当 doFrame 成为 ANR 的"事故现场"

先聊一个我印象特别深的案例。某个版本上线后,线上反馈出现了一批"应用无法响应"的弹窗,用户点击屏幕无反应,页面卡死,只能强制杀掉重启。当时我拿到反馈后的第一反应是:查主线程在干什么。结果日志一行行扫下来,发现一个诡异的现象——ANR in com.xxx.core的堆栈里,等待的最长执行点就落在Choreographer.doFrame()上,但诡异的是,doFrame 内部并没有出现明显的 CPU 密集计算,也没有死循环。可主线程就是被"钉"在里面超过 5 秒没出来。

这就很有意思了——doFrame 是一个"入口",不是"病灶"。我的意思是,你看到主线程卡在 doFrame 里,不代表 doFrame 里的代码写得多烂,而是说明主线程在"往 doFrame 跑的这条路上"就已经被堵死了,只是卡顿现场恰好被系统采集堆栈时"抓"在了这个位置。后面的排查过程让我把 doFrame、主线程、ANR 这三者的关系彻底吃透了。

这篇内容适合所有做 Android 应用开发、尤其是负责性能优化、卡顿治理、线上稳定性的同学。如果你现在还在"线上 ANR 一抓一大把,但拿到日志不知道从哪看起"的阶段,这篇文章就是为你准备的。我会把 doFrame 机制、ANR 产生的条件、堆栈阅读方法、常用排查工具和实际优化方案一次性讲透,内容全部来自真实项目里的排查和修复经验。

2. doFrame 机制深度拆解:为什么它如此重要又如此脆弱

2.1 Choreographer 的工作模型

在 Android 的渲染体系里,Choreographer是一个极其核心的组件。你可以把它当成一个"军训教官",负责协调输入事件处理、动画更新、View 布局测量、绘制以及 VSYNC 信号之间的"列队作息"。

Choreographer的核心任务是根据垂直同步信号(VSYNC,即 Vertical Synchronization)来触发每一帧的渲染工作。每一次 VSYNC 信号到达,系统会在主线程回调Choreographer.FrameCallback,也就是我们常说的doFrame()方法。这个方法体内必须完成当前帧所有 UI 相关的工作——包括处理输入事件、执行动画、调用measure、layout、draw——然后才能把结果交给 SurfaceFlinger 进行合成上屏。

也就是说,doFrame()是一个"同步事件集合点",所有 UI 阶段的活儿都必须在这一次回调中同步完成。一旦它超时,就意味着这一帧没画出来,用户看到的就是卡顿;如果连续很多帧都画不出来,或者单次阻塞时间过长,系统就会判定 ANR。

注意:doFrame()本身不是造成 ANR 的原因,而是"主线程是否在正常干活"的检测窗口。它就像一个计时器,如果主线程连"回来报到"都做不到了,那说明整个 UI 线程的健康度已经出大问题了。

2.2 ANR 的判定阈值与 doFrame 的关系

先把 ANR 的触发条件说清楚。Android 系统中,几种常见的 ANR 场景有着各自的超时阈值:

  • 输入事件处理超时:Input ANR,一般是 5 秒。用户触摸事件在 5 秒内没有被处理完成或没有新的输入事件被及时消费,就可能触发。
  • 广播接收器超时:前台广播 10 秒,后台广播 60 秒未执行完成,触发 Broadcast ANR。
  • 服务执行超时:前台服务 20 秒、后台服务 200 秒未执行完,可能触发 Service ANR。
  • ContentProvider 超时:进程首次启动时 provider 未能在较短时间内完成初始化,也会 ANR。

而我们在实际线上看到的"主线程在 doFrame 的 ANR",绝大多数属于第一类——Input ANR。原因是:doFrame 回调对应的是帧的生成和投递,如果主线程一直阻塞在 doFrame 内部,输入事件(Touch、Key、MotionEvent)就无法及时分发和处理。当用户连续点击、滑动,系统累积的事件超过临界值,就会判定应用无响应。

用大白话解释就是:你的应用窗口还在桌面上,但"前台服务员"(主线程)已经趴在吧台上不动了。客人(用户)怎么招手都没反应,系统只能在后台偷偷记上一笔"无响应记录",然后弹一个"关闭/等待"的对话框。

2.3 doFrame 的超时为何容易被"背锅"

这里有一个非常容易误导人的地方:从严格意义上讲,doFrame()方法在常规写法中只是三个 UI 阶段的"入口"。你的应用代码不会直接在这个方法里写业务逻辑,因为回调是系统按帧调度的。真正的问题往往出在这个方法调起的一连串子调用里——比如ViewRootImpl.performTraversals()、View.measure()、View.layout()、View.draw()、以及各种View.OnClickListener.onClick()、RecyclerView.onBindViewHolder()等等。

因此,当我们拿到一份 ANR trace,看到堆栈顶层停留在android.view.Choreographer.doFrame()时,一定要冷静:这并不是说你在 doFrame 里写了什么坏代码,而是你的主线程在这一帧的执行窗口内被某段耗时操作卡住了。系统采样时恰好抓到了这个位置的快照,所以才会把 doFrame 显示出来。你需要继续往堆栈深处看,找到真正导致耗时的大头。

3. 主线程在 doFrame 中 ANR 的六类常见原因

3.1 锁等待:看似在做 UI,实际在抢锁

我最先要讲的就是锁竞争,因为它的伪装性太强了。很多次线上 ANR 的堆栈里,主线程就停在doFrame内部的某个synchronized (mLock)或ReentrantLock.lock(),而持锁的却是某个后台线程。

举个例子:业务代码里有一个全局的配置缓存单例,后台线程在刷新配置时持锁写文件,耗时 3 秒。恰好主线程在执行onDraw时需要读取缓存,于是主线程被迫在锁前排队等待。整个过程,主线程看起来"跑"到了doFrame,实际是在锁等。这种问题在日志里非常隐蔽——你要不是熟悉锁的等待关系,很容易以为只是 UI 代码执行慢。

排查建议:在 trace 文件里,找到主线程后,重点看waiting to lock <0x...>这类信息,再全局搜索同一个锁地址,找到持锁线程和持锁位置。如果持锁线程是一个普通的后台线程,且处于"正在 do some IO"状态,恭喜,罪魁祸首找到了。

3.2 主线程直接执行了耗时 IO

这是最"朴素"的 ANR 原因了——主线程做了网络请求、磁盘读写、数据库查询。虽然现在大部分项目都有主线程联网检查器,但总有一些漏网之鱼,比如:

  • SharedPreferences的apply()虽然是异步写磁盘,但如果提交量太大、频繁 commit 导致积压,主线程的QueuedWork.waitToFinish()会在 Activity 生命周期关键节点阻塞等待。
  • SQLiteDatabase的rawQuery()在主线程直接执行,尤其在前台页面查询大量数据时,耗时可能远超预期。
  • 直接用FileInputStream读文件、BitmapFactory.decodeFile()解大图,这两者极少被 lint 检查到,但非常致命。

doFrame 场景下最坑的一个,是onBindViewHolder里加载图片并同步解码。RecyclerView 在滑动过程中每一帧都会调用 doFrame,而 bind 里的同步解码会让每一帧都变慢一点。当图片特别大(比如 2000px 以上的全屏图)时,解码耗时可达 200~500ms。连续滑几帧,主线程就卡死了。

3.3 measure/layout 阶段的计算爆炸

另一个高频原因是布局过度复杂。Android 的布局计算公式是——任何一层 View 的measure、layout、draw变化,都可能引起父容器及其所有子 View 的重新测量与绘制。一个常见的"万恶之源"是:在onMeasure里写了多层嵌套的LinearLayout,内部每个子 View 都依赖父级的测量结果动态计算,这时候系统的测量事件会以指数级增长。

doFrame 的单次调用里,如果performTraversals()中measure阶段耗时超过 1 秒,那整个 UI 线程会被死死的按在View.measure()的调用栈里。系统再来一个 Input ANR,堆栈顶自然就指向 doFrame。

3.4 渲染管线阻塞:等待 Surface 锁或 GPU 操作

你可能会说:"我主线程没有做耗时操作,为什么也 ANR 了?"这种情况也真实存在,而且相当难排查。原因之一就是渲染管线的阻塞。

当应用启用硬件加速时,主线程在 doFrame 中会将绘制命令同步给 RenderThread,然后等待 RenderThread 返回"帧就绪"信号。如果 RenderThread 被卡住(比如 GPU 负载过高、Surface 异常、GPU 驱动 bug),主线程也会长时间阻塞在RenderProxy.syncAndDrawFrame()或类似方法。从堆栈上看,主线程是停留在 doFrame 内部的,但真正的瓶颈却在 RenderThread 线程上。

一个典型案例是:低端机在播放视频时,解码的 Surface 与主 Surface 同步出现问题,主线程在lockCanvas或syncAndDrawFrame阶段等了很久。这种问题靠"优化主线程代码"完全无效,你得去排查 SurfaceView、TextureView 的过渡使用,或者考虑关闭硬件加速。

3.5 主线程过载:消息队列积压,doFrame 排不上号

有些时候,主线程并没有"卡死在某个调用里",而是消息队列里积压了太多消息。比如滑动列表时,每一帧的 doFrame 消息后面又跟着大量的onScrollChanged、图片加载回调、状态更新等等,这些消息的堆积导致下一个 doFrame 无法及时执行。

从 ANR trace 上看,主线程可能正在处理一个比较耗时的消息(比如某个自定义动画的onAnimationUpdate),而后面排队等待的 doFrame 迟迟没被调度。这种情况下,doFrame 在 trace 里可能只是"后续排队"状态,但系统在 Input ANR 的判断时,会把这笔账记在"主线程无响应"上。

解决这个问题的关键,是梳理消息队列中的高频任务和耗时任务,尽可能减少主线程上的"非 UI 必要消息"。尤其要注意:不要在滚动类回调里做重量级操作,比如在OnScrollListener中频繁更新整页数据。

3.6 频繁 GC 导致的长时间暂停

如果应用在短时间内创建了大量短生命周期对象(比如大量 String 拼接、频繁 alloc 临时对象),系统会频繁触发 GC。在某些情况下,主线程会直接参与 GC(CMS 并发失败时退化为 Full GC),导致长时间停顿。

这种 ANR 的堆栈特征是:主线程停留在 GC 相关的ART::Heap调用上,而不是你自己的代码逻辑。虽然堆栈可能不会直接显示 doFrame,但如果你知道 GC 停顿往往发生在帧渲染的间隔期间,就很容易联想到"doFrame 明明在跑,却被 GC 掐住脖子"。

4. 完整排查流程:从拿到日志到定位根因

4.1 学会读dropbox目录下的 ANR trace

当 ANR 发生时,系统会把主线程和所有子线程的堆栈 dump 到/data/anr/目录下。线上环境我们一般通过DropBoxManager上报,或者直接集成第三方收集平台(如 Bugly 等)。

拿到 trace 后,我的习惯是先看主线程堆栈,按以下顺序排查:

  1. 锁定线程状态:查看主线程是nativePollOnce等待输入事件,还是blocked在锁上,或runnable正在疯狂执行。
  2. 看锁等待信息:如果有waiting to lock,立刻记录锁地址。
  3. 看 JNI/系统调用边界:如果停在java.io的方法里,说明在做 IO;如果停在android.graphics里面,说明在走图形相关逻辑。
  4. 看业务方法栈:从堆栈上层往下扫,找到你应用自己的类名和方法,这就是"案发地点"。

主线程堆栈是"唯一真相"。要记住,:main的状态基本能决定 ANR 的类型:

主线程状态含义常见原因
RUNNABLE正在执行代码CPU 密集型计算、死循环、measure/draw 超长
BLOCKED被锁阻塞等待持锁线程释放锁
WAITING调用 Object.wait()等待 notify / 条件不满足
TIMED_WAITINGsleep 或 timed-waitIO 超时等待、Object.wait(timeout)
NATIVE执行 native 代码JNI/图形/GPU 相关耗时

4.2 借助自定义 FrameCallback 做线上监控

找 bug 不能每次都等用户触发 ANR 然后被动看日志。更主动的办法是自己在代码里注册一个FrameCallback,统计doFrame的执行时长。

写一个简单但实用的监控工具很容易。思路是:在主线程的Choreographer上挂一个自定义 FrameCallback,利用回调的时间戳差值计算单帧耗时。如果单帧耗时超过阈值(比如 300ms),就认为是一次卡顿发生时点,并把当前时间点切到后台线程去 dump 主线程的堆栈。

这是我项目里一个非常实用的精简版本:

public class FrameMonitor implements Choreographer.FrameCallback { private static final long FRAME_SKIP_THRESHOLD_MS = 300L; private long mLastFrameTimeNanos; @Override public void doFrame(long frameTimeNanos) { long currentTimeMillis = TimeUnit.NANOSECONDS.toMillis(frameTimeNanos); if (mLastFrameTimeNanos > 0) { long intervalMs = currentTimeMillis - TimeUnit.NANOSECONDS.toMillis(mLastFrameTimeNanos); if (intervalMs > FRAME_SKIP_THRESHOLD_MS) { // 发生了卡顿,丢到后台线程采集主线程堆栈 ThreadUtils.runOnBackground(() -> dumpMainThreadStack()); } } mLastFrameTimeNanos = frameTimeNanos; Choreographer.getInstance().postFrameCallback(this); } private void dumpMainThreadStack() { // 用 ThreadMXBean / Looper.getMainLooper().getThread().getStackTrace() 抓取主线程堆栈 } }

注意:postFrameCallback里每帧都要重新挂一次,否则只会在第一帧回调,后续不再响应。这个细节很容易踩坑,建议直接写上。

有了这个监控器,配合线上抽样上报,你就能在 ANR 发生前捕获到"主线程卡在哪个业务栈"的证据链,然后反推修复方案。

4.3 结合Systrace定位渲染链路耗时

如果本地能复现,Systrace(或新版 Android Studio 中的CPU Profiler+System Trace)是定位 doFrame 耗时最直观的工具。它可以把你应用的 UI 渲染链路按时间轴展开——比如能看到某个 VSYNC 信号对应帧,measure 花了多少时间,layout 花了多少时间,draw 花了多少时间。

需要重点关注的三个指标:

  • doFrame 到 vsync 的次数偏移:如果 doFrame 的触发比 vsync 晚了很多,说明主线程消息队列里有积压,前面有任务在排队。
  • render thread 的耗时:主线程 if 执行很快,但 render thread 花了很长时间,说明 GPU 提交或合成阶段出了问题。
  • 主线程 CPU 占用:如果主线程一直处于running状态且 CPU 被占满,说明是代码执行慢;如果 CPU 占用很低但主线程很慢,说明是在等待锁或 IO。

4.4 从系统日志找被忽略的"隐藏帮凶"

有时 ANR 的根因不在应用代码,而在系统服务。比如系统内存不足触发lmkd杀进程,或 CPU 被其他进程抢占导致主线程迟迟得不到调度。这些情况在 trace 里可能看不太出来,需要结合系统日志logcat -b events -b system一起看。

典型线索包括:

  • am_anr事件:系统主动判定 ANR 的入口,会打印 why 类型。
  • low_memory_kill:低内存杀进程事件,说明当时内存压力大。
  • cpu_time:ANR 前各进程的 CPU 使用率,知道是系统问题还是自家应用问题(如果 CPU 100%,但都是另一个进程在占用,说明主线程被饿死了)。

5. 根治方案与优化策略:从"不卡"到"不慌"

5.1 异步化改造:把非 UI 必要的任务全部移出主线程

这是最核心的一步。凡是与 UI 无关的耗时操作,统统丢到子线程。重点检查这几类:

  • 网络请求:必须走异步,禁止用同步execute()在主线程执行网络操作。
  • JSON 解析:在回调里拿到原始字符串后,先切子线程做JSONObject或Gson.fromJson,解析完成再切回主线程刷新 UI。
  • 数据库操作:用Room的 suspend/异步查询,避免主线程直接操作 SQLite。
  • 图片加载:统一接入 Glide 或 Fresco,它们内部会自动处理子线程解码和缓存,避免主线程同步 decode。

如果项目已经有大量存量代码,建议用字节码插桩技术(比如自定义 Transform 或 ASM 插件)在编译期检测主线程上的危险调用,直接抛异常或上报。这种强约束比靠人自觉有效得多。

5.2 布局优化:让 measure/layout 变"廉价"

布局优化看上去是老生常谈,但它在 doFrame ANR 里的影响比想象中大得多。特别是复杂的动态列表页面,每个 item 的布局层级如果过深,会导致每次刷新都重新测量整棵树。我的经验原则是:

  • 减少层级:用ConstraintLayout替代多层嵌套的LinearLayout/RelativeLayout,把层级控制在一层到两层。
  • 合并重绘:如果多个子 View 背景相同,考虑在父布局统一绘制背景,减少绘制次数。
  • 延迟初始化:不一次性加载所有 View,用ViewStub做占位,在真正需要显示时才 inflate。
  • 复用作画对象:绘制里不要频繁创建Paint、Path等对象,尽量声明成成员变量或静态变量。这个我亲测有效,能明显降低 GC 和绘制耗时。

5.3 避免主线程锁竞争:用无锁结构或降低锁粒度

锁竞争的排查和修复是这轮 ANR 优化的硬骨头。我的经验是"能不用锁就不用锁,必须用锁就缩小范围"。

比如用ConcurrentHashMap代替HashMap+ synchronized,用ReadWriteLock代替独占锁(读多写少的场景),或者把锁的持有时间缩短——不要在锁内部做 IO、不要在网络回调里持锁。

有一个特别容易忽略的地方:SharedPreferences的getXxx()内部是有锁的。如果多个线程高频访问同一个 SP,锁竞争会非常明显。修复思路是:改为 App 启动时一次性读取全部配置到内存缓存,后面所有线程直接读内存,写时合并批量提交。

对,这一步踩过的坑真的很深。有一次线上 ANR 堆栈,主线程停在SharedPreferencesImpl.getLocked()里,而持锁线程是一个做崩溃埋点的后台线程,它拿着锁在刷盘大量日志。排查了一天,最后靠锁地址的全局搜索才找到持锁线程。这类问题一旦出现,优化收益非常可观。

5.4 渲染管线优化:从主线程侧释放压力

如果是渲染相关的问题,可以从几个角度优化:

  • 压缩过度绘制:通过"调试 GPU 过度绘制"开关定位重绘区域,减少不必要的背景和图层叠加。
  • 减少透明层级:半透明区域叠加会导致 GPU 混合计算量暴增。
  • 用setLayerType谨慎处理:LAYER_TYPE_HARDWARE能缓存绘制结果,但会占用 GPU 内存,使用时要确保不是频繁变化的区域。
  • 评估是否需要关闭硬件加速:少数畸形 Surface 场景下,关闭硬件加速可规避卡死,但这是降级方案,不能用做默认手段。

5.5 减少主线程消息积压:治理"高频消息"与"低效回调"

消息队列的积压问题,本质上是主线程的任务调度健康度问题。我建议定期做一次"主线程任务盘点":

  • 用Looper.getMainLooper().setMessageLogging()在 debug 环境打点,统计主线程处理每条消息的耗时,找出耗时 > 16ms 的消息。
  • 检查高频回调(scroll、touch、animation、data callback),避免在其中触发新任务。
  • 尽量用Choreographer.postFrameCallback替代分散的Handler.post来处理帧同步逻辑,避免重复调度导致的抖动。

6. 实战案例复盘:一次线上 ANR 的完整排查与修复

6.1 现象与初步分析

某次线上版本,反馈量最大的 ANR 集中在首页信息流。ANR 详情页显示主线程堆栈停留在RecyclerView的bindViewHolder和doFrame附近。当时第一反应是"item 布局太复杂导致 measure 超时",但本地模拟怎么都复现不出来——低端机、中端机都试了,滑动流畅度尚可。

于是转去看完整 trace,反复读了几遍后发现一个问题:主线程虽然停留在 doFrame 相关调用,但旁边的"finalizer"线程却长期处于RUNNABLE状态,而且 CPU 占用极高。再结合cpu_time的数据,发现 ANR 发生时,整个进程的总 CPU 飙到了 300%(多核),且大部分消耗在libart.so的 GC 相关函数上。

6.2 根因定位

真相浮出水面:首页信息流的图片加载到一个遗留的图片库,每次滑到新 item 时都会在主线程附近创建大量临时byte[]用于解析图片尺寸。由于 byte 数组大多直接分配在堆上,并且 item 滑动速度很快,短时间内的对象分配剧增,导致 GC 频繁触发,最终由于内存碎片化引发了ConcurrentGarbageCollector的长时间停顿。

doFrame 其实"很想正常执行",但每次执行到一半被 GC 掐住,主线程停滞在ArtMethod相关调用,整个帧管线彻底停摆。再加上 input 事件队列里积压了用户的触摸事件,系统自然判定"主线程在 doFrame 的 ANR"。

6.3 修复手段与效果

后续的修复做了三件事:

  1. 把图片尺寸信息改成使用线程池预加载和解析,避免在滑动过程内创建大量临时对象。
  2. 在onBindViewHolder里移除所有主线程图片解码逻辑,统一走 Glide 的异步 decode。
  3. 针对图片列表页,做了RecyclerView的复用池优化,减少重新 inflate 的创建压力。

修复后,这个页面的 ANR 率直接下降了 80% 以上,帧率也从平均 28fps 提升到了 50fps 左右。后面又用同样的思路排查了其他几个页面,基本都稳定解决。

6.4 复盘总结

这个案例给了我三个明确的经验:

  • 看到 doFrame 的堆栈,先不要急着改 doFrame 里的任何代码,因为那里通常根本没有你自己的业务代码。
  • 多线程同时运行的情况下,GC 导致的停顿,在堆栈上的伪装度极高,必须结合 CPU 数据和线程状态一起判断。
  • 对象分配优化是性能治理最底层、最容易被人忽略、但收益最大的一部分。很多卡顿不是"代码逻辑写得慢",而是"对象太多把 GC 惹火了"。

7. 配套工具与日常预防机制

7.1 建立线上 ANR 监控与告警

光靠修复单点问题,很容易按下葫芦浮起瓢。更合理的方式是建立一套线上的 ANR 监控体系。我们可以通过自定义Application中的Thread.UncaughtExceptionHandler是管不了 ANR 的,ANR 不属于 Java 崩溃,所以需要用WatchDog机制或系统 ANR 回调来捕获。

轻量方案是使用FileObserver监听/data/anr/目录,文件一旦生成就立刻抓取并上报。虽然部分高版本系统(Android 13 以上)对 ANR trace 文件的读取权限做了限制,但对于可调试版本或在 Root 设备上仍然有效。生产环境更稳妥的做法是接入成熟的三方性能监控 SDK(如微信 Matrix 的ANRMonitor模块)。

Matrix 的原理值得学习:它利用消息队列空闲检测和 looper 分发耗时统计,在主线程卡顿超过阈值时就 dump 当前堆栈,这个思路和我在第 4 节写的 FrameMonitor 类似,但做得更完整,还带 CPU 采样。如果你的项目有性能治理需求,直接集成 Matrix 是非常划算的。

7.2 常态化性能回归:卡顿阈值落地到 CI

有了监控还要有防守。我建议在项目的 CI 流程里加入性能回归测试,把"主线程耗时"作为一个硬性指标。比如在自动化测试阶段,跑一段核心路径的滑动脚本,并记录平均帧耗时、卡顿时长、主线程空闲率。如果超过阈值,直接把构建标红。

这个做法刚开始推行的时候阻力会很大,因为大量存量页面本来就卡,一跑就挂。但你会惊讶地发现,坚持跑几个月之后,开发对性能的意识会大幅提升——毕竟没人想在自己的 MR 上看到"性能回归"的红灯。

8. 给新手的三个实操建议

最后,基于我反复排查这类问题的实际体会,给刚开始接触 Android 性能优化的同学三个建议:

第一个建议:别怕 ANR trace 长。一份 trace 可能有一万行,但真正需要你读的只有前 500 行——尤其是前 100 行的主线程部分。心理上先过了这个坎,你能解决一半问题。

第二个建议:拿到 ANR 后先复现,复现不了就看日志里的蛛丝马迹。如果连 logcat 里都没有有效信息,就去查设备上报的 battery 和 cpu 状态。很多 ANR 其实和环境有关,单看代码不一定能看出来。

第三个建议:性能问题要治本,不要只打补丁。比如卡在SharedPreferences的锁竞争,最简单的补丁是"把那个持锁线程放到非高峰期执行",但更稳的治本方案是"消除这个进程内的 SP 热点"。补丁能维持一版,治本才能让代码安分很久。

主线程在 doFrame 的 ANR,看着吓人,本质上是主线程健康度全面亮红灯的前兆。把"谁让主线程变慢"这个问题从堆栈里挖出来,用合理的异步化、锁治理、渲染优化去解决掉,你会发现这一类问题真的没有想象中那么难。希望这篇文章里记录的经验,也能帮你少踩几个坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询