addOnSuccessListener内CompletableFuture.complete()失效致应用冻结
问题场景
编写如下获取位置的函数,希望在addOnSuccessListener监听器中完成CompletableFuture的结果返回:
private fun genericFunction(): CompletableFuture<Location?> { val completableFuture = CompletableFuture<Location?>() Executors.newCachedThreadPool().submit { val fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) fusedLocationClient.lastLocation .addOnSuccessListener { location: Location? -> Toast.makeText(...).show() completableFuture.complete(location) } } return completableFuture }
实际运行中观察到的现象:
- 不等待该
Future结果时,Toast可以正常弹出 - 一旦等待该
Future的结果,应用直接冻结
最初的猜测是不能在addOnSuccessListener中调用completableFuture.complete(),但监听器内部可以正常访问该Future的引用,猜测逻辑存在矛盾。
问题根本成因
- 之前的猜测方向完全错误:在成功监听器中调用
completableFuture.complete()是完全合法的操作,不存在访问限制。 - 触发冻结的核心是跨线程死锁,完整逻辑链:
- Android定位服务的
FusedLocationProviderClient默认回调规则:如果调用addOnSuccessListener时没有显式指定执行Looper,会自动绑定到注册监听器时所在线程的Looper;如果当前线程没有绑定Looper(比如示例中用Executors.newCachedThreadPool()创建的无Looper的工作线程),系统会自动 fallback 到主线程Looper执行所有回调逻辑。 - 拿到返回的
CompletableFuture后,通常是直接在主线程调用get()这类阻塞方法等待结果,这会直接挂起主线程,停止主线程Looper的消息循环。 - 此时定位服务的成功回调被排队到主线程消息队列中,但主线程已经被阻塞、不再消费队列任务,导致回调永远无法执行,
completableFuture.complete()永远不会被触发,主线程和回调任务互相等待,最终表现为应用完全冻结。
- Android定位服务的
- 观察到的“不等待Future时Toast正常弹出”的现象刚好印证了这个逻辑:不阻塞主线程时,主线程Looper正常消费消息,回调可以顺利执行,Toast和
complete()逻辑都能跑通。
调试定位方法
- 冻结发生时,用Android Studio的Debug面板抓取全量Java线程堆栈:如果看到主线程堆栈停在
CompletableFuture.get()的阻塞逻辑上,同时主线程消息队列中存在未执行的OnSuccessListener相关任务,即可直接确认死锁问题。 - 在成功监听器的第一行加日志,打印当前执行线程的名称:
Log.d("LocationDebug", "回调执行线程:${Thread.currentThread().name}"),大部分场景下会看到输出为main,验证定位回调最终运行在主线程的结论。 - 对照测试:把阻塞等待
Future结果的逻辑从主线程移到单独的工作线程执行,观察应用是否还会冻结——如果移走后冻结问题消失,就可以反向验证是主线程阻塞导致的回调无法执行。
内容的提问来源于stack exchange,提问作者Luca Dibattista
相关产品推荐
相关产品推荐

