卡顿优化总结

本文详细探讨了系统卡顿的根源——丢帧,涉及View绘制过程、Vsync信号的作用、Choreographer监控机制,以及布局和耗时优化、内存影响及优化方法,包括垃圾回收策略和内存泄漏预防。

1.系统卡顿的根本原因

系统卡顿的根本原因是因为丢帧导致的,即本该在一个Vsync周期内显示的画面帧,由于种种原因并没有即时显示到屏上,而是过了好几个Vsync信号周期后才显示出来。
出现这种情况大体分为两个原因:
1.1页面太复杂,导致View绘制的时候耗时太多,超过了16.6ms
1.2主线程存在耗时的任务,导致在Vsync信号开始之后,没有及时开始执行绘制的任务

2.View的绘制过程

首先在performLaunchActivity的时候会创建PhoneWindow,然后在setContentView的时候创建DecorView,并加载XML布局文件到DecorView的contentParent这个父布局上,在onResume的时候WindowManagerGlobal在addView方法中创建ViewRootImpl,然后执行ViewRootImpl的setView方法,然后ViewRootImpl会执行requestLayout方法。requestLayout方法是View绘制的入口方法,我们经常调用的view的invalidate或者requestLayout方法,都会执行到ViewRootImpl的requestLayout方法。
requestLayout方法会执行到scheduleTraversals方法,在scheduleTraversals里面,会添加同步屏障消息,以便及时响应界面绘制的异步消息。然后会给Choreographer注册一个TraversalRunnable回调,并让Choreographer去做Vsync垂直同步信号的监听,在Choreographer内部监听到Vsync垂直同步信号之后,会回调到TraversalRunnable的回调接口,然后移除同步屏障消息,执行performTraversals,这里才会真正开始执行测量、布局、绘制的流程。其中绘制过程是ActivityThread(CPU) + RenderThread(GPU)一起完成,然后把绘制好的buffer通过queuebuffer添加到缓冲队列,交给SurfaceFlinger。SurfaceFlinger在收到缓冲buffer,并监听到Vsync信号后,开始调用HWC或者OpenGL ES做合成处理,然后把合成的缓冲帧通过HWC给到显示驱动做显示。

3.vsync垂直同步信号

Vsync信号是由显示设备发出来的,经过HWC传递给SurfaceFlinger,SurfaceFlinger会把信号传给自己内部处理,同时还会传给各个应用进程的Choreographer。大部门显示设备的刷新频率是60HZ,也就是每个信号的间隔为16.6ms,刷新率和Vsync的频率是一致的。Vsync信号会触发View的测量、布局、绘制;也会触发SurfaceFlinger的合成缓冲帧。Vsync机制加上triple buffer缓存,可以最大程度保证屏幕的刷新率和软件绘制的帧率保持一致,尽量保证了系统的流畅性。

4.Choreographer监控丢帧情况

使用Choregrapher的postFrameCallback,去注册监听每一帧的开始刷新的时间,重而发现丢帧的情况。监听接口回调的时间单位是纳秒,也就是1/1000000ms。

// Application.java
public void onCreate() {
    super.onCreate();
    Choreographer.getInstance().postFrameCallback(new FPSFrameCallback(System.nanoTime()));
}

public class FPSFrameCallback implements Choreographer.FrameCallback {

    private static final String TAG = "FPS_TEST";
    private long mLastFrameTimeNanos = 0;
    private long mFrameIntervalNanos;

    public FPSFrameCallback(long lastFrameTimeNanos) {
        mLastFrameTimeNanos = lastFrameTimeNanos;
        mFrameIntervalNanos = (long)(1000000000 / 60.0);
    }

    
    public void doFrame(long frameTimeNanos) {
        // 初始化时间
        if (mLastFrameTimeNanos == 0) {
            mLastFrameTimeNanos = frameTimeNanos;
        }
        final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;
        if (jitterNanos >= mFrameIntervalNanos) {
            final long skippedFrames = jitterNanos / mFrameIntervalNanos;
            if(skippedFrames > 30){
                // 丢帧30以上打印日志
                Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                        + "The application may be doing too much work on its main thread.");
            }
        }
        mLastFrameTimeNanos = frameTimeNanos;
        // 注册下一帧回调
        Choreographer.getInstance().postFrameCallback(this);
    }
}

实际上在Choreographer内部,也同样是有这样一套判断是否掉帧的代码。

