欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android子线程与更新UI问题的深入讲解

程序员文章站 2022-06-03 19:15:41
前言 在android项目中经常有碰到这样的问题,在子线程中完成耗时操作之后要更新ui,下面就自己经历的一些项目总结一下更新的方法。话不多说了,来一起看看详细的介绍吧...

前言

在android项目中经常有碰到这样的问题,在子线程中完成耗时操作之后要更新ui,下面就自己经历的一些项目总结一下更新的方法。话不多说了,来一起看看详细的介绍吧

引子:

情形1

 @override
 protected void oncreate(bundle savedinstancestate) {
 super.oncreate(savedinstancestate);
 setcontentview(r.layout.activity_main);

 textview textview = findviewbyid(r.id.home_tv);
 imageview imageview = findviewbyid(r.id.home_img);

 new thread(new runnable() {
  @override
  public void run() {
  textview.settext("更新textview");
  imageview.setimageresource(r.drawable.img);
  }
 }).start();
 }

运行结果:正常运行!!!

情形二

 @override
 protected void oncreate(bundle savedinstancestate) {
 super.oncreate(savedinstancestate);
 setcontentview(r.layout.activity_main);

 textview textview = findviewbyid(r.id.home_tv);
 imageview imageview = findviewbyid(r.id.home_img);

 new thread(new runnable() {
  @override
  public void run() {
  try {
   thread.sleep(5000);
  } catch (interruptedexception e) {
   e.printstacktrace();
  }
  textview.settext("更新textview");
  imageview.setimageresource(r.drawable.img);
  }
 }).start();
 }

运行结果:异常

    android.view.viewrootimpl$calledfromwrongthreadexception: only the original thread that created a view hierarchy can touch its views.
        at android.view.viewrootimpl.checkthread(viewrootimpl.java:6357)
        at android.view.viewrootimpl.requestlayout(viewrootimpl.java:874)
        at android.view.view.requestlayout(view.java:17476)
        at android.view.view.requestlayout(view.java:17476)
        at android.view.view.requestlayout(view.java:17476)
        at android.view.view.requestlayout(view.java:17476)
        at android.view.view.requestlayout(view.java:17476)
        at android.view.view.requestlayout(view.java:17476)
        at android.widget.relativelayout.requestlayout(relativelayout.java:360)
        at android.view.view.requestlayout(view.java:17476)
        at android.widget.textview.checkforrelayout(textview.java:6871)
        at android.widget.textview.settext(textview.java:4057)
        at android.widget.textview.settext(textview.java:3915)
        at android.widget.textview.settext(textview.java:3890)
        at com.dong.demo.mainactivity$1.run(mainactivity.java:44)
        at java.lang.thread.run(thread.java:818)

不是说,子线程不能更新ui吗,为什么情形一可以正常运行,情形二不能正常运行呢;

子线程修改ui出现异常,与什么方法有关

首先从出现异常的log日志入手,发现出现异常的方法调用顺序如下:

textview.settext(textview.java:4057)

textview.checkforrelayout(textview.java:6871)

view.requestlayout(view.java:17476)

relativelayout.requestlayout(relativelayout.java:360)

view.requestlayout(view.java:17476)

viewrootimpl.requestlayout(viewrootimpl.java:874)

viewrootimpl.checkthread(viewrootimpl.java:6357)

更改imageview时,出现的异常类似;

首先看textview.settext()方法的源码

 private void settext(charsequence text, buffertype type,
    boolean notifybefore, int oldlen) {
 
 //省略其他代码

 if (mlayout != null) {
  checkforrelayout();
 }

 sendontextchanged(text, 0, oldlen, textlength);
 ontextchanged(text, 0, oldlen, textlength);

 //省略其他代码

然后,查看以下checkforrelayout()方法的与源码。

 private void checkforrelayout() {
 // if we have a fixed width, we can just swap in a new text layout
 // if the text height stays the same or if the view height is fixed.

 if ((mlayoutparams.width != layoutparams.wrap_content

  //省略代码

  // we lose: the height has changed and we have a dynamic height.
  // request a new view layout using our new text layout.
  requestlayout();
  invalidate();
 } else {
  // dynamic width, so we have no choice but to request a new
  // view layout with a new text layout.
  nulllayouts();
  requestlayout();
  invalidate();
 }
 }

checkforrelayout方法,首先会调用需要改变的view的requestlayout方法,然后执行invalidate()重绘操作;

textview没有重写requestlayout方法,requestlayout方法由view实现;

查看requestlayout方法的源码:

 public void requestlayout() {
 //省略其他代码
 if (mparent != null && !mparent.islayoutrequested()) {
  mparent.requestlayout();
 }
 if (mattachinfo != null && mattachinfo.mviewrequestinglayout == this) {
  mattachinfo.mviewrequestinglayout = null;
 }
 }

view获取到父view(类型是viewparent,viewpaerent是个接口,requestlayout由子类来具体实现),mparent,然后调用父view的requestlayout方法,比如示例中的父view就是xml文件的根布局就是relativelayout。

 @override
 public void requestlayout() {
 super.requestlayout();
 mdirtyhierarchy = true;
 }

继续跟踪super.requestlayout()方法,即viewgroup没有重新,即调用的是view的requestlayout方法。

经过一系列的调用viewparent的requestlayout方法,最终调用到viewrootimp的requestlayout方法。viewrootimp实现了viewparent接口,继续查看viewrootimp的requestlayout方法源码。

 @override
 public void requestlayout() {
  if (!mhandlinglayoutinlayoutrequest) {
   checkthread();
   mlayoutrequested = true;
   scheduletraversals();
  }
 }

viewrootimp的requestlayout方法中有两个方法:

一、checkthread,检查线程,源码如下

 void checkthread() {
  if (mthread != thread.currentthread()) {
   throw new calledfromwrongthreadexception(
     "only the original thread that created a view hierarchy can touch its views.");
  }
 }

判断当前线程,是否是创建viewrootimp的线程,而创建viewrootimp的线程就是主线程,当前线程不是主线程的时候,就抛出异常。

二、scheduletraversals(),查看源码:

 void scheduletraversals() {
  if (!mtraversalscheduled) {
   mtraversalscheduled = true;
   mtraversalbarrier = mhandler.getlooper().getqueue().postsyncbarrier();
   mchoreographer.postcallback(
     choreographer.callback_traversal, mtraversalrunnable, null);
   if (!munbufferedinputdispatch) {
    scheduleconsumebatchedinput();
   }
   notifyrendererofframepending();
   pokedrawlockifneeded();
  }
 }

查看mtraversalrunnable中run()方法的具体操作

 final class traversalrunnable implements runnable {
  @override
  public void run() {
   dotraversal();
  }
 }

继续追踪dotraversal()方法

 void dotraversal() {
  if (mtraversalscheduled) {
   mtraversalscheduled = false;
   mhandler.getlooper().getqueue().removesyncbarrier(mtraversalbarrier);

   if (mprofile) {
    debug.startmethodtracing("viewancestor");
   }

   performtraversals();

   if (mprofile) {
    debug.stopmethodtracing();
    mprofile = false;
   }
  }
 }

查看到performtraversals()方法,熟悉了吧,这是view绘制的起点。

Android子线程与更新UI问题的深入讲解

总结一下:

1.android更新ui会调用view的requestlayout()方法,在requestlayout方法中,获取viewparent,然后调用viewparent的requestlayout()方法,一直调用下去,直到调用到viewrootimp的requestlayout方法;

2.viewrootimp的requetlayout方法,主要有两部操作一个是checkthread()方法,检测线程,一个是scheduletraversals,执行绘制相关工作;

情形3

 @override
 protected void oncreate(bundle savedinstancestate) {
  log.i("dong", "activity: oncreate");
  super.oncreate(savedinstancestate);
  setcontentview(r.layout.activity_main);

  new thread(new runnable() {
   @override
   public void run() {

    looper.prepare();

    try {
     thread.sleep(5000);
    } catch (interruptedexception e) {
     e.printstacktrace();
    }

    toast.maketext(mainactivity.this, "显示toast", toast.length_long).show();

    looper.loop();
   }
  }).start();
 }

运行结果:正常

分析

下面从toast源码进行分析:

 public static toast maketext(context context, charsequence text, @duration int duration) {
  return maketext(context, null, text, duration);
 }

maketext方法调用了他的重载方法,继续追踪

 public static toast maketext(@nonnull context context, @nullable looper looper,
   @nonnull charsequence text, @duration int duration) {
  toast result = new toast(context, looper);

  layoutinflater inflate = (layoutinflater)
    context.getsystemservice(context.layout_inflater_service);
  view v = inflate.inflate(com.android.internal.r.layout.transient_notification, null);
  textview tv = (textview)v.findviewbyid(com.android.internal.r.id.message);
  tv.settext(text);

  result.mnextview = v;
  result.mduration = duration;

  return result;
 }

新建了一个toast对象,然后对显示的布局、内容、时长进行了设置,并返回toast对象。

继续查看new toast()的源码

 public toast(@nonnull context context, @nullable looper looper) {
  mcontext = context;
  mtn = new tn(context.getpackagename(), looper);
  mtn.my = context.getresources().getdimensionpixelsize(
    com.android.internal.r.dimen.toast_y_offset);
  mtn.mgravity = context.getresources().getinteger(
    com.android.internal.r.integer.config_toastdefaultgravity);
 }

继续查看核心代码 mtn = new tn(context.getpackagename(), looper);

tn初始化的源码为:

  tn(string packagename, @nullable looper looper) {
   //省略部分不相关代码
   if (looper == null) {
    // 没有传入looper对象的话,使用当前线程对应的looper对象
    looper = looper.mylooper();
    if (looper == null) {
     throw new runtimeexception(
       "can't toast on a thread that has not called looper.prepare()");
    }
   }
   //初始化了handler对象
   mhandler = new handler(looper, null) {
    @override
    public void handlemessage(message msg) {
     switch (msg.what) {
      case show: {
       ibinder token = (ibinder) msg.obj;
       handleshow(token);
       break;
      }
      case hide: {
       handlehide();
       // don't do this in handlehide() because it is also invoked by
       // handleshow()
       mnextview = null;
       break;
      }
      case cancel: {
       handlehide();
       // don't do this in handlehide() because it is also invoked by
       // handleshow()
       mnextview = null;
       try {
        getservice().canceltoast(mpackagename, tn.this);
       } catch (remoteexception e) {
       }
       break;
      }
     }
    }
   };
  }

继续追踪handleshow(token)方法:

  public void handleshow(ibinder windowtoken) {
   //省略部分代码
   if (mview != mnextview) {
    // remove the old view if necessary
    handlehide();
    mview = mnextview;
    context context = mview.getcontext().getapplicationcontext();
    string packagename = mview.getcontext().getoppackagename();
    if (context == null) {
     context = mview.getcontext();
    }
    mwm = (windowmanager)context.getsystemservice(context.window_service);
    /*
    ·*省略设置显示属性的代码
    ·*/
    if (mview.getparent() != null) {
     if (locallogv) log.v(tag, "remove! " + mview + " in " + this);
     mwm.removeview(mview);
    }
=    try {
     mwm.addview(mview, mparams);
     trysendaccessibilityevent();
    } catch (windowmanager.badtokenexception e) {
     /* ignore */
    }
   }
  }

通过源码可以看出,toast显示内容是通过mwm(windowmanager类型)的直接添加的,更正:mwm.addview 时,对应的viewrootimp初始化发生在子线程,checkthread方法中的mthread != thread.currentthread()判断为true,所以不会抛出只能在主线程更新ui的异常。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。