温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

Android子线程与更新UI的示例分析

发布时间:2021-08-26 14:16:05 来源:亿速云 阅读:149 作者:小新 栏目:移动开发

这篇文章主要为大家展示了“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的异常。

以上是“Android子线程与更新UI的示例分析”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注亿速云行业资讯频道!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI