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绘制的起点。
总结一下:
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的异常。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。