从系统角度理解Android的界面绘制

对于ViewTree的绘制流程,Android开发者都很熟悉了,但如果要从整个系统的全局角度出发,理解Android的界面绘制机制,就需要了解系统的层级分工和设计实现,本文记录了个人对该机制的一些理解。

我们先尝试理解整个系统的分工,再看Activity如何利用这个分工体系,最后再看View的绘制,其实,整个结构大概是这样的:
Android的界面绘制

整个系统的分工

任何一个操作系统要实现界面绘制,都需要处理好应用层、系统层和硬件层的分工协作,一般来说:
应用层负责定义画面的内容;
系统层负责综合整个屏幕的画面并保证流畅;
硬件层负责把数据输出到显示设备上。

我们分别来看:
应用层
除了系统窗口(如Toast),我们主要在Activity中绘制界面,这需要解决两个问题:

  1. 定义显示内容,基本原理就是在Canvas上绘制界面,然后调用surfaceholder.unlockCanvasAndPost函数,渲染到Surface中(视频是解码出视频帧,渲染到Surface上),Surface实际处于系统层,通过Ashmem共享内存传给Activity使用。
  2. 定义显示位置、层次和生命期,基本原理就是Activity的PhoneWindow利用Bindler机制和系统层通信,交给系统层去统一管理,如addView、removeView等都是通过WMS去做的。

系统Framework层
Android系统是在linux基础上扩展出来的,结构相对复杂,仅系统启动画面就有三个:BootLoader、linux内核、android系统服务,这三个都启动完,才会打开Launcher。

对于应用开发来说,最重要的是系统层中的Framework层,主要包括WMS和SurfaceFlinger两个系统服务,都运行在SystemServer进程中:

  1. WMS,主要负责两件事,window的层级、window的管理:
    层级上,WMS把所有界面分为应用window、子window和系统window三种,分别有自己的层级范围(1 - ~、1000 - ~、2000 - ~)。
    管理上,WMS要负责添加和移除window,管理这些window的位置、大小和生命变化。
    另外,WMS在调整window时,还需要通知SurfaceFlinger去更新界面,这样用户才能看到界面调整后的效果。
  2. SurfaceFlinger,主要负责两件事,为应用提供Surface、整合图形数据:
    为应用提供Surface,Activity获取Surface时,是WMS代为向SurfaceFlinger做的请求
    整合图形数据,根据WMS的窗口层级,把相关的Surface整合起来,并放到BufferQueue里,供底层绘制界面,实际上起到了生产者的作用。

系统HAL层、系统Linux Kernel层和硬件层
把系统层的这两部分和硬件层放在一起说,是因为他们联系更紧密,更偏底层,平时做应用开发时也基本不涉及到。
HAL层:是个抽象接口,处理界面的是Gralloc接口,HAL是为了解决linux硬件驱动的版权问题(Android开源,但是有些厂商的硬件驱动不开源,用HAL可以规避这些问题)。
Linux Kernel层:Linux 内核使用帧缓冲FrameBuffer来实现显示功能,作为内存缓冲区(有32个Slot),既是操作硬件设备的接口,又可以缓解画面流畅和完整性的问题(队列、生产和消费)。
硬件层:利用驱动把数据输出到显示设备上。

Activity与Framework层的合作机制

了解过系统分工,我们就知道,Activity需要与Framework层的SurfaceFlinger和WMS合作,才能实现界面绘制,这个合作机制需要解决这样几个问题:

  1. Window如何创建与使用
  2. Surface如何获取与使用
  3. View如何创建与绘制

Window的创建与使用

Window的管理核心在WMS,所以Window的创建和使用都需要与WMS建立通信,并交给WMS管理和调度。
关于创建,Activity启动时创建PhoneWindow,并与WMS建立通信,以便统一管理。
关于使用,在ViewRootImpl中调用WMS的addToDisplay,实现添加窗口。

具体过程如下:
首先,主线程ActivityThread启动Activity时,调用的performLaunchActivity会执行activity的attach函数关联context,application等,这时就会创建PhoneWindow,并把PhoneWindow和WMS关联,还会给WMS提供反向访问的Bindler参数mToken:

    //Activity源码
    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window) {
        ...
        mWindow = new PhoneWindow(this, window);
        ...
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);

setWindowManager函数是个多态特性,并不是PhoneWindow的函数,而是抽象类Window的函数:

//Window源码
    public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) {
        ...
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }

所以,PhoneWindow持有一个WindowManagerImpl实例,我们再看WindowManagerImpl的源码:

//WindowManagerImpl源码
public final class WindowManagerImpl implements WindowManager {
    //持有WindowManagerGlobal的单例对象
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

所以,App中所有addView的操作,都会经过Activity-->PhoneWindow-->WindowManagerImpl-->WindowManagerGlobal的路径,最终在WindowManagerGlobal中执行,
addView操作是在主线程resume Activity时发起的:

    //ActivityThread源码
    final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        ...
        ActivityClientRecord r = mActivities.get(token);
        ...
        r = performResumeActivity(token, clearHide, reason);
        ...
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
        ...
                ViewManager wm = a.getWindowManager();
        ...
                    wm.addView(decor, l);

根据前面的分析,addView真正的执行函数是在WindowManagerGlobal中:

//WindowManagerGlobal源码
    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
      ...
        ViewRootImpl root;
            root = new ViewRootImpl(view.getContext(), display);
      ...   //WindowManagerGlobal会保存每个窗口的viewrootimpl,decorview和params的
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
      ...
            root.setView(view, wparams, panelParentView);

然后,在ViewRootImpl的setView函数中,会调用WMS去addToDisplay:

//ViewRootImpl源码
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
        ...
        //DecorView放在attachInfo对象里
        mAttachInfo.mRootView = view;
                    //mWindow是IWindow对象,实际上是个用来跨进程通信的Bindler,这样WMS可以反向通信
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
        ...

这里面涉及到的类的关系,可以参考Android 窗口管理:如何添加窗口到WMS总结的一张类图

图片来自《Android 窗口管理:如何添加窗口到WMS》

所以,App内部的所有窗口由WindowManagerGlobal统一管理,而android系统的所有窗口由WMS统一管理。

Surface的获取

Surface是ViewRootImpl通过Bindler机制从SurfaceFlinger中通过Ashmem共享内存获取到的。
实际上,ViewRootImpl持有一个Surface对象,所以问题在于,ViewRootImpl中如何为Surface关联到了SurfaceFlinger中的对象。

具体过程如下:
首先,ViewRootImpl在setView和relayoutWindow时,都会调用relayoutWindow:

//ViewRootImpl源码
    
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
         ...
         requestLayout();
    ...
    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
    ...
    void scheduleTraversals() {
           ...
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    ...
    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    ...
    void doTraversal() {
            ...
            performTraversals();
    ...
    private void performTraversals() {
       ...
       relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
    
    ...
    private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
            boolean insetsPending) throws RemoteException {
            ...
               int relayoutResult = mWindowSession.relayout(
                mWindow, mSeq, params,
                (int) (mView.getMeasuredWidth() * appScale + 0.5f),
                (int) (mView.getMeasuredHeight() * appScale + 0.5f),
                viewVisibility, insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0,
                mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
                mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingConfiguration,
                mSurface);

最后的mWindowSession.relayout实际上就是Bindler通信了。
然后,WMS会先创建一个SurfaceControl,然后利用copyFrom获取其中的Surface。

//WindowManagerService源码
SurfaceControl surfaceControl = winAnimator.createSurfaceLocked();
if (surfaceControl != null) {
    outSurface.copyFrom(surfaceControl);

这会执行native函数nativeCreateFromSurfaceControl
最后,native层会通过SurfaceComposerClient去访问SurfaceFlinger,SurfaceFlinger从BufferQueue中dequeuBuffer,最终返回Surface。(为C++源码)

View如何创建与绘制

我们定义的ViewTree其实是DecorView中R.id.content那一部分,所以View的创建与绘制,核心在于建立与Window的关联,并能访问Surface。
关于Window,DecorView是关联了PhoneWindow。
关于Surface,DecorView通过ViewRootImpl访问Surface。

具体过程如下:
首先,View是从Activity的setContentView开始创建的:

    //Activity源码
    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);

实际上调用了PhoneWindow的setContentView:

    //PhoneWindow源码
    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
            ...
            installDecor();
    ...
    private void installDecor() {
            ...
            mDecor = generateDecor(-1);
            ...
            mDecor.setWindow(this);
    ...
    

在这个过程中,DecorView得到了一个PhoneWindow对象:

//DecorView源码
private PhoneWindow mWindow;
...
    void setWindow(PhoneWindow phoneWindow) {
        mWindow = phoneWindow;

可以看到,DecorView和PhoneWindow是互相引用的
这样,DecorView就完成了与PhoneWindow的关联,这样就可以被WMS管理
然后,DecorView需要获取到ViewRootImpl,DecorView的顶级父类View提供了getViewRootImpl()函数:

    //View源码
    public ViewRootImpl getViewRootImpl() {
        if (mAttachInfo != null) {
            return mAttachInfo.mViewRootImpl;
        }
        return null;
    }

Attachnfo是View的内部类,ViewRootImpl在初始化时,会创建这个对象,并把自己传进去:

ViewRootImpl源码
    public ViewRootImpl(Context context, Display display) {
      ...
      mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);

而且,ViewRootImpl在setView时也会设置自己的父控件:

//ViewRootImpl源码
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    ...
    view.assignParent(this);

这样,DecorView的具体功能就可以交给ViewRootImpl去实现:

//DecorView源码
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        ...
            getViewRootImpl().requestInvalidateRootRenderNode();
    }

最后,ViewRootImpl持有的Surface提供Canvas,用于绘制界面内容:

    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {
            ...
            canvas = mSurface.lockCanvas(dirty);

所以,利用ViewRootImpl的Surface提供的Canvas,就可以绘制了。

小结

总的来说,对开发者来说,除了Activity之外,最重要的就是ViewRootImpl、PhoneWindow和WindowManagerGlobal
1.ViewRootImpl
ViewRootImpl是绘制的起点(控制DectorView的绘制),也是绘制的目标(mSurface),每次WindowManagerGlobal中addView,都会生成并保存一个ViewRootImpl对象;最终的绘制canvas,也是渲染到ViewRootImpl持有的mSurface中去。
2.PhoneWindow
PhoneWindow夹在Activity和DecorView之间,主要起到解耦和减负的作用,可以把Activity与View的管理/window的管理切割开。
例如,添加View实际上是交给WindowManager去addView/removeView/updateView,但是Activity不需要直接与WindowManager交互,而是让PhoneWindow去setContentView,PhoneWindow再去调用WindowManager的addView操作。
3.WindowManager
WindowManager是个抽象类,只有WindowManagerImpl一个实现,而WindowManagerImpl实际上。
ViewRootImpl被WindowManagerGlobal严密地管理了起来,WMS管理window时,也是通过操纵ViewRootImpl实现的,所以都说ViewRootImpl是WindowManager和DecorView的连接纽带。

其他

深入理解Android的界面绘制机制,我们就能理解很多扩展功能的原理:
扩展场景1
自定义系统开机画面,虽然没有启动Android,但是可以操作硬件驱动、或操作linux的framebuffer实现界面绘制,这也是各厂商自己定制系统时的修改方法。
扩展场景2
侧滑App,为什么可以向一侧滑动整个App界面,考虑到App实际上是向DecorView添加了ViewTree,这就可以在DecorView和ViewGroup中间插一层透明的View,这样就能滑动原有的ViewTree,达到侧滑效果。
原ViewTree

public class SWLayout extends FrameLayout{
        ...
        //用当前ViewGroup代替ViewTree的根节点
        ViewGroup decorView= (ViewGroup) activity.getWindow().getDecorView();
        View child=decorView.getChildAt(0);
        decorView.removeView(child);
        addView(child);
        decorView.addView(this);
        ...
}

扩展场景3
我们知道事件分发是从Activity开始的,但是悬浮窗也可以设置为允许响应事件,但是悬浮窗是没有Activity的,只做了addView,那么悬浮窗的事件是如何响应的?
硬件层拦截到事件,会从WMS传递到ViewRootImpl,而ViewRootImpl是WindowManagerGlobal在addView时创建的,所以悬浮窗虽然只做了addView,也有ViewRootImpl,也能响应事件。
当然,有Activity的情况下,ViewRootImpl的mView是DecorView,所以会把事件传递给DecorView,DecorView处理事件的函数为:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }

其中,mWindow是持有DecorView的PhoneWindow对象,而这个PhoneWindow对象的Callback,是在Activity的attach中,创建出PhoneWindow后,把Activity作为了Callback:

//Activity源码
    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window) {
        ...
        mWindow = new PhoneWindow(this, window);
        ...
        mWindow.setCallback(this);

所以,在有Activity的情况下,事件传递是ViewRootImpl-->DectorView-->PhoneWindow.Callback也就是Activity,然后再传递给PhoneWindow-->DectorView-->ViewTree,这也可以解释为什么DectorView里同时存在dispatchTouchEvent和superDispatchTouchEvent两个函数。
而在没有Activity的情况下,ViewRootImpl的mView是我们addView时传入的ViewTree,事件就直接传递给ViewTree了。

参考

《深入理解Android内核设计思想》
(Activity、View、Window的理解一篇文章就够了)[https://mp.weixin.qq.com/s/7vlWU3HWPZ8pYFeG5FQBlA]
Android系统的开机画面显示过程分析
手把手教你读懂源码,View的加载流程详细剖析
Android中MotionEvent的来源和ViewRootImpl
Android中View的量算、布局及绘图机制
公共技术点之 View 绘制流程
Android应用程序与SurfaceFlinger服务的关系概述和学习计划
Window与WMS通信过程
Android窗口管理分析(1):View如何绘制到屏幕上的主观理解
android阿里面试题锦集
Android 6.0 inflate过程分析
Android窗口管理分析(1):View如何绘制到屏幕上的主观理解
公共技术点之 View 事件传递
公共技术点之 View 绘制流程

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

推荐阅读更多精彩内容