通过与 Jira 对比,让您更全面了解 PingCode

  • 首页
  • 需求与产品管理
  • 项目管理
  • 测试与缺陷管理
  • 知识管理
  • 效能度量
        • 更多产品

          客户为中心的产品管理工具

          专业的软件研发项目管理工具

          简单易用的团队知识库管理

          可量化的研发效能度量工具

          测试用例维护与计划执行

          以团队为中心的协作沟通

          研发工作流自动化工具

          账号认证与安全管理工具

          Why PingCode
          为什么选择 PingCode ?

          6000+企业信赖之选,为研发团队降本增效

        • 行业解决方案
          先进制造(即将上线)
        • 解决方案1
        • 解决方案2
  • Jira替代方案

25人以下免费

目录

从TextView.setText()看requestLayout和invalidate方法有什么不同

说到 View 的性能优化,一般都是想办法减少 measure、layout、draw 方法的时间。那 Textview 可以减少 measure、layout、draw 方法时间吗?invalidat方法没有将mLayoutRequested设置为true,所以后面到了 performTraversals方法里不会执行 performMeasure方法和performLayout方法。

一、从TextView.setText()看requestLayout和invalidate方法的不同

有时候 Android 面试会遇到这样一类问题:

1)TextView 如何优化?

2)给 TextView 设置新的字符串,整个布局会重绘吗?

3)requestLayout 和 invalidate 方法有什么区别?

这三个问题再一定程度上可以理解为一个问题,就是 requestLayout 和 invalidate 方法有什么区别,为什么这么说,让我们从下面这个问题开始入手:

TextView 如何优化

我们这里只说 TextView 的性能优化,说到 View 的性能优化,一般都是想办法减少 measure、layout、draw 方法的时间,那 Textview 可以减少 measure、layout、draw 方法时间吗?

网上一般的资料都是给 TextView 设置固定的宽高,这样在 TextView 文本发生变化的时候就只会重新走 TextView 的 draw 方法,没有重新 measure 和 layout,达到了性能优化的目的。

那事实是这样吗,我们直接从源码分析:

TextView 的 setText 方法会调用下面的 setText 方法:

———TextView———-

private void setText(CharSequence text, BufferType type,

                         boolean notifyBefore, int oldlen) {

        mTextSetFromXmlOrResourceId = false;

        if (text == null) {

            text = “”;

        }

        …

        …

        setTextInternal(text);

        …

        …

        // 这里是检查是否需要重新布局的逻辑

        if (mLayout != null) {

            checkForRelayout();

        }

        sendOnTextChanged(text, 0, oldlen, textLength);

        onTextChanged(text, 0, oldlen, textLength);

        notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);

        if (needEditableForNotification) {

            sendAfterTextChanged((Editable) text);

        } else {

            notifyListeningManagersAfterTextChanged();

        }

        // SelectionModifierCursorController depends on textCanBeSelected, which depends on text

        if (mEditor != null) mEditor.prepareCursorControllers();

    }

这里面代码也比较长,有一些文本解析和格式设置的地方省略掉了,我们这里只需要关注 checkForRelayout() 方法:

———TextView———-

private void checkForRelayout() {

        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();

            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,

                          mRight – mLeft – getCompoundPaddingLeft() – getCompoundPaddingRight(),

                          false);

            // 不是跑马灯样式的文本

            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {

                // 高度是一个精确的值

                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT

                        && mLayoutParams.height != LayoutParams.MATCH_PARENT) {

                    autoSizeText();

                    invalidate();

                    return;

                }

                // 新文本的高度和原来文本的高度一致

                if (mLayout.getHeight() == oldht

                        && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {

                    autoSizeText();

                    invalidate();

                    return;

                }

            }

            requestLayout();

            invalidate();

        } else {

            nullLayouts();

            requestLayout();

            invalidate();

        }

    }

这里得出我们自己需要的信息就是,如果 TextView 的 width 属性不是 WRAP_CONTENT 并且高度不会发生变化,那么就不会走 requestLayout() 方法,TextView 的高度不会发生变化有两种情况,一种是 TextView 的 height 被设置成一个精确值,一种是新文本的高度和原来文本的高度一致,这两种情况都会只调用 invalidate() 方法。

调用 invalidate 方法只会让 view 重新走 draw 方法,而调用 requestLayout 会重走布局重绘的三个方法,但是我们知道,布局重绘的 measure、layout、draw 方法都是从 ViewRootImpl 的 performTraversals 方法开始的,那 invalidate 方法是如何只让 View 重走一个 draw 方法的?

还是得看源码,先看 View 的 RequestLayout 方法是如何实现的:

———View———-

