天天看點

貝殼找房FEED流曝光政策

摘要:目前貝殼找房的曝光政策邊界條件比較單一, 都是APP端寫死的邏輯;對标一線公司解決方案, 是由API下發每種卡片/Feed的門限條件, 進而得到更精準的資料。

一、背景

目前貝殼找房APP端的曝光時機是寫死的, 觸發條件:卡片必須要完整展示在界面上; 在清單界面上下/左右滑動時單次/多次曝光同一個卡片。

現有方案的不足:

1、門限條件應改為API下發的; 2、缺少卡片在界面上顯示的時長;

反例:

1、比如說清單有1000條記錄,快速滑動清單到最後一條;使用者并沒有看清中間的900多條記錄,這時要不要為這些記錄做曝光埋點?

2、例如一個卡片高度為100px,實際上隻顯示了80px,是否要做一次曝光埋點。

貝殼找房FEED流曝光政策

目前問題:如果隻滑動這個程度,目前app不會為“附近地圖”做曝光埋點,但該卡片的主要資訊都已經展示了

行業對标:

今日頭條、手機百度的曝光埋點政策做的很細, 比如卡片劃入、劃出時間,卡片顯示多少比例可以算曝光等等。

二、解決方案

參考今日頭條、手機百度的做法,實作類似的曝光政策。

1、為每種卡片設定不同的曝光政策;

2、APP根據API下發的門限條件觸發埋點;

3、記錄卡片移入、移出螢幕的時間, 統計每個卡片真正顯示的時長;

4、界面銷毀、顯示/隐藏是否觸發曝光埋點。例如按home鍵時是否觸發曝光埋點,再次進入是否觸發埋點。 這些場景由API下發配置開關。

貝殼找房FEED流曝光政策

雙向隊列緩存目前RecyclerView顯示的所有ViewHolder, 用于執行卡片的曝光埋點函數。

在監聽RecyclerView滑動事件時得到第一個可見位置、最後一個可見位置,根據參數判斷是上滑或下滑,通過判斷ViewHolder的itemView top、bottom參數值得出剛剛移入螢幕的卡片顯示比例, 并根據API下發的門限值(最低顯示比例)記錄開始時間,在卡片即将劃出螢幕時(API下發的門限值)觸發曝光埋點。 進而得出卡片的顯示周期。

貝殼找房FEED流曝光政策

三、參考代碼

滑動回調

