Android性能優化-TextView的setText方法會致使界面重繪?

問題現象

大概就是咱們在自定義一個視頻組件的ui時,發現了一段異常的效果。 我簡述一下:android

  • 視頻的控制器 底部通常都是 顯示時間(textview)和進度條(seekbar)的
  • 通常要實現這個效果 都是開個定時任務 每隔一段時間去從新setText一個時間。

效果以下:api

而後測試mm們發現一個必現的異常,視頻的進度條 也就是這個seekbar 老是會在視頻開始的前幾秒的時候 回退一下。而後才能正常展現進度條。bash

修復此問題的方法

通過一段時間的努力,咱們發現 這個問題的解決方案 是把textview的 width屬性 從wrap_content 改爲 固定的xxdp值就能夠。 問題的解決看似比較簡單,可是背後的邏輯沒有弄清楚。爲何把textview的屬性 改了一下,這個關於seekbar的 問題就修復了?markdown

還原問題現場

爲了找到事情的根本緣由,咱們作了一次最小粒度還原。也就是新建一個乾淨的工程,排除其餘問題的干擾, 看看究竟是哪裏出了問題?app

首先看一下佈局,這個佈局和一開始咱們出bug的佈局是差很少的。less

<LinearLayout
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@color/colorPrimary"
        android:orientation="horizontal"
        tools:ignore="MissingConstraints">

        <TextView
            android:id="@+id/tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:gravity="center"
            android:text="Hello World!" />

        <SeekBar
            android:id="@+id/sb"
            android:layout_width="100dp"
            android:layout_height="match_parent"
            android:layout_marginLeft="10dp">

        </SeekBar>
    </LinearLayout>
複製代碼

而後看一下咱們的關鍵 復現問題的代碼:ide

button = findViewById(R.id.bt);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread() {
                    @Override
                    public void run() {
                        //每隔一段時間 去刷新一下界面
                        while (true) {
                            try {
                                Thread.sleep(3000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
    
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    // 問題就在這裏了,setText 之後 seekbar的狀態會被莫名奇妙設置一次
                                    tv.setText(System.currentTimeMillis() + "");
                                }
                            });
                        }

                    }
                }.start();
            }
        });
複製代碼

源碼分析

首先咱們看看爲何在wrap_content的時候 textview的setText 會致使一系列的問題?函數

跟一下 setText的源碼: 源碼分析

咱們把這個 函數總體貼上來佈局

/**
     * Check whether entirely new text requires a new view layout
     * or merely a new text layout.
     */
    @UnsupportedAppUsage
    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) {
                    autoSizeText();
                    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)) {
                    autoSizeText();
                    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();
        }
    }
複製代碼

翻譯一下,就是 只要知足必定的條件就不會觸發 requestLayout這個操做。 稍微看看源碼 也能夠知道 這個條件,大概就是 寬度不要是wrap_content的屬性,而且不能是跑馬燈的屬性。 固然對高度也有必定的要求,可是最重要的條件就是width的wrap_content屬性(其餘屬性咱們用的比較少),只要不是他,咱們就不會 走到requestLayout的流程中。

緣由真的分析完畢了嗎?

再仔細想想,在一個view樹中,一個子view的重繪 一定會致使 整個view樹 所有進行重繪嗎?

看一下view自己的源碼

咱們翻譯一下這段代碼 其實就是

只要View本身沒有requestLayout或者再次measure時,MeasureSpec沒變,就不須要從新measure

因此得出來一個結論,雖然 在知足width 等於wrap content的狀況下,setText會觸發 requestLayout, 可是 這並不必定致使 這個textview的兄弟節點 每次都會measure。 這也就是這個bug的表象是 只有第一次setText會致使seekbar 重繪進而形成bug,可是後面的setText 都不會致使seekBar重繪的真正緣由了。

結論

對於textview這樣的控件而言,setText是有一個隱形的觸發界面重繪的操做,咱們在週期性的對textview的 setText方法進行調用時,必定多加考慮,最好將textview的寬度設置成固定值或者是match,避免引起各類奇葩bug,或者是下降了界面渲染的效率。 特別是對於咱們動畫功能的編寫,必定要多去看看api,看看這個api下面是否是會引起requestLayout,從而提升效率。

相關文章
相關標籤/搜索