public void requestLayout() {

        if (mMeasureCache != null) mMeasureCache.clear();

        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {

            // Only trigger request-during-layout logic if this is the view requesting it,

            // not the views in its parent hierarchy

            ViewRootImpl viewRoot = getViewRootImpl();

            if (viewRoot != null && viewRoot.isInLayout()) {

                if (!viewRoot.requestLayoutDuringLayout(this)) {

                    return;

                }

            }

            mAttachInfo.mViewRequestingLayout = this;

        }

        // ① 给 View 设置 PFLAG_FORCE_LAYOUT 标志,后面会用到

        mPrivateFlags |= PFLAG_FORCE_LAYOUT;

        mPrivateFlags |= PFLAG_INVALIDATED;

        if (mParent != null && !mParent.isLayoutRequested()) {

            mParent.requestLayout();  // ② 委托给父布局

        }

        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {

            mAttachInfo.mViewRequestingLayout = null;

        }

    }

从这个方法可以知道 View 会将 requestLayout 这个任务委托给它的父布局,而父布局也会再委托给它自己的父布局,那最后这个 requestLayout 就会执行到 DecorView 中,然后就就回到了 ViewRootImpl 的 requestLayout 方法中,所以我们看 ViewRootImpl.requestLayout 方法:

———ViewRootImpl———-

public void requestLayout() {

        if (!mHandlingLayoutInLayoutRequest) {

            checkThread();  // ①

            mLayoutRequested = true; // ②

            scheduleTraversals();  // ③

        }

    }

① 是检查当前线程是不是创建 ViewRootImpl 的线程

② 是一个非常重要的标志,后面会用到

③ 继续看代码

———ViewRootImpl———-

void scheduleTraversals() {

        if (!mTraversalScheduled) {

            mTraversalScheduled = true;

            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();

            mChoreographer.postCallback(

                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

            notifyRendererOfFramePending();

            pokeDrawLockIfNeeded();

        }

    }

postSyncBarrier() 方法是发送一个同步屏障消息,这样 MessageQueue 在执行 next() 方法的时候,就会先屏蔽掉同步消息,先执行异步消息,这样就会提高某些任务的效率,这里肯定是为了提高 UI 刷新效率,可以简单看看实现,面试中偶尔会问到:

———MessageQueue———-

private int postSyncBarrier(long when) {

        // Enqueue a new sync barrier token.

        // We don’t need to wake the queue because the purpose of a barrier is to stall it.

        synchronized (this) {

            final int token = mNextBarrierToken++;

            final Message msg = Message.obtAIn();

            msg.markInUse();

            msg.when = when;

            msg.arg1 = token;

            Message prev = null;

            Message p = mMessages;

            if (when != 0) {

                while (p != null && p.when <= when) {

                    prev = p;

                    p = p.next;

                }

            }

            if (prev != null) { // invariant: p == prev.next

                msg.next = p;

                prev.next = msg;

            } else {

                msg.next = p;

                mMessages = msg;

            }

            return token;

        }

    }

这个方法就是插入一个消息,但是这个消息没有 target,然后看 MessageQueue 的 next 方法:

———MessageQueue———-

Message next() {

        final long ptr = mPtr;

        if (ptr == 0) {

            return null;

        }

        int pendingIdleHandlerCount = -1; // -1 only during first iteration

        int nextPollTimeoutMillis = 0;

        for (;;) {

            if (nextPollTimeoutMillis != 0) {

                Binder.flushPendingCommands();

            }

            // 休眠 nextPollTimeoutMillis 时间,释放 cpu

            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {

                // Try to retrieve the next message.  Return if found.

                final long now = SystemClock.uptimeMillis();

                Message prevMsg = null;

                Message msg = mMessages;

                if (msg != null && msg.target == null) {  // ①

                    // Stalled by a barrier.  Find the next asynchronous message in the queue.

                    do {

                        prevMsg = msg;

                        msg = msg.next;

                    } while (msg != null && !msg.isAsynchronous());

                }

                …

            }

            …

    }

从①处我们看到当 Message 的 target 为空时,就是遍历后面的链表,取出异步消息执行

回到正题,scheduleTraversals 方法里面通过 mChoreographer 给主线程发了一个消息, mChoreographer.postCallback 方法最终会调用 postCallbackDelayedInternal 方法:

———Choreographer———-

private void postCallbackDelayedInternal(int callbackType,

            Object action, Object token, long delayMillis) {

        if (DEBUG_FRAMES) {

            Log.d(TAG, “PostCallback: type=” + callbackType

                    + “, action=” + action + “, token=” + token

                    + “, delayMillis=” + delayMillis);

        }

        synchronized (mLock) {

            final long now = SystemClock.uptimeMillis();

            final long dueTime = now + delayMillis;

            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            if (dueTime <= now) {

                scheduleFrameLocked(now);

            } else {

                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);

                msg.arg1 = callbackType;

                msg.setAsynchronous(true);

                mHandler.sendMessageAtTime(msg, dueTime);

            }

        }

    }

这里代码就比较容易理解,就是通过 mHandler 给主线程插入一个异步任务,所以我们看这个任务的代码:

任务里的 run 方法会调用 ViewRootImpl.doTraversal 方法

———ViewRootImpl———-

void doTraversal() {

        if (mTraversalScheduled) {

            mTraversalScheduled = false;

            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {

                Debug.startMethodTracing(“ViewAncestor”);

            }

            performTraversals();

            if (mProfile) {

                Debug.s较好MethodTracing();

                mProfile = false;

            }

        }

    }

到这,终于看到之前说的的 performTraversals 方法了。

performTraversals 方法过长,就截取我们需要的一段代码分析:

    boolean layoutRequested = mLayoutRequested && (!mS较好ped || mReportNextDraw);

        if (layoutRequested) {

            …

            // Ask host how big it wants to be

            windowSizeMayChange |= measureHierarchy(host, lp, res,

                    desiredWindowWidth, desiredWindowHeight);

        }

    final boolean didLayout = layoutRequested && (!mS较好ped || mReportNextDraw);

    boolean triggerGlobalLayoutListener = didLayout

            || mAttachInfo.mRecomputeGlobalAttributes;

    if (didLayout) {

        performLayout(lp, mWidth, mHeight);

        …

    }

    if (!cancelDraw) {

            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {

                for (int i = 0; i < mPendingTransitions.size(); ++i) {

                    mPendingTransitions.get(i).startChangingAnimations();

                }

                mPendingTransitions.clear();

            }

            performDraw();

        }

measureHierarchy 方法里面会调用 performMeasure 方法,而这里面影响 measureHierarchy 和 performLayout 方法调用的一个共同因素就是 mLayoutRequested 变量,而 requestLayout 方法里面正是将这个变量置为了 true,所以 requestLayout 方法调用后,布局会重新走 measure、layout、draw 三个过程。

回到前面说的问题,那调用 invalidate 为什么只走 draw 方法,这时候就可以开始分析invalidate 方法源码了:

———View———-   

public void invalidate(boolean invalidateCache) {

        invalidateInternal(0, 0, mRight – mLeft, mBottom – mTop, invalidateCache, true);

    }

    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,

            boolean fullInvalidate) {

        …

        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);  // ①

            }

            …

        }

    }

