非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.");
        }
    }

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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值