• Skip to primary navigation
  • Skip to main content
  • Skip to primary sidebar

陈文管的博客

分享有价值的内容

  • Android
  • Affiliate
  • SEO
  • 前后端
  • 网站建设
  • 自动化
  • 开发资源
  • 关于

Android 子线程更新UI详解

2019年9月6日发布 | 最近更新于 2023年8月28日

本文内容介绍子线程操作UI抛出的CalledFromWrongThreadException异常解析,子线程操作UI的几种方法,及几种在子线程中操作UI导致的异常现象说明。

一、子线程中操作UI一定会报错?

不一定,如果是在onCreate中子线程操作UI控件,不会发生异常,可以正常更新UI,下面看下源码。

1. 首先是ViewRootImpl抛异常的地方

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

这个异常是在ViewRootImpl类checkThread()中抛出的,在更新UI控件内容之后,刷新显示requestLayout重新布局会有线程检查的动作,mThread是当前的UI线程,Thread.currentThread()获取的是当前操作的线程,检查到线程不一样就抛出异常。

2. 子线程操作UI控件不报错的情况

当Activity生命周期走到onCreate的时候ViewRootImpl还没创建,所以此时在子线程更新UI不会抛异常。

在页面onResume,调用WindowManagerGlobal类addView添加显示页面布局,这个时候才初始化了ViewRootImpl,在这之后用子线程操作UI控件才会报异常,不过在一些设备的系统上也有出现过例外的情况。

public void addView(android.view.View view, ViewGroup.LayoutParams params,
                    Display display, Window parentWindow) {
    ViewRootImpl root;
    android.view.View panelParentView = null;
    synchronized (mLock) {
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
    }
    // do this last because it fires off messages to start doing things
    try {
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
    }
}

详细的源码分析可以参考:Android中子线程真的不能更新UI吗?

二、子线程更新UI的几种方法

1、Handler和Message

public static class RefreshUIHandler extends Handler {
    private WeakReference<Activity> activityWeakRef;
    public RefreshUIHandler(Activity activity){
        activityWeakRef = new WeakReference<>(activity);
    }
    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
    @Override
    public void handleMessage(Message msg) {
        Activity activity = activityWeakRef.get();
        if(activity == null || activity.isFinishing() || activity.isDestroyed()){
            return;
        }
        switch (msg.what) {
            case 0:
                // do some UI refresh operation
                break;
            default:
                break;
        }
    }
}

//在主线程初始化RefreshUIHandler对象
RefreshUIHandler refreshUIHandler = new RefreshUIHandler(this);
//在子线程中用发送消息的方式来异步刷新UI
Message refreshMsg = refreshUIHandler.obtainMessage();
refreshMsg.what = 0;
refreshUIHandler.sendMessage(refreshMsg);

使用Handler需要注意几个点:

1)类必须声明为静态的,避免实例化之后隐士持有当前Activity或类对象的引用,会导致内存泄漏的异常。

2)在页面onDestroy里面最好加上下面清空消息队列的操作,避免消息阻塞或延迟导致的内存泄漏或当前使用的对象不存在异常。

refreshUIHandler.removeCallbacksAndMessages(null);

3)发送消息的时候最好直接从Handler的消息池里面去获取Message对象,而不是每次都new一个对象,这样消息可以重复利用。避免多余的性能损耗。

4)也可以直接在主线程创建一个Handler对象来更新。

//直接在主线程创建Handler对象,使用主线程的Looper作为参数。
Handler uiHandler = new Handler(getMainLooper());
//或直接在主线程创建Handler对象,不传参数默认使用当前所在的线程
Handler uiHandler = new Handler();

uiHandler.post(new Runnable() {
    @Override
    public void run() {
    }
});

2、Activity的runOnUiThread方法

runOnUiThread(new Runnable() {
    @Override
    public void run() {
    }
});

可以看下这个方法的源码实现,如果当前操作线程不是UI线程,就使用主线程的Handler去更新,如果已经在UI线程,直接调用run方法执行,本质上也是使用Hander的方式来实现。

/**
 * Runs the specified action on the UI thread. If the current thread is the UI
 * thread, then the action is executed immediately. If the current thread is
 * not the UI thread, the action is posted to the event queue of the UI thread.
 *
 * @param action the action to run on the UI thread
 */
