去年面試餓了麼的時候吧,被問到了個技術問題。java
面試官:據說你作過自動化埋點,那麼咱們聊聊view的曝光監控吧。面試
我:以前我是把咱們廣告的曝光監控放在廣告的模型層,而後在bindview的時候作一次曝光的,而後內部作了一次曝光防抖動,避免屢次曝光。app
面試官:你這樣就意味着快速滑動的狀況下也會計算一次曝光了,若是我須要的是一個停留超過1.5s同時出現超過view的一半做爲有效曝光呢。ide
我:源碼分析
來個背景音樂吧。學習
面試官:回去等通知吧。ui
要解決問題,先概括下都有那些問題.this
先解決RecyclerView的1.5s這個問題,你們第一個想到的可能都是addOnScrollListener,而後經過layoutmanager計算可見區域,以後計算兩次滑動以後的差別區間。可是很差意思,在下不可能這麼簡單的被大家猜透。google
override fun onAttachedToWindow() {
super.onAttachedToWindow()
exposeChecker.updateStartTime()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
onExpose()
exposeChecker.updateStartTime()
}
複製代碼
我在一篇技術博客傳送門,我看到這兩個方法在RecyclerView內部會在View移動出可視區域的時候被觸發。可是爲何呢???帶着問題分析源代碼。spa
若是各位關心過view的繪製流程,那麼應該都知道這兩個方法。這兩個方法會在頁面綁定到window的時候被觸發,核心源代碼在ViewRootimp的 host.dispatchVisibilityAggregated(viewVisibility == View.VISIBLE);
被觸發以後,host就是咱們的Activity的DecorView。
mChildHelper = new ChildHelper(new ChildHelper.Callback(){
@Override
public void addView(View child, int index) {
if (VERBOSE_TRACING) {
TraceCompat.beginSection("RV addView");
}
RecyclerView.this.addView(child, index);
if (VERBOSE_TRACING) {
TraceCompat.endSection();
}
dispatchChildAttached(child);
}
@Override
public void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams) {
final ViewHolder vh = getChildViewHolderInt(child);
if (vh != null) {
if (!vh.isTmpDetached() && !vh.shouldIgnore()) {
throw new IllegalArgumentException("Called attach on a child which is not"
+ " detached: " + vh + exceptionLabel());
}
if (DEBUG) {
Log.d(TAG, "reAttach " + vh);
}
vh.clearTmpDetachFlag();
}
RecyclerView.this.attachViewToParent(child, index, layoutParams);
}
}
複製代碼
ChildHelper是RecyclerView內部負責專門管理全部子View的一個幫助類。其中經過暴露了接口回調的方式讓它和RecyclerView能夠綁定到一塊兒。其中咱們能夠看到當child的add,attach都會觸發attachViewToParent,重頭戲天然在這個地方,而這個核心源在ViewGroup內了,咱們繼續看。
protected void removeDetachedView(View child, boolean animate) {
if (mTransition != null) {
mTransition.removeChild(this, child);
}
if (child == mFocused) {
child.clearFocus();
}
if (child == mDefaultFocus) {
clearDefaultFocus(child);
}
if (child == mFocusedInCluster) {
clearFocusedInCluster(child);
}
child.clearAccessibilityFocus();
cancelTouchTarget(child);
cancelHoverTarget(child);
if ((animate && child.getAnimation() != null) ||
(mTransitioningViews != null && mTransitioningViews.contains(child))) {
addDisappearingView(child);
} else if (child.mAttachInfo != null) {
child.dispatchDetachedFromWindow();
}
if (child.hasTransientState()) {
childHasTransientStateChanged(child, false);
}
dispatchViewRemoved(child);
}
protected void attachViewToParent(View child, int index, LayoutParams params) {
child.mLayoutParams = params;
if (index < 0) {
index = mChildrenCount;
}
addInArray(child, index);
child.mParent = this;
child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK
& ~PFLAG_DRAWING_CACHE_VALID)
| PFLAG_DRAWN | PFLAG_INVALIDATED;
this.mPrivateFlags |= PFLAG_INVALIDATED;
if (child.hasFocus()) {
requestChildFocus(child, child.findFocus());
}
dispatchVisibilityAggregated(isAttachedToWindow() && getWindowVisibility() == VISIBLE
&& isShown());
notifySubtreeAccessibilityStateChangedIfNeeded();
}
@Override
boolean dispatchVisibilityAggregated(boolean isVisible) {
isVisible = super.dispatchVisibilityAggregated(isVisible);
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
// Only dispatch to visible children. Not visible children and their subtrees already
// know that they aren't visible and that's not going to change as a result of
// whatever triggered this dispatch.
if (children[i].getVisibility() == VISIBLE) {
children[i].dispatchVisibilityAggregated(isVisible);
}
}
return isVisible;
}
複製代碼
其中dispatchVisibilityAggregated就是咱們最前面說的ViewRoot所觸發的ViewGroup內的方法,會逐層向下view分發View的attach方法。那麼也就是當RecyclerView的子控件被添加到RecyclerView上時,就會觸發子view的attachToWindow方法。
剩下來的就是View的detch方法是在哪裏被觸發的呢,這個就是要看recyclerview的另一個方法了,就是tryGetViewHolderForPositionByDeadline了。
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
if (position < 0 || position >= mState.getItemCount()) {
throw new IndexOutOfBoundsException("Invalid item position " + position
+ "(" + position + "). Item count:" + mState.getItemCount()
+ exceptionLabel());
}
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can't be used
if (!dryRun) {
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
}
........
return holder;
}
複製代碼
當ViewHolder要被回收的時候就會觸發RecyclerView的tryGetViewHolderForPositionByDeadline這個方法,而後咱們能夠觀察到當holder.isScrap()的時候會removeDetachedView(holder.itemView, false);而這個正好觸發了子項的viewDetch方法。
從上面的代碼分析完以後,咱們能夠在onAttachedToWindow的方法尾部打上第一個曝光開始的節點,在onDetachedFromWindow的方法下面埋下曝光結束的方法,計算他們的差值,若是當值大於1.5s以後,則調用接口。
這個吧,提及來有點丟臉,我google查出來的,其中核心在於 view.getLocalVisibleRect,這個方法會返回當前的view是否出如今window上了。
fun View.isCover(): Boolean {
var view = this
val currentViewRect = Rect()
val partVisible: Boolean = view.getLocalVisibleRect(currentViewRect)
val totalHeightVisible =
currentViewRect.bottom - currentViewRect.top >= view.measuredHeight
val totalWidthVisible =
currentViewRect.right - currentViewRect.left >= view.measuredWidth
val totalViewVisible = partVisible && totalHeightVisible && totalWidthVisible
if (!totalViewVisible)
return true
while (view.parent is ViewGroup) {
val currentParent = view.parent as ViewGroup
if (currentParent.visibility != View.VISIBLE) //if the parent of view is not visible,return true
return true
val start = view.indexOfViewInParent(currentParent)
for (i in start + 1 until currentParent.childCount) {
val viewRect = Rect()
view.getGlobalVisibleRect(viewRect)
val otherView = currentParent.getChildAt(i)
val otherViewRect = Rect()
otherView.getGlobalVisibleRect(otherViewRect)
if (Rect.intersects(viewRect, otherViewRect)) {
//if view intersects its older brother(covered),return true
return true
}
}
view = currentParent
}
return false
}
fun View.indexOfViewInParent(parent: ViewGroup): Int {
var index = 0
while (index < parent.childCount) {
if (parent.getChildAt(index) === this) break
index++
}
return index
}
複製代碼
凡事仍是不能忽略到頁面切換,當頁面切換的時候,咱們須要從新計算頁面的曝光,你說對不對,最簡單的方式是什麼呢。
不知道各位有沒有關心過viewTree裏面的onWindowFocusChanged這個方法,其實當頁面切換的狀況下,就會觸發這個方法。
核心原理其實也是ViewRootImp的handleWindowFocusChanged這個方法會向下分發是否脫離window的方法,而後當接受到IWindow.Stub接受到了WMS的信號以後,則會給ViewRootImp發送一個message,而後從ViewRootImp開始向下分發view變化的生命週期。
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
super.onWindowFocusChanged(hasWindowFocus)
if (hasWindowFocus) {
exposeChecker.updateStartTime()
} else {
onExpose()
}
}
複製代碼
總結性結論咯,也就是咱們只要在ViewHolder的控件最外面包裹一個咱們自定義的Layout,而後經過接口回調的方式,咱們就能監控到view的有效曝光時間了。
我以爲即便面試失敗的狀況下,咱們也仍是須要在其中學習到一些東西的,畢竟機會仍是給有準備的人。固然據我如今所知,應該餓了麼用的是阿里的那套控件曝光自動化埋點的方案,仍是有些不一樣的。