Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 非UI線程更新UI!?

非UI線程更新UI!?

編輯:關於Android編程

今天晚上被弟弟告知他在子線程中更新了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.");
        }
    }

這次的探查過程給了我很大的啟示,遇見問題時,在必要時首先要回憶之前遇到的相似問題,並合理利用網上搜索的信息去自己探索問題的真相,一個根據關鍵信息推導出來的合理假設將使我們事半功倍,並且注意不要盲目的相信網上的一些結論,紙上得來終覺淺,絕知此事要躬行!

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved