A
第 35 章JAVA50 分钟

性能优化

Android 性能优化全景:内存优化(LeakCanary、Bitmap 优化、弱引用与软引用)、启动优化(冷 / 温 / 热启动、延迟初始化、启动框架)、渲染优化(过度绘制、RecyclerView 缓存)、ANR 分析与 StrictMode。

学习目标

  • 掌握内存优化手段:LeakCanary 集成、Bitmap 采样、弱引用与软引用
  • 理解冷 / 温 / 热启动差异并能够用延迟初始化优化冷启动
  • 了解渲染优化:过度绘制检测、RecyclerView 多级缓存
  • 能够分析 ANR trace 文件并定位主线程耗时
  • 掌握 StrictMode 配置与告警解读

学习目标

  • 掌握内存优化手段:LeakCanary 集成、Bitmap 采样、弱引用与软引用;
  • 理解冷 / 温 / 热启动差异并能够用延迟初始化优化冷启动;
  • 了解渲染优化:过度绘制检测、RecyclerView 多级缓存;
  • 能够分析 ANR trace 文件并定位主线程耗时;
  • 掌握 StrictMode 配置与告警解读。

性能优化

性能优化是体验的最后一块拼图,本章分四块:内存、启动、渲染、稳定性(ANR)。每块都给完整代码示例,让优化可落地。

内存优化

Android 内存模型

  • 每个进程有最大堆限制(一般 256MB~512MB,因设备而异),可用 Runtime.maxMemory() 查询;
  • GC(Garbage Collection)通过可达性分析回收不可达对象;
  • 内存泄漏 = 对象不再使用但仍被强引用持有。

强 / 软 / 弱 / 虚引用

引用 回收时机
StrongReference 永不(直到 GC root 不可达)
SoftReference 内存不足时回收
WeakReference 下一次 GC 就回收
PhantomReference 对象被回收后通知(仅用于 finalize 监听)

弱引用典型用于“缓存持有但不阻止回收”:

public class ImageCache {
    private final Map<String, WeakReference<Bitmap>> cache = new HashMap<>();

    public void put(String key, Bitmap bitmap) {
        cache.put(key, new WeakReference<>(bitmap));
    }

    public Bitmap get(String key) {
        WeakReference<Bitmap> ref = cache.get(key);
        return ref != null ? ref.get() : null;
    }
}

LeakCanary

LeakCanary 自动检测 Activity / Fragment / ViewModel 泄漏,是 Android 项目标配:

dependencies {
    debugImplementation "com.squareup.leakcanary:leakcanary-android:2.14"
}

不需要手动初始化,引入依赖后自动 Application 装配。运行 debug 包,每次 Activity 销毁后 LeakCanary 检查 WeakReference 是否被回收,未回收则 dump hprof 分析并展示泄漏链。

自定义监听对象:

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // 监听自定义对象
        AppWatcher.INSTANCE.getObjectWatcher().watch(
                myObject, "myObject");
    }
}

Bitmap 采样优化

加载大图(如原图 4000x3000)直接 decode 会占 4000*3000*4 = 48MB 内存。BitmapFactory.Options.inSampleSize 降采样:

public class BitmapLoader {
    public static Bitmap decodeSampledFromFile(String path, int reqW, int reqH) {
        BitmapFactory.Options opts = new BitmapFactory.Options();
        opts.inJustDecodeBounds = true;       // 只读尺寸不分配内存
        BitmapFactory.decodeFile(path, opts);

        opts.inSampleSize = calculateSampleSize(opts, reqW, reqH);
        opts.inJustDecodeBounds = false;
        opts.inPreferredConfig = Bitmap.Config.RGB_565; // 2 字节 / 像素
        return BitmapFactory.decodeFile(path, opts);
    }

    private static int calculateSampleSize(BitmapFactory.Options opts, int reqW, int reqH) {
        int w = opts.outWidth;
        int h = opts.outHeight;
        int sample = 1;
        while (w / sample / 2 >= reqW && h / sample / 2 >= reqH) {
            sample *= 2;
        }
        return sample;
    }
}

要点:

  • inJustDecodeBounds 先量尺寸不分配内存;
  • inSampleSize 为 2 的整数次幂系统直接生效;
  • RGB_565 比 ARGB_8888 省一半内存,对透明度不敏感的图(照片)可接受;
  • 加载完成后调用 Bitmap.recycle() 释放(仅手动管理时)。

Glide 等图片库已封装这套机制,但理解原理便于排查 OOM。

监听内存压力

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        registerComponentCallbacks(new ComponentCallbacks2() {
            @Override
            public void onTrimMemory(int level) {
                if (level == TRIM_MEMORY_COMPLETE || level == TRIM_MEMORY_RUNNING_CRITICAL) {
                    ImageCache.clear();
                }
            }
            @Override public void onLowMemory() { ImageCache.clear(); }
            @Override public void onConfigurationChanged(Configuration newConfig) { }
        });
    }
}

onTrimMemory(level) 比 onLowMemory 粒度更细,是后台清理的最佳时机。

启动优化

启动三态

  • 冷启动(Cold Start):进程不存在,从 fork 进程开始,加载 Application、首个 Activity,到首帧绘制;
  • 温启动(Warm Start):进程在后台但 Activity 已销毁,需要重建;
  • 热启动(Hot Start):进程和 Activity 都在,按 back 返回,几乎瞬时回到原状态。

启动时间官方用 Display ? 标签的 displayed 时间表示,可用 adb shell am start -W 测量:

adb shell am start -W com.example.myapp/.MainActivity

冷启动优化原则

冷启动的关键路径是:

  1. Application.onCreate;
  2. 首个 Activity 的 onCreate 到 onWindowFocusChanged;
  3. 首帧绘制。

要做的就是:把这些阶段里非必要的初始化挪走。

依赖分析与延迟初始化

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // 必须在主线程、阻塞 UI 的
        initCrashReporter();
        initLogger();

        // 不必要的(可后台线程)
        AppScope.execute(() -> {
            initAnalytics();
            initRemoteConfig();
            initPush();
        });

        // 真正按需才初始化(懒加载)
        // 例如登录后才需要的支付 SDK
    }
}

启动框架 App Startup

Jetpack App Startup 让 SDK 初始化集中、可声明依赖:

dependencies {
    implementation "androidx.startup:startup-runtime:1.2.0"
}

每个初始化器实现 Initializer<T>:

public class AnalyticsInitializer implements Initializer<Analytics> {
    @NonNull
    @Override
    public Analytics create(@NonNull Context context) {
        Analytics analytics = new Analytics();
        analytics.init(context);
        return analytics;
    }

    @NonNull
    @Override
    public List<Class<? extends Initializer<?>>> dependencies() {
        // 在 Logger 初始化后再做
        return Collections.singletonList(LoggerInitializer.class);
    }
}

AndroidManifest.xml 注册:

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <meta-data
        android:name="com.example.AnalyticsInitializer"
        android:value="androidx.startup" />
</provider>

框架会在 Application 启动后自动按拓扑顺序执行。

Splash Screen

Android 12+ 引入 SplashScreen API:

<style name="Theme.App.Starting" parent="Theme.SplashScreen">
    <item name="windowSplashScreenBackground">@color/seed</item>
    <item name="windowSplashScreenAnimatedIcon">@drawable/ic_logo</item>
    <item name="postSplashScreenTheme">@style/Theme.MyApp</item>
