23、Android性能优化(上):布局优化、内存优化、卡顿检测与Systrace使用

性能优化这个话题,说实话,是每个Android工程师绕不开的坎。我做了这么多年系统层开发,见过太多应用因为性能问题被用户吐槽。今天咱们就聊聊布局优化、内存优化、卡顿检测这几个硬骨头,还有Systrace这个神器怎么用。

一、布局优化:从渲染源头下手

布局优化说白了就是让界面加载更快、更流畅。你想想看,用户点开一个页面,如果卡个一两秒才出来,第一印象就毁了。

1. 减少层级嵌套

我个人习惯,写布局时尽量保持层级在3层以内。为什么?因为每多一层,measure和layout就要多走一遍,CPU开销翻倍。

核心原则:能用RelativeLayout解决的就别用LinearLayout嵌套。能用ConstraintLayout的就别用多层RelativeLayout。

我在项目中遇到过这样一个案例:一个列表项布局嵌套了7层,滑动时明显掉帧。后来用ConstraintLayout重构,层级降到2层,帧率从40fps直接拉回到58fps。

2. 使用复用布局

复用布局时,是标配。但有个坑——如果include进来的布局根节点是多余的,记得用包裹。

<!-- 父布局 -->
<LinearLayout>
    <include layout="@layout/common_header"/>
</LinearLayout>

<!-- common_header.xml -->
<merge xmlns:android="...">
    <TextView .../>
    <Button .../>
</merge>

嗯,这里要注意:不会生成额外的ViewGroup,直接减少一层嵌套。

3. ViewStub延迟加载

有些布局不是一开始就需要显示的,比如错误提示、广告位。用ViewStub,只有inflate时才真正加载。

<ViewStub
    android:id="@+id/stub_error"
    android:layout="@layout/layout_error"
    android:inflatedId="@+id/error_layout"/>

// 使用时
ViewStub stub = findViewById(R.id.stub_error);
stub.inflate();

小技巧:ViewStub一旦inflate后,自身就被替换掉了。所以不要重复inflate,否则会抛异常。

二、内存优化:别让OOM找上门

内存优化这块,说白了就是管好你的对象生命周期。我曾经接手过一个项目,内存泄漏严重到每次打开相册就崩。查了三天,最后发现是一个静态变量持有了Activity引用。

1. 避免静态变量持有Activity

静态变量的生命周期和应用一样长。如果你把Activity赋值给静态变量,Activity退出后无法被GC回收。

// 错误示例
public class Utils {
    private static Activity sActivity;
    public static void setActivity(Activity activity) {
        sActivity = activity;
    }
}

// 正确做法:使用弱引用
public class Utils {
    private static WeakReference<Activity> sActivityRef;
    public static void setActivity(Activity activity) {
        sActivityRef = new WeakReference<>(activity);
    }
}

2. 使用LeakCanary检测泄漏

LeakCanary是我每次新项目必加的工具。它能在发生内存泄漏时自动弹出通知,告诉你哪个对象泄漏了、引用链是什么。

dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
}

加完依赖,什么都不用配。运行App,如果有泄漏,通知栏会提示。点进去就能看到完整的引用链。

注意:LeakCanary只建议在debug版本使用。release版本一定要去掉,否则会影响性能。

3. Bitmap内存管理

Bitmap是内存大户。我见过一个App,加载一张4K图片直接吃掉30MB内存。优化方法其实很简单:

  • 使用BitmapFactory.Options的inSampleSize进行采样压缩
  • 使用LruCache做内存缓存
  • 及时调用bitmap.recycle()(Android 3.0之前需要手动回收)
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.big_image, options);

// 计算采样率
int imageWidth = options.outWidth;
int imageHeight = options.outHeight;
int scaleFactor = Math.min(imageWidth / targetWidth, imageHeight / targetHeight);

options.inJustDecodeBounds = false;
options.inSampleSize = scaleFactor;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.big_image, options);

