當前位置:網站首頁>Android-面試官:View-post()-為什麼能够獲取到-View-的寬高-?

Android-面試官:View-post()-為什麼能够獲取到-View-的寬高-?

2022-01-27 15:22:37 m0_66155412


// 獲取 ComponentName
ComponentName component = r.intent.getComponent();

// 創建 ContextImpl 對象
ContextImpl appContext = createBaseContextForActivity;

// 反射創建 Activity 對象
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);

// 創建 Application 對象
Application app = r.packageInfo.makeApplication(false, mInstrumentation);

// attach 方法中會創建 PhoneWindow 對象
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);

// 執行 onCreate()
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}

return activity;
}

mInstrumentation.callActivityOnCreate() 方法最終會回調 Activity.onCreate() 方法。到這裏,setContentView() 方法就被執行了。setContentView() 邏輯很複雜,但幹的事情很直白。創建 DecorView ,然後根據我們傳入的布局文件 id 解析 xml,將得到的 view 塞進 DecorView 中。注意,到現在,我們得到的只是一個 空殼子 View 樹,它並沒有被添加到屏幕上,其實也不能添加到屏幕上。所以,在 onCreate() 回調中獲取視圖寬高顯然是不可取的。

看完 onCreate() , 我們跳過 onStart(),裏面沒幹啥太重要的事情,直接來到 onResume()

注:Activity 的生命周期是由 ClientLifecycleManager 類來調度的,具體原理可以看這篇文章 從源碼看 Activity 生命周期

ActivityThread.java

public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {

// 1. 回調 onResume
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
···
View decor = r.window.getDec
orView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
// 2. 添加 decorView 到 WindowManager
wm.addView(decor, l);

}

兩件事,回調 onResume 和 添加 DecorView 到 WindowManager 。所以,在 onResume() 回調中獲取 view 的寬高其實和 onCreate() 中沒啥區別,都獲取不到。

wm.addView(decor, l) 最終調用到 WindowManagerGlobal.addView()

public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {

// 1. 重點,初始化 ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
// 2. 重點,發起繪制並顯示到屏幕上
root.setView(view, wparams, panelParentView);

這裏兩行代碼都是重中之重。先來看看注釋 1 處 ViewRootImpl 的構造函數。

public ViewRootImpl(Context context, Display display) {

// 1. IWindowSession 代理對象,與 WMS 進行 Binder 通信
mWindowSession = WindowManagerGlobal.getWindowSession();

// 2.
mWidth = -1;
mHeight = -1;

// 3. 初始化 AttachInfo
// 記住 mAttachInfo 是在這裏被初始化的
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
context);

// 4. 初始化 Choreographer,通過 Threadlocal 存儲
mChoreographer = Choreographer.getInstance();
}

  1. 初始化 mWindowSession,它可以 WMS 進行 Binder 通信
  2. 這裏能看到寬高還未賦值
  3. 初始化 AttachInfo,這裏著重記一下,後面會再提到
  4. 初始化 Choreographer,上篇文章 面試官:如何監測應用的 FPS ? 詳細介紹過

再看注釋 2 處的 ViewRootImpl.setView() 方法。

ViewRootImpl.java

// 參數 view 就是 DecorView
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;

// 1. 發起首次繪制
requestLayout();

// 2. Binder 調用 Session.addToDisplay(),將 window 添加到屏幕
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);

// 3. 將 decorView 的 parent 賦值為 ViewRootImpl
view.assignParent(this);
}
}
}

requestLayout() 方法發起了首次繪制。

ViewRootImpl.java

public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 檢查線程
checkThread();
mLayoutRequested = true;
// 重點
scheduleTraversals();
}
}

ViewRootImpl.scheduleTraversals() 方法在 上篇文章 中詳細介紹過,這裏大致總結一下:

  1. ViewRootImpl.scheduleTraversals() 方法中會建立同步屏障,優先處理异步消息。通過 Choreographer.postCallback() 方法提交了任務 mTraversalRunnable,這個任務就是負責 View 的測量,布局,繪制。
  2. Choreographer.postCallback() 方法通過 DisplayEventReceiver.nativeScheduleVsync() 方法向系統底層注册了下一次 vsync 信號的監聽。當下一次 vsync 來臨時,系統會回調其 dispatchVsync() 方法,最終回調 FrameDisplayEventReceiver.onVsync() 方法。
  3. FrameDisplayEventReceiver.onVsync() 方法中取出之前提交的 mTraversalRunnable 並執行。這樣就完成了一次繪制流程。

mTraversalRunnable 中執行的是 doTraversal() 方法。

ViewRootImpl.java

void doTraversal() {
if (mTraversalScheduled) {
// 1. mTraversalScheduled 置為 false
mTraversalScheduled = false;
// 2. 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

// 3. 開始布局,測量,繪制流程
performTraversals();

}

ViewRootImpl.java

private void performTraversals() {

// 1. 綁定 Window,重點記憶一下
host.dispatchAttachedToWindow(mAttachInfo, 0);
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);

getRunQueue().executeActions(mAttachInfo.mHandler);

// 2. 請求 WMS 計算窗口大小
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);

// 3. 測量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

// 4. 布局
performLayout(lp, mWidth, mHeight);

// 5. 繪制
performDraw();
}

performTraversals() 方法的邏輯甚是複雜,這裏精簡出幾個重要的方法調用。到這裏,View 的整體繪制流程已經完成,毫無疑問,在這個時候肯定是可以獲取到寬高的。

View 被測量的時機已經找到了。現在就來驗證一下 View.post() 是不是在這個時機執行回調的。

探秘 View.post()

View.java

public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
// 1. attachInfo 不為空,通過 mHandler 發送
return attachInfo.mHandler.post(action);
}
// 2. attachInfo 為空,放入隊列中
getRunQueue().post(action);
return true;
}

這裏的關鍵是 attachInfo 是否為空。在上一節中介紹過,再來回顧一下:

  • attachInfo 是在 ViewRootImpl 的構造函數中初始化的,
  • ViewRootImpl 是在 WindowManagerGlobal.addView() 創建的
  • WindowManagerGlobal.addView() 是在 ActivityThread 的 handleResumeActivity() 中調用的,但是是在 Activity.onResume() 回調之後

所以,如果 attachInfo 不為空的話,至少已經處在進行視圖繪制的這次消息處理當中。把 post() 方法要執行的 Runnable 利用 Handler 發送出去,當包含這個 Runnable 的 Message 被執行時,是一定可以獲取到 View 的寬高的。

onCreate()onResume() 這兩個回調中,attachInfo 肯定是空的,這時候就要依賴 getRunQueue().post(action) 。原理也很簡單,把 post() 方法要執行的 Runnable 存儲在一個隊列中,在合適的時機(View 已被測量)拿出來執行。先來看看 getRunQueue() 拿到的是一個什麼隊列。

View.java

private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}

public class HandlerActionQueue {
private HandlerAction[] mActions;
private int mCount;

public void post(Runnable action) {
postDelayed(action, 0);
}

// 發送任務
public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}

// 執行任務
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;
}
}

private static class HandlerAction {
final Runnable action;
final long delay;

public HandlerAction(Runnable action, long delay) {
this.action = action;
this.delay = delay;
}

public boolean matches(Runnable otherAction) {
return otherAction == null && action == null
|| action != null && action.equals(otherAction);
}
}
}

隊列 HandlerActionQueue 是一個初始容量是 4 的 HandlerAction 數組。HandlerAction 有兩個成員變量,要執行的 Runnable 和延遲執行的時間。

隊列的執行邏輯在 executeActions(handler) 方法中,通過傳入的 handler 進行任務分發。現在我們只要找到 executeActions() 的調用時機就可以了。在 View.java 中就可以找到,在 dispatchAttachedToWindow() 方法中分發了任務。

Action);
}
}
}

隊列 HandlerActionQueue 是一個初始容量是 4 的 HandlerAction 數組。HandlerAction 有兩個成員變量,要執行的 Runnable 和延遲執行的時間。

隊列的執行邏輯在 executeActions(handler) 方法中,通過傳入的 handler 進行任務分發。現在我們只要找到 executeActions() 的調用時機就可以了。在 View.java 中就可以找到,在 dispatchAttachedToWindow() 方法中分發了任務。

版權聲明
本文為[m0_66155412]所創,轉載請帶上原文鏈接,感謝
https://cht.chowdera.com/2022/01/202201271522370019.html

隨機推薦