性能优化
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
冷启动优化原则
冷启动的关键路径是:
Application.onCreate;- 首个 Activity 的
onCreate到onWindowFocusChanged; - 首帧绘制。
要做的就是:把这些阶段里非必要的初始化挪走。
依赖分析与延迟初始化
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);
}
}
}
常见优化:
- 复用 RecycledViewPool:多 Tab 共享 Adapter:
RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool();
recyclerView1.setRecycledViewPool(pool);
recyclerView2.setRecycledViewPool(pool);
-
setHasFixedSize(true):Adapter 内容变化不会改变 RecyclerView 尺寸时调用,跳过requestLayout。 -
setItemViewCacheSize:默认 2,可调高减少 onBindViewHolder 调用:
recyclerView.setItemViewCacheSize(4);
- 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 模式开启,否则会影响生产性能。
常见坑与最佳实践
- LeakCanary 引到 release 包:会 dump 内存且产生日志,应只在 debug 引入。
- 直接
BitmapFactory.decodeFile加载大图:OOM 高发点。务必inJustDecodeBounds量尺寸 +inSampleSize降采样。 - 未释放 Cursor:数据库查询返回的
Cursor用完没close(),会导致 StrictMode 告警甚至连接池耗尽。 - Application.onCreate 全量初始化:所有 SDK 都挤在主线程,冷启动时间膨胀。能用
App Startup拓扑排序或后台线程就分出去。 - 过度
RecyclerView.notifyDataSetChanged:整列表刷新闪烁且无动画。改用DiffUtil局部刷新。 - NestedScrollView 嵌套 RecyclerView:嵌套会破坏回收机制,所有 Item 都 inflate,等同 ListView。考虑
NestedScrollWeb或ConcatAdapter。 - 主线程 SharedPreferences.commit:commit 同步写盘,多个并发会卡。用
apply。 - 忽视 onTrimMemory:内存紧张时不释放缓存,下一个 GC 来不及就 OOM。
- 混淆 StrictMode 告警:把告警当噪声忽略。每个告警都应跟进原因,开发期就修掉。
- 以为 StrictMode 上线就行:会拖慢生产性能,仅 debug 开启。
存量项目维护要点:性能优化的安全边界
在存量项目上做性能优化,风险高于新项目,需要更谨慎的策略:
- 先测量再优化:用 Android Profiler、Systrace、StrictMode 收集数据,不要凭直觉改代码
- 保留可回滚性:每次只改一个性能点,独立提交,出问题能快速回滚
- 优先改高频路径:列表滚动、启动耗时、ANR 这类用户可感知的问题优先于冷门路径
- 不要在优化中混入重构:性能优化与架构重构分开做,避免一次改动引入两类风险
- 警惕“过度优化”:老代码的“丑陋”往往有其历史原因(兼容旧机型、绕过系统 bug),优化前先用 git blame 了解背景
- 内存泄漏优先治:老项目最常见的性能问题是内存泄漏(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 集成、调试技巧(断点 / 日志点 / 条件断点 / 异常断点)。