Android使用SingleThreadExecutor执行含动画Runnable时主线程偶发冻结
问题分析
当前实现的潜在风险:
- 每次创建新的
SingleThreadExecutor且未显式关闭,长期运行可能导致线程泄漏,占用系统资源间接影响主线程。 - 子线程持有
View引用,若动画执行期间View被销毁(如页面跳转),post的Runnable可能引发空指针或UI线程隐性阻塞。 - 依赖子线程
sleep(combatSpeed)控制时序,系统调度可能导致休眠时间不准;若线程被中断,后续UI操作逻辑混乱,进而影响主线程。
优化方案
方案1:利用Android原生动画框架+动画监听器串联动画
Android的Animation/Animator体系基于主线程消息队列运行,不会阻塞UI,且能通过监听动画结束事件精准启动下一个动画,是最符合Android UI规范的方案。
示例代码:
// 维护动画任务队列 private Queue<Runnable> animationQueue = new LinkedList<>(); private boolean isAnimating = false; // 添加动画任务到队列 public void addAnimationTask(Runnable task) { animationQueue.add(task); if (!isAnimating) { startNextAnimation(); } } // 启动下一个动画 private void startNextAnimation() { if (animationQueue.isEmpty()) { isAnimating = false; return; } isAnimating = true; Runnable currentTask = animationQueue.poll(); if (currentTask instanceof AnimationTask) { ((AnimationTask) currentTask).runWithEndCallback(this::startNextAnimation); } else { currentTask.run(); startNextAnimation(); } } // 封装带结束回调的动画任务 private abstract static class AnimationTask implements Runnable { private Runnable endCallback; public void runWithEndCallback(Runnable callback) { this.endCallback = callback; run(); } protected void onAnimationEnd() { if (endCallback != null) { endCallback.run(); } } } // 改造miss动画 private AnimationTask animateMissOnTarget(String target) { return new AnimationTask() { @Override public void run() { View targetCombatantView = combatantViewLookup.get(target); if (targetCombatantView == null) { onAnimationEnd(); return; } ImageView missImageView = targetCombatantView.findViewById(R.id.combatant_miss_image); if (missImageView == null) { onAnimationEnd(); return; } missImageView.setVisibility(View.VISIBLE); AnimationDrawable missAnim = (AnimationDrawable) missImageView.getBackground(); missAnim.start(); // 计算动画总时长,用于延迟执行收尾操作 int totalDuration = 0; for (int i = 0; i < missAnim.getNumberOfFrames(); i++) { totalDuration += missAnim.getDuration(i); } missImageView.postDelayed(() -> { missAnim.stop(); missImageView.setVisibility(View.INVISIBLE); onAnimationEnd(); // 动画结束后启动下一个任务 }, totalDuration); } }; }
方案2:使用Handler串联任务
通过绑定主线程Looper的Handler按顺序执行动画任务,所有任务在主线程消息队列中排队,避免子线程的不确定性问题。
示例代码:
private Handler mainHandler = new Handler(Looper.getMainLooper()); private Queue<Runnable> animationQueue = new LinkedList<>(); private boolean isProcessing = false; public void addAnimation(Runnable animation) { animationQueue.add(animation); if (!isProcessing) { processNext(); } } private void processNext() { if (animationQueue.isEmpty()) { isProcessing = false; return; } isProcessing = true; Runnable task = animationQueue.poll(); mainHandler.post(() -> { task.run(); // 任务执行完后延迟指定时长启动下一个 mainHandler.postDelayed(this::processNext, combatSpeed); }); } // 改造miss动画,直接在主线程执行 private Runnable animateMissOnTarget(String target) { return () -> { View targetCombatantView = combatantViewLookup.get(target); if (targetCombatantView == null) return; ImageView missImageView = targetCombatantView.findViewById(R.id.combatant_miss_image); if (missImageView == null) return; missImageView.setVisibility(View.VISIBLE); AnimationDrawable missAnim = (AnimationDrawable) missImageView.getBackground(); missAnim.start(); mainHandler.postDelayed(() -> { missAnim.stop(); missImageView.setVisibility(View.INVISIBLE); }, combatSpeed); }; }
方案3:使用Java的CompletableFuture(Java 8+)
通过CompletableFuture链式调用实现顺序执行,结合runAsync和thenRun串联任务,同时确保UI操作切换到主线程。
示例代码:
// 初始化一个已完成的Future作为链式调用起点 CompletableFuture<Void> future = CompletableFuture.completedFuture(null); // 复用单个线程池,避免频繁创建 ExecutorService singleExecutor = Executors.newSingleThreadExecutor(); // 批量添加动画任务到链式调用 for (Runnable animation : animationTasks) { future = future.thenRunAsync(() -> { // 切换到主线程执行动画 new Handler(Looper.getMainLooper()).post(animation); // 等待指定时长 try { Thread.sleep(combatSpeed); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }, singleExecutor); } // 所有任务完成后的收尾(比如关闭线程池) future.thenRun(() -> { singleExecutor.shutdown(); });
关键注意事项
- 所有UI操作必须在主线程执行,禁止子线程直接操作View。
- 页面销毁时(如
onDestroy),需清空动画队列并取消所有待执行任务,防止内存泄漏和空指针。 - 线程池推荐复用,避免每次创建新的
SingleThreadExecutor。
内容的提问来源于stack exchange,提问作者ProblemOverflow97
相关产品推荐
相关产品推荐

