非UI线程更新UI!?

今天晚上被弟弟告知他在子线程中更新了UI,问我是不是版本的问题,我果断说是他的代码写错了,不过分分钟被打脸,经过我一番仔细的探查最终发现了原因,或许这件事的结果不是多么的重要,但是我认为探查的过程还是有一定的参考价值的.

  • 首先,遇见这种问题时下意识的是去google,所以我采取了下面的措施(请忽视我不堪入目的英语,相信google的强大….)

  • 然而我发现我并没有得到我想要的结果,大部分的答案是告诉我如何在子线程中转到主线程中更新UI,好吧,难道是我不应该用?号,所以,我做了下面的事.

  • 可悲的是google觉得我表达的是一个意思…(可能是我英语太差了,请不要告诉我这个事实),没办法了,只能自己上阵了,感谢google搜索不到,才让自己有了这次探索的经历!

  • 首先,我们先看一下代码,代码的意思很简单,出乎意料的时,它正确运行了,并且在手机界面上显示的是Changed,这打破了我们在非主线程中不能更新UI的认识
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final TextView tv = (TextView) findViewById(R.id.tv_test);
        new Thread(new Runnable() {
            @Override
            public void run() {
                tv.setText("Changed");
            }
        }).start();
    }
}
  • 之后我就意识到,这个问题可能跟之前我碰到的一个在onCreate中直接获取View的宽高无法得到正确的值一样,受某些东西延迟加载的因素,为了验证我的想法,我又运行了下面的代码
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final TextView tv = (TextView) findViewById(R.id.tv_test);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                tv.setText("Changed");
            }
        }).start();
    }
}
  • 正确的错误终于出现了,请看一下令人高兴的久违的错误

  • 然后我们就探查tv.setText("Changed");内部做了什么,不断的跟进内部方法,我们会走到这个方法中,我们会注意到,最终都会调用invalidate()方法重新绘制,这也是非常符合自然逻辑的,所以我们就去探索invalidate()中做了什么
/**
     * Check whether entirely new text requires a new view layout
     * or merely a new text layout.
     */
    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 ||
                (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) &&
                (mHint == null || mHintLayout != null) &&
                (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
            // Static width, so try making a new text layout.

            int oldht = mLayout.getHeight();
            int want = mLayout.getWidth();
            int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();

            /*
             * No need to bring the text into view, since the size is not
             * changing (unless we do the requestLayout(), in which case it
             * will happen at measure).
             */
            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
                          mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
                          false);

            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
                // In a fixed-height view, so use our new text layout.
                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT &&
                    mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                    invalidate();
                    return;
                }

                // Dynamic height, but height has stayed the same,
                // so use our new text layout.
                if (mLayout.getHeight() == oldht &&
                    (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                    invalidate();
                    return;
                }
            }

            // 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();
        }
    }
  • 同理,我一步一步跟进代码会走到下面的方法中(在浏览代码时我们要注意我们的目的是什么,我们是在找在哪里去判断是否在主线程中),请关注p.invalidateChild(this, damage);这句代码,p是一个ViewParent,熟悉View绘制流程的小伙伴看到ViewParent就会恍然大悟,著名的ViewRootImpl就是ViewParent的子类,所以我们直接去ViewRootImpl中搜寻invalidateChild方法
 void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
        if (mGhostView != null) {
            mGhostView.invalidate(true);
            return;
        }

        if (skipInvalidate()) {
            return;
        }

        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
                || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
                || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
                || (fullInvalidate && isOpaque() != mLastIsOpaque)) {
            if (fullInvalidate) {
                mLastIsOpaque = isOpaque();
                mPrivateFlags &= ~PFLAG_DRAWN;
            }

            mPrivateFlags |= PFLAG_DIRTY;

            if (invalidateCache) {
                mPrivateFlags |= PFLAG_INVALIDATED;
                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
            }

            // Propagate the damage rectangle to the parent view.
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                p.invalidateChild(this, damage);
            }

            // Damage the entire projection receiver, if necessary.
            if (mBackground != null && mBackground.isProjected()) {
                final View receiver = getProjectionReceiver();
                if (receiver != null) {
                    receiver.damageInParent();
                }
            }

            // Damage the entire IsolatedZVolume receiving this view‘s shadow.
            if (isHardwareAccelerated() && getZ() != 0) {
                damageShadowReceiver();
            }
        }
  • 在ViewRootImpl中invalidateChild方法调用了以下这个方法,值得高兴的是,我们终于找到了,请关注函数中第一句代码checkThread(),点进去看这个函数的实现后发现他做的事是我们再熟悉不过的了,熟悉的代码熟悉的报错信息,到此一切都真相大白了,检查当前线程是否是主线程的逻辑在ViewRootImpl方法中,熟悉View绘制流程的小伙伴肯定知道ViewRootImpl是在onResume方法中去创建的,所以说,只要在onResume方法调用之前,都是可以在子线程中更新UI的
   @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();
        if (DEBUG_DRAW) Log.v(TAG, "Invalidate child: " + dirty);

        if (dirty == null) {
            invalidate();
            return null;
        } else if (dirty.isEmpty() && !mIsAnimating) {
            return null;
        }

        if (mCurScrollY != 0 || mTranslator != null) {
            mTempRect.set(dirty);
            dirty = mTempRect;
            if (mCurScrollY != 0) {
                dirty.offset(0, -mCurScrollY);
            }
            if (mTranslator != null) {
                mTranslator.translateRectInAppWindowToScreen(dirty);
            }
            if (mAttachInfo.mScalingRequired) {
                dirty.inset(-1, -1);
            }
        }

        invalidateRectOnScreen(dirty);

        return null;
    }
    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