public class CardExposureHelper extends RecyclerView.OnScrollListener {
 //緩存卡片的雙向隊列
  private Deque<BaseHomeCard> deque;
  //隊列頂部Card的position
  private int preFirstExposure;
  //隊列底部Card的position
  private int preLastExposure;
  /**
   * 處理垂直方向卡片曝光
   * @param manager
   * @param isUp 是否向上滑動
   */
  private void onVerticalExposure(LinearLayoutManager manager,boolean isUp) {
    int firstVisiblePosition = manager.findFirstVisibleItemPosition();
    int lastVisiblePosition = manager.findLastVisibleItemPosition();
    //根據曝光比例判斷第一個可見卡片是否需要曝光
    firstVisiblePosition = isVerticalExposure(firstVisiblePosition)?firstVisiblePosition:firstVisiblePosition+1;
    //根據曝光比例判斷最後一個可見卡片是否需要曝光
    lastVisiblePosition = isVerticalExposure(lastVisiblePosition)?lastVisiblePosition:lastVisiblePosition-1;
    //第一次曝光,曝光所有符合曝光比例的Card
    if (preFirstExposure==0&&preLastExposure==0){
      offerVerticalVisibleQueue(firstVisiblePosition,lastVisiblePosition,true);
    }else if (isUp){
      //向上滑動,把頂部不可見Card從頂部出隊,底部進入可曝光的卡片入隊
      popVerticalVisibleQueue(preFirstExposure,firstVisiblePosition-1,true);
      offerVerticalVisibleQueue(preLastExposure+1,lastVisiblePosition,false);
    }else {
      //對應向下滑動的政策
      popVerticalVisibleQueue(lastVisiblePosition+1,preLastExposure,false);
      offerVerticalVisibleQueue(firstVisiblePosition,preFirstExposure-1,true);
    }
    //更新隊列的頂部position和底部position
    preFirstExposure = firstVisiblePosition;
    preLastExposure = lastVisiblePosition;
  }  
  /**
   * 入隊操作
   * @param start
   * @param end
   * @param isFirst 是否從頂部入隊
   */
  private void offerVerticalVisibleQueue(int start,int end,boolean isFirst){
    if (start>=0 && end<recyclerView.getAdapter().getItemCount() && start<=end){
      if (isFirst){
        for (int i=end;i>=start;i--){
          onVerticalItemSlideInto(i,true);
        }
      }else {
        for (int i=start;i<=end;i++){
          onVerticalItemSlideInto(i,false);
        }
      }
    }
  } 
  /**
   * 出隊操作
   * @param start
   * @param end
   * @param isFirst 是否從頂部出隊
   */
  private void popVerticalVisibleQueue(int start,int end,boolean isFirst){
    if (start>=0 && end<recyclerView.getAdapter().getItemCount() && start<=end){
      if (isFirst){
        for (int i=start;i<=end;i++){
          onVerticalItemSlideOut(i,isFirst);
        }
      }else {
        for (int i=end;i>=start;i--){
          onVerticalItemSlideOut(i,isFirst);
        }
      }
    }
  }
   /**
   * 處理滑入(入隊)可曝光的卡片
   * @param position
   * @param isFirst 是否從頂部滑入(入隊)
   */
  private void onVerticalItemSlideInto(int position,boolean isFirst){
    BaseHomeCard card = getBaseHomeCard(position);
    if (isFirst){
      deque.offerFirst(card);
    }else {
      deque.offerLast(card);
    }
    //回調卡片開始曝光事件
    callItemExposure(card,position);
  }  
  /**
   * 處理滑出(出隊)停止曝光的卡片
   * @param position
   * @param isFirst 是否從頂部滑出(出隊)
   */
  private void onVerticalItemSlideOut(int position,boolean isFirst){
    BaseHomeCard card;
    if (isFirst){
      card = deque.removeFirst();
    }else {
      card = deque.removeLast();
    }
    //回調卡片結束曝光事件
    callItemEndExposure(card,position,isFirst);
  }		
           

四、FEED初次渲染時長

重新整理FEED流埋點統計冷啟動完成時間, 包括https/http時間、端上資料處理用時、measure/layout/draw用時, 參考今日頭條的做法将第一條Feed繪制完第一幀認為是冷啟動完成事件。

public class TimeActivity extends Activity implements FrameDrawCallBack {
 private ListView listView;
 private long beginTime;  //界面啟動開始時間

 @Override protected void onCreate(@Nullable Bundle savedInstanceState) {
   beginTime = System.currentTimeMillis();

   super.onCreate(savedInstanceState);

   setContentView(R.layout.activity_timelist);

   listView = (ListView) findViewById(R.id.listview);
   final TimeAdapter adapter = new TimeAdapter(this, this);
   new Handler().postDelayed(new Runnable() {
     @Override public void run() {
       listView.setAdapter(adapter);
     }
   }, 1000);
 }

 @Override public void onDrawComplet(long time) {
   long diff = time - beginTime;
   Log.d("brycegao", "清單首幀渲染完成用時:" + diff);  //FEED流第一幀渲染完成時間,埋點上報
 }

 private class TimeAdapter extends BaseAdapter {
   private Context mContext;
   private FrameDrawCallBack mCallBack;
    TimeAdapter(Context context, FrameDrawCallBack callBack) {
      mContext = context;
      mCallBack = callBack;
    }
   @Override public int getCount() {
     return 30;
   }

   @Override public Object getItem(int position) {
     return null;
   }

   @Override public long getItemId(int position) {
     return position;
   }

   private void addFirstItemDraw(final View view) {
      if (view == null) {
        return;
      }
     view.getViewTreeObserver().addOnDrawListener(
         new ViewTreeObserver.OnDrawListener() {
           @Override public void onDraw() {
             view.getViewTreeObserver().removeOnDrawListener(this);
             mCallBack.onDrawComplet(System.currentTimeMillis());
           }
         });
   }

   @Override public View getView(int position, View convertView, ViewGroup parent) {
      ViewHolder holder;
      if (convertView == null) {
        convertView = LayoutInflater.from(mContext).inflate(R.layout.item_testlistview_layout,
            null, false);
        holder = new ViewHolder();
        holder.tvContent = (TextView) convertView.findViewById(R.id.tv_content);
        convertView.setTag(holder);

        if (position == 0) {
          addFirstItemDraw(convertView);
        }
      } else {
        holder = (ViewHolder) convertView.getTag();
      }

      holder.tvContent.setText("第" + position + "條記錄");
     return convertView;
   }
 }

 private class ViewHolder {
   TextView tvContent;
 }
}