说到 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 也就是说:在计算机,如果你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉了。