</style>
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        SplashScreen.installSplashScreen(this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

installSplashScreen 让首屏在有系统 Splash 的同时显示应用品牌,并允许 setKeepOnScreenCondition 等待数据就绪:

SplashScreen splash = SplashScreen.installSplashScreen(this);
splash.setKeepOnScreenCondition(() -> !dataReady);

渲染优化

16ms 帧预算

Android 默认 60Hz 屏幕每帧 16.6ms。如果主线程一帧做不到绘制 + 业务就掉帧。120Hz 屏幕是 8.3ms。

过度绘制(Overdraw)

过度绘制指同一像素被绘制多次。常见于:

  • 根布局 android:background + Activity 主题背景叠加;
  • 卡片布局 + 卡片内 View 都设背景;
  • ViewPager 嵌套 Fragment,每层都设背景。

调试:开发者选项 → 调试 GPU 过度绘制。颜色含义:

  • 蓝色(1x):可接受;
  • 绿色(2x):尚可;
  • 粉色(3x):要注意;
  • 红色(4x+):必优化。

优化方法:移除不必要的 background,让父布局直接承担背景。

RecyclerView 缓存

RecyclerView 有四级缓存:

层级 角色 容量
Scrap 屏内临时 不限
First Cache 屏外回收 默认 2
ViewCacheExtension 自定义 用户实现
RecycledViewPool 跨 Adapter 复用 按类型默认 5

ViewHolder 复用是核心机制:

public class UserAdapter extends RecyclerView.Adapter<UserAdapter.VH> {
    private final List<User> data = new ArrayList<>();

    @NonNull
    @Override
    public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_user, parent, false);
        return new VH(v);
    }

    @Override
    public void onBindViewHolder(@NonNull VH holder, int position) {
        User u = data.get(position);
        holder.bind(u);
    }

    @Override
    public int getItemCount() { return data.size(); }

    static class VH extends RecyclerView.ViewHolder {
        TextView name;
        VH(@NonNull View v) {
            super(v);
            name = v.findViewById(R.id.tv_name);
        }
        void bind(User u) {
            name.setText(u.name);
        }
    }
}

常见优化:

  1. 复用 RecycledViewPool:多 Tab 共享 Adapter:
RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool();
recyclerView1.setRecycledViewPool(pool);
recyclerView2.setRecycledViewPool(pool);
  1. setHasFixedSize(true):Adapter 内容变化不会改变 RecyclerView 尺寸时调用,跳过 requestLayout。

  2. setItemViewCacheSize:默认 2,可调高减少 onBindViewHolder 调用:

recyclerView.setItemViewCacheSize(4);
  1. DiffUtil:列表更新用 DiffUtil 只刷新真正变化的项:
public class UserDiffCallback extends DiffUtil.ItemCallback<User> {
    @Override
    public boolean areItemsTheSame(@NonNull User oldU, @NonNull User newU) {
        return oldU.id == newU.id;
    }
    @Override
    public boolean areContentsTheSame(@NonNull User oldU, @NonNull User newU) {
        return oldU.equals(newU);
    }
}

// 使用 ListAdapter
public class UserAdapter extends ListAdapter<User, UserAdapter.VH> {
    public UserAdapter() {
        super(new UserDiffCallback());
    }
    // ... 不再需要 getItemCount / setData,调用 submitList
}

异步布局

复杂 Item 在 onCreateViewHolder 视图重用,不要在 onBindViewHolder 里 inflate。Glide 加载图片要异步,不要同步 decodeResource。

prefetch 与 LinearLayoutManager

LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setItemPrefetchEnabled(true);
layoutManager.setInitialPrefetchItemCount(4); // 嵌套时给主列表预取
recyclerView.setLayoutManager(layoutManager);

ANR 分析

ANR 触发条件

类型 触发条件
InputDispatching Timeout 主线程 5s 内未响应输入事件
Service Timeout 前台服务 20s / 后台 200s 未完成
BroadcastReceiver 前台 10s / 后台 60s 未完成
ContentProvider 发布超时 10s

trace 文件

ANR 时系统会 dump 主线程栈到 /data/anr/traces.txt。可以拉取:

adb pull /data/anr/traces.txt

定位主线程栈:

"main" prio=5 tid=1 Blocked
  | waiting to lock monitor 0x...
  at com.example.MyActivity.void doWork() (MyActivity.java:42)
  - waiting to lock 0x... held by thread 12

关键看 主线程在做什么:Blocked 等锁、Native 在 JNI、Sleeping 在 sleep。

常见 ANR 原因

  • 主线程做 IO(数据库、SharedPreferences.commit、文件读写);
  • 主线程做网络请求;
  • 主线程等待锁被其他线程持有;
  • 主线程做大量计算;
  • BroadcastReceiver.onReceive 跑太久。

避免主线程 IO

// ❌ 阻塞主线程
SharedPreferences sp = getSharedPreferences("config", MODE_PRIVATE);
sp.edit().putString("k", "v").commit(); // commit 同步

// ✓ 异步 apply
sp.edit().putString("k", "v").apply();

// ✓ 大量数据走 Room
Executors.newSingleThreadExecutor().execute(() -> {
    db.userDao().insert(user);
});