这次的探查过程给了我很大的启示,遇见问题时,在必要时首先要回忆之前遇到的相似问题,并合理利用网上搜索的信息去自己探索问题的真相,一个根据关键信息推导出来的合理假设将使我们事半功倍,并且注意不要盲目的相信网上的一些结论,纸上得来终觉浅,绝知此事要躬行!

时间: 2024-12-18 06:30:26

非UI线程更新UI!?的相关文章

Android异步处理一:使用Thread+Handler实现非UI线程更新UI界面

Android应用的开发过程中需要把繁重的任务(IO,网络连接等)放到其他线程中异步执行,达到不阻塞UI的效果. 下面将由浅入深介绍Android进行异步处理的实现方法和系统底层的实现原理. 本文介绍Android异步处理一:使用Thread+Handler实现非UI线程更新UI界面: 即如何使用Thread+Handler的方式从非UI线程发送界面更新消息到UI线程. 概述:每个Android应用程序都运行在一个dalvik虚拟机进程中,进程开始的时候会启动一个主线程(MainThread),

学习通过Thread+Handler实现非UI线程更新UI组件

[Android线程机制] 出于性能考虑,Android的UI操作并不是线程安全的,这就意味着如果有多个线程并发操作UI组件,可能导致线程安全问题.为了解决这个问题,Android制定了一条简单的规则:只允许UI线程修改Activity里的UI组件 当一个程序第一次启动时,Android会同时启动一条主线程(Main Thread),主线程主要负责处理与UI相关的事件,如用户的按键事件,用户接触屏幕的事件及屏幕绘图事件,并把相关的事件分发到对应的组件进行处理.所以主线程通常又被叫做UI线程 [H

Android异步处理系列文章四篇之一使用Thread+Handler实现非UI线程更新UI界面

目录: Android异步处理一:使用Thread+Handler实现非UI线程更新UI界面Android异步处理二:使用AsyncTask异步更新UI界面Android异步处理三:Handler+Looper+MessageQueue深入详解Android异步处理四:AsyncTask的实现原理 Android异步处理一:使用Thread+Handler实现非UI线程更新UI界面 概述:每个Android应用程序都运行在一个dalvik虚拟机进程中,进程开始的时候会启动一个主线程(MainTh

学习通过Thread+Handler实现非UI线程更新UI组件(转)

[Android线程机制] 出于性能考虑,Android的UI操作并不是线程安全的,这就意味着如果有多个线程并发操作UI组件,可能导致线程安全问题.为了解决这个问题,Android制定了一条简单的规则:只允许UI线程修改Activity里的UI组件 当一个程序第一次启动时,Android会同时启动一条主线程(Main Thread),主线程主要负责处理与UI相关的事件,如用户的按键事件,用户接触屏幕的事件及屏幕绘图事件,并把相关的事件分发到对应的组件进行处理.所以主线程通常又被叫做UI线程 [H

Android异步机制一:使用Thread+Handler实现非UI线程更新UI界面

概述:每个Android应用程序都运行在一个dalvik虚拟机进程中,进程开始的时候会启动一个主线程(MainThread),主线程负责处理和ui相关的事件,因此主线程通常又叫UI线程.而由于Android采用UI单线程模型,所以只能在主线程中对UI元素进行操作.如果在非UI线程直接对UI进行了操作,则会报错: CalledFromWrongThreadException only the original thread that created a view hierarchy can tou

Android非UI线程更新UI的几种方法

Android用于实现非UI线程与UI线程的交互方法如下: 1.Handler 2.Activity.runOnUIThread(Runnable) 3.View.Post(Runnable) 4.View.PostDelayed(Runnabe,long) 5.AsyncTask

非ui线程更新ui问题

android初学者,刚开始并没有意识到,android中更新UI只能用UI线程,写了一个下载线程,在线程里更新progessbar,并用textview显示下载进度 public void listenProgress(){          new Thread(new Runnable() {                            @Override              public void run() {                  while(progre

安卓 异步线程更新Ui

异步跟新UI: 1.handler+Thread(runnable):如果handler和Thread都写在了一个Java文件中,就不说了,如果runnable定义在了一个单独的类文件中,可以通过在构造方法里接收handler参数,然后执行完耗时操作后,通过handler发送消息来通知主UI线程更新UI 2.接口回调,定义一个接口,然后在主UI执行耗时操作的时候,借助匿名内部类,在这里写异步线程返回来的数据处理操作,异步线程接收一个匿名的内部类实例,然后在执行完耗时操作后回调接口的方法,可以把耗

android中子线程更新UI的方式浅析

一.为何写作此文 ??你是不是经常看到很多书籍中说:不能在子线程中操作ui,不然会报错.你是不是也遇到了如下的疑惑(见下面的代码): @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv = (TextView) findViewById(R.id.tv); Threa