本文内容介绍子线程操作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 Home键之后后台启动Activity延迟5秒
转载请注明出处:陈文管的博客 – Android 子线程更新UI详解
扫码或搜索:文呓
微信公众号 扫一扫关注