序文
毎年恒例の 1024 Programmer's Day ですが、今日何も書かないとこのお祭りが気の毒に思います。熟考の末、Viewの描画について書いただけです。この記事では、View 描画の 3 つの主要なコールバック関数である onMeasure、onLayout、onDraw には焦点を当てませんが、Android フレームワークの観点から View 描画を分析します。
- ビューはどのように画面にレンダリングされますか?
- ViewRoot、DecorView、Activity、Window、および WindowManager の関係は何ですか?
- ビューとサーフェスの関係は何ですか?
- View と SurfaceFlinger および OpenGL ES の関係は何ですか?
コンピュータの画像は一般に、GPU が必要とするデータを基盤となる画像エンジンを介して GPU に出力する必要があり、ディスプレイはレンダリングされたデータを GPU から継続的に取り出して画面に表示します。
Android アーキテクチャに精通している人なら誰でも、画像をレンダリングするための基盤となる Android エンジンが OpenGL ES/Vulkan であることを知っています。では、View は誰によってレンダリングされるのでしょうか? そうです、View は最終的に基盤となるレンダリング エンジンに引き渡されますが、View から OpenGL ES まではどのようなプロセスを経てきたのでしょうか。
setContentView() プロセス
アクティビティの onCreate では、通常、setContentView を介してアクティビティのインターフェイス レイアウトを設定します。この時点で、Activity はレンダリングを開始しますか? いいえ、setContentView は DecorView ツリー全体を構築するだけです。
//android.app.Activity
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
setContentView は Window の setContentView を呼び出し、PhoneWindow は Window の唯一の実装クラスです。
//com.android.internal.policy.PhoneWindow
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();//1,安装DecorView
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);//2,解析layoutResID
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
1 主に新しい DecorView を作成するために DecorView のインストールを開始し、id が content と等しいレイアウトを見つけ、mContentParent を介して参照します。xml に記述したレイアウトは、この mContentParent コンテナーに追加されます。
//com.android.internal.policy.PhoneWindow
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);//new一个DecorView
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);//找到id==content的ViewGroup
...
}
2. 受信した layoutResID を LayoutInflator で解析し、View に解析して mContentParent に追加します。mContentParent は、xml インターフェイスの id がコンテンツと等しいレイアウトです:
要約すると、setContentView は主に 2 つの機能を完了します:
1. DecorView をビルドします
。
そのため、setContentView は実際にはまだ画像のレンダリングを開始していません。
考察: setContentView を呼び出さなければ、Activity は正常に開始できますか? なぜ?
WindowManager.addView プロセス
すべての Android ビューは、WindowManager.addView を通じて画面に追加されます。アクティビティの DecorView が画面に追加されるのはいつですか?
答えはActivityThread
次の方法にありますhandleResumeActivity
。
//android.app.ActivityThread
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
...
//执行Activity的onResume生命周期
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
...
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();//1、调用了window.getDecorView()方法
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);//2、开始调用WindowManager.addView将view添加到屏幕
} else {
// The activity will get a callback for this {@link LayoutParams} change
// earlier. However, at that time the decor will not be set (this is set
// in this method), so no action will be taken. This call ensures the
// callback occurs with the decor set.
a.onWindowAttributesChanged(l);
}
}
// If the window has already been added, but during resume
// we started another activity, then don't yet make the
// window visible.
} else if (!willBeVisible) {
if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
...省略无关代码
wm.addView
これは、レンダリングを開始するためのエントリ ポイントですDecorView
。また、そのトリガー時間はonResume
Activity のライフ サイクルの後であるため、View は onResume の後に画面に表示され、View の幅はレンダリングが完了した後にのみ取得できます。
主に、window の getDecorView() メソッドが呼び出される 1 つの場所を見てください。
//com.android.internal.policy.PhoneWindow
@Override
public final @NonNull View getDecorView() {
if (mDecor == null || mForceDecorInstall) {
installDecor();
}
return mDecor;
}
setContentView を呼び出さなかったにもかかわらず、DecorView が初期化され、空白のページが表示されることがわかります。
次に、コードの 2 か所に注目し、WindowManager の addView メソッドを使用して DecorView をウィンドウに追加します。wm.addView(decor, l)
addView の分析を続ける前に、必要な基礎知識を整理します。
上記の wm は ViewManager 型ですが、実際には WindowManager です。
WindowManager は ViewManager を継承したインターフェースです。
public interface WindowManager extends ViewManager {
...
}
見えるWindowManager
実装クラスは、WindowManagerImpl
以下のWindowManager
機能がすべてWindowManagerImpl
で実装されていることです。
Window は抽象クラスであり、PhoneWindow はその実装です。WindowManager は Window のメンバー変数であり、Window と WindowManager の両方が Activity の attach メソッドで初期化されます。
//android.app.Activity
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window, activityConfigCallback);//1,初始化window
...省略无关代码
mWindow.setWindowManager( //2、给window初始化windowManager
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if (mParent != null) {
mWindow.setContainer(mParent.getWindow());
}
mWindowManager = mWindow.getWindowManager();//3、Activity通过mWindowManager引用window中的WindowManager,两个wm是同一个东西。
mCurrentConfig = config;
mWindow.setColorMode(info.colorMode);
setAutofillOptions(application.getAutofillOptions());
setContentCaptureOptions(application.getContentCaptureOptions());
}
- 1 ウィンドウの初期化を開始し、Activity のメンバー変数 mWindow に割り当てます。
- 2 ウィンドウの windowManager を設定します
- 3 つのアクティビティは、mWindowManager を介してウィンドウ内の WindowManager を参照し、2 つの wm は同じものです。
次に、setWindowManager メソッドの実装に注目します。
//android.view.Window
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
mAppToken = appToken;
mAppName = appName;
mHardwareAccelerated = hardwareAccelerated;
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
wm.addView
基本的な関係を整理したら、プロセスを振り返ります。
//android.view.WindowManagerImpl
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
wm.addView が mGolbal オブジェクトに渡されていることがわかります。
mGolbal は WindowManagerGlobal 型のグローバル シングルトンです。
public final class WindowManagerImpl implements WindowManager {
@UnsupportedAppUsage
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
...
WindowManagerGlobal.addView の実装方法を引き続き確認してください。
//android.view.WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
// ...省略无关代码
ViewRootImpl root;
View panelParentView = null;
// ...省略无关代码
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
ViewRootImpl
ご覧のとおり、ここにオブジェクト ルートが作成され、 view
、root
、wparams
がコレクションに保存されます。ViewRootImpl
最後に、 setView メソッドを呼び出してビューを設定します。
ViewRootImpl
追跡する方法setView
。
//android.view.ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
//...省略不重要代码
mAdded = true;
int res; /* = WindowManagerImpl.ADD_OKAY; */
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
mInputChannel = new InputChannel();
}
mForceDecorViewVisibility = (mWindowAttributes.privateFlags
& PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY) != 0;
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
setFrame(mTmpFrame);
} catch (RemoteException e) {
mAdded = false;
mView = null;
mAttachInfo.mRootView = null;
mInputChannel = null;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
throw new RuntimeException("Adding window failed", e);
} finally {
if (restore) {
attrs.restore();
}
}
//...省略不重要代码
if (view instanceof RootViewSurfaceTaker) {
mInputQueueCallback =
((RootViewSurfaceTaker)view).willYouTakeTheInputQueue();
}
if (mInputChannel != null) {
if (mInputQueueCallback != null) {
mInputQueue = new InputQueue();
mInputQueueCallback.onInputQueueCreated(mInputQueue);
}
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
Looper.myLooper());
}
view.assignParent(this);
//...省略不重要代码
}
}
}
WMS は Android のウィンドウ管理システムです.ビュー ツリーを WMS に登録する前に,レイアウトを実行する必要があります.WMS はウィンドウ管理だけでなく,さまざまなイベントのディスパッチも担当するため,アプリはビュー ツリーを保証してから WMS に登録します.イベントを受け取る準備ができました。
ViewRoot は仲介役であり、View ツリーの管理者であり、WMS との通信機能も果たします。
mWindowSession.addToDisplay は、View のレンダリングを WindowManagerService に渡します。
mWindowSession は IWindowSession 型の変数で、サーバー側の実装クラスは Binder オブジェクトである Session.java です。
@UnsupportedAppUsage
public static IWindowSession getWindowSession() {
synchronized (WindowManagerGlobal.class) {
if (sWindowSession == null) {
try {
// Emulate the legacy behavior. The global instance of InputMethodManager
// was instantiated here.
// TODO(b/116157766): Remove this hack after cleaning up @UnsupportedAppUsage
InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
IWindowManager windowManager = getWindowManagerService();
sWindowSession = windowManager.openSession(
new IWindowSessionCallback.Stub() {
@Override
public void onAnimatorScaleChanged(float scale) {
ValueAnimator.setDurationScale(scale);
}
});
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
return sWindowSession;
}
}
@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
Rect outOutsets, InputChannel outInputChannel) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
outContentInsets, outStableInsets, outOutsets, outInputChannel);
}
ようやくWindowManagerService
Windowの追加が完了したことがわかります。
つづく...