[Android]View.post(),android7.0(sdk24以上)不執行的問題(部分Click點選事件無效的原因)
我們熟知View.post()和Handler.post(),雖然最後執行過程還會走到Handler的post()方法中,但是View.post()做了許多額外的工作,所以我認為如非迫不得己,建議直接使用Handler.post()方法,詳情見此文。
如果在android7.0(sdk 24及以上)開發過程中,如果你的view沒有通過addView新增到檢視的時候,就會導致對應view的點選事件無效,以及view.post不執行,可能就是本文原因了,
以下是view.post在sdk版本24及以上的post方法
public boolean post(Runnable action) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.post(action); } // Postpone the runnable until we know on which thread it needs to run. // Assume that the runnable will be successfully placed after attach. getRunQueue().post(action); return true; }
下面是sdk23及以下的post方法
我們很容易的發現,24以上的sdk使用的是 getRunQueue().post(action);而sdk23以及以下是用的ViewRootImpl.getRunQueue().post(action);public boolean post(Runnable action) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.post(action); } // Assume that post will succeed later ViewRootImpl.getRunQueue().post(action); return true; }
其中,getRunQueue()的方法是
/** * Returns the queue of runnable for this view. * * @return the queue of runnables for this view */ private HandlerActionQueue getRunQueue() { if (mRunQueue == null) { mRunQueue = new HandlerActionQueue(); } return mRunQueue; }
這個mRunQueue是View類中一個私有變數。
ViewRootImpl可以理解是一個activity的View樹的樹根,每個ViewRootImpl管理對應的DecoView和View樹
ViewRootImpl中的佇列是一個靜態變數,也就是隻有一個這個佇列存在於這個app的生命週期中
static HandlerActionQueue getRunQueue() {
HandlerActionQueue rq = sRunQueues.get();
if (rq != null) {
return rq;
}
rq = new HandlerActionQueue();
sRunQueues.set(rq);
return rq;
}
在view.post中,並不是post完畢後就會執行,無論高低版本的View.post,只是把Runnable新增到佇列,等待進行操作,這和Handler.post不同
其中,SDK24以上是HandlerActionQueue類中
public void executeActions(Handler handler) {
synchronized (this) {
final HandlerAction[] actions = mActions;
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
mActions = null;
mCount = 0;
}
}
sdk 23以下是ViewRootImpl的靜態方法
void executeActions(Handler handler) {
synchronized (mActions) {
final ArrayList<HandlerAction> actions = mActions;
final int count = actions.size();
for (int i = 0; i < count; i++) {
final HandlerAction handlerAction = actions.get(i);
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
actions.clear();
}
}
在SDk24及以上我們可以瞭解到只有在View的dispatchAttachedToWindow方法中執行,如果這個view不是通過addview等方法加入父檢視的話,就無法呼叫dispatchAttachedToWindow,從而無法執行View.post,而post方法影響著click方法,即為
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
從而高版本sdk24及以上可能導致點選事件失效。
而在23及以下版本中,ViewRootImpl的executeActions會頻繁的呼叫,ViewRootImpl中的TraversalRunnable進行呼叫doTraversa來()進行呼叫。而TraversalRunnable是通過Choreographer的postCallBack迴圈呼叫,這個Choreographer通過doScheduleCallback進行一個MSG_DO_SCHEDULE_CALLBACK型別的迴圈操作(它每隔一段時間操作(ms級別))。詳情請檢視以後的Choreographer文章。
說完了問題原因,解決方法如下:
關於點選事件,首先一定保證對應的view已經addview到父檢視中,這樣可能解決問題,當然不一定滿足業務需求,也不一定能完美解決,那麼可以通過重寫View的對應post方法進行處理,如下
private Handler mHandler;
@Override
public boolean post(Runnable action) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && action!= null && !isAttachedToWindow()) {
mHandler = new Handler();
return mHandler.post(action);
}
return super.post(action);
}
@Override
public boolean removeCallbacks(Runnable action) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && action != null && !isAttachedToWindow()&& mHandler != null) {
mHandler.removeCallbacks(action);
return true;
}
return super.removeCallbacks(action);
}