You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 07:54:53