ps: Activity 的显示真是看的我头晕眼花啊,之前看 app 启动流程都没这么费劲,没办法,Activity 显示中有很多没接触过的概念,流程,找资料搞明白这些相当费事
Activity 的显示过程中,这哥几个是迈不过去的:Window、ViewRootImpl、WindowManageService、Surface 其他我们可以暂时忽略,但是这3个是 Activity 显示的关键,我们必须要明白
概念解释
1. Window
Window 在 Activity 体系中负责曾在 view 视图,Window 是个接口,PhoneWindow 是 Activity 中 Window 的具体实现类
PhoneWindow 在构造函数时会创建出 Window 窗口的根视图 DecorView,DecorView 的高度可是包括状态烂的,这点要注意
2. WindowManageService
android 系统自己的进程,用于屏幕显示的逻辑控制,Window 只是持有每个 Activity 的 UI 呢容,但是具体在屏幕上如何显示,和其他类型 Window 窗口的交互控制都是由 WindowManageService 这个服务来完成的
Window 窗口的类型有很多,PhoneWindow 是页面的窗口类型,输入法的输入框也是一种 Window 窗口,比如我们在点击 edittext 弹出输入框把我们所在 Activity 向上顶起,就是由 WindowManageService 操作的。 Window 最终会把 view 传给 WindowManageService 去操作
3. Window 与 WindowManageService 通讯
妥妥的这又是一对典型的双向 AIDL ,看图吧,我相信大家在看完 app 启动流程 之后都已经熟悉这个套路了
ViewRootImpl 持有与 WindowManageService 通讯的 AIDL WindowSession ,WindowManageService 持有与 ViewRootImpl 通讯的 AIDL IWindow,这个 IWindow 也是在某个时候由 ViewRootImpl 注册到 WindowManageService 里面的
WindowManageService 会接收所有的输入事件,比如 touch 事件,再通过 IWindow 通知 ViewRootImpl
与WMS通信4. WindowManage
大家不用想啊,有 XX 必有 XXManage,这是我们最熟悉的框架结构了,显示和控制做功能分离,这是里氏替换原则的跟啊,面向接口编程
WindowManage 依然是个接口,在 Activity 中 WindowManage 的实现类是 WindowManagerImpl ,WindowManagerImpl 内部使用的是 WindowManagerGlobal,一看到
Global 这个单词,大家就能联想到 WindowManagerGlobal 这是个静态单例了吧,事实上的确是的
WindowManagerGlobal类中主要有3个非常重要的变量
private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams =
new ArrayList<WindowManager.LayoutParams>();
- mViews 保存的是View对象,DecorView
- mRoots 保存和顶层View关联的ViewRootImpl对象
- mParams 保存的是创建顶层View的layout参数。
WindowManagerGlobal 在初始化时会联通 WindowManageService,和 WindowManageService 通信的 WindowSession 这个 AIDL WindowManagerGlobal 获取后会传给 ViewRootImpl
5. ViewRootImpl
ViewRootImpl 负责和 WindowManageService 通信,和 WindowManageService 通信的 AIDL WindowSession 虽然不是 ViewRootImpl 自己创建的,但是 WindowManagerGlobal 在和 WindowManageService 联通之后,可是把 WindowSession 传给了 ViewRootImpl 的。WindowManageService 和 window 通信的 AIDL IWindow 也是 ViewRootImpl 创建的,可见 ViewRootImpl 在功能上来说就是负责通信的
除了通信外,ViewRootImpl 负责整个 window 窗口里所有的 UI 视图的 Measure,Layout,Draw ,ViewRootImpl.performTraversals() 是整个视图树 Measure,Layout,Draw 的起点
6. Surface
官方概念:
一个Surface就是一个对象,该对象持有一群像素(pixels),这些像素是要被组合到一起显示到屏幕上的。你在手机屏幕上看到的每一个window(如对话框、全屏的activity、状态栏)都有唯一一个自己的surface,window将自己的内容(content)绘制到该surface中。Surface Flinger根据各个surface在Z轴上的顺序(Z-order)将它们渲染到最终的显示屏上。
我们自己理解呢,可以简单的把 Surface 当做显存来看,Surface 最终也是通过 JNI 方法往内存写入数据
我们在自定义 view 时用 canvas 来绘制图形,其实 canvas 就是操作的 window 所属的 Surface,把图形绘制到 Surface 的显存上,然后再交由系统来完成其他操作
SurfaceFlinger 是一个系统进程,专门管理 Surface,Surface 平时我们 new 一个出来也没用,必须要 SurfaceFlinger 创建给我们才有用。所以这里又涉及到 AIDL 了,没错 ISurface 就是需要的和 SurfaceFlinger 通信的 AIDL ,每个窗口都持有一个 ISurface ,具体来说 WindowManageService 持有的 ISurface 用来进行窗口大小改变,动画等操作,ViewRootImpl 持有的 ISurface 用来进行 UI 的绘制,所以 canvas 才有用武之地
其他人的解释,有的我看挺好:
在Android系统中,窗口是独占一个Surface实例的显示区域,每个窗口的Surface由WindowManagerService分配。我们可以把Surface看作一块画布,应用可以通过Canvas或OpenGL在其上面作画。画好之后,通过SurfaceFlinger将多块Surface按照特定的顺序(即Z-order)进行混合,而后输出到FrameBuffer中,这样用户界面就得以显示。
Activity 显示流程
好了上面一通儿概念介绍完,现在我们开始串串 Activity 显示流程,先把整体跑下来,之后有疑问的地方再说说,大家记住流程,以后面试笔试会用到。不了解时,看着跟天书一样,理解了之后也就那样了,10W 小时理论,我们觉得 30% 时间都耗在理解各种思路,概念上了
在 app 启动流程 一文中,我们说了 Activity 的起始是 ActivityThread.performLaunchActivity() 这个方法,里面 Instrumentation new 了一个 Activity 对象出来,然后 Activity.attch 进行 初始化,最后触发 Activity.onCreate
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
...
} catch (Exception e) {
...
}
try {
// 返回之前创建过的 application 对象
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
...
if (activity != null) {
...
// attach 到 window 上
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);
...
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
...
}
} catch (Exception e) {
...
}
return activity;
}
那么 Activity 的其实我们就从 attch 这个方法看起
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) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow (this, window);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
mWindow.setWindowManager(
(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();
mCurrentConfig = config;
}
没用的我们去掉,可以看到在 Activity.attach 里,创建了 PhoneWindow ,并给 PhoneWindow 绑定了管理器 WindowManage ,这里 window,WindowManage 就初始化好了
下面就会执行 Activity.onCreate 方法了,onCreate 里面有什么呢,就是 setContentView ,这里进行 window UI 部分的初始化了
Activity 调的是 Window 的 setContentView
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
PhoneWindow 干了2件事,第一个就是 installDecor 把 window 窗口的根视图 DecorView new 出来,另一件事就是把我们的内容视图加载出来添加到 DecorView 里面
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} 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);
}
}
这回视图 view 都出来了,后面会继续走 Activity 的生命周期,一直到 onResume 之后,发送一个 MSG_RESUME_PENDING 消息
@Override
protected void onResume() {
super.onResume();
mHandler.sendEmptyMessage(MSG_RESUME_PENDING);
mResumed = true;
mFragments.execPendingActions();
}
发送消息过并不会直接执行消息,因为此时 onResume 方法还没执行完呢,只有等 UI 线程空闲了才会执行 looper 消息队列里面的任务
我们才测试下,点击按钮,先用 handle 发送一个消息,再打印一个标记,我们看看先执行谁
var handle = object : Handler() {
override fun handleMessage(msg: Message?) {
super.handleMessage(msg)
Log.d("AA", "handle 接受消息, handle")
}
}
btn_toast.setOnClickListener({
handle.sendMessage(Message.obtain())
Thread.sleep(1000)
Log.d("AA", "onCreate 方法执行任务,BBBBBB")
})
很明显先执行的方法任务,再执行的 looper 消息队列的任务
这里我想说什么呢,我想说若是我们在 Activity 的 onResume 方法里写逻辑代码,一样是会卡页面的,此时 Activity 页面只是把 UI 参数初始化出来了,但是页面可是还没显示到屏幕上,这点要注意啊,有些新人在这里犯过错误,之前也看到过有人说 onResume 时页面就显示出来了,真是大错特错,误导新人啊。所以啊,大家自己过一遍 Activity 的显示流程,好多问题答案自己就出来了,好多错误的观点也能找到证据了,我们在这方面就不迷糊了,以后就能正确的在这块编写代码了,不会再出现以前那么莫名其妙的问题了
题外话说完了,我们回来,MSG_RESUME_PENDING 消息最终会触发 ActivityThread.handleResumeActivity
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason){
if (r != null) {
final Activity a = r.activity;
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.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);
} 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);
}
}
}
handleResumeActivity 方法里面最主要的是调用了 windowManage.addView
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
ViewRootImpl root;
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;
}
}
}
new 了 ViewRootImpl 对象出来,然后把 DocverView 和 ViewRootImpl 存到 windowManage 里面,最后调用 ViewRootImpl.setView 正式开始启动 UI 的显示逻辑了,我精简一下,留下主要的
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
// 刷新 UI
requestLayout();
// 和 WindowManageService 通讯添加 window
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
}
}
}
ViewRootImpl 的 setView 函数就干了这2件事
requestLayout 函数是给 UI 线程添加了一个 callback 任务,这个任务只有在 UI 线程空闲时才会执行,也就是说会在 ViewRootImpl 的 setView 方法执行完后才执行,这个任务就是 ViewRootImpl.performTraversals() 这个方法会测量 widnow 窗口的大小,然后请求 WindowManageService 去 SurfaceFlinger 申请显存,然后遍历 viewTree,对所有 view 进行 measure,layout,draw,这样页面就可以在屏幕上显示出来了
但是 performTraversals() 逻辑太复杂了,有 800 行代码,很难懂,下面会结合 2次 measure的问题说一说
addToDisplay 最终会调用 WindowManageService 的 addWindow 函数,在 WindowManageService 端生成对应的 window 对象,获取和 SurfaceFlinger 通信的 AIDL 对象 ISurface,并分别返回给 WindowManageService 和 ViewRootImpl ,有了 ISurface 之后,我们才能通过 ISurface 和 SurfaceFlinger 通信,申请显存,才能绘制图形出来
requestLayout() 虽说是写在 addToDisplay() 之前的,但是他是添加了一个 handle 任务,所以是先执行 addToDisplay ,然后才是 requestLayout,注意这个顺序,这样后面才能理解,要不然你会觉得很多操作都是倒着的
大体到着就完事了,这样页面就可以在屏幕上看到了,但是后面这2个方法,我会再说说的,首先说明非常复杂啊,我也是跟着前人大体看的,有不清楚的地方大家谅解
addToDisplay
@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);
}
注意啊 addToDisplay 是在 WindowManageService 系统进程里执行的,这时已经不在应用进程了
应用进程把 IWindow 这个 AIDL 传进来,实现 WindowManageService 和 应用进程的通信,就会在创建一个 WindowState 对象来描述与该 IWindow 所关联的 Window 窗口状态,并且以后就通过这个 IWindow 对象来控制对应的 Window 窗口状态
每一个Activity组件在 ActivityManagerService 服务内部,都对应有一个 ActivityRecord 对象,这个 ActivityRecord 对象是 Activity 组件启动的过程中创建的,用来描述 Activity 组件的运行状态。这样,每一个 Activity 组件在应用程序进程、WindowManagerService 服务和 ActivityManagerService 服务三者之间就分别一一地建立了连接
应用程序进程通过 ViewRootImpl 里面的 sWindowSession 和 WindowManagerService 服务通信,包括以下内容,有助于我们理解页面的显示:
-
在Activity组件的启动过程中,调用这个IWindowSession接口的成员函数add可以将一个关联的W对象传递到WindowManagerService服务,以便WindowManagerService服务可以为该Activity组件创建一个WindowState对象。
-
在Activity组件的销毁过程中,调用这个这个IWindowSession接口的成员函数remove来请求WindowManagerService服务之前为该Activity组件所创建的一个WindowState对象。
-
在Activity组件的运行过程中,调用这个这个IWindowSession接口的成员函数relayout来请求WindowManagerService服务来对该Activity组件的UI进行布局,以便该Activity组件的UI可以正确地显示在屏幕中。
-
当一个Activity组件的窗口的大小发生改变后,WindowManagerService服务就会调用这个IWindow接口的成员函数resized来通知该Activity组件,它的大小发生改变了。
-
当一个Activity组件的窗口的可见性之后,WindowManagerService服务就会调用这个IWindow接口的成员函数dispatchAppVisibility来通知该Activity组件,它的可见性发生改变了。
-
当一个Activity组件的窗口获得或者失去焦点之后,WindowManagerService服务就会调用这个IWindow接口的成员函数windowFoucusChanged来通知该Activity组件,它的焦点发生改变了。
WMS 和 AMS 之间也是双向 AIDL 通信的,具体看下面这篇,我这里就不多说了,说起来又是一大堆
接着说 addToDisplay ,addWindow 中创建 WindowState 对象后,会触发 WindowState .attach ,该函数会创建一个关联的 SurfaceSession 对象,以便可以用来和 SurfaceFlinger 服务通信
void attach () {
if (WindowManagerService.localLOGV) Slog.v(
TAG, "Attaching " + this + " token=" + mToken
+ ", list=" + mToken.windows);
mSession.windowAddedLocked();
}
void windowAddedLocked() {
if (mSurfaceSession == null) {
mSurfaceSession = new SurfaceSession ();
mService.mSessions.add(this);
if (mLastReportedAnimatorScale != mService.getCurrentAnimatorScale()) {
mService.dispatchNewAnimatorScaleLocked(this);
}
}
mNumWindow++;
}
SurfaceSession 的构造方法内会创建和 SurfaceFlinger 服务通信的 AIDL SurfaceComposerClient,通过 SurfaceComposerClient 申请创建绘制表面 Surface 的操作
public SurfaceSession () {
mNativeClient = nativeCreate();
}
static jlong nativeCreate(JNIEnv * env, jclass clazz) {
SurfaceComposerClient * client = new SurfaceComposerClient ();
client->incStrong((void*)nativeCreate);
return reinterpret_cast<jlong>(client);
}
nativeCreate 是 JNI 方法,SurfaceComposerClient 是 C++ 的,到这就差不多了,更多的请看: kc专栏 里的 android 下是部分吧,addToDisplay 方法到这里就 OK 了
requestLayout
requestLayout 还是要看看的,记住他是添加了一个 handle 任务就行了
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals () {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
看到 postCallback 没有,mTraversalRunnable 这个 Runnable 就是添加的 handle 任务了
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal () {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
走到 performTraversals 就行了,到这就开始绘制界面了,这个方法就是绘制界面用的
performTraversals
performTraversals 有 800 行,我精简一下,留下关键的
private void performTraversals() {
// Ask host how big it wants to be newSurface
windowSizeMayChange | = measureHierarchy(host, lp, res,desiredWindowWidth, desiredWindowHeight);
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (!cancelDraw && !newSurface) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();
} else {
if (isViewVisible) {
// Try again
scheduleTraversals();
} else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).endChangingAnimations();
}
mPendingTransitions.clear();
}
}
}
一上来先走了 measureHierarchy ,目的是测量 window 窗口的大小,后面 relayoutWindow 是重新布局 window 窗口,只有 window 窗口的大小,位置改变了才会生效,首次测量肯定会执行的
relayoutWindow 的返回值会标记 newSurface ,newSurface = true 表示窗口变化了,首次进来窗口是没有所属显存的 mSurface ,然后 WindowState 会和 SurfaceFlinger 申请一块显存出来,这都是 C 了,在 java 层表现就是这个 mSurface 对象了,然后把 mSurface 对象传递给 WindowState 和 ViewRootImpl
后面又走一次 performMeasure 测量 view 大小,因为 window 窗口的大小可能有变化
之后就是 layout 布局了,这里我没写
到最后了,很关键,因为首次进来,新创建窗口所属 mSurface ,newSurface 会 = true,这样就不会走 performDraw ,就不会绘制 UI,而是重新执行一次 scheduleTraversals 方法,scheduleTraversals 里面就是 performTraversals 这个方法,所以 viewTree 绘制一次会跑2次 performTraversals
最后还有一个点,就是 View为什么会至少进行2次onMeasure、onLayout,这个问题下面几个情况会触发:
- 做为视图容器的 ViewGroup 宽高不是 match_parent活具体数值时,都会对子 view 进行2次 measure,layout
- ViewRootImpl.performTraversals 会对 viewTree 进行2次 measure,layout
- 每个 view 自身的大小位置改变都会触发其父容器的 requestLayout,进而造成父容器重新 measure,layout 所有子 view
其他不说了,这里说下 ViewRootImpl.performTraversals
看代码我们知道 performTraversals 方法回执行2次,一次 performTraversals 会2次 measure,1次 layout,那么2次 performTraversals 是不是就总共会 4次 measure,2次 layout 呢,答案不是
performMeasure 测量方法有优化的,若是 forceLayout = false 或是测量参数没有变化,那么就是用上次测量的值,就不会重新测量。forceLayout = true 这表示强制重新布局,可以通过View.requestLayout() 来实现
performLayout 方法会把 forceLayout 置为 false ,这样在第一次 performTraversals 时,先执行2次测量,之后布局,forceLayout 就 = false 了,第二次 performTraversals 进来,根据优化原则就不会再进行测量了
网友评论