StrictMode

StrictMode 在开发期帮你发现主线程 IO、网络、内存泄漏等问题。

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        if (BuildConfig.DEBUG) {
            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork()
                    .detectCustomSlowCalls()
                    .penaltyLog()           // logcat 输出
                    .penaltyDialog()        // 弹窗提示
                    .build());

            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                    .detectActivityLeaks()
                    .detectLeakedClosableObjects()  // 未 close 的 Cursor / InputStream
                    .detectLeakedRegistrationObjects() // BroadcastReceiver / ServiceConnection 未反注册
                    .penaltyLog()
                    .build());
        }
        super.onCreate();
    }
}

仅在 debug 模式开启,否则会影响生产性能。

常见坑与最佳实践

  1. LeakCanary 引到 release 包:会 dump 内存且产生日志,应只在 debug 引入。
  2. 直接 BitmapFactory.decodeFile 加载大图:OOM 高发点。务必 inJustDecodeBounds 量尺寸 + inSampleSize 降采样。
  3. 未释放 Cursor:数据库查询返回的 Cursor 用完没 close(),会导致 StrictMode 告警甚至连接池耗尽。
  4. Application.onCreate 全量初始化:所有 SDK 都挤在主线程,冷启动时间膨胀。能用 App Startup 拓扑排序或后台线程就分出去。
  5. 过度 RecyclerView.notifyDataSetChanged:整列表刷新闪烁且无动画。改用 DiffUtil 局部刷新。
  6. NestedScrollView 嵌套 RecyclerView:嵌套会破坏回收机制,所有 Item 都 inflate,等同 ListView。考虑 NestedScrollWeb 或 ConcatAdapter。
  7. 主线程 SharedPreferences.commit:commit 同步写盘,多个并发会卡。用 apply。
  8. 忽视 onTrimMemory:内存紧张时不释放缓存,下一个 GC 来不及就 OOM。
  9. 混淆 StrictMode 告警:把告警当噪声忽略。每个告警都应跟进原因,开发期就修掉。
  10. 以为 StrictMode 上线就行:会拖慢生产性能,仅 debug 开启。

存量项目维护要点:性能优化的安全边界

在存量项目上做性能优化,风险高于新项目,需要更谨慎的策略:

  1. 先测量再优化:用 Android Profiler、Systrace、StrictMode 收集数据,不要凭直觉改代码
  2. 保留可回滚性:每次只改一个性能点,独立提交,出问题能快速回滚
  3. 优先改高频路径:列表滚动、启动耗时、ANR 这类用户可感知的问题优先于冷门路径
  4. 不要在优化中混入重构:性能优化与架构重构分开做,避免一次改动引入两类风险
  5. 警惕“过度优化”:老代码的“丑陋”往往有其历史原因(兼容旧机型、绕过系统 bug),优化前先用 git blame 了解背景
  6. 内存泄漏优先治:老项目最常见的性能问题是内存泄漏(Handler、内部类、静态引用),用 LeakCanary 检测比手工排查更可靠

关键原则:存量项目的性能优化目标是“消除显著瓶颈”,而不是“榨干每一毫秒”。安全比极致更重要。

章节小结

本章覆盖了 Android 性能优化全景:内存优化(LeakCanary、Bitmap inSampleSize 采样、软 / 弱引用、onTrimMemory)、启动优化(冷启动路径、延迟初始化、Jetpack App Startup、Android 12 SplashScreen)、渲染优化(16ms 帧预算、过度绘制检测、RecyclerView 四级缓存、DiffUtil 局部刷新、setRecycledViewPool 共享)、ANR 分析(trace 文件定位主线程、避免主线程 IO)、StrictMode 在开发期发现主线程 IO 与泄漏。核心要点:主线程绝不阻塞;Bitmap 必采样;列表用 DiffUtil;LeakCanary 在 debug 常驻;启动期 SDK 初始化延迟到后台。

下一章预告

最后一章进入调试与日志领域:Log 与 Timber 日志库、Android Studio Profiler(CPU / Memory / Energy)、Layout Inspector、LeakCanary 集成、调试技巧(断点 / 日志点 / 条件断点 / 异常断点)。