从①我们知道,方法会调用到 ViewGroup 的 invalidateChild 方法

———ViewGroup———-   

public final void invalidateChild(View child, final Rect dirty) {

        final AttachInfo attachInfo = mAttachInfo;

        if (attachInfo != null && attachInfo.mHardwareAccelerated) {

            // HW accelerated fast path

            onDescendantInvalidated(child, child);

            return;

        }

        …

}

ViewGroup 在开启硬件加速的条件下,会调用自己的 onDescendantInvalidated 方法

———ViewGroup———-

public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {

    …

    …

    if (mParent != null) {

        mParent.onDescendantInvalidated(this, target);

    }

}

这样就又和 requestLayout 方法一样,开始向上委托,最后会调用 ViewRootImpl的onDescendantInvalidated方法,而 ViewRootImpl.onDescendantInvalidated 方法会调用ViewRootImpl.invalidate 方法:

———ViewRootImpl———-

void invalidate() {

    mDirty.set(0, 0, mWidth, mHeight);

    if (!mWillDrawSoon) {

        scheduleTraversals();

    }

}

到这就回到了之前 requestLayout 的逻辑上,只不过 invalidate 方法没有将 mLayoutRequested 设置为 true,所以后面到了 performTraversals 方法里不会执行 performMeasure 方法和 performLayout 方法。

到这 文章开头的 1 和 3 问题解决了,第二个问题是分情况的,如果给的条件只会让 TextView 调用 invalidate 方法,那么只会走这个 TextView 的 draw 方法。

那如果走了 requestLayout 方法,可以从前面 requestLayout 方法里找到答案,需要被重新布局的 view 和它相关联的父布局都会被打上一个标记 PFLAG_FORCE_LAYOUT ,然后在后面会把带 PFLAG_FORCE_LAYOUT 标记的 view 过滤出来,执行布局方法,所以和这个TextView 关联的View 和父布局都会重新布局。

延伸阅读:

二、Copy On Write Array List设计原理

CopyOnWriteArrayList底层实现是通过Object[]存储元素的,内部的可变操作(add,set 等方法)都是把数据copy到一个新数组里,对新数组进行操作,再把新数组赋值给原来的对象,从而达到修改目的。

这样做的好处是不修改原数组,所以写操作不会影响到读操作。

从 CopyOnWriteArrayList 的名字就能看出CopyOnWriteArrayList 是满足CopyOnWrite 的 ArrayList,所谓CopyOnWrite 也就是说:在计算机,如果你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉了。

相关文章