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)。


相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
liangshanbo12152 天前
React 性能优化实战
性能优化·react
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas