【凯子哥带你学Framework】Activity界面显示全解析(下)

咱们接着上篇继续讲,上篇没看的请戳:【凯子哥带你学Framework】Activity界面显示全解析(上)

如何验证上一个问题

首先,说明一下运行条件:

 //主题
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的,是由系统同一控制的。
关于这一块不想说太多,具体实现机制请参考后面的文章。

为了看下面的内容,你需要知道以下几件事情:

  1. Toast的显示是由系统Toast服务控制的,与系统之间的通信方式是Binder
  2. 整个Toast系统会维持最多50个Toast的队列,依次显示
  3. 负责现实工作的是Toast的内部类TN,它负责最终的显示与隐藏操作
  4. 负责给系统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<ToastRecord>,最多存放50个Toast,但是NotificationManagerService只负责管理Toast,具体的现实工作由Toast::TN来实现

最后来一张Android的窗口管理框架(转自ariesjzj

OK,关于Activity的界面显示就说到这里吧,本篇文章大部分的内容来自于阅读下面参考文章之后的总结和思考,想了解更详细的可以研究下。

下次再见,拜拜~

参考文章


尊重原创,转载请注明:From 凯子哥(<a href="http://blog.csdn.net/zhaokaiqiang1992">http://blog.csdn.net/zhaokaiqiang1992</a>) 侵权必究!

关注我的微博,可以获得更多精彩内容:http://weibo.com/zhaokaiqiang1992

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容