Android-深入理解 Android 回调(Callback)机制

深入理解 Android 回调(Callback)机制

一篇从原理到实战、从 Java 到 Kotlin、从源码示例到面试高频问答的 Android Callback 机制详解。

阅读时间:约 12 分钟。适用读者:Android 初、中级工程师,或准备面试的同学。


目录


一、回调到底是什么

一句话定义:

回调(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)。
  • 多回调方法 :接口里放多个方法,类似 OnTouchListeneronTouch() 一个方法,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();

典型源码代表:AsyncTaskHandler.CallbackSurfaceHolder.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();
});

源码链路:InputManagerServiceViewRootImplDecorView → 目标 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 在收到系统 AMSCREATE/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 仍是回调:你把 RunnableMessage 投递到目标 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...
    }
}

通用防泄漏套路:

  1. 静态内部类 + 弱引用 持有 Activity / View。
  2. onDestroy / onStop主动取消call.cancel()disposable.dispose()job.cancel()handler.removeCallbacksAndMessages(null)
  3. 使用 LiveData(感知生命周期自动取消)替代手工回调。

LeaksCanary 是检测 Callback 泄漏的常用工具。


九、面试高频问答

Q1:回调接口为什么通常只有一个方法? A:只有一个抽象方法的接口叫函数式接口(Functional Interface) ,可用 Java 8 / Kotlin Lambda 简化。如 OnClickListener.onClickFunction<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)。


相关推荐
古法安卓1 小时前
Android-车机 GNSS 定位数据接收问题排查
android·java·android studio
FungLeo3 小时前
Flutter 吸顶分组列表实战:语义桶分组 + 点击头平滑滚动
android·flutter
阿巴斯甜4 小时前
Android 自定义权限
android
fivebliss4 小时前
取算存三步细化及延迟指标解析
人工智能·性能优化
FungLeo4 小时前
Flutter 超长 StatefulWidget 拆分术:part of + extension on State 实战
android·flutter·dart
小王C语言5 小时前
MySQL 数据类型:数值类型、字符串类型、日期和时间类型、enum 和 set、find_in_set 查询
android·数据库·mysql
我命由我123456 小时前
Android 控件 - CardView(快速实现圆角)
android·java·java-ee·kotlin·android studio·android-studio·android runtime
GitLqr6 小时前
Impeller 时代:Shader Jank 消失了,但渲染性能的战场也变了
flutter·面试·性能优化
码云数智-园园6 小时前
Android 全方位性能优化合集
android·性能优化