“好记性不如烂笔头”,感觉有必要总结一下工作一年以来遇到的典型问题,就从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界面,但是这个操作不是线程安全的。

更多相关文章

  1. [置顶] Android中在界面上动态显示歌词
  2. Android多线程处理机制中的Handler使用介绍
  3. Android官方教程翻译(3)——创建一个简单的用户界面
  4. [置顶] Android(安卓)经典面试题整理(一)(附答案)
  5. Android显示提示信息,实现两个界面之间的跳转
  6. OpenMP 在NDK中使用
  7. Android(安卓)简单音乐播放器开发
  8. Android(安卓)Canvas绘制直方图
  9. 常用的Android(安卓)Widget组件学习①--Button and TextView

随机推荐

  1. android6.0 netd设置dns
  2. Android(安卓)开发常用代码片段
  3. Android之万能适配器Adapter的使用
  4. AIR 2.5 App for Android(安卓)emulator
  5. android litepal(还是手写db的好用)
  6. android -------- 混淆打包报错(warning
  7. Ubuntu 下开发 Android(安卓)环境变量设
  8. android飞翔的小鸟……
  9. android
  10. 设置默认来电铃声 android