Android 应用界面显示流程
Android最重要的两个模块(个人认为),线程和UI。
线程我之前写了一篇博客了,感觉还算满意。AsyncTask源码解析 从AsyncTask讲到线程池
至于UI,趁最近有空,必须得好好整理一下脑子里各种零碎的知识点+再学习学习,整理出几篇博客才行。
初探Window
相信大部分学习Android第一个学到的都是Activity,如果没有研究一下,很容易会理解成Activity就是一张白纸,然后我们在上面画上各种View。
但其实并不是,因为Android同一时间并不是只绘制一个应用。比如状态栏就不是某个我们开发的应用绘制的。比如我们处于A应用时,能看到B应用的Toast。比如我们随时能看到QQ、微信的消息悬浮窗。比如在Android早些时代,那些各种卫士吹嘘的提速手机用的悬浮球。这些例子都说明,Android需要同一时间绘制多个进程所要显示的UI。
很容易猜出,Android应该有个统一调度,然后绘制各个应用UI的大管家。事实上也的确如此,就是我们的SystemServer进程里面的WindowManagerService这个服务了。如同ActivityManagerService用于管理所有应用的Activity,PackageManagerService用于管理所有应用的信息查询一样。手机屏幕的某一时刻可能同时有多个Window叠放并存着,此时就需要WindowManagerService(简称WMS)管理协调他们。
Window的定义和分类
所以Window是个什么东西?
网上一直有个很形象的比喻。Activity就像工匠,Window就像是窗户,View就像是窗花,LayoutInflater像剪刀,Xml配置像窗花图纸。
Window就是用来承载和管理View的。可能刚开始Android设计人员并没有区分出Activity和Window两者,把他们看成一个整体。但是为了独立生命周期,任务栈等(Activity作用) 和 View绘制(Window作用)这些功能降低耦合,把这个整个划分为现在这样了。每个Activity都会持有一个Window,每个Window会持有一个DecorView。
根据《Android开发艺术探索》,Window 有三种类型,分别是应用 Window、子 Window 和系统 Window。应用类 Window 对应一个 Acitivity,子 Window 不能单独存在,需要依附在特定的父 Window 中,比如常见的一些 Dialog 就是一个子 Window。系统 Window是需要声明权限才能创建的 Window,比如 Toast 和系统状态栏都是系统 Window。
Window 是分层的,每个 Window 都有对应的 z-ordered,层级大的会覆盖在层级小的 Window 上面,这和 HTML 中的 z-index 概念是完全一致的。在三种 Window 中,应用 Window 层级范围是 1~99,子 Window 层级范围是 1000~1999,系统 Window 层级范围是 2000~2999。
从setContentView开发分析执行的流程
以android5.1.1的源码来分析,我们让Activity显示出View的方法是setContentView,它有3个重载方法。
private Window mWindow;
public void More ...setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
public void setContentView(View view) {
getWindow().setContentView(view);
initWindowDecorActionBar();
}
public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
initWindowDecorActionBar();
}
public Window getWindow() {
return mWindow;
}
里面的工作都是调用Window的setContentView,可以看出Activity的视图其实是附属在Window上的。这个Window的实际对象是一个PhoneWindow,是唯一一个继承Window的子类。这个Window是在Activity的attach方法里创建了,先略过等下再说,我们先找到PhoneWindow.java看看里面的setContentView方法
@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);
}
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
...
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
...
}
...
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
protected ViewGroup generateLayout(DecorView decor) {
...
//layoutResource会根据是否dialog情况,title情况,actionbar情况,去取不同的xml资源。
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
...
return contentParent;
}
PhoneWindow.setContentView()里先执行了installDecor(),这个方法也是第一时间执行generateDecor(),其实就是new出来一个DecorView。DecorView是一个根布局,是一个FrameLayout。DecorView如果初始化完成,会是这样的结构。当然这只是其中的一种,事实上会根据主题,Title情况而有不同的结构。
这是精简了一些不切题的控件后的图,完整的应该是这样的。有些同学喜欢在DecorView外围再画一层Window,Window外围再画一层Activity。的确结构是Activity持有一个Window实例,Window持有一个DecorView实例。但个人感觉并不应该这么画出来,毕竟他们不是ViewGroup和View这样的控件树层级结构,会让人感觉有歧义。
分析下代码,generateLayout方法里,layoutResource会根据是否dialog情况,title情况,actionbar情况,去取不同的xml资源。inflate出来后DecorView会调用addView方法把它添加进自身里。可以在android源码的res/layout目录找到这些文件。
比如在这个界面上,就是上图DecorView的第一个子View,一个LinearLayout布局。所以成员变量mContentRoot就是这个LinearLayout了。
而看到ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT)这句代码,就是从布局中找到id为android:id/content的这个布局,比如在这里就是ContentFrameLayout了,最终赋值给成员变量mContentParent。
再回到PhoneWindow的setContentView(int)的方法里,里面会执行一个我们比较熟悉的方法。mLayoutInflater.inflate(layoutResID, mContentParent),所以inflater会创建出我们传进去的layout资源(比如是一个activity_main.xml布局文件),然后添加mContentParent里面。到这里就完成Activity的布局文件附在DecorView上面。
到现在为止,我们DecorView已经准备好了,但是这时候DecorView还不能起作用显示出来。因为WindowManager还没有接管这个DecorView。这方面代码在ActivityThread.java里面。
根据我之前一篇博客《android Activity启动过程 简析》,Activity的显示会走到依次走到ActivityThread.handleLaunchActivity -> ActivityThread.handleResumeActivity里。
来看下里面的ActivityThread.handleResumeActivit做了什么处理
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
...
ActivityClientRecord r = performResumeActivity(token, clearHide);
if (r != null) {
...
if (!r.activity.mFinished && willBeVisible
&& r.activity.mDecor != null && !r.hideForNow) {
...
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}
...
}
执行到Activity.makeVisible()方法,如下
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
makeVisible方法里,看到一句wm.addView(mDecor, ...)。并把Decor设置成visible。
先从名字看出wm的引用类型是个WindowManager,但WindowManager是个接口,继承自ViewManager接口,接口当然创建不出对象。沿着getWindowManager()方法跟踪进去,发现成员变量mWindowManager在attach方法里被赋值。
public WindowManager getWindowManager() {
return mWindowManager;
}
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) {
...
mWindow = PolicyManager.makeNewWindow(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
...
mUiThread = Thread.currentThread();
...
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
...
mWindowManager = mWindow.getWindowManager();
...
}
在attach方法里,通过PolicyManager类new出了一个PhoneWindow。并想通过Window成员变量持有WindowManager。public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
mAppToken = appToken;
mAppName = appName;
mHardwareAccelerated = hardwareAccelerated
|| SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
看setWindowManager方法,原来先是通过获取SystemService的里面的WindowManager服务,然后用createLocalWindowManager方法创建出一个本地的WindowManager的意思?。看下里面的这个create方法
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mDisplay, parentWindow);
}
直接new出了一个新的WindowManagerImpl实例。
然后说到调用了WindowManagerImpl.addView(mDecor, ...),进去看看addView方法
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
...
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
这里面调用了一个代理类mGlobal的方法,进去看WindowManagerGlobal.addView()方法。提前爆料一下WindowManagerImpl的很多方法都会交给WindowManagerGlobal实现。
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
...
synchronized (mLock) {
...
root = new ViewRootImpl(view.getContext(), display);
...
}
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
...
}
}
addView方法里,会创建一个ViewRootImpl的实例。ViewRootImpl并不是View,不像它的名字一样,它并不是Activity的根布局,Decor才是根布局。PhoneWindow是使用这个ViewRootImpl来管理Decor的,可以理解为PhoneWindow使用了ViewRootImpl来建造了Decor。PhoneWindow和ViewRootImpl是1对1的关系。
然后调用setView方法,view依然是DecorView。
final W mWindow;
public ViewRootImpl(Context context, Display display) {
...
mWindow = new W(this);
...
}
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
// 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();
...
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mInputChannel);
...
}
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
...
performTraversals();
...
}
private void performTraversals() {
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
performDraw();
...
}
static class W extends IWindow.Stub {
private final WeakReference<ViewRootImpl> mViewAncestor;
private final IWindowSession mWindowSession;
W(ViewRootImpl viewAncestor) {
mViewAncestor = new WeakReference<ViewRootImpl>(viewAncestor);
mWindowSession = viewAncestor.mWindowSession;
}
...
}
看到setView方法里,这句mWindowSession.addToDisplay方法。首先mWindowSession是一个Session实例。在ViewRootImpl持有的IWindowSession引用,由WindowManagerGlobal.java执行具体的创建逻辑,实际调用WindowManagerService的openSession方法,new出一个返回。
它是干嘛的呢?像我以前写B/S后台时,session表示一个用户对于一个网站的身份标识。这里WindowSession就是应用程序想与WMS通信的一个标识。
然后mWindowSession的引用是父类IWindowSession类型持有子类Session对象,IWindowSession是Android跨进程通信那套的aidl文件,所以在Session的addToDisplay方法里。
final WindowManagerService mService;
public Session(WindowManagerService service, IWindowSessionCallback callback,
IInputMethodClient client, IInputContext inputContext) {
mService = service;
...
}
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
InputChannel outInputChannel) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
outContentInsets, outStableInsets, outInputChannel);
}
它将调用WindowManagerService的addWindow,把最后这个IWindow交给WMS管理。IWindow和ViewRootImpl是双向持有的关系。所以WMS拿到IWindow,就能拿到ViewRootImpl,就能操作到DecorView。
setView的另一个重要方法是requestLayout,如下是方法调用流程requestLayout() -> scheduleTraversals() -> doTraversal() -> performTraversals()。
performTraversals这个方法厉害了,总共有700多行代码(Android5.1.1)。我们知道一个Activity的根布局就是DecorView,就是在这个方法里,开始绘制这个DecorView的第一步的。熟悉自定义控件,我们都知道自定义需要我们去重写onMeasure、onLayout和onDraw方法,对应的performTraversals也有3个方法performMeasure、performLayout和performDraw。由于对View的操作是递归的,所以会不断的去进入子View进行测量、布局和绘制,最终完成整个Activity界面的绘制。
总结一下
1.Activity里面setContentView,实际上是利用PhoneWindow类创建DecorView,然后inflater出xml布局,放进DecorView内。此时View层级结构已经准备好,但还没跑measure、layout和draw三件套,也没有被大佬WMS接管显示出来。
2.想被WMS接管,需要操作IPC那套流程,所以需要使用WindowManager进行操作,它会通过ViewRootImpl,使用IWindowSession和IWindow两个IPC接口(应用程序访问WMS使用IWindowSession,WMS访问应用程序使用IWindow),最终让WMS接管。
3.界面显示出来是,会经过一系列流程条用到ViewRootImpl.performTraversals方法,里面会从DecorView开始,递归的执行measure、layout和draw三件套方法,最终确定大小,绘制界面。