Ahab's Studio.

开发编写的 View 控件,是怎么变成屏幕上图像的?

字数统计: 1.9k阅读时长: 8 min
2020/11/08 Share

介绍一下 Android 屏幕显示原理,开发编写的 View 控件,是怎么变成屏幕上图像的?

这个问题该怎么回答呢?一个思路是先整体串讲,宏观的把Android UI 显示原理的关键知识点都涉及到,然后再细化具体介绍,知识点如下:

  • 一、Activity 与 Window
    1. setContentView 原理
    2. Activity 窗口添加过程
  • 二、UI 刷新机制
    1. 发起 UI 重绘请求
    2. 执行 UI 绘制时机
    3. vSync 的生成与分发
  • 三、UI 绘制流程
  • 四、Surface 与 SurfaceFlinger

一、Activity 与 Window

1. setContentView 原理

Activity 中将 setContentView 工作交给了 Window 对象去处理:

1
2
3
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
}

每个 Activity 内部都持有一个 Window 对象,而 Window 对象是在创建 Activity 流程中 attach 时就实例化的:

1
2
3
final void attach(...){
mWindow = new PhoneWindow(this, window, activityConfigCallback);
}

PhoneWindow 中会实际去 inflate 布局:

1
2
3
4
5
6
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
}
mLayoutInflater.inflate(layoutResID, mContentParent);
}

而 installDecor 中伪代码逻辑如下:

1
2
3
4
5
6
7
8
private void installDecor() {
if (mDecor == null) {
mDecor = new DecorView(...);
}
if (mContentParent == null) {
mContentParent = (ViewGroup)mDecor.findViewById(R.id.content);
}
}

每个 Activity 内部都持有一个 window 对象,其实现为 PhoneWindow,在 Activity attachContext 时创建。

PhoneWindow 的根布局为 DecorView,其包括 TitleView 和 ContentView,Activity 的 setContentView 就是把布局添加到 ContentView。

2. Activity 窗口添加过程

setContentView 只是创建好了 View 视图结构,还没告知 WMS。在 ActivityThread handleResumeActivity 方法中,才会与 WMS 通信开始添加 Activity 窗口,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void handleResumeActivity(IBinder token, ...) {
final ActivityClientRecord r = performResumeActivity(token, ...);
final Activity a = r.activity;
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
ViewManager wm = a.getWindowManager();
a.mDecor = decor;
wm.addView(decor, l);
}
r.activity.makeVisible();
}

其中 performResumeActivity 方法会回调 onResume 生命周期,之后调用 WindowManager 的 addView 方法并将 DecorView 传入。这里的 WindowManager 为 WindowManagerImpl 对象,与 Activity 内部的 window 一样,都是在 Activity attachContext 时创建。

WindowManagerImpl 中 addView 方法如下:

1
2
3
4
5
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

其进一步交给了 mGlobal 即 WindowManagerGlobal 去处理,接着来看 WindowManagerGlobal 的 addView 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
int index = findViewLocked(view, false);
if (index >= 0) {
if (mDyingViews.contains(view)) {
mRoots.get(index).doDie();
} else {
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
}
}
ViewRootImpl root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
root.setView(view, wparams, panelParentView);
}

由于 WindowManagerGlobal 为进程单例,其内部的 mViews 则记录了全局添加的 View。当重复添加 View 时,就会抛出 “View has already been added to the window manager” 异常。

接着创建一个与 View 对应的 ViewRootImpl,将 View、ViewRootImpl 记录在 WindowManagerGlobal 中后,调用了 ViewRootImpl 的 setView 方法。先来看 ViewRootImpl 的构造函数:

1
2
3
4
5
6
public ViewRootImpl(Context context, Display display) {
mContext = context;
mWindowSession = WindowManagerGlobal.getWindowSession();
mWindow = new W(this);
...
}

其中 mWindowSession 是通过 WMS openSession 获取的匿名 binder,用于应用调用 WMS;mWindow 也是一个 binder 接口,用于 WMS 调用应用端。

接着看 ViewRootImpl setView 方法,关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
requestLayout();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
}
}
}

值得注意的是,不需把 view 传给 WMS,这是因为 WMS 并不关心 View 树所表达的具体 UI 内容,它只要知道各应用进程显示界面的大小、窗口层级值即可。