三、卡顿检测:找到元凶

卡顿的本质是掉帧。Android每16.6ms刷新一帧,如果某帧的绘制时间超过这个阈值,就会丢帧。我习惯用两种方式检测:

1. 使用Choreographer监听帧率

Choreographer是系统级的帧率控制器。我们可以注册FrameCallback来监听每一帧的绘制时间。

Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
    long lastFrameTimeNanos = 0;
    @Override
    public void doFrame(long frameTimeNanos) {
        if (lastFrameTimeNanos == 0) {
            lastFrameTimeNanos = frameTimeNanos;
        }
        long diffMs = (frameTimeNanos - lastFrameTimeNanos) / 1000000;
        if (diffMs > 16.6f) {
            // 掉帧了
            Log.w("FrameMonitor", "掉帧: " + diffMs + "ms");
        }
        lastFrameTimeNanos = frameTimeNanos;
        Choreographer.getInstance().postFrameCallback(this);
    }
});

2. 使用BlockCanary检测主线程耗时

BlockCanary是另一个好用的工具。它通过Looper的Printer机制,检测主线程中超过指定阈值的耗时操作。

Looper.getMainLooper().setMessageLogging(new Printer() {
    @Override
    public void println(String x) {
        if (x.startsWith(">>>>> Dispatching to")) {
            // 开始执行
        } else if (x.startsWith("<<<<< Finished to")) {
            // 执行结束,计算耗时
        }
    }
});

经验之谈:我一般把阈值设为200ms。超过200ms的操作,用户就能感知到卡顿了。

四、Systrace使用:系统级性能分析

Systrace是Google官方提供的性能分析工具。它能抓取系统各个进程的运行状态,包括CPU调度、线程状态、GC次数等。说白了,就是给你一张全局的「性能地图」。

1. 基本使用

通过命令行启动Systrace:

python systrace.py -t 10 -o trace.html sched gfx view wm am

参数说明:

  • -t 10:抓取10秒
  • -o trace.html:输出文件名
  • sched gfx view wm am:要跟踪的模块

2. 如何分析Systrace

打开生成的trace.html,你会看到时间轴。重点关注以下几点:

  • Alerts区域:系统自动标记的异常点,比如掉帧、GC频繁
  • CPU调度:看线程是否被频繁切换,如果切换太频繁,说明CPU资源紧张
  • UI Thread:看主线程是否有长时间的空闲或阻塞
  • GC事件:如果GC太频繁,说明内存分配有问题

3. 自定义Trace标签

在代码中打标签,可以更精确地定位问题:

// 在关键操作前后打标签
Trace.beginSection("loadData");
loadData();
Trace.endSection();

这样在Systrace中就能看到"loadData"这个区间,直接知道它花了多少时间。

核心观点:Systrace不是用来找具体bug的,而是用来发现「系统级」的性能瓶颈。比如某个线程占用了太多CPU、GC太频繁导致掉帧。这些宏观问题,用Systrace一眼就能看出来。

五、知识体系总览

下面这张图,是我梳理的Android性能优化核心脉络。你可以把它当作一个检查清单:

Android性能优化 布局优化 减少层级嵌套 include/merge复用 ViewStub延迟加载 内存优化 避免静态泄漏 LeakCanary检测 Bitmap内存管理 卡顿检测 Choreographer帧率 BlockCanary耗时 主线程监控 Systrace系统级分析 命令行抓取trace 分析Alerts/CPU/GC 自定义Trace标签

这张图把今天讲的内容串起来了。布局优化解决渲染效率,内存优化解决资源泄漏,卡顿检测帮你定位问题,Systrace则从系统层面给出全局视角。四者配合,基本能覆盖90%的性能问题。

好了,这一章就到这里。性能优化是个持续的过程,没有银弹。多动手、多分析、多总结,慢慢你就会有感觉。

公众号:蓝海资料掘金营,微信deep3321