深入理解 Android 回调(Callback)机制
一篇从原理到实战、从 Java 到 Kotlin、从源码示例到面试高频问答的 Android Callback 机制详解。
阅读时间:约 12 分钟。适用读者:Android 初、中级工程师,或准备面试的同学。
目录
- 一、回调到底是什么
- 二、为什么需要回调
- [三、Java/Android 实现回调的三种方式](#三、Java/Android 实现回调的三种方式 "#%E4%B8%89javaandroid-%E5%AE%9E%E7%8E%B0%E5%9B%9E%E8%B0%83%E7%9A%84%E4%B8%89%E7%A7%8D%E6%96%B9%E5%BC%8F")
- [四、Android 源码里最常见的 6 个回调场景](#四、Android 源码里最常见的 6 个回调场景 "#%E5%9B%9Bandroid-%E6%BA%90%E7%A0%81%E9%87%8C%E6%9C%80%E5%B8%B8%E8%A7%81%E7%9A%84-6-%E4%B8%AA%E5%9B%9E%E8%B0%83%E5%9C%BA%E6%99%AF")
- [五、回调与 Handler、RxJava、协程的关系](#五、回调与 Handler、RxJava、协程的关系 "#%E4%BA%94%E5%9B%9E%E8%B0%83%E4%B8%8E-handlerrxjava%E5%8D%8F%E7%A8%8B%E7%9A%84%E5%85%B3%E7%B3%BB")
- 六、回调地狱与现代化解决方案
- 七、自定义一个完整的回调(手把手)
- 八、内存泄漏:回调使用中最隐蔽的坑
- 九、面试高频问答
- 十、一图总结
- 十一、小结
一、回调到底是什么
一句话定义:
回调(Callback)= 把一段可执行的代码(函数/方法)作为参数传给对方,由对方在合适的时机反向调用它。
英文叫 Call Back ,直译就是「回过头来调用」。它本质是一种 控制反转(IoC, Inversion of Control) :
- 不是「你主动去问结果」,而是「你把电话号码留下,有事我打给你」。
- 调用关系从「A 调 B 」变成了 「A 调 B,并把自己作为参数交给 B;B 在某个时机反过来调 A 提供的方法」。
类比理解
你去餐厅点餐,付完款后不用站在出餐口傻等,而是拿一个小票号。服务员出餐后会喊号(甚至扫码通知你):你就可以回来取餐。这个「留下小票号 / 留下电话」的动作,就是注册回调;「服务员喊你」的动作,就是异步回调。
时序示意
css
┌─────────────┐ ┌─────────────┐
│ A (调用方) │ │ B (被调方) │
└──────┬──────┘ └──────┬──────┘
│ 1. 调用 B.doSomething(this) │
├───────────────────────────────▶│
│ 2. B 开始干活 │
│ │
│ 3. B 完成工作 │
│ 4. B 回调 A.onResult(...) │
│◀───────────────────────────────┤
│ 5. A 在 onResult 里更新 UI │
│ │
二、为什么需要回调
Android 之所以几乎「无处不回调」,源于它的四大特性:
| 特性 | 回调解决了什么 |
|---|---|
| 异步性 | 网络、解码、IO 不能在主线程阻塞,结果回来时必须有人通知 UI |
| 框架控制反转 | Activity 生命周期由系统调度,开发者只能实现 onCreate / onResume 等回调点 |
| 事件驱动 | 触摸、点击、滑动都是系统层事件,必须反向通知业务代码 |
| 解耦复用 | RecyclerView 不知道你的数据长什么样,只要求你实现 Adapter 的几个回调 |
一句话:Android 是事件驱动的异步框架,回调是它的骨髓。
三、Java/Android 实现回调的三种方式
方式 1:接口回调(最经典,源码最多)
typescript
// ① 定义回调接口
public interface OnResultListener {
void onSuccess(String result);
void onFailed(String error);
}
// ② 被调用方:持有接口引用,在合适时机触发
public class Worker {
private OnResultListener listener;
public void setOnResultListener(OnResultListener l) {
this.listener = l;
}
public void doWork() {
// 模拟耗时
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
if (listener != null) {
if (new Random().nextBoolean()) listener.onSuccess("ok");
else listener.onFailed("fail");
}
}
}
// ③ 调用方:实现接口并注册
Worker worker = new Worker();
worker.setOnResultListener(new OnResultListener() {
@Override public void onSuccess(String result) { Log.d("TAG", "成功: " + result); }
@Override public void onFailed(String error) { Log.d("TAG", "失败: " + error); }
});
worker.doWork();
要点:
- 判空 :
if (listener != null),防止别人没注册就触发导致 NPE。 - 单方法接口:可用 Java 8 Lambda 简化(见方式 3)。
- 多回调方法 :接口里放多个方法,类似
OnTouchListener有onTouch()一个方法,TextWatcher有三个。
方式 2:抽象类回调(接口的变体)
抽象类相比接口,可以带状态、带默认实现:
csharp
public abstract class VideoDecoder {
protected byte[] buffer; // 可以有字段
public final void decode() { // 模板方法:固定流程
prepare();
byte[] frame = doDecode(); // 子类实现
onFrameDecoded(frame); // 回调点
}
protected void prepare() { /* 默认实现 */ }
protected abstract byte[] doDecode();
public abstract void onFrameDecoded(byte[] frame); // 回调抽象方法
}
new VideoDecoder() {
@Override protected byte[] doDecode() { return new byte[1024]; }
@Override public void onFrameDecoded(byte[] frame) { Log.d("TAG", "decoded"); }
}.decode();
典型源码代表:AsyncTask、Handler.Callback、SurfaceHolder.Callback。
方式 3:Kotlin 高阶函数 / Lambda(现代写法)
Kotlin 中函数是一等公民,回调可以直接用 函数类型 表达:
kotlin
class Worker {
private var onResult: ((Result) -> Unit)? = null
fun setOnResultListener(l: (Result) -> Unit) {
onResult = l
}
fun doWork() {
// ...
onResult?.invoke(Result.Ok("ok")) // ?. 安全调用,相当于判空
}
}
// 调用方:
Worker().apply {
setOnResultListener { result ->
Log.d("TAG", result.toString())
}
doWork()
}
更进一步:用 inline + crossinline 还能写 Worker.with { ... } 的 DSL 风格。
四、Android 源码里最常见的 6 个回调场景
1. 点击监听:View.OnClickListener
kotlin
button.setOnClickListener(v -> {
Toast.makeText(this, "点了", Toast.LENGTH_SHORT).show();
});
源码链路:InputManagerService → ViewRootImpl → DecorView → 目标 View 的 dispatchTouchEvent() → ACTION_UP 时触发 performClick() → 调 mOnClickListener.onClick(this)。
2. 列表适配器:RecyclerView.Adapter
less
new RecyclerView.Adapter<MyHolder>() {
@Override public MyHolder onCreateViewHolder(@NonNull ViewGroup p, int type) { ... }
@Override public void onBindViewHolder(@NonNull MyHolder h, int pos) { ... }
@Override public int getItemCount() { return list.size(); }
};
RecyclerView 把「怎么创建 ViewHolder」「怎么绑定数据」「有多少项」三个回调点留给业务方,自己只负责复用、滚动、动画 ------ 完美的 控制反转。
3. 生命周期:Activity / Fragment
less
@Override protected void onCreate(Bundle b) { ... }
@Override protected void onResume() { ... }
@Override protected void onPause() { ... }
ActivityThread 在收到系统 AMS 的 CREATE/RESUME/PAUSE 消息后,反射调用 Instrumentation.callActivityOnCreate(),最终回调你的 onCreate()。开发者只是「填空」,调度权在系统。
4. 异步任务:AsyncTask(已废弃但思想还在)
less
new AsyncTask<Void, Integer, String>() {
@Override protected String doInBackground(Void... voids) { ... }
@Override protected void onProgressUpdate(Integer... values) { ... }
@Override protected void onPostExecute(String s) { ... }
}.execute();
doInBackground 在子线程跑 → 通过 Handler 切回主线程 → onPostExecute 回调 UI。
5. 网络库:Retrofit 的 Callback<T>
less
api.login(req).enqueue(new Callback<LoginResp>() {
@Override public void onResponse(Call<LoginResp> c, Response<LoginResp> r) { ... }
@Override public void onFailure(Call<LoginResp> c, Throwable t) { ... }
});
底层用 OkHttp 的 enqueue,再封装一层回调分发主线程(MainThreadExecutor)。
6. 跨进程:AIDL 的回调
远程 Service 暴露一个接口,客户端把 IBinder 传过去,服务端在合适时机 transact 回来 ------ 本质也是回调,只是跨进程做了 Binder 序列化。
五、回调与 Handler、RxJava、协程的关系
| 概念 | 与回调的关系 |
|---|---|
| Handler / Message | 仍是回调:你把 Runnable 或 Message 投递到目标 Looper,目标 Looper 取出后回调 handleMessage() / run()。是「跨线程回调」的底层基础设施。 |
| Listener | Listener 是「一组回调方法的集合」,常表示「等待事件」。View.OnScrollChangeListener 有 1 个方法,TextWatcher 有 3 个。 |
| LiveData / Flow | 仍是回调:observe(owner, observer) 把 observer 作为回调,数据变化时通知。 |
| RxJava | Observable 模式,把回调链 可组合、可变换、可过滤 ,但底层 onNext/onError/onComplete 仍是回调。 |
| Coroutines 挂起 | 写起来像同步,底层编译为状态机 + 回调 (CPS 变换)。挂起函数恢复时靠 Continuation.resume() 回调通知。 |
可以这样理解演进:回调(一对一)→ Handler/RxJava(一对多 + 线程 + 变换)→ 协程(线性写法的回调地狱终结者) 。
六、回调地狱与现代化解决方案
什么是回调地狱
串行多个异步任务,回调层层嵌套:
typescript
api.login(name, new Callback() {
@Override public void onSuccess(String x) {
api.fetchUser(x, new Callback() {
@Override public void onSuccess(String y) {
api.fetchFeed(y, new Callback() {
@Override public void onSuccess(String z) {
runOnUiThread(() -> showToast(z));
}
});
}
});
}
});
可读性差、错误处理分散、取消传播困难 ------ 这就是回调地狱。
解决方案
1. Kotlin 协程(推荐)
kotlin
suspend fun flow() = coroutineScope {
val x = api.login(name) // 看起来同步,其实挂起
val y = api.fetchUser(x)
val z = api.fetchFeed(y)
showToast(z)
}
底层:每个 suspend 函数编译成 Continuation 状态机,恢复时回调 resumeWith()。对开发者屏蔽了回调,但本质还是回调。
2. RxJava
scss
api.login(name)
.flatMap { x -> api.fetchUser(x) }
.flatMap { y -> api.fetchFeed(y) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { z -> showToast(z) }
用操作符把回调链拼成「事件流」。
3. LiveData + ViewModel + Repo 做架构上的解耦,UI 不直接持有回调,而是观察数据。
七、自定义一个完整的回调(手把手)
需求:封装一个「下载器」,下载完通知调用方进度与结果。
scss
// 1) 接口
public interface DownloadCallback {
void onProgress(int percent);
void onSuccess(File file);
void onFailed(Exception e);
}
// 2) 下载器
public class Downloader {
private DownloadCallback cb;
private volatile boolean canceled;
public void setCallback(DownloadCallback cb) { this.cb = cb; }
public void start(String url) {
new Thread(() -> {
try {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
long total = conn.getContentLength();
InputStream in = conn.getInputStream();
File out = new File(...);
FileOutputStream fos = new FileOutputStream(out);
byte[] buf = new byte[8192];
long read = 0; int n;
while ((n = in.read(buf)) != -1 && !canceled) {
fos.write(buf, 0, n);
read += n;
int p = (int) (read * 100 / total);
runOnMain(() -> { if (cb != null) cb.onProgress(p); });
}
fos.close(); in.close();
if (!canceled) runOnMain(() -> { if (cb != null) cb.onSuccess(out); });
} catch (Exception e) {
runOnMain(() -> { if (cb != null) cb.onFailed(e); });
}
}).start();
}
public void cancel() { canceled = true; }
private void runOnMain(Runnable r) {
new Handler(Looper.getMainLooper()).post(r); // 切回主线程回调
}
}
// 3) 使用
new Downloader().apply {
setCallback(new DownloadCallback() {
@Override public void onProgress(int percent) { progressBar.setProgress(percent); }
@Override public void onSuccess(File f) { imageView.setImageBitmap(...); }
@Override public void onFailed(Exception e) { Toast.makeText(ctx, e.getMessage(), 0).show(); }
});
start("https://...");
}
要点复盘:
- 三个回调点:
onProgress / onSuccess / onFailed。 - 子线程做耗时,回调切回主线程 (
Handler.post------ 这就是 Handler 作为回调分发器的体现)。 - 判空、取消标志位。
八、内存泄漏:回调使用中最隐蔽的坑
匿名内部类(如 new Callback())默认持有外部类 this 引用。如果回调持有方长期存活(单例、静态、Handler 队列),外层 Activity 会被它一直引用 → 泄漏。
经典场景:
scala
// ❌ 泄漏:Handler 持有 Activity,延迟 5 分钟的消息会拦住 Activity 销毁
private static class InnerHandler extends Handler { // ① 改为静态内部类
private final WeakReference<Activity> ref; // ② 弱引用持有
InnerHandler(Activity a) { super(Looper.getMainLooper()); ref = new WeakReference<>(a); }
@Override public void handleMessage(Message m) {
Activity a = ref.get(); if (a == null) return;
// ...更新 UI...
}
}
通用防泄漏套路:
- 静态内部类 + 弱引用 持有 Activity / View。
- 在
onDestroy/onStop中主动取消 :call.cancel()、disposable.dispose()、job.cancel()、handler.removeCallbacksAndMessages(null)。 - 使用
LiveData(感知生命周期自动取消)替代手工回调。
LeaksCanary 是检测 Callback 泄漏的常用工具。
九、面试高频问答
Q1:回调接口为什么通常只有一个方法? A:只有一个抽象方法的接口叫函数式接口(Functional Interface) ,可用 Java 8 / Kotlin Lambda 简化。如 OnClickListener.onClick、Function<T,R>。多方法建议拆分或用抽象类。
Q2:回调一定在主线程吗? A:不一定 。取决于「触发回调的代码所在线程」。Retrofit 的 onResponse 默认切回主线程;OkHttp 原生 Callback 在子线程;Handler 的 handleMessage 在目标 Looper 线程。永远不要假设回调线程,要么在文档中约定,要么自己切。
Q3:回调和观察者模式一样吗? A:观察者模式 = 回调的「多订阅」版本。回调通常一对一,观察者一对多(一个 Subject 维护 List)。RxJava、LiveData 是观察者模式的实现。
Q4:回调 vs 接口 vs 事件总线(EventBus)? A:
- 接口回调:清晰、强类型、一对一 → 默认首选。
- EventBus / 广播:解耦但隐式、易失控 → 慎用。
- 经验法则:能接口就别用总线,避免维护「谁知道谁」的迷雾。
Q5:Kotlin 高阶函数回调会不会泄漏? A:会。Lambda 如果捕获了外部 this(如调用了成员方法),编译后会生成持外类的匿名对象,与匿名内部类一致。防泄漏思路相同。
Q6:为什么协程能解决回调地狱? A:CPS 变换 + 状态机 。每个 suspend 调用点编译为状态机一个 case,挂起时把 Continuation 当回调传给底层异步 API;恢复时调 continuation.resume(value) 跳回对应 case 继续。对开发者屏蔽了「回调」概念,可以像同步一样线性写。
十、一图总结
scss
┌─────────────────────────────────────────────┐
│ 调用方(Activity / Fragment / ViewModel) │
└─────┬───────────────────────────────────────┘
│ ① 实现接口并注册:worker.setOnXxxListener(this)
▼
┌─────────────────────────────────────────────┐
│ 被调方(Worker / 框架 / 网络库 / 系统) │
│ │
│ ② 持有回调引用 │
│ ③ 执行业务(耗时 / 等事件) │
│ ④ 在合适时机通过引用调用 listener.onXxx() │
│ - 同线程回 │
│ - 或经 Handler 切回主线程 │
└─────┬───────────────────────────────────────┘
│ ⑤ 回调 Active.onXxx() 被执行
▼
┌─────────────────────────────────────────────┐
│ 调用方在回调里:更新 UI / 启动下一步 │
└─────────────────────────────────────────────┘
三要素:① 接口/函数类型 ② 注册 ③ 时机触发
三陷阱:① 忘记判空 ② 线程混乱 ③ 内存泄漏
十一、小结
- 回调=代码(函数)当参数传给对方,由对方在事件发生时反向调用。
- Android 一切异步、解耦、事件驱动、IoC 都靠它支撑。
- 写法三步走:定义接口 → 注册 → 时机触发。
- 现代工程应优先使用 Kotlin 高阶函数 、协程,但理解回调和 Handler 仍是掌握 Android 框架的钥匙。
- 注意 判空、线程切换、内存泄漏 三大坑,必要时用 弱引用 + 生命周期感知组件( LiveData / repeatOnLifecycle / viewModelScope ) 规避。
掌握回调,你就掌握了 Android 的「血液」。下一步推荐阅读:Handler 原理、Lifecycle 源码、Kotlin 协程内部实现(CPS 与 Continuation)。
完