5.布局优化

5.1减少过度绘制
5.2减少布局层级
5.3使用include和merge标签
5.4使用viewStub懒加载View

6.耗时优化

6.1使用IdelHandler处理非紧急任务
6.2使用AsyncLayoutInflater去加载复杂的布局
6.3使用线程池去处理耗时任务

7.内存影响和优化

每次GC的时候,需要做一次GCRoot引用链的可达标记,这会挂起进程,暂停所有线程,在ART虚拟机中,这个时间大概是3ms。因此如果内存不足,会引起频繁的GC,那么就会导致主线程绘制受影响,从而导致丢帧卡顿。

7.1内存分析方式
7.1.1查看系统日志

在系统logcat日志齐全的情况下,可以查看是否是因为低内存导致的问题,如下会打印lowmemorykiller相关的tag:
在这里插入图片描述
或者看频繁存在GC的打印信息:
在这里插入图片描述

7.1.2 adb命令生成hprof文件分析内存状态

.hprof是Java堆内存的一个快照,内部记录了当时堆内存的状态,包括所有存活的对象和它们的引用关系。
.hprof文件可以用adb命令生成:adb dumpheap 包名 生成文件位置
在生成hprof文件的时候,最好是在卡顿前和卡顿后各采集一次,做对比。
然后使用Android Studio的Profiler打开这个.hprof文件

7.1.3直接使用Android Studio的Profiler

直接在Android Studio的Profiler选择MEMORY,然后选择Capture heap dump,然后选择Record开始采集:在这里插入图片描述
然后就会得到如下的堆栈分析界面:
在这里插入图片描述
其中的Leaks表示的是存在内存泄漏的点,点击reference然后jump to source可以跳转到泄露点代码的位置。
直接跳转到泄露点的位置这种情况,是一种比较理想的情况,实际排查内存泄露的时候,可能会更复杂一点。包括是否是完全的复现的场景,或者不会直接显示leaks内存泄露点。

7.2垃圾判断算法

java虚拟机判断垃圾的方法,是从一系列GCRoot开始做引用链的可达搜索,不在引用链上的对象就会被判断为垃圾。
GCRoot包括:虚拟机栈引用的对象;本地native方法引用的对象;方法区中静态变量引用的对象;存活线程中存在的对象。

7.3几种垃圾回收算法

标记清除算法:先做可达性的标记算法,把GCRoot没有直接或者间接引用的对象标记为垃圾对象;然后把垃圾对象直接清除。
优点是算法简单,不需要移动存活的对象;缺点是容易产生内存碎片,导致频繁的GC。
复制算法:把内存分成两块,只使用其中一块内存。垃圾回收的时候,把当前内存块中存活的对象直接复制到另外一块空闲的内存,然后清除掉当前的内存块,调整两个内存块的角色,完成垃圾回收。
优点是不会存在碎片内存;缺点是内存使用缩小了一半,对象高存活的时候,会导致大量频繁复制。
标记压缩算法:首先从GCRoot做可达性标记,然后把存活的对象压缩到内存的一端,然后清楚边界外的所有空间。
优点是避免了内存碎片的产生,有不需要两块内存空间,性价比高;缺点是压缩内存,需要移动存活的对象,存在一定的效率降低。

7.4分代回收策略

在ART虚拟机中采用内存分代的策略,把堆内存划分为新生代和老年代。
新生代:
新创建的对象默认放入新生代的内存区域,新生代内存的对象存活率低,回收率高,一次GC平均回收70%~90%的对象,所以在新生代中的垃圾回收算法使用的是复制算法。
新生代中的内存区域,还可以被细分成一个Eden区和两个Survive区,比例是8:1:1。Eden用来存在新创建的对象,GC的时候会把Eden中存活的对象和Survive区中的对象一起拷贝到另一个空闲的Survive区中。
老年代:
对象在多次GC后仍然没有被回收(15次),那就会移入老年代内存。老年代内存的大小一般会比新生代内存更大,内部的对象存活时间长,存活率也高,因此采用的垃圾回收算法是标记-压缩算法。
当新创建的对象很大,且新生代的内存不足的情况下,对象会被直接放入老年代。

7.5.优化内存的方法和实践
7.5.1防止内存泄漏

