- Android 触摸屏事件派发机制详解与源码分析一 View 篇
- Android 触摸屏事件派发机制详解与源码分析二 ViewGroup 篇
- Android 触摸屏事件派发机制详解与源码分析三 Activity 篇
- Android 应用 setContentView 与 LayoutInflater 加载解析机制源码分析
- Android 应用 Context 详解及源码解析
- Android 异步消息处理机制详解及源码分析
- Android 应用 Activity、Dialog、PopWindow、Toast 窗口添加机制及源码分析
- Android ListView 工作原理完全解析,带你从源码的角度彻底理解
- Activity 启动过程全解析
- Android 应用 AsyncTask 处理机制详解及源码分析
- 说说 PendingIntent 的内部机制
- Android Activity.startActivity 流程简介
- Activity 界面显示全解析
- 框架层理解 Activity 生命周期(APP 启动过程)
- APK 安装过程及原理详解
- Android 构建过程简述
- Android 应用层 View 绘制流程与源码分析
Activity 界面显示全解析
前几天凯子哥写的 Framework 层的解析文章 《Activity 启动过程全解析》 ,反响还不错,这说明“写让大家都能看懂的 Framework 解析文章”的思想是基本正确的。
我个人觉得,深入分析的文章必不可少,但是对于更多的 Android 开发者——即只想做应用层开发,不想了解底层实现细节——来说,“整体上把握,重要环节深入“是更好的学习方式。因为这样既可以有完整的知识体系,又不会在浩瀚的源码世界里迷失兴趣和方向。
所以呢,今天凯子哥又带来一篇文章,接着上一篇的结尾,重点介绍 Activity 开启之后,Android 系统对界面的一些操作及相关知识。
- 本期关键字
- 学习目标
- 写作方式
- onCreate 中的 setContentView 到底做了什么为什么不能在 setContentView 之后设置某些 Window 属性标志
- Activity 中的 findViewById 本质上是在做什么
- Window 和 PhoneWindow 是什么关系 WindowManager 是做什么的
- Activity 中 Window 类型的成员变量 mWindow 是什么时候初始化的
- PhoneWindowsetContentView 到底发生了什么
- 如何验证上一个问题
- 我们通过 setContentView 设置的界面为什么在 onResume 之后才对用户可见呢
- ViewManagerWindowManagerWindowManagerImplWindowManagerGlobal 到底都是些什么玩意
- ViewRootImpl 是什么有什么作用 ViewRootImpl 如何与 WMS 通信
- 从什么时候开始绘制整个 Activity 的 View 树的
- Window 的类型有几种分别在什么情况下会使用到哪一种
- 为什么使用 PopWindow 的时候不设置背景就不能触发事件
- 在 Activity 中使用 Dialog 的时候为什么有时候会报错 Unable to add window token is not valid is your activity running
- 结语
- 参考文章
本期关键字
- Window
- PhoneWindow
- WindowManager
- WindowManagerImpl
- WindowManagerGlobal
- RootViewImpl
- DecorView
- Dialog
- PopWindow
- Toast
学习目标
- 了解 Android 中 Activity 界面显示的流程,涉及到的关键类,以及关键流程
- 解决在开发中经常遇到的问题,并在源码的角度弄清楚其原因
- 了解 Framework 层与 Window 相关的一些概念和细节
写作方式
老样子,咱们还是和上次一样,采用一问一答的方式进行学习,毕竟“带着问题学习”才是比较高效的学习方式。
进入正题
话说,在上次的文章中,我们解析到了从手机开机第一个 zygote 进程开启,到 App 的第一个 Activity 的 onCreate() 结束,那么我们这里就接着上次留下的茬,从第一个 Activity 的 onCreate() 开始说起。
onCreate() 中的 setContentView() 到底做了什么?为什么不能在 setContentView() 之后设置某些 Window 属性标志?
一个最简单的 onCreate() 如下:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
通过上面几行简单的代码,我们的 App 就可以显示在 activity_main.xml 文件中设计的界面了,那么这一切到底是怎么做到的呢?
我们跟踪一下源码,然后就在 Activity 的源码中找到了 3 个 setContentView() 的重载函数:
public void 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();
}
我们上面用到的就是第一个方法。虽然 setContentView() 的重载函数有 3 种,但是我们可以发现,内部做的事情都是基本一样的。首先是调用
getWindow() 获取到一个对象,然后调用了这个对象的相关方法。咱们先来看一下,getWindow() 到底获取到了什么对象。
private Window mWindow;
public Window getWindow() {
return mWindow;
}
喔,原来是一个 Window 对象,你现在可能不知道 Window 到底是个什么玩意,但是没关系,你只要能猜到它肯定和咱们的界面现实有关系就得了,毕
竟叫“Window”么,Windows 系统的桌面不是叫“Windows”桌面么,差不多的东西,反正是用来显示界面的就得了。
那么 initWindowDecorActionBar() 函数是做什么的呢?
写了这么多程序,看名字也应该能猜出八九不离十了,init 是初始化,Window 是窗口,Decor 是装饰,ActionBar 就更不用说了,所以这个方法应该就是”初始化装饰在窗口上的 ActionBar”,来,咱们看一下代码实现:
/**
* Creates a new ActionBar, locates the inflated ActionBarView,
* initializes the ActionBar with the view, and sets mActionBar.
*/
private void initWindowDecorActionBar() {
Window window = getWindow();
// Initializing the window decor can change window feature flags.
// Make sure that we have the correct set before performing the test below.
window.getDecorView();
if (isChild() || !window.hasFeature(Window.FEATURE_ACTION_BAR) || mActionBar != null) {
return;
}
mActionBar = new WindowDecorActionBar(this);
mActionBar.setDefaultDisplayHomeAsUpEnabled(mEnableDefaultActionBarUp);
mWindow.setDefaultIcon(mActivityInfo.getIconResource());
mWindow.setDefaultLogo(mActivityInfo.getLogoResource());
}
哟,没想到这里第一行代码就又调用了 getWindow(),接着往下调用了 window.getDecorView(),从注释中我们知道,在调用这个方法之后,Window
的特征标志就被初始化了,还记得如何让 Activity 全屏吗?
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FILL_PARENT, WindowManager.LayoutParams.FILL_PARENT);
setContentView(R.layout.activity_main);
}
而且这两行代码必须在 setContentView() 之前调用,知道为啥了吧?因为在这里就把 Window 的相关特征标志给初始化了,在 setContentView() 之后调
用就不起作用了!
如果你还不确定的话,我们可以再看下 window.getDecorView() 的部分注释
/**
* Note that calling this function for the first time "locks in"
* various window characteristics as described in
* {@link #setContentView(View, android.view.ViewGroup.LayoutParams)}
*/
public abstract View getDecorView();
“注意,这个方法第一次调用的时候,会锁定在 setContentView()中描述的各种 Window 特征”
所以说,这也同样解释了为什么在 setContentView() 之后设置 Window 的一些特征标志,会不起作用。如果以后遇到类似问题,可以往这方面想一下。
Activity 中的 findViewById() 本质上是在做什么?
在上一个问题里面,咱们提到了一个很重要的类——Window,下面先简单看一下这个类的几个方法:
public abstract class Window {
public abstract void setContentView(int layoutResID);
public abstract void setContentView(View view);
public abstract void setContentView(View view, ViewGroup.LayoutParams params);
public View findViewById(int id) {
return getDecorView().findViewById(id);
}
}
哇塞,有个好眼熟的方法,findViewById()!
是的,你每次在 Activity 中用的这个方法,其实间接调用了 Window 类里面的方法!
public View findViewById(int id) {
return getWindow().findViewById(id);
}
不过,findViewById() 的最终实现是在 View 及其子类里面的,所以 getDecorView() 获取到的肯定是一个 View 对象或者是 View 的子类对象:
public abstract View getDecorView();
Activity、Window 中的 findViewById() 最终调用的,其实是 View 的 findViewById()。
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
public final View findViewById(int id) {
if (id < 0) {
return null;
}
return findViewTraversal(id);
}
protected View findViewTraversal(int id) {
if (id == mID) {
return this;
}
return null;
}
}
但是,很显然,最终调用的肯定不是 View 类里面的 findViewTraversal(),因为这个方法只会返回自身。
而且,findViewById() 是 final 修饰的,不可被重写,所以说,肯定是调用的被子类重写的 findViewTraversal(),再联想到,我们的界面上有很多的 View,那么既能作为 View 的容器,又是 View 的子类的类是什么呢?很显然,是 ViewGroup!
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
@Override
protected View findViewTraversal(int id) {
if (id == mID) {
return this;
}
final View[] where = mChildren;
final int len = mChildrenCount;
for (int i = 0; i < len; i++) {
View v = where[i];
if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
v = v.findViewById(id);
if (v != null) {
return v;
}
}
}
return null;
}
}
所以说,在 onCreate() 中调用 findViewById() 对控件进行绑定的操作,实质上是通过在某个 View 中查找子 View 实现的,这里你先记住,这个 View 叫做
DecorView,而且它位于用户窗口的最下面一层。
Window 和 PhoneWindow 是什么关系?WindowManager 是做什么的?
话说,咱们前面介绍 Window 的时候,只是简单的介绍了下 findViewById(),还没有详细的介绍下这个类,下面咱们一起学习一下。
前面提到过,Window 是一个抽象类,抽象类肯定是不能实例化的,所以咱们需要使用的是它的实现类,Window 的实现类有哪些呢?咱们从 Dash 中看下 Window 类的文档
Window 只有一个实现类,就是 PhoneWindow!所以说这里扯出了 PhoneWindow 这个类。
而且文档还说,这个类的一个实例,也就是 PhoneWindow,应该被添加到 Window Manager 中,作为顶层的 View,所以,这里又扯出了一个 WindowManager 的概念。
除此之外,还说这个类提供了标准的 UI 策略,比如背景,标题区域,和默认的按键处理等等,所以说,咱们还知道了 Window 和 PhoneWindow 这两个类的作用!
所以说,看文档多重要呀!
OK,现在咱们已经知道了 Window 和唯一的实现类 PhoneWindow,以及他们的作用。而且我们还知道了 WindowManager,虽然不知道干嘛的,但是从名字也可以猜出是管理 Window 的,而且还会把 Window 添加到里面去,在下面的模块中,我会详细的介绍 WindowManager 这个类。
Activity 中,Window 类型的成员变量 mWindow 是什么时候初始化的?
在每个 Activity 中都有一个 Window 类型的对象 mWindow,那么是什么时候初始化的呢?
是在 attach() 的时候。
还记得 attach() 是什么时候调用的吗?是在 ActivityThread.performLaunchActivity() 的时候:
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
Activity activity = null;
try {
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
} catch (Exception e) {
...ignore some code...
}
try {
...ignore some code...
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.voiceInteractor);
...ignore some code...
} catch (Exception e) { }
return activity;
}
在 attach() 里面做了些什么呢?
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback {
private Window mWindow;
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, IVoiceInteractor voiceInteractor) {
...ignore some code...
//就是在这里实例化了 Window 对象
mWindow = PolicyManager.makeNewWindow(this);
//设置各种回调
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
//这就是传说中的 UI 线程,也就是 ActivityThread 所在的,开启了消息循环机制的线程,所以在 Actiivty 所在线程中使用 Handler 不需要使用 Loop 开启消息循环。
mUiThread = Thread.currentThread();
...ignore some code...
//终于见到了前面提到的 WindowManager,可以看到,WindowManager 属于一种系统服务
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();
}
}
attach() 是 Activity 实例化之后,调用的第一个函数,在这个时候,就实例化了 Window。那么这个 PolicyManager 是什么玩意?
mWindow = PolicyManager.makeNewWindow(this);
来来来,咱们一起 RTFSC(Read The Fucking Source Code)!
public final class PolicyManager {
private static final String POLICY_IMPL_CLASS_NAME =
"com.android.internal.policy.impl.Policy";
private static final IPolicy sPolicy;
static {
// Pull in the actual implementation of the policy at run-time
try {
Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
sPolicy = (IPolicy)policyClass.newInstance();
} catch (ClassNotFoundException ex) {
throw new RuntimeException(
POLICY_IMPL_CLASS_NAME + " could not be loaded", ex);
} catch (InstantiationException ex) {
throw new RuntimeException(
POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
} catch (IllegalAccessException ex) {
throw new RuntimeException(
POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
}
}
private PolicyManager() {}
public static Window makeNewWindow(Context context) {
return sPolicy.makeNewWindow(context);
}
}
“Policy”是“策略”的意思,所以就是一个策略管理器,采用了策略设计模式。而 sPolicy 是一个 IPolicy 类型,IPolicy 实际上是一个接口
public interface IPolicy {}
所以说,sPolicy 的实际类型是在静态代码块里面,利用反射进行实例化的 Policy 类型。静态代码块中的代码在类文件加载进类加载器之后就会执行,
sPolicy 就实现了实例化。
那我们看下在 Policy 里面实际上是做了什么
public class Policy implements IPolicy {
//看见 PhoneWindow 眼熟么?还有 DecorView,眼熟么?这就是前面所说的那个位于最下面的 View,findViewById() 就是在它里面找的
private static final String[] preload_classes = {
"com.android.internal.policy.impl.PhoneLayoutInflater",
"com.android.internal.policy.impl.PhoneWindow",
"com.android.internal.policy.impl.PhoneWindow$1",
"com.android.internal.policy.impl.PhoneWindow$DialogMenuCallback",
"com.android.internal.policy.impl.PhoneWindow$DecorView",
"com.android.internal.policy.impl.PhoneWindow$PanelFeatureState",
"com.android.internal.policy.impl.PhoneWindow$PanelFeatureState$SavedState",
};
//由于性能方面的原因,在当前 Policy 类加载的时候,会预加载一些特定的类
static {
for (String s : preload_classes) {
try {
Class.forName(s);
} catch (ClassNotFoundException ex) {
Log.e(TAG, "Could not preload class for phone policy: " + s);
}
}
}
//终于找到 PhoneWindow 了,我没骗你吧,前面咱们所说的 Window 终于可以换成 PhoneWindow 了~
public Window makeNewWindow(Context context) {
return new PhoneWindow(context);
}
}
PhoneWindow.setContentView() 到底发生了什么?
上面说了这么多,实际上只是追踪到了 PhoneWindow.setContentView(),下面看一下到底在这里执行了什么:
@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);
}
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
@Override
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
} else {
mContentParent.addView(view, params);
}
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
当我们第一次调用 serContentView() 的时候,mContentParent 是没有进行过初始化的,所以会调用 installDecor()。
为什么能确定 mContentParent 是没有初始化的呢?因为 mContentParent 就是在 installDecor() 里面赋值的
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
...
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
}
}
在 generateDecor() 做了什么?返回了一个 DecorView 对象。
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
还记得前面推断出的,DecorView 是一个 ViewGroup 的结论吗?看下面,DecorView 继承自 FrameLayout,所以咱们的推论是完全正确的。而且
DecorView 是 PhoneWindow 的私有内部类,这两个类关系紧密!
public class PhoneWindow extends Window implements MenuBuilder.Callback {
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {}
}
咱们再看一下在对 mContentParent 赋值的 generateLayout(mDecor) 做了什么
protected ViewGroup generateLayout(DecorView decor) {
...判断并设置了一堆的标志位...
//这个是我们的界面将要采用的基础布局 xml 文件的 id
int layoutResource;
//根据标志位,给 layoutResource 赋值
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
}
...我们设置不同的主题以及样式,会采用不同的布局文件...
else {
//我们在下面代码验证的时候,就会用到这个布局,记住它哦
layoutResource = R.layout.screen_simple;
}
//要开始更改 mDecor 啦~
mDecor.startChanging();
//将 xml 文件解析成 View 对象,至于 LayoutInflater 是如何将 xml 解析成 View 的,咱们后面再说
View in = mLayoutInflater.inflate(layoutResource, null);
//decor 和 mDecor 实际上是同一个对象,一个是形参,一个是成员变量
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
//这里的常量 ID_ANDROID_CONTENT 就是 public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
//而且,由于是直接执行的 findViewById(),所以本质上还是调用的 mDecor.findViewById()。而在上面的 decor.addView() 执行之前,decor 里面是空白的,所以我们可以断定,layoutResource 所指向的 xml 布局文件内部,一定存在一个叫做“content”的 ViewGroup
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
......
mDecor.finishChanging();
//最后把 id 为 content 的一个 ViewGroup 返回了
return contentParent;
}
当上的代码执行之后,mDecor 和 mContentParent 就初始化了,往下就会执行下面的代码,利用 LayoutInflater 把咱们传进来的 layoutResID 转化成
View 对象,然后添加到 id 为 content 的 mContentParent 中
mLayoutInflater.inflate(layoutResID, mContentParent);
所以到目前为止,咱们已经知道了以下几个事实,咱们总结一下:
- DecorView 是 PhoneWindow 的内部类,继承自 FrameLayout,是最底层的界面
- PhoneWindow 是 Window 的唯一子类,他们的作用就是提供标准 UI,标题,背景和按键操作
- 在 DecorView 中会根据用户选择的不同标志,选择不同的 xml 文件,并且这些布局会被添加到 DecorView 中
- 在 DecorView 中,一定存在一个叫做“content”的 ViewGroup,而且我们在 xml 文件中声明的布局文件,会被添加进去
既然是事实,那么怎么才能验证一下呢?
如何验证上一个问题
首先,说明一下运行条件:
//主题
name="AppTheme" parent="@android:style/Theme.Holo.Light.NoActionBar"
//编译版本
android {
compileSdkVersion 19
buildToolsVersion '19.1.0'
defaultConfig {
applicationId "com.socks.uitestapp"
minSdkVersion 15
targetSdkVersion 19
versionCode 1
versionName "1.0"
}
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.android.support:appcompat-v7:19.1.0'
}
//Activity 代码
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
//activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="Hello World!"
android:textSize="20sp" />
OK,咱们的软件已经准备好了,采用的是最简单的布局,界面效果如下:
下面用 Hierarchy 看一下树状结构:
第一层,就是上面的 DecorView,里面有一个线性布局,上面的是 ViewStub,下面就是 id 为 content 的 ViewGroup,是一个 FrameLayout。而我们通过 setContentView() 设置的布局,就是 TextView 了。
能不能在源码里面找到源文件呢?当然可以,这个布局就是 screen_simple.xml
frameworks/base/core/res/res/layout/screen_simple.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
所以,即使你不调用 setContentView(),在一个空 Activity 上面,也是有布局的。而且肯定有一个 DecorView,一个 id 为 content 的 FrameLayout。
你可以采用下面的方式获取到 DecorView,但是你不能获取到一个 DecorView 实例,只能获取到 ViewGroup。
下面贴上这个图,你就可以看明白了(转自 工匠若水 )
ViewGroup view = (ViewGroup) getWindow().getDecorView();
我们通过 setContentView() 设置的界面,为什么在 onResume() 之后才对用户可见呢?
有开发经验的朋友应该知道,我们的界面元素在 onResume() 之后才对用户是可见的,这是为啥呢?
那我们就追踪一下,onResume() 是什么时候调用的,然后看看做了什么操作就 Ok 了。
这一下,我们又要从 ActivityThread 开始说起了,不熟悉的快去看前一篇文章《Activity 启动过程全解析》]( http://blog.csdn.net/zhaokaiqiang1992/article/details/49428287 )。
话说,前文说到,我们想要开启一个 Activity 的时候,ActivityThread 的 handleLaunchActivity() 会在 Handler 中被调用
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
//就是在这里调用了 Activity.attach() 呀,接着调用了 Activity.onCreate() 和 Activity.onStart() 生命周期,但是由于只是初始化了 mDecor,添加了布局文件,还没有把
//mDecor 添加到负责 UI 显示的 PhoneWindow 中,所以这时候对用户来说,是不可见的
Activity a = performLaunchActivity(r, customIntent);
......
if (a != null) {
//这里面执行了 Activity.onResume()
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed);
if (!r.activity.mFinished && r.startsNotResumed) {
try {
r.activity.mCalled = false;
//执行 Activity.onPause()
mInstrumentation.callActivityOnPause(r.activity);
}
}
}
}
所以说,ActivityThread.handleLaunchActivity 执行完之后,Activity 的生命周期已经执行了 4 个(onCreate、onStart()、onResume、onPause())。
下面咱们重点看下 handleResumeActivity() 做了什么
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
//这个时候,Activity.onResume() 已经调用了,但是现在界面还是不可见的
ActivityClientRecord r = performResumeActivity(token, clearHide);
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 对用户不可见
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
//这里记住这个 WindowManager.LayoutParams 的 type 为 TYPE_BASE_APPLICATION,后面介绍 Window 的时候会见到
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
//终于被添加进 WindowManager 了,但是这个时候,还是不可见的
wm.addView(decor, l);
}
if (!r.activity.mFinished && willBeVisible
&& r.activity.mDecor != null && !r.hideForNow) {
//在这里,执行了重要的操作!
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}
}
从上面的分析中我们知道,其实在 onResume() 执行之后,界面还是不可见的,当我们执行了 Activity.makeVisible() 方法之后,界面才对我们是可见的
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
OK,其实讲到了这里,关于 Activity 中的界面显示应该算是告一段落了,我们知道了 Activity 的生命周期方法的调用时机,还知道了一个最简单的 Activity
的界面的构成,并了解了 Window、PhoneWindow、DecorView、WindowManager 的存在。
但是我还是感觉不过瘾,因为上面只是在流程上大体上过了一遍,对于 Window、WindowManager 的深入了解还不够,所以下面就开始讲解 Window、WindowManager 等相关类的稍微高级点的知识。
前面看累了的朋友,可以上个厕所,泡个咖啡,休息下继续往下看。
ViewManager、WindowManager、WindowManagerImpl、WindowManagerGlobal 到底都是些什么玩意?
WindowManager 其实是一个接口,和 Window 一样,起作用的是它的实现类
public interface WindowManager extends ViewManager {
//对这个异常熟悉么?当你往已经销毁的 Activity 中添加 Dialog 的时候,就会抛这个异常
public static class BadTokenException extends RuntimeException {
public BadTokenException() {
}
public BadTokenException(String name) {
super(name);
}
}
//其实 WindowManager 里面 80%的代码是用来描述这个内部静态类的
public static class LayoutParams extends ViewGroup.LayoutParams
implements Parcelable {
}
}
WindowManager 继承自 ViewManager 这个接口,从注释和方法我们可以知道,这个就是用来描述可以对 Activity 中的子 View 进行添加和移除能力的接口。
/** Interface to let you add and remove child views to an Activity. To get an instance
* of this class, call {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.
*/
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 的时候,到底是在使用哪个类呢?
是 WindowManagerImpl。
public final class WindowManagerImpl implements WindowManager {}
怎么知道的呢?那我们还要从 Activity.attach() 说起
话说,在 attach() 里面完成了 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, IVoiceInteractor voiceInteractor) {
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
mWindowManager = mWindow.getWindowManager();
}
那我们只好看下(WindowManager)context.getSystemService(Context.WINDOW_SERVICE) 是什么玩意了。
这里要说明的是,context 是一个 ContextImpl 对象,这里先记住就好,以后再细说。
class ContextImpl extends Context {
//静态代码块,完成各种系统服务的注册
static {
......
registerService(WINDOW_SERVICE, new ServiceFetcher() {
Display mDefaultDisplay;
public Object getService(ContextImpl ctx) {
Display display = ctx.mDisplay;
if (display == null) {
if (mDefaultDisplay == null) {
DisplayManager dm = (DisplayManager)ctx.getOuterContext().
getSystemService(Context.DISPLAY_SERVICE);
mDefaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY);
}
display = mDefaultDisplay;
}
//没骗你吧
return new WindowManagerImpl(display);
}});
......
}
@Override
public Object getSystemService(String name) {
ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
return fetcher == null ? null : fetcher.getService(this);
}
}
要注意的是,这里返回的 WindowManagerImpl 对象,最终并不是和我们的 Window 关联的,而且这个方法是有可能返回 null 的,所以在
Window.setWindowManager() 的时候,进行了处理
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);
}
//设置 parentWindow,创建真正关联的 WindowManagerImpl 对象
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
public final class WindowManagerImpl implements WindowManager {
//最终调用的这个构造
private WindowManagerImpl(Display display, Window parentWindow) {
mDisplay = display;
mParentWindow = parentWindow;
}
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mDisplay, parentWindow);
}
}
所以说,每一个 Activity 都有一个 PhoneWindow 成员变量,并且也都有一个 WindowManagerImpl,而且,PhoneWindow 和 WindowManagerImpl
在 Activity.attach() 的时候进行了关联。
插一张类图(转自 工匠若水 )
知道了这些,那下面的操作就可以直接看 WindowManagerImpl 了。
其实 WindowManagerImpl 这个类也没有什么看头,为啥这么说呢?因为他其实是代理模式中的代理。是谁的代理呢?是 WindowManagerGlobal。
public final class WindowManagerImpl implements WindowManager {
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
private final Display mDisplay;
private final Window mParentWindow;
@Override
public void addView(View view, ViewGroup.LayoutParams params) {
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
@Override
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
mGlobal.updateViewLayout(view, params);
}
@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}
@Override
public void removeViewImmediate(View view) {
mGlobal.removeView(view, true);
}
}
从上面的代码中可以看出来,WindowManagerImpl 里面对 ViewManager 接口内方法的实现,都是通过代理 WindowManagerGlobal 的方法实现的,
所以重点转移到了 WindowManagerGlobal 这个类。
还记得前面我们的 DecorView 被添加到了 WindowManager 吗?
wm.addView(decor, l);
其实最终调用的是 WindowManagerGlobal.addView();
public final class WindowManagerGlobal {
private static IWindowManager sWindowManagerService;
private static IWindowSession sWindowSession;
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>();
//WindowManagerGlobal 是单例模式
private static WindowManagerGlobal sDefaultWindowManager;
public static WindowManagerGlobal getInstance() {
synchronized (WindowManagerGlobal.class) {
if (sDefaultWindowManager == null) {
sDefaultWindowManager = new WindowManagerGlobal();
}
return sDefaultWindowManager;
}
}
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
......
synchronized (mLock) {
ViewRootImpl root;
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
......
try {
//注意下这个方法,因为下面介绍 ViewRootImpl 的时候会用到
root.setView(view, wparams, panelParentView);
}catch (RuntimeException e) {
}
}
}
我们看到,WindowManagerGlobal 是单例模式,所以在一个 App 里面只会有一个 WindowManagerGlobal 实例。在 WindowManagerGlobal 里面维
护了三个集合,分别存放添加进来的 View(实际上就是 DecorView),布局参数 params,和刚刚实例化的 ViewRootImpl 对象,
WindowManagerGlobal 到底干嘛的呢?
其实,WindowManagerGlobal 是和 WindowManagerService(即 WMS) 通信的。
还记得在上一篇文章中我们介绍 ActivityThread 和 AMS 之间的 IBinder 通信的吗?是的,这里也是 IBinder 通信。
public static IWindowSession getWindowSession() {
synchronized (WindowManagerGlobal.class) {
if (sWindowSession == null) {
try {
InputMethodManager imm = InputMethodManager.getInstance();
IWindowManager windowManager = getWindowManagerService();
sWindowSession = windowManager.openSession(
......
} catch (RemoteException e) {
Log.e(TAG, "Failed to open window session", e);
}
}
return sWindowSession;
}
}
public static IWindowManager getWindowManagerService() {
synchronized (WindowManagerGlobal.class) {
if (sWindowManagerService == null) {
//ServiceManager 是用来管理系统服务的,比如 AMS、WMS 等,这里就获取到了 WMS 的客户端代理对象
sWindowManagerService = IWindowManager.Stub.asInterface(
ServiceManager.getService("window"));
}
return sWindowManagerService;
}
}
首先通过上面的方法获取到 IBinder 对象,然后转化成了 WMS 在本地的代理对象 IWindowManager,然后通过 openSession() 初始化了 sWindowSession
对象。这个对象是干什么的呢?
“Session“是会话的意思,这个类就是为了实现与 WMS 的会话的,谁和 WMS 的对话呢?WindowManagerGlobal 类内部并没有用这个类呀!
是 ViewRootImpl 与 WMS 的对话。
ViewRootImpl 是什么?有什么作用?ViewRootImpl 如何与 WMS 通信
你还记得么?在前面将 WindowManagerGlobal.addView() 的时候,实例化了一个 ViewRootImpl,然后添加到了一个集合里面,咱们先看下 ViewRootImpl 的构造函数吧
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
public ViewRootImpl(Context context, Display display) {
mContext = context;
//获取 WindowSession
mWindowSession = WindowManagerGlobal.getWindowSession();
mDisplay = display;
......
mWindow = new W(this);
//默认不可见
mViewVisibility = View.GONE;
//这个数值就是屏幕宽度的 dp 总数
mDensity = context.getResources().getDisplayMetrics().densityDpi;
mChoreographer = Choreographer.getInstance();
mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
}
}
在这个构造方法里面,主要是完成了各种参数的初始化,并且最关键的,获取到了前面介绍的 WindowSession,那么你可能好奇了,这个 ViewRootImpl
到底有什么作用呢?
ViewRootImpl 负责管理视图树和与 WMS 交互,与 WMS 交互是通过 WindowSession。而且 ViewRootImpl 也负责 UI 界面的布局与渲染,负责把一些事件分发至 Activity,以便 Activity 可以截获事件。大多数情况下,它管理 Activity 顶层视图 DecorView,它相当于 MVC 模型中的 Controller。
WindowSession 是 ViewRootImpl 获取之后,主动和 WMS 通信的,但是我们在前面的文章知道,客户端和服务器需要互相持有对方的代理引用,才能实现双向通信,那么 WMS 是怎么得到 ViewRootImpl 的通信代理的呢?
是在 ViewRootImpl.setView() 的时候。
还记得不?在上面介绍 WindowManagerGlobal.addView() 的时候,我还重点说了下,在这个方法的 try 代码块中,调用了 ViewRootImpl.setView(),下面咱们看下这个方法干嘛了:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
int res;
requestLayout();
try {
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mInputChannel);
}catch (RemoteException e) {
throw new RuntimeException("Adding window failed", e);
} finally {
}
}
}
}
为了突出重点,我简化了很多代码,从上面可以看出来,是 mWindowSession.addToDisplay() 这个方法把 mWindow 传递给我 WMS,WMS 就持有了
当前 ViewRootlmpl 的代理,就可以调用 W 对象让 ViewRootlmpl 做一些事情了。
这样,双方都有了对方的接口,WMS 中的 Session 注册到 WindowManagerGlobal 的成员 WindowSession 中,ViewRootImpl::W 注册到 WindowState 中的成员 mClient 中。前者是为了 App 改变 View 结构时请求 WMS 为其更新布局。后者代表了 App 端的一个添加到 WMS 中的 View,每一个像这样通过 WindowManager 接口中 addView() 添加的窗口都有一个对应的 ViewRootImpl,也有一个相应的 ViewRootImpl::W。它可以理解为是 ViewRootImpl 中暴露给 WMS 的接口,这样 WMS 可以通过这个接口和 App 端通信。
另外源码中很多地方采用了这种将接口隐藏为内部类的方式,这样可以实现六大设计原则之一——接口最小原则。
从什么时候开始绘制整个 Activity 的 View 树的?
注意前面代码中的 requestLayout();因为这个方法执行之后,我们的 ViewRootImpl 才开始绘制整个 View 树!
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//暂停 UI 线程消息队列对同步消息的处理
mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
//向 Choreographer 注册一个类型为 CALLBACK_TRAVERSAL 的回调,用于处理 UI 绘制
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
}
}
“Choreographer 就是一个消息处理器,根据 vsync 信号 来计算 frame“
解释起来比较麻烦,我们暂时不展开讨论,你只要知道,当回调被触发之后,mTraversalRunnable 对象的 run() 就会被调用
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
doTraversal() 中最关键的,就是调用了 performTraversals(),然后就开始 mesure,layout,draw 了,这里面的具体逻辑本篇文章不讲,因为重点是
Activity 的界面显示流程,这一块属于 View 的,找时间单独拿出来说
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "performTraversals");
try {
performTraversals();
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
来回倒腾了这么多,终于看见界面了,让我哭会 T^T
Window 的类型有几种?分别在什么情况下会使用到哪一种?
Window 的类型是根据 WindowManager.LayoutParams 的 type 属性相关的,根据类型可以分为三类:
- 取值在 FIRST_APPLICATION_WINDOW 与 LAST_APPLICATION_WINDOW 之间(1-99),是常用的顶层应用程序窗口,须将 token 设置成 Activity 的 token,比如前面开启 Window 的时候设置的类型即为 TYPE_APPLICATION
- 在 FIRST_SUB_WINDOW 和 LAST_SUB_WINDOW(1000-1999) 之间,与顶层窗口相关联,需将 token 设置成它所附着宿主窗口的 token,比如 PopupWindow 就是 TYPE_APPLICATION_PANEL
- 取值在 FIRST_SYSTEM_WINDOW 和 LAST_SYSTEM_WINDOW(2000-2999) 之间,不能用于应用程序,使用时需要有特殊权限,它是特定的系统功能才能使用,比如 Toast 就是 TYPE_TOAST=2005,所以不需要特殊权限
下面是所有的 Type 说明
//WindowType:开始应用程序窗口
public static final int FIRST_APPLICATION_WINDOW = 1;
//WindowType:所有程序窗口的 base 窗口,其他应用程序窗口都显示在它上面
public static final int TYPE_BASE_APPLICATION = 1;
//WindowType:普通应用程序窗口,token 必须设置为 Activity 的 token 来指定窗口属于谁
public static final int TYPE_APPLICATION = 2;
//WindowType:应用程序启动时所显示的窗口,应用自己不要使用这种类型,它被系统用来显示一些信息,直到应用程序可以开启自己的窗口为止
public static final int TYPE_APPLICATION_STARTING = 3;
//WindowType:结束应用程序窗口
public static final int LAST_APPLICATION_WINDOW = 99;
//WindowType:SubWindows 子窗口,子窗口的 Z 序和坐标空间都依赖于他们的宿主窗口
public static final int FIRST_SUB_WINDOW = 1000;
//WindowType: 面板窗口,显示于宿主窗口的上层
public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;
//WindowType:媒体窗口(例如视频),显示于宿主窗口下层
public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW+1;
//WindowType:应用程序窗口的子面板,显示于所有面板窗口的上层
public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW+2;
//WindowType:对话框,类似于面板窗口,绘制类似于顶层窗口,而不是宿主的子窗口
public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW+3;
//WindowType:媒体信息,显示在媒体层和程序窗口之间,需要实现半透明效果
public static final int TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW+4;
//WindowType:子窗口结束
public static final int LAST_SUB_WINDOW = 1999;
//WindowType:系统窗口,非应用程序创建
public static final int FIRST_SYSTEM_WINDOW = 2000;
//WindowType:状态栏,只能有一个状态栏,位于屏幕顶端,其他窗口都位于它下方
public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;
//WindowType:搜索栏,只能有一个搜索栏,位于屏幕上方
public static final int TYPE_SEARCH_BAR = FIRST_SYSTEM_WINDOW+1;
//WindowType:电话窗口,它用于电话交互(特别是呼入),置于所有应用程序之上,状态栏之下
public static final int TYPE_PHONE = FIRST_SYSTEM_WINDOW+2;
//WindowType:系统提示,出现在应用程序窗口之上
public static final int TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW+3;
//WindowType:锁屏窗口
public static final int TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW+4;
//WindowType:信息窗口,用于显示 Toast
public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;
//WindowType:系统顶层窗口,显示在其他一切内容之上,此窗口不能获得输入焦点,否则影响锁屏
public static final int TYPE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+6;
//WindowType:电话优先,当锁屏时显示,此窗口不能获得输入焦点,否则影响锁屏
public static final int TYPE_PRIORITY_PHONE = FIRST_SYSTEM_WINDOW+7;
//WindowType:系统对话框
public static final int TYPE_SYSTEM_DIALOG = FIRST_SYSTEM_WINDOW+8;
//WindowType:锁屏时显示的对话框
public static final int TYPE_KEYGUARD_DIALOG = FIRST_SYSTEM_WINDOW+9;
//WindowType:系统内部错误提示,显示于所有内容之上
public static final int TYPE_SYSTEM_ERROR = FIRST_SYSTEM_WINDOW+10;
//WindowType:内部输入法窗口,显示于普通 UI 之上,应用程序可重新布局以免被此窗口覆盖
public static final int TYPE_INPUT_METHOD = FIRST_SYSTEM_WINDOW+11;
//WindowType:内部输入法对话框,显示于当前输入法窗口之上
public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;
//WindowType:墙纸窗口
public static final int TYPE_WALLPAPER = FIRST_SYSTEM_WINDOW+13;
//WindowType:状态栏的滑动面板
public static final int TYPE_STATUS_BAR_PANEL = FIRST_SYSTEM_WINDOW+14;
//WindowType:安全系统覆盖窗口,这些窗户必须不带输入焦点,否则会干扰键盘
public static final int TYPE_SECURE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+15;
//WindowType:拖放伪窗口,只有一个阻力层(最多),它被放置在所有其他窗口上面
public static final int TYPE_DRAG = FIRST_SYSTEM_WINDOW+16;
//WindowType:状态栏下拉面板
public static final int TYPE_STATUS_BAR_SUB_PANEL = FIRST_SYSTEM_WINDOW+17;
//WindowType:鼠标指针
public static final int TYPE_POINTER = FIRST_SYSTEM_WINDOW+18;
//WindowType:导航栏(有别于状态栏时)
public static final int TYPE_NAVIGATION_BAR = FIRST_SYSTEM_WINDOW+19;
//WindowType:音量级别的覆盖对话框,显示当用户更改系统音量大小
public static final int TYPE_VOLUME_OVERLAY = FIRST_SYSTEM_WINDOW+20;
//WindowType:起机进度框,在一切之上
public static final int TYPE_BOOT_PROGRESS = FIRST_SYSTEM_WINDOW+21;
//WindowType:假窗,消费导航栏隐藏时触摸事件
public static final int TYPE_HIDDEN_NAV_CONSUMER = FIRST_SYSTEM_WINDOW+22;
//WindowType:梦想(屏保) 窗口,略高于键盘
public static final int TYPE_DREAM = FIRST_SYSTEM_WINDOW+23;
//WindowType:导航栏面板(不同于状态栏的导航栏)
public static final int TYPE_NAVIGATION_BAR_PANEL = FIRST_SYSTEM_WINDOW+24;
//WindowType:universe 背后真正的窗户
public static final int TYPE_UNIVERSE_BACKGROUND = FIRST_SYSTEM_WINDOW+25;
//WindowType:显示窗口覆盖,用于模拟辅助显示设备
public static final int TYPE_DISPLAY_OVERLAY = FIRST_SYSTEM_WINDOW+26;
//WindowType:放大窗口覆盖,用于突出显示的放大部分可访问性放大时启用
public static final int TYPE_MAGNIFICATION_OVERLAY = FIRST_SYSTEM_WINDOW+27;
//WindowType:......
public static final int TYPE_KEYGUARD_SCRIM = FIRST_SYSTEM_WINDOW+29;
public static final int TYPE_PRIVATE_PRESENTATION = FIRST_SYSTEM_WINDOW+30;
public static final int TYPE_VOICE_INTERACTION = FIRST_SYSTEM_WINDOW+31;
public static final int TYPE_ACCESSIBILITY_OVERLAY = FIRST_SYSTEM_WINDOW+32;
//WindowType:系统窗口结束
public static final int LAST_SYSTEM_WINDOW = 2999;
为什么使用 PopWindow 的时候,不设置背景就不能触发事件?
我们在使用 PopupWindow 的时候,会发现如果不给 PopupWindow 设置背景,那么就不能触发点击返回事件,有人认为这个是 BUG,其实并不是的。
我们以下面的方法为例,其实所有的显示方法都有下面的流程:
public void showAtLocation(IBinder token, int gravity, int x, int y) {
if (isShowing() || mContentView == null) {
return;
}
mIsShowing = true;
mIsDropdown = false;
WindowManager.LayoutParams p = createPopupLayout(token);
p.windowAnimations = computeAnimationResource();
//在这里会根据不同的设置,配置不同的 LayoutParams 属性
preparePopup(p);
if (gravity == Gravity.NO_GRAVITY) {
gravity = Gravity.TOP | Gravity.START;
}
p.gravity = gravity;
p.x = x;
p.y = y;
if (mHeightMode < 0) p.height = mLastHeight = mHeightMode;
if (mWidthMode < 0) p.width = mLastWidth = mWidthMode;
invokePopup(p);
}
我们重点看下 preparePopup()
private void preparePopup(WindowManager.LayoutParams p) {
//根据背景的设置情况进行不同的配置
if (mBackground != null) {
final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
int height = ViewGroup.LayoutParams.MATCH_PARENT;
//如果设置了背景,就用一个 PopupViewContainer 对象来包裹之前的 mContentView,并设置背景后
PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);
PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, height
);
popupViewContainer.setBackground(mBackground);
popupViewContainer.addView(mContentView, listParams);
mPopupView = popupViewContainer;
} else {
mPopupView = mContentView;
}
}
为啥包了一层 PopupViewContainer,就可以处理按钮点击事件了?因为 PopupWindow 没有相关事件回调,也没有重写按键和触摸方法,所以接收不
到对应的信号
public class PopupWindow {}
而 PopupViewContainer 则可以,因为它重写了相关方法
private class PopupViewContainer extends FrameLayout {
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (getKeyDispatcherState() == null) {
return super.dispatchKeyEvent(event);
}
if (event.getAction() == KeyEvent.ACTION_DOWN
&& event.getRepeatCount() == 0) {
KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null) {
state.startTracking(event, this);
}
return true;
} else if (event.getAction() == KeyEvent.ACTION_UP) {
//back 键消失
KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null && state.isTracking(event) && !event.isCanceled()) {
dismiss();
return true;
}
}
return super.dispatchKeyEvent(event);
} else {
return super.dispatchKeyEvent(event);
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
return true;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int x = (int) event.getX();
final int y = (int) event.getY();
//触摸在外面就消失
if ((event.getAction() == MotionEvent.ACTION_DOWN)
&& ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
dismiss();
return true;
} else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
dismiss();
return true;
} else {
return super.onTouchEvent(event);
}
}
}
在 Activity 中使用 Dialog 的时候,为什么有时候会报错“Unable to add window – token is not valid; is your activity running?”?
这种情况一般发生在什么时候?一般发生在 Activity 进入后台,Dialog 没有主动 Dismiss 掉,然后从后台再次进入 App 的时候。
为什么会这样呢?
还记得前面说过吧,子窗口类型的 Window,比如 Dialog,想要显示的话,比如保证 appToken 与 Activity 保持一致,而当 Activity 销毁,再次回来的时候,Dialog 试图重新创建,调用 ViewRootImp 的 setView() 的时候就会出问题,所以记得在 Activity 不可见的时候,主动 Dismiss 掉 Dialog。
if (res < WindowManagerGlobal.ADD_OKAY) {
switch (res) {
case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not valid; is your activity running?");
case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not for an application");
case WindowManagerGlobal.ADD_APP_EXITING:
throw new WindowManager.BadTokenException(
"Unable to add window -- app for token " + attrs.token
+ " is exiting");
case WindowManagerGlobal.ADD_DUPLICATE_ADD:
throw new WindowManager.BadTokenException(
"Unable to add window -- window " + mWindow
+ " has already been added");
case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
// Silently ignore -- we would have just removed it
// right away, anyway.
return;
case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
throw new WindowManager.BadTokenException(
"Unable to add window " + mWindow +
" -- another window of this type already exists");
case WindowManagerGlobal.ADD_PERMISSION_DENIED:
throw new WindowManager.BadTokenException(
"Unable to add window " + mWindow +
" -- permission denied for this window type");
case WindowManagerGlobal.ADD_INVALID_DISPLAY:
throw new WindowManager.InvalidDisplayException(
"Unable to add window " + mWindow +
" -- the specified display can not be found");
}
throw new RuntimeException(
"Unable to add window -- unknown error code " + res);
}
}
为什么 Toast 需要由系统统一控制,在子线程中为什么不能显示 Toast?
首先 Toast 也属于窗口系统,但是并不是属于 App 的,是由系统同一控制的。
关于这一块不想说太多,具体实现机制请参考后面的文章。
为了看下面的内容,你需要知道以下几件事情:
- Toast 的显示是由系统 Toast 服务控制的,与系统之间的通信方式是 Binder
- 整个 Toast 系统会维持最多 50 个 Toast 的队列,依次显示
- 负责现实工作的是 Toast 的内部类 TN,它负责最终的显示与隐藏操作
- 负责给系统 Toast 服务发送内容的是 INotificationManager 的实现类,它负责在 Toast.show() 里面把 TN 对象传递给系统消息服务,service.enqueueToast(pkg, tn, mDuration);这样 Toast 服务就持有客户端的代理,可以通过 TN 来控制每个 Toast 的显示与隐藏。
再来张图(转自 工匠若水 )
ok,现在假如你知道上面这些啦,那么我们下面就看为什么在子线程使用 Toast.show() 会提示
"No Looper; Looper.prepare() wasn't called on this thread."
原因很简单,因为 TN 在操作 Toast 的时候,是通过 Handler 做的
@Override
public void show() {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.post(mShow);
}
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.post(mHide);
}
所以说,TN 初始化的线程必须为主线程,在子线程中使用 Handler,由于没有消息队列,就会造成这个问题。
结语
上面写了这么多,你可能看了前面忘了后面,下面,凯子哥给你总结一下,这篇文章到底讲了什么东西:
- 每个 Activity,都至少有一个 Window,这个 Window 实际类型为 PhoneWindow,当 Activity 中有子窗口,比如 Dialog 的时候,就会出现多个 Window。Activity 的 Window 是我们控制的,状态栏和导航栏的 Window 由系统控制。
- 在 DecorView 的里面,一定有一个 id 为 content 的 FraneLayout 的布局容器,咱们自己定义的 xml 布局都放在这里面。
- Activity 的 Window 里面有一个 DecorView,它使继承自 FrameLayout 的一个自定义控件,作为整个 View 层的容器,及 View 树的根节点。
- Window 是虚拟的概念,DecorView 才是看得见,摸得着的东西,Activity.setContentView() 实际调用的是 PhoneWindow.setContentView(),在这里面实现了 DecorView 的初始化和 id 为 content 的 FraneLayout 的布局容器的初始化,并且会根据主题等配置,选择不同的 xml 文件。而且在 Activity.setContentView() 之后,Window 的一些特征位将被锁定。
- Activity.findViewById() 实际上调用的是 DecorView 的 findviewById(),这个方法在 View 中定义,但是是 final 的,实际起作用的是在 ViewGroup 中被重写的 findViewTraversal() 方法。
- Activity 的 mWindow 成员变量是在 attach() 的时候被初始化的,attach() 是 Activity 被通过反射手段实例化之后调用的第一个方法,在这之后生命周期方法才会依次调用
- 在 onResume() 刚执行之后,界面还是不可见的,只有执行完 Activity.makeVisible(),DecorView 才对用户可见
- ViewManager 这个接口里面就三个接口,添加、移除和更新,实现这个接口的有 WindowManager 和 ViewGroup,但是他们两个面向的对象是不一样的,WindowManager 实现的是对 Window 的操作,而 ViewGroup 则是对 View 的增、删、更新操作。
- WindowManagerImpl 是 WindowManager 的实现类,但是他就是一个代理类,代理的是 WindowManagerGlobal,WindowManagerGlobal 一个 App 里面就有一个,因为它是单例的,它里面管理了 App 中所有打开的 DecorView,ContentView 和 PhoneWindow 的布局参数 WindowManager.LayoutParams,而且 WindowManagerGlobal 这个类是和 WMS 通信用的,是通过 IWindowSession 对象完成这个工作的,而 IWindowSession 一个 App 只有一个,但是每个 ViewRootImpl 都持有对 IWindowSession 的引用,所以 ViewRootImpl 可以和 WMS 喊话,但是 WMS 怎么和 ViewRootImpl 喊话呢?是通过 ViewRootImpl::W 这个内部类实现的,而且源码中很多地方采用了这种将接口隐藏为内部类的方式,这样可以实现六大设计原则之一——接口最小原则,这样 ViewRootImpl 和 WMS 就互相持有对方的代理,就可以互相交流了
- ViewRootImpl 这个类每个 Activity 都有一个,它负责和 WMS 通信,同时相应 WMS 的指挥,还负责 View 界面的测量、布局和绘制工作,所以当你调用 View.invalidate() 和 View.requestLayout() 的时候,都会把事件传递到 ViewRootImpl,然后 ViewRootImpl 计算出需要重绘的区域,告诉 WMS,WMS 再通知其他服务完成绘制和动画等效果,当然,这是后话,咱们以后再说。
- Window 分为三种,子窗口,应用窗口和系统窗口,子窗口必须依附于一个上下文,就是 Activity,因为它需要 Activity 的 appToken,子窗口和 Activity 的 WindowManager 是一个的,都是根据 appToken 获取的,描述一个 Window 属于哪种类型,是根据 LayoutParam.type 决定的,不同类型有不同的取值范围,系统类的的 Window 需要特殊权限,当然 Toast 比较特殊,不需要权限
- PopupWindow 使用的时候,如果想触发按键和触摸事件,需要添加一个背景,代码中会根据是否设置背景进行不同的逻辑判断
- Dialog 在 Activity 不可见的时候,要主动 dismiss 掉,否则会因为 appToken 为空 crash
- Toast 属于系统窗口,由系统服务 NotificationManagerService 统一调度,NotificationManagerService 中维持着一个集合 ArrayList,最多存放 50 个 Toast,但是 NotificationManagerService 只负责管理 Toast,具体的现实工作由 Toast::TN 来实现
最后来一张 Android 的窗口管理框架(转自 ariesjzj )
OK,关于 Activity 的界面显示就说到这里吧,本篇文章大部分的内容来自于阅读下面参考文章之后的总结和思考,想了解更详细的可以研究下。
下次再见,拜拜~
参考文章
- Android 应用 Activity、Dialog、PopWindow、Toast 窗口添加机制及源码分析
- Android 应用 setContentView 与 LayoutInflater 加载解析机制源码分析
- Android 4.4(KitKat) 窗口管理子系统 - 体系框架
- Android 之 Window、WindowManager 与窗口管理
- 图解 Android - Android GUI 系统 (1) - 概论
尊重原创,转载请注明:From 凯子哥( http://blog.csdn.net/zhaokaiqiang1992 ) 侵权必究!
关注我的微博,可以获得更多精彩内容
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论