到达 WMS 所在 system_server 进程后,WindowSession addToDisplay 会进一步调用 WindowManagerService 的 addWindow 方法,执行添加用户窗口工作,包括:

  • 对用户窗口进行权限检查,比如 TYPE_PHONE 等窗口类型需要 SYSTEM_ALERT_WINDOW 权限
  • 检查 mWindow,窗口最终会记录到 <IBinder,WindowState> HashMap 中,其中 IBinder 为应用端的 mWindow,即一个 mWindow 只允许添加唯一的窗口
  • 检查窗口类型,比如子窗口必须依赖于一个父窗口
  • 按照窗口层级添加合适的位置
  • 等等…

小结一下,当 Activity 第一次回调 onResume 后,将 Activity 对应的窗口添加到 WMS 的过程:

  • 首先调用了 WindowManagerImpl,WindowManagerImpl 进一步调用进程单例的 WindowManagerGlobal
  • WindowManagerGlobal 中创建了与 DecorView 对应的 ViewRootImpl,并将 DecorView 和 ViewRootImpl 记录下来
  • WindowManagerImpl 和 WindowManagerGlobal 都还在应用进程,与 WMS 没什么关系
  • 在 ViewRootImpl 中与 WMS 发生交互,应用端通过 WindowSession 调用 WMS,WMS 通过 IWindow 调用应用端
  • WMS 中会对窗口进行权限、类型等检查,最终将应用窗口信息记录下来

二、UI 刷新机制

1. 发起 UI 重绘请求

ViewRootImpl 中的 performTraversals 中会依次调用 performMeasure、performLayout、performDraw,分别对应于 measure、layout、draw,由顶而下的进行界面绘制逻辑。

调用 View 控件 requestLayout、invalidate 等方法请求 UI 重绘时,会统一调用到 ViewRootImpl 的 scheduleTraversals 方法,代码如下:

1
2
3
4
5
6
7
8
9
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
}

其中 postSyncBarrier 插入一个消息屏障 block 普通消息,以保证主线程可以优先来执行接下来的绘制工作。mTraversalRunnable 的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}

void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
performTraversals();
}
}

通过 mTraversalScheduled 变量可以看出,不是每次调用 requestLayout、invalidate 方法就会触发一次 UI 重绘的,而是要等 mTraversalRunnable 被执行后才会接收下一次的重绘请求。

在 mTraversalRunnable 中调用了 performTraversals() 进行真正的 UI 绘制,而 UI 真正绘制的时机则取决于 mChoreographer 触发回调的时机。

执行 UI 绘制时机

ViewRootImpl 接收 UI 重绘请求后,将真正的 UI 绘制时机交给了 Choreographer,而 Choreographer 中会在每次 vSync 信号到来时执行 UI 绘制。

调用 Choreographer 的 postCallback 方法将 UI 绘制 TraversalRunnable 传入后,会进一步调用 Choreographer 的 postCallbackDelayedInternal 方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}

首先将 UI 绘制 action 记录到 mCallbackQueues 队列中,然后根据处理时间决定立即调用 scheduleFrameLocked ,或发送异步消息延时调用 scheduleFrameLocked。

scheduleFrameLocked 方法关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
private void scheduleFrameLocked(long now) {
// If running on the Looper thread, then schedule the vsync immediately,
// otherwise post a message to schedule the vsync from the UI thread
// as soon as possible.
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
}

如注释所示,scheduleFrameLocked 中需要切换到指定线程中调用 scheduleVsyncLocked:

1
2
3
private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}

scheduleVsync 表示要接受下一次 vSync 信号,等到 vSync 信号到来时会由 SurfaceFlinger 回调通知。直接来看 Choreographer 接受到 vSync 信号后的处理,关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
}
}
...
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
}

当要绘制的图像时间戳晚于一个帧刷新周期时,会去进一步计算异常跳过的帧数,如果跳过的帧数过大,就可以看到非常眼熟的一条日志了:“Skipped xx frames! The application may be doing too much work on its main thread”

随后通过 doCallbacks 回调触发执行 UI 绘制,也就是执行 ViewRootImpl 传过来的 TraversalRunnable、调用 performTraversals 方法,由顶而下的执行界面绘制逻辑。

原文作者:Ahab

原文链接:http://yhaowa.gitee.io/f9fd2518/

发表日期:November 8th 2020, 2:24:57 pm

更新日期:November 15th 2020, 12:38:27 pm

版权声明:本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可

CATALOG
  1. 1. 一、Activity 与 Window
    1. 1.0.1. 1. setContentView 原理
    2. 1.0.2. 2. Activity 窗口添加过程
  • 2. 二、UI 刷新机制
    1. 2.0.1. 1. 发起 UI 重绘请求
    2. 2.0.2. 执行 UI 绘制时机