public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        action.run();
    }
}

3、使用UI控件的post或postDelayed方法

mTextView.post(new Runnable() {
    @Override
    public void run() {
    }
});

mTextView.postDelayed(new Runnable() {
    @Override
    public void run() {
    }
}, 3000);

可以看下下面的源码实现,也是使用到Handler来更新,如果没有Handler,则直接放到主线程的消息队列里面处理。

public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().post(action);
    return true;
}

public boolean postDelayed(Runnable action, long delayMillis) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.postDelayed(action, delayMillis);
    }
    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().postDelayed(action, delayMillis);
    return true;
}

4、AsyncTask

在doInBackground做耗时的处理,比如文件下载,读写文件、网络访问等,在onPreExecute做UI预处理显示,在onPostExecute做最终的UI刷新显示处理。

new AsyncTask<Void, Integer, Void>() {
    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        // do UI prepare work
    }
    @Override
    protected Void doInBackground(Void... params) {
        // do background work
        return null;
    }
    @Override
    protected void onPostExecute(Void aVoid) {
        super.onPostExecute(aVoid);
        // do UI refresh work
    }
}.execute();

三、子线程操作UI抛异常之后的现象

从开发过程中遇到的来看有下面几种异常现象。

1、应用直接Crash退出

系统层面和应用内都没有做异常捕获的处理,直接从日志中可以找到Crash堆栈信息。

2、应用没有Crash,但是界面上显示异常

应用没有Crash,但是界面上的UI控件显示错乱,比如界面所有的UI控件都显示到右上角,或部分UI控件不显示,或UI控件状态显示异常,只显示点击态或其他异常的状态。

在这个时候通常会在系统日志上输出捕获到的异常,因为这个异常已经被系统捕获,应用不会Crash。

W Binder : Caught a RuntimeException from the binder stub implementation.
W Binder : android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
W Binder : at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6594)
W Binder : at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:919)
W Binder : at android.view.View.requestLayout(View.java:18730)
W Binder : at android.view.View.requestLayout(View.java:18730)
W Binder : at android.view.View.requestLayout(View.java:18730)
W Binder : at android.view.View.requestLayout(View.java:18730)
W Binder : at android.view.View.requestLayout(View.java:18730)
W Binder : at android.support.constraint.ConstraintLayout.requestLayout(ConstraintLayout.java:3115)
W Binder : at android.view.View.requestLayout(View.java:18730)
W Binder : at android.support.constraint.ConstraintLayout.requestLayout(ConstraintLayout.java:3115)
W Binder : at android.view.View.requestLayout(View.java:18730)
W Binder : at android.support.constraint.ConstraintLayout.requestLayout(ConstraintLayout.java:3115)
W Binder : at android.view.View.requestLayout(View.java:18730)
W Binder : at android.support.constraint.ConstraintLayout.requestLayout(ConstraintLayout.java:3115)
W Binder : at android.view.View.setFlags(View.java:10591)
W Binder : at android.view.View.setVisibility(View.java:7431)

3、没有任何的异常,可以正常更新UI

这边说的不是上面在onCreate的时候更新UI,而是界面已经完全初始化显示之后的子线程操作UI,只在极个别的车机系统上遇到过,没有发生任何的异常,可以正常更新UI,可能是ROM厂家对系统做了些修改。

四、参考资料

Android中子线程真的不能更新UI吗?

Android子线程真的不能更新UI么

扩展阅读:

Android Home键之后后台启动Activity延迟5秒

Toast 自定义布局重复添加异常分析

Android OOM问题分析

转载请注明出处:陈文管的博客 – Android 子线程更新UI详解

扫码或搜索:文呓

博客公众号

微信公众号 扫一扫关注

文章目录

  • 一、子线程中操作UI一定会报错?
  • 二、子线程更新UI的几种方法
    • 1、Handler和Message
    • 2、Activity的runOnUiThread方法
    • 3、使用UI控件的post或postDelayed方法
    • 4、AsyncTask
  • 三、子线程操作UI抛异常之后的现象
    • 1、应用直接Crash退出
    • 2、应用没有Crash,但是界面上显示异常
    • 3、没有任何的异常,可以正常更新UI
  • 四、参考资料
博客公众号

闽ICP备18001825号-1 · Copyright © 2025 · Powered by chenwenguan.com