Android 开发做久了会发现,真正拉开效率差距的往往不是对框架的宏观理解,而是那些散落在日常编码细节里的 Tricks。这些窍门有的能帮你省掉几十行样板代码,有的能直接避免一次线上崩溃。下面整理几个我在实际项目中反复使用、验证有效的技巧,配合代码逐一说明。

线程相关:别再手动 new Looper 了
用 HandlerThread 替代手写 Looper 线程
很多同学在需要后台线程处理串行消息时,会习惯性地写一个 Thread 子类,然后在 run 方法里调用 Looper.prepare() 和 Looper.loop()。这段代码不仅啰嗦,而且容易忘记调用 quit() 导致线程泄漏。其实 Android 早就提供了现成的 HandlerThread,它把 Looper 的初始化和退出管理都封装好了。
// 手写方式:容易出错的样板代码
class MyThread extends Thread {
public Handler handler;
@Override
public void run() {
Looper.prepare();
handler = new Handler(Looper.myLooper());
Looper.loop();
}
}
// 推荐:HandlerThread 一步到位
HandlerThread thread = new HandlerThread("worker");
thread.start();
Handler handler = new Handler(thread.getLooper());
handler.post(() -> doHeavyWork());
// 用完记得释放,避免线程泄漏
thread.quitSafely();
这里的 quitSafely() 是个容易忽略的点。它和 quit() 的区别在于前者只移除尚未到执行时间的延迟消息,正在排队等待执行的普通消息会继续处理完,而后者会把队列里所有消息直接丢弃。如果你的消息队列里存在尚未落盘的写操作,用 quit() 就可能丢数据。
IdleHandler 做延迟初始化
App 启动时如果在 Application.onCreate 里塞了一堆 SDK 初始化,冷启动时间会被拖得很长。一个常见技巧是利用主线程的 IdleHandler,把非必要的初始化挪到主线程空闲时执行:
Looper.myQueue().addIdleHandler(() -> {
initAnalytics(); // 非关键 SDK,可以延后
initPush();
return false; // 返回 false 表示执行完即移除
});
返回 true 表示这个 IdleHandler 会一直驻留,每次队列空闲都会回调,适合做心跳类的低频轮询;返回 false 则执行一次就自动移除。绝大多数延迟初始化场景用 false 就够了。需要注意的是,如果主线程一直有消息在排队,IdleHandler 可能迟迟不执行,所以关键路径上的逻辑千万别依赖它。
数据存储:apply 和 commit 的坑
SharedPreferences 的 apply() 和 commit() 是面试高频题,但实际项目里真正用对的并不多。两者的核心差异在于:commit() 是同步写,会阻塞调用线程直到文件写入完成并返回布尔结果;apply() 是异步写,先把修改提交到内存,然后由后台线程落盘。
// 错误示范:主线程同步写文件,可能造成卡顿
SharedPreferences sp = getSharedPreferences("config", MODE_PRIVATE);
sp.edit().putString("token", token).commit();
// 正确姿势:异步写入
sp.edit().putString("token", token).apply();
但 apply() 也不是万能的。它有个隐蔽的坑:如果短时间内频繁调用 apply(),QueuedWork 中会堆积大量写任务,在 onStop 或 onPause 时系统会等待这些任务全部完成,反而造成生命周期切换卡顿。所以正确的做法是合并写入,把一批修改放在同一个 edit() 里提交,而不是循环里每次都调一次。
另外,如果确实需要知道写入是否成功,比如保存支付相关的关键数据,这时同步的 commit() 反而是正确选择,只需确保在后台线程调用它。异步与同步没有绝对优劣,关键看调用场景。
界面调试:卡顿排查三板斧
Layout Inspector 看布局层级
界面卡顿的常见元凶之一是布局层级过深。Android Studio 自带的 Layout Inspector 可以直观展示运行时真实的视图树。打开方式是 Tools 菜单里的 Layout Inspector,选中正在运行的进程即可。重点观察两个指标:一是视图树的深度,超过十层基本就要考虑用 ConstraintLayout 扁平化重构;二是是否存在宽高为 0 或重复绘制的无用节点,<merge> 标签和 <ViewStub> 是解决这类问题的利器。
SysTrace 分析掉帧原因
SysTrace(新版本里集成在 System Tracing 工具中)能把你 App 每一帧的渲染时间线画出来。导出 trace 文件后用 Perfetto 打开,找到掉帧的位置,看这一帧里主线程在干什么。常见的几种掉帧模式都有明显特征:如果 GC 任务频繁出现,说明内存分配过猛,应该减少循环内创建临时对象;如果大量时间花在 inflate 上,说明布局加载太重,考虑用 AsyncLayoutInflater 或 ViewStub 延迟加载;如果 binder 调用占了大头,通常是主线程在做跨进程通信,比如频繁读写 ContentProvider,需要挪到后台。
// ViewStub 延迟加载示例
<ViewStub
android:id="@+id/stub_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/banner_container"
android:layout="@layout/layout_banner" />
// 需要显示时再真正 inflate ViewStub stub = findViewById(R.id.stub_banner); View banner = stub.inflate(); // 第二次调用会抛异常,注意判空
使用 ViewStub 时有个细节:调用 inflate() 之后,ViewStub 自己会从视图树中移除,再次调用同一个实例会直接抛出 IllegalStateException。所以要么把 inflate 后的 View 缓存下来复用,要么在调用前判断 stub 的 parent 是否为 null。
生命周期与状态保存的冷门技巧
进程被系统杀掉后重建,是很多崩溃和数据错乱的根源。传统的做法是在 onSaveInstanceState 里塞 Bundle,代码又臭又长。Jetpack 提供的 SavedStateHandle 把这件事和 ViewModel 结合了起来,配置变更和进程死亡两种场景都能覆盖:
public class LoginViewModel extends ViewModel {
private final SavedStateHandle state;
public LoginViewModel(SavedStateHandle state) {
this.state = state;
}
public MutableLiveData<String> getInput() {
return state.getLiveData("input");
}
}
它的原理是在进程重建时通过反射调用 ViewModel 的构造函数,把系统保存的 SavedStateHandle 重新传入,所以数据恢复对业务代码完全透明。相比手写 onSaveInstanceState,这种方式让状态管理集中在 ViewModel 里,Activity 只负责渲染,结构清晰得多。
另一个相关技巧是善用 Lifecycle 的观察者来替代手写 Handler 延迟任务。比如页面销毁时要取消一个延迟执行的任务,很多人会声明一个 Handler 然后在 onDestroy 里调 removeCallbacks。用 LifecycleCoroutineScope 或 launchWhenStarted 这类生命周期感知的 API,可以让取消逻辑自动完成,从根源上消灭一类因为疏忽导致的内存泄漏和空指针。
总结
这些 Tricks 看起来零散,背后其实有共同的思路:优先使用系统或 Jetpack 已经封装好的能力,而不是自己手写底层逻辑;任何涉及主线程的 IO 都要警惕;状态管理要考虑进程被杀这种极端场景。Android 官方这些年一直在把最佳实践下沉到框架层,多翻一翻 androidx 的包文档,经常能发现比自己手写方案更省心的现成工具。把上面这些技巧挨个在项目里实践一遍,你会发现代码量和线上问题都会明显减少。
Android开发Android Tricks开发技巧修改时间:2026-09-14 11:20:44