Android非UI线程中更新UI界面
“好记性不如烂笔头”,感觉有必要总结一下工作一年以来遇到的典型问题,就从Android子线程更新UI界面引起的crash开始吧。
项目中的Gallery在显示照片详细信息中使用了Google Map来标记照片的拍摄地点,测试工程师反馈了一个严重的bug,点击照片中的详细信息查看地图上照片拍摄地点,如果旋转屏幕,Gallery有时会崩溃,查看一下crash log:
05-12 14:44:11.992500 25232 25344 E AndroidRuntime: Caused by: java.lang.IllegalStateException: Not on the main thread05-12 14:44:11.992500 25232 25344 E AndroidRuntime: at com.google.maps.api.android.lib6.common.k.a(:com.google.android.gms.DynamiteModulesB:131)05-12 14:44:11.992500 25232 25344 E AndroidRuntime: at com.google.maps.api.android.lib6.common.r.a(:com.google.android.gms.DynamiteModulesB:30)05-12 14:44:11.992500 25232 25344 E AndroidRuntime: at com.google.maps.api.android.lib6.impl.az.a(:com.google.android.gms.DynamiteModulesB:814)05-12 14:44:11.992500 25232 25344 E AndroidRuntime: at com.google.android.gms.maps.internal.l.onTransact(:com.google.android.gms.DynamiteModulesB:83)05-12 14:44:11.992500 25232 25344 E AndroidRuntime: at android.os.Binder.transact(Binder.java:504)05-12 14:44:11.992500 25232 25344 E AndroidRuntime: at com.google.android.gms.maps.internal.IGoogleMapDelegate$zza$zza.moveCamera(Unknown Source)05-12 14:44:11.992500 25232 25344 E AndroidRuntime: at com.google.android.gms.maps.GoogleMap.moveCamera(Unknown Source)05-12 14:44:11.992500 25232 25344 E AndroidRuntime: at com.tct.gallery3d.app.fragment.DetailsFragment.initLatLng(DetailsFragment.java:79)05-12 14:44:11.992500 25232 25344 E AndroidRuntime: at com.tct.gallery3d.app.fragment.DetailsFragment.onDetailsReady(DetailsFragment.java:67)05-12 14:44:11.992500 25232 25344 E AndroidRuntime: at com.tct.gallery3d.app.adapter.DetailsAdapter.initDetails(DetailsAdapter.java:433)05-12 14:44:11.992500 25232 25344 E AndroidRuntime: at com.tct.gallery3d.app.adapter.DetailsAdapter.access$000(DetailsAdapter.java:37)05-12 14:44:11.992500 25232 25344 E AndroidRuntime: at com.tct.gallery3d.app.adapter.DetailsAdapter$DetailsTask.doInBackground(DetailsAdapter.java:198)05-12 14:44:11.992500 25232 25344 E AndroidRuntime: at com.tct.gallery3d.app.adapter.DetailsAdapter$DetailsTask.doInBackground(DetailsAdapter.java:191)05-12 14:44:11.992500 25232 25344 E AndroidRuntime: at com.tct.gallery3d.image.AsyncTask$2.call(AsyncTask.java:313)05-12 14:44:11.992500 25232 25344 E AndroidRuntime: at java.util.concurrent.FutureTask.run(FutureTask.java:237)05-12 14:44:11.992500 25232 25344 E AndroidRuntime: ... 4 more
把问题定位到DetailsTask这类的doInBackground方法中:
protected CustomArrayList doInBackground(MediaObject... params) { ...... initLatLng(); ......}/** * Init the latLng for the google map */private synchronized void initLatLng() { if (mGoogleMap == null || mLatLng == null) { return; } // Move to the position. updateMapPosition();}private void updateMapPosition(){ CameraUpdate update = CameraUpdateFactory.newLatLngZoom(mLatLng, 10f); mGoogleMap.moveCamera(update); mGoogleMap.setMapType(GoogleMap.MAP_TYPE_NORMAL); mGoogleMap.addMarker(newMarkerOptions().position(mLatLng) .title("Marker").flat(true)); mContext.onLatLngReady();}
DetailTask继承至AsyncTask,那么doInBackground()这个方法就是在子线程中运行的,这里居然在子线程中更新UI界面,那么问题来了,作为一个Android工程师都应该知道——在子线程中更新UI肯定会抛出异常,为什么测试反馈说这个crash并不是必现的呢?估计非必现的原因,所以这位同事没有意识到他在子线程中修改了UI界面。非UI线程居然能更新UI界面,颠覆了我的Android世界观。最后百度发现,Android非UI线程真的能更新UI界面,这是为什么呢?自己写了一个demo来学习一下。
@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = findViewById(R.id.text_view); button = findViewById(R.id.button); button.setOnClickListener(this); new Thread(new Runnable() { @Override public void run() { textView.setText("setText on worker thread"); } }).start();}@Overridepublic void onClick(View v) { new Thread(new Runnable() { @Override public void run() { textView.setText("onClick on worker thread"); } }).start();}
把demo推入手机运行一下,“this is in worker thread”能正常显示,果真在子线程中可以修改UI界面:
然后再点一下BUTTON按钮,程序崩溃了,以下是程序崩溃时的log:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7687) at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1203) at android.view.View.requestLayout(View.java:19996) at android.view.View.requestLayout(View.java:19996) at android.view.View.requestLayout(View.java:19996) at android.view.View.requestLayout(View.java:19996) at android.view.View.requestLayout(View.java:19996) at android.widget.TextView.checkForRelayout(TextView.java:7793) at android.widget.TextView.setText(TextView.java:4547) at android.widget.TextView.setText(TextView.java:4404) at android.widget.TextView.setText(TextView.java:4379) at com.fengbangquan.myapplication.MainActivity$2.run(MainActivity.java)
同样的代码,为什么在OnCreate()中开启工作线程去更新TextView没有问题,但是点击button主动起一个线程却会导致crash?我们跟进TextView.setText()去看一下,这个方法中首先调用checkForRelayout(),在这个方法中又调用View.requestLayout(),最终View.requestLayout()调用了ViewRootImpl.requestLayout()去重新绘制view,在requestLayout()中发现了一个很关键的方法checkThread():
public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } } void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views."); } }
异常就是在这里被抛出来的,原来viewRootImpl这个类在我们setText()后首先检测当前代码执行在哪个Thread中,如果不是在mThread中就不会执行scheduleTraversals()去重新绘制界面,mThread又是哪一个线程呢?(十有八九就是UI线程)。我们看一下ViewRootImpl中的mThread是在哪里赋值:
public ViewRootImpl(Context context, Display display) { ...... mContext = context; mThread = Thread.currentThread(); ......}
原来mThread是在ViewRootImpl在初始化的时候赋值的,那ViewRootImpl又是在什么时候初始化的?找到了ViewRootImpl初始化的时间,也就找到了问题的答案。可以猜想一下,我们在Activity的OnCreate()中开启新线程时,ViewRootImpl是不是还没有初始化,所以checkThread()这个方法没有被调用去检测当前环境处在哪一个Thread。Activity的生命周期是在ActivityThread中调用的,查看ActivityThread的源码,最后发现ViewRootImpl是ActivityThread.handleResumeActivity()中得到初始化的:
final void handleResumeActivity(IBinder token, boolean clearHide,boolean isForward, boolean reallyResume, int seq, String reason) { ...... View decor = r.window.getDecorView(); ...... ViewRootImpl impl = decor.getViewRootImpl(); ......}
原来ViewRootImpl是Activity进入OnResume()的时候才得到初始化,所以我们可以在Oncreate()开启一个线程去更新UI界面的时候,checkThread()这个方法并没有执行, 进而能够执行scheduleTraversals()去重新绘制view更新UI界面。虽然可以在子线程中更新UI界面,但是这个操作不是线程安全的。
更多相关文章
- [置顶] Android中在界面上动态显示歌词
- Android多线程处理机制中的Handler使用介绍
- Android官方教程翻译(3)——创建一个简单的用户界面
- [置顶] Android(安卓)经典面试题整理(一)(附答案)
- Android显示提示信息,实现两个界面之间的跳转
- OpenMP 在NDK中使用
- Android(安卓)简单音乐播放器开发
- Android(安卓)Canvas绘制直方图
- 常用的Android(安卓)Widget组件学习①--Button and TextView