Android 重学系列 WMS在Activity启动中的职责 计算窗体的大小(四)
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 重学系列 WMS在Activity启动中的职责 计算窗体的大小(四)相关的知识,希望对你有一定的参考价值。
参考技术A通过启动窗口为例子,大致上明白了WMS是如何添加,更新,移除窗口的工作原理。本文将会重点聊一聊窗口的大小计算逻辑。
下面的源码都是来自Android 9.0
计算窗口的大小和Android 4.4相比变化很大。花了一点心思去重新学习了。在Android 4.4中,窗体的计算在onResume中调用了ViewRootImpl调用relayoutWindow对整个Window重新测量窗口大小以及边距。
relayoutWindow这个方法是做什么的呢?当我们在Activity的生命周期到达了onResume的阶段,此时ViewRootImpl的setView,开始走渲染的View的流程,并且调用requestLayout开始测量渲染。其中有一个核心的逻辑就是调用WMS的relayoutWindow,重新测量Window。
在Android 9.0中把这个流程和DisplayContent绑定起来。让我们稍微解剖一下这个方法。
relayout大致上要做了以下的事情:
relayout的方法有点长,本次我们将关注这一部分核心的逻辑。分别是两个方法:
能看到在这里面对performSurfacePlacementLoop做最多为6次的循环,这六次循环做什么呢?
能看到这里面的核心逻辑,首先会检查WMS下mForceRemoves集合中是否还有对象。有则调用removeImmediately清空WindowState的中SurfaceControl和WindowContainer之间的绑定和Surface对象,以及销毁WindowAnimator中的Surface。
做这个得到目的很简单,因为下一个步骤将会申请一个Surface对象,而此时如果Android系统内存过大了(OOM),mForceRemoves就存在对象,就可以销毁不需要的Surface。这一点的设计和Davlik虚拟机申请对象时候的思路倒是一致的。
销毁需要一点时间,因此就需要做一个250毫秒的的等待。接着会调用RootWindowContainer的performSurfacePlacement做真正的执行。最后会通过handler通过ViewServer通知事件给DebugBridge调试类中。
每一次loop的最后,如果发现RootWindowContainer需要重新测量,就会把当前这个方法,放入Handler中,等待下次的调用,也是调用6次。这样就能最大限度的保证在这段时间内Window能够测量每一次的窗体参数。
下面这个方法十分长,我们只看核心;
我在上面划分了9个部分:
这里只给总览,之后有机会再进去里面抓细节。
我们能够看到无论是在哪里,如果窗口发生了变化,都会调用updateFocusedWindowLocked方法。实际上这个方法才是真正的核心测量窗口大小逻辑。
这里注意一下isWindowChange是判断输入法焦点是否一致,而窗体焦点则是通过不同的WindowState来判断。
实际上核心测量的真正动作是DisplayContent.performLayout。我们仔细一想也就知道,在Android 9.0的时候,DisplayContent象征着逻辑屏幕,我们讨论无分屏的情况,实际上就是指我们当前窗体铺满逻辑显示屏各个边距的大小。
在正式开始聊窗体大小的测量之前,实际上,在Android系统中,为了把Window各个边界标记出来,实际上随着时代和审美潮流的演进,诞生越来越多的边距类型,我们往往可以通过这些边距来测定窗体的大小。
在DisplayFrame中有了大致的分区,如下:
可以看到,这些窗体的边距实际上是跟着这些年潮流走的。如Android 7.0的自由窗体模式,嵌套窗体模式,刘海屏等等,这些边距的共同作用,才会诞生一个真正的Window大小。有了这些基础知识之后,我们去看看测量大小的逻辑。
我们这里把这个方法拆成如下几个部分:
能看到,此时会设置当前显示屏幕的大小,以及获取过扫描区域,还会判断当前手机屏幕是否支持刘海屏。这一切实际上都是由硬件回馈到DisplayService,我们再从中获取的信息。
实际上如果有读者注意到我写的WMS第一篇就会看到实际上WMS初始化的时候,我们能够看到WMS会初始化一个WindowManagerPolicy的策略,而这个策略就是PhoneWindowManager。实际上这也支持了系统开发自定义策略,从而办到自己想要的窗体计算结果。
首先初始化几个参数,父窗体,屏幕,过扫描,可见区域,输入法区域为当前逻辑显示屏的大小,等到后面做裁剪。
能看到所有的事情实际上是关注的是系统UI上的判断,检测NavBar,StatusBar大小。最后再判断当前刘海屏的不允许交叉的区域顶部和显示屏顶部哪个大。如果mDisplayCutoutSafe的top大于mUnrestricted的top,说明mDisplayCutoutSafe在mUnrestricted下面,也就是我上面那个包含一段黑色的区域。此时会拿稳定的应用区域和刘海区域顶部的最大值,作为刘海屏幕的区域。这样就能保证刘海屏的顶部就是状态栏。
提一句如果NavigationBar隐藏,则会创建一个虚假的区域把输入事件都捕捉起来。
里面有四个关键函数:
可以看到所有的所有的间距将会设置为mUnrestricted的初始宽高,也就是不包含OverScan区域。如果是遇到刘海屏,则会根据设置的SafeInset区域来设置mDisplayCutoutSafe的安全区域。也就是我上面那种情况。比如设置了LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT这种情况,显示区域将不会超过刘海屏的底部。
我们关注到mTmpNavigationFrame这个对象的赋值,在正常的情况下的范围是如下:
此时mStable和mStableFullscreen区域的底部都是对应着top,也就是对应着Navigation顶部。System系统元素的底部也是Navigation顶部。
最后经过computeFrameLw重新计算这个区域的值。这个方法稍后会聊到,但是在正常手机开发中,其实是没有变化的。也就说,实际上对于mNavigationBar来说:
同理对于statusBar来说:
注意,此时如果statusBar可见,则做如下计算:
这种情况挺常见的,我们从一个隐藏状态栏的页面跳转到有状态栏的页面,国有有个PopupWindow,你能看到这个popwindow会明显向下移动。
在这个方法中mScreenDecorWindows这个集合实际上是在adjustWindowParamsLw以及prepareAddWindowLw这两个方法中加入。加入的条件是,每当有新的Window加入(WMS的addView)或者Window需要重新调整(WMS的relayoutWindow),当前新增得到Window或者需要重新relayout的Window有StatusBar有权限,且显示则会添加到mScreenDecorWindows集合。
mScreenDecorWindows从上面的描述,能得知实际上这个步骤还没有根据层级作区分。但是没关系,此时仅仅只是初步的测量。
明白了mScreenDecorWindows之后,我们阅读上面这个方法就很简单了。
layoutScreenDecorWindows做的事情就如名字一样,要测量Window上装饰部分,如StatusBar,如输入法。此时经过循环,自尾部往头部调用所有的WindowState的computeFrameLw计算每一个WindowState的对应Window的窗体大小。
当计算出每一个窗体大小之后,将会把事件分成两个情况,当计算出来的当前的Window的left和top都小于等于0,也就是说,当前的Window的顶部边缘并且左边缘超过了当前的屏幕。
说明了有什么东西在右下侧把整个Window定上去了。因此dockFrame的计算就很简单了:
如果计算出来的bottom大于等于屏幕高度且right大于等于屏幕宽度。说明有什么东西在左上方把整个Window顶下去了。
最后再设置这个把displayFrames的可见等区域都设置为dockFrame。联合上下文,实际上这里就是把整个区域的顶部移动到了statusBar之下。
Android---WindowWindowManager和WMS体系
前言
本篇文章会从源码层面上分析Activity从创建直到在页面上显示的过程。
- 首先分析一下在这个过程中会遇到的一些接口和类
- 了解它们的源码以及功能之后再将它们串起来就能够了解整体的流程了
概述
类和接口
1. Window体系
Window体系说白了就是要在页面是显示的View,这个体系中包含多个类来共同完成view的显示其中包括
Activity、Window、PhoneWindow、DecorView
- Activity
- 这个概念在我们一开始学Android的时候就理解为:一个Activity就是一个可视化的页面。
- 因为它的setContentView()方法加载一个一个布局,这个布局也就跟我们看到的页面一样,所以可以说一个Activity就是一个页面
- 然后我们深入setContentView()方法查看发现是调用了getWindow的setContentView方法,我们再来说一下Window
public void setContentView(@LayoutRes int layoutResID)
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
-
Window
- 查看Window源码发现Window是一个抽象类,他具体是实现类是PhoneWindow
- 所以上述 getWindow().setContentView(layoutResID);方法中getWindow返回的就是PhoneWindow,也就是调用了PhoneWindow的setContentView()方法
-
PhoneWindow
public PhoneWindow(Context context, Window preservedWindow,
ActivityConfigCallback activityConfigCallback)
this(context);
// Only main activity windows use decor context, all the other windows depend on whatever
// context that was given to them.
mUseDecorContext = true;
if (preservedWindow != null)
mDecor = (DecorView) preservedWindow.getDecorView();
...
...
- 在PhoneWindow的构造方法中我们发现PhoneWindow获取了它的内部类DecorView
@Override
public void setContentView(int layoutResID)
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
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); //注意这个地方
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed())
cb.onContentChanged();
mContentParentExplicitlySet = true;
- 到这里我们就可以知道PhoneWindow调用setContentView()方法将布局文件渲染在mContentParent这个viewGroup上了
- 然后我们在找mContentParent是什么东西,源码第一句就判断mContentParent为空的估走了个installDecor()方法,我们判断这个方法因该是创建这个installDecor吧,好叻~继续扒源码
private void installDecor()
mForceDecorInstall = false;
if (mDecor == null)
mDecor = generateDecor(-1);
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);
.....
- 可以看到installDecor方法再次判断mContentParent是否为空,然后调用了 mContentParent = generateLayout(mDecor);
- 此方法不难看出,接收一个DecorView返回的这个名为mContentParent的ViewGroup ,ok继续
protected ViewGroup generateLayout(DecorView decor)
// Apply data from current theme.
TypedArray a = getWindowStyle();
if (false)
System.out.println("From style:");
String s = "Attrs:";
for (int i = 0; i < R.styleable.Window.length; i++)
s = s + " " + Integer.toHexString(R.styleable.Window[i]) + "="
+ a.getString(i);
System.out.println(s);
.....
mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);//ID_ANDROID_CONTENT =
//com.android.internal.R.id.content;
if (contentParent == null)
throw new RuntimeException("Window couldn't find content container view");
........
return contentParent;
-
generateLayout() 这个方法源码贼多,但是我们大略可以猜到它是干嘛的,其实就是创建一个ViewGroup嘛,找return方法就完了
-
return contentParent; 说白了mContentParent 其实就是 DecorView的 android:id="@android:id/content"
-
这个时候mContentParent不等于空了,然后 mLayoutInflater.inflate(layoutResID, mContentParent); 也就把这个布局渲染给了 DecorView的 android:id="@android:id/content"
-
DecorView
- PhoneWindow的内部类,setContentView()方法最终完成的是将布局文件渲染到DecorView的content布局文件中
2. WindowManager体系
WindowManager体系的作用也就是管理Window最终管理的也就是渲染的View,主要有一下类或接口
ViewManager、 WindowManager、WindowManagerImpl、WindowManagerGlobal、 ViewRootImpl、WindowManagerService
- ViewManager
ViewManager是一个接口,它内部只有三个方法。添加、修改、删除
public interface ViewManager
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
-
WindowManager
WindowManager也是一个接口它继承自ViewManager,它具体的实现是由WindowManagerImpl完成的 -
WindowManagerImpl
WindowManagerImpl继承自WindowManager,重写了ViewManager的增删改方法,但是具体是委托给WindowManagerGlobal完成的
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params)
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params)
applyDefaultToken(params);
mGlobal.updateViewLayout(view, params);
@Override
public void removeView(View view)
mGlobal.removeView(view, false);
-
WindowManagerGlobal
-
WindowManagerGlobal具体的增删改都是有它来完成的,以addView为例,将view,LayoutParams 作为参数传入到addView方法中。
创建ViewRootImpl,并将view,ViewRootImpl,LayoutParams 添加到WindowManagerGlobal的List中
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>();
- ViewRootImpl
- 调用 root.setView(view, wparams, panelParentView); 这个方法主要做了两件事1、调用measure,layout,draw更新页面2、通知WindowManagerService进行Window的添加
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView)
if (mView == null)
mView = view;
...
//渲染UI
requestLayout();
...
try
...
//AIDL通知WindowManagerService
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
mAttachInfo.mRootView = view;
v
v
v
@Override
public void requestLayout()
if (!mHandlingLayoutInLayoutRequest)
checkThread();
mLayoutRequested = true;
scheduleTraversals();
v
v
v
void scheduleTraversals()
if (!mTraversalScheduled)
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch)
scheduleConsumeBatchedInput();
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
//scheduleTraversals()中会通过handler去异步调用mTraversalRunnable接口。
v
v
v
final class TraversalRunnable implements Runnable
@Override
public void run()
doTraversal();
//也就是调用doTraversal方法
v
v
v
void doTraversal()
if (mTraversalScheduled)
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile)
Debug.startMethodTracing("ViewAncestor");
performTraversals();//最终会调用performTraversals
if (mProfile)
Debug.stopMethodTracing();
mProfile = false;
v
v
v
private void performTraversals()
......
//测量View的宽高
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
//布置View的位置
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
//监听事件
if (triggerGlobalLayoutListener)
mAttachInfo.mRecomputeGlobalAttributes = false;
//触发OnGlobalLayoutListener的onGlobalLayout()函数
mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
......
//渲染View
performDraw();
......
//完成view的测量、布局、和 渲染
- WindowManagerService
ViewRootImpl通过进程间通信通知WindowManagerService添加Window,这地方有点复杂,我也看的不大清楚,就不误人子弟了 - -。
@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);
整体流程
最后我在说一下我理解的整体的从Activity创建到显示页面的整体流程
- Avtivity的创建要从startActivity说起,首先会调用它的重载方法startActivityForResult(),由ActivityManagerService通过进程间的通信会调用到ApplicationActivity的ScheduleLaunchActivity发送一个启动Activity的消息交个Handler处理
- Handler收到消息后调用HandleLaunchActivity,最终会调用PerformLaunchActivity方法,使用类加载器加载Activity并调用Attach方法创建Window
- Window会使用其唯一子类PhoneWindow创建,并将layout渲染到PhoneWindow的内部类DectorView的ContentView中,渲染完成后。再执行Activity的OnResume方法,将Activity的Window对象中为View添加到WnidowManager中,此过程是有WindowManagerGlobal完成的。
- 在WindowManagerGlobal中创建ViewRootImpl,通过ViewRootImpl完成页面的更新和通知WindowManagerService完成Window的添加
以上是关于Android 重学系列 WMS在Activity启动中的职责 计算窗体的大小(四)的主要内容,如果未能解决你的问题,请参考以下文章