6.5.1.1避免使用非静态的内部类,非静态的内部类会持有外部类的引用,导致外部类不能被回收。
典型的就是在Activity或者Fragment中使用匿名内部类的方式实现Handler的实例。由于Handler持有Activity的引用,在Message延迟等原因没有执行的情况下,Activity就不能被回收,存在风险。
还有就是在Activity中去注册广播、系统回调之后,没有在ondestroy的时候去注销监听,也是会引起内存泄露的。
但是Activity的View设置匿名函数的监听并不会导致内存泄漏,是因为在Activity的ondestroy的时候会主动让View去释放掉监听的引用。
6.5.1.2静态变量的引用
在单例模式中,我们会创建一个静态的变量去持有这个实例。当实例中引入的Context是Activity的时候,会导致Activity被一直强引用,导致内存泄漏。这种情况,可以使用Application Context的去做为单例模式中的Context参数。

7.5.2使用软引用和弱引用

在恰当的时候使用软引用和弱引用,可以降低内存的占用,降低GC的频率。

7.5.3避免在ondraw中创建对象

在自定义View的时候,避免在onDraw中去创建对象,或者去加载图片。因为onDraw会频繁调用,频繁创建对象或者加载资源,会容易造成内存的抖动。

7.5.4对图片的优化处理

图片是很占用内存的,内存占用 = width * height *像素点占用字节,如果是RGB_8888类型,占用4字节,如果RGB_4444则是占用2字节。因此一张800 x 600像素的图片,加载到内存 = 1080 * 720 * 4 = 3037KB = 2.96M的内存。
现在对图片的处理,很多时候会用到Gilde框架处理,确实也挺强大,但是很多时候我们还是需要自己去处理图片问题,特别是图片的缓存和内存的管理。
我们可以使用Android SDK提供的LruCache去构建一个图片缓存容器,它可以用图片的名称或者绝对路径做key,然后图片自己做value来缓存。当图片的缓存达到上限的时候,就移除掉最久没有使用的图片,释放内存。

<think>我们正在讨论Android卡顿优化方法。根据引用内容,卡顿问题主要与CPU时间(用户时间和系统时间)、线程优先级、I/O操作等有关。以下将系统性地整理卡顿优化方法,并参考引用内容进行说明。 ### 卡顿优化方法 #### 1. **优化CPU时间消耗** - **减少用户时间(应用代码执行时间)**: - 避免在主线程执行耗时操作(如复杂计算、大量循环),使用异步任务或线程池处理[^3]。 - 优化算法和数据结构,降低时间复杂度(例如用HashMap替代线性查找)。 - 减少不必要的对象创建和垃圾回收(GC)压力,避免内存抖动[^2]。 - **减少系统时间(内核调用时间)**: - 精简I/O操作:合并文件读写、使用缓冲流、避免频繁的小文件操作[^1]。 - 优化锁竞争:减少同步锁范围,用并发容器(如`ConcurrentHashMap`)替代同步容器[^3]。 #### 2. **线程调度优化** - 避免主线程等待后台线程(如锁竞争),防止高优先级线程空转[^3]。 - 合理设置线程优先级:后台任务使用低优先级(`THREAD_PRIORITY_BACKGROUND`),避免抢占主线程资源。 - 使用线程池管理并发任务,避免无限制创建线程。 #### 3. **内存与GC优化** - 内存泄漏会导致频繁Full GC,引发卡顿。使用LeakCanary等工具检测泄漏[^2]。 - 优化大对象分配(如Bitmap),使用对象池复用(如`BitmapPool`)。 - 尽量使用基本类型数组(如`int[]`)替代对象数组,减少内存占用。 #### 4. **I/O与存储优化** - 避免主线程进行文件读写或数据库查询,尤其是大文件或复杂查询[^1]。 - 对于频繁读写的轻量数据,改用SharedPreferences(异步提交)或内存缓存。 - 数据库优化:建立索引、批量操作、异步线程执行(如Room的`@Query`配合LiveData)。 #### 5. **UI渲染优化** - **减少布局层级**:使用ConstraintLayout替代多层嵌套布局,减少测量/布局时间。 - 避免过度绘制:关闭不必要的背景(如`android:background="?attr/selectableItemBackground"`替代固定色块)。 - 复杂列表使用RecyclerView的优化技巧(如`setHasFixedSize(true)`、视图缓存池配置)。 #### 6. **监控与分析工具** - **系统工具**: - Systrace:分析系统调用和CPU调度,定位锁竞争或I/O阻塞[^1][^4]。 - Perfetto:替代Systrace的更强大性能分析工具。 - **三方工具**: - BlockCanary:自动监控主线程卡顿,定位堆栈信息[^2]。 - Profiler(Android Studio):分析CPU、内存、网络实时占用。 #### 7. **前沿技术应用** - **WebAssembly加速**:对计算密集型任务(如图像处理),用C++编写并编译为WebAssembly,通过JNI调用[^5]。 - **硬件加速**:合理使用GPU渲染(如`View.setLayerType(LAYER_TYPE_HARDWARE, null)`),但注意图层滥用会导致内存增加。 ### 总结与实践步骤 1. **定位瓶颈**:先用工具(如Systrace)确定卡顿是CPU、I/O还是渲染问题[^1][^4]。 2. **分优先级解决**:先优化主线程阻塞(如异步化),再解决内存泄漏,最后调优算法。 3. **持续监控**:集成线上APM系统(如Matrix),监控卡顿率、ANR率等指标。 > 例如,若Systrace显示主线程在`onCreate`中被文件读取阻塞,应改为异步读取并用Handler更新UI[^1]。 §§相关问题§§ 1. 如何用Systrace分析Android卡顿问题? 2. 常见的线程池配置有哪些?如何避免线程池滥用? 3. 内存泄漏检测工具(如LeakCanary)的工作原理是什么? 4. RecyclerView在哪些场景下会导致卡顿?如何优化? </think>### Android卡顿优化方法 卡顿问题主要源于CPU时间消耗、线程调度和资源竞争等问题。以下是系统性的优化方案,基于Android系统特性实现流畅体验: #### 1. **CPU时间优化** [^1][^2] - **减少用户态时间**: - 算法优化:将$O(n^2)$算法优化为$O(n\log n)$,如用快速排序替代冒泡排序 - 对象复用:使用对象池避免频繁GC - **降低系统调用时间**: - 合并I/O操作:批量读写代替频繁小文件操作 - 异步处理:使用`AsyncTask`或协程处理文件/网络操作 #### 2. **线程调度优化** [^3] ```java // 正确设置线程优先级示例 new Thread(new Runnable() { @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); // 后台任务代码 } }).start(); ``` - 避免主线程(优先级$P_{\text{main}}$)等待低优先级线程($P_{\text{low}}$) - 使用`HandlerThread`管理长时间任务 - 线程池配置核心公式: $$ \text{optimal\_size} = N_{\text{cores}} \times (1 + \frac{W_{\text{wait}}}{W_{\text{compute}}}) $$ #### 3. **渲染性能优化** [^4] - **UI层级优化**: - 使用`ConstraintLayout`减少嵌套层级(目标深度$\leq 5$) - 启用`merge`和`ViewStub`延迟加载 - **过度绘制控制**: - 开启开发者选项中的"显示过度绘制区域" - 目标:蓝色区域(1x过度绘制) $\geq 80\%$,避免红色区域(4x+) #### 4. **存储I/O优化** [^1][^2] - 数据库操作: - 事务批量处理:减少磁盘写入次数 - 索引优化:对$WHERE$条件字段建立索引 - 文件读写: - 使用`BufferedInputStream`/`BufferedOutputStream` - 避免主线程读写(尤其大文件$size > 100\text{KB}$) #### 5. **前沿技术应用** [^5] - **WebAssembly加速**: ```javascript WebAssembly.instantiateStreaming(fetch('optimized.wasm')) .then(obj => obj.instance.exports.compute()); ``` - **CDN优化**: ```html <script type="module"> import { createApp } from 'https://cdn.skypack.dev/vue'; </script> ``` #### 6. **监控工具链** [^1][^5] | 工具 | 适用场景 | 关键指标 | |------|----------|----------| | Systrace | CPU调度分析 | 线程状态占比 | | Perfetto | 系统级追踪 | CPU频率波动 | | LeakCanary | 内存泄漏 | 对象持有链 | | Battery Historian | 功耗关联 | 唤醒锁时长 | > **优化实例**:某列表页卡顿分析发现$95\%$的CPU时间消耗在图片解码,解决方案: > 1. 启用硬件位图`Bitmap.Config.HARDWARE` > 2. 预加载缩略图尺寸:$W_{\text{view}} \times H_{\text{view}} \times 1.5$ > 3. 使用`RecyclerView`预取机制
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值