Android的页面是如何展示出来的?

背景

其实这是一个非常宽泛非常大的命题,面试也被问过,自己答的不是很好,脑海里零零散散有些surface,window,surfaceFlinger,viewRootImpl之类的东西转来转去,但是没法串起来讲的清楚,平心而论这个东西确实是一个Android的核心的内容,非常具有对Android系统整体的认知情况的理解的参考价值,正好整体性的过一下,尽量合成一个自己的理解

片块理解

从页面的底层渲染到展示在用户界面整体还是一个比较庞大复杂的流程,在进行整体性的认知前先对每一个部分进行片块化的理解认知

1.View的绘制

老生常谈的一个Android的知识,进行自定义View的时候我们会新建类并重写对应的三个方法onMeasure,onLayout,onDraw,其中onMeasure用于测量和确定view的宽高,layout在viewGroup处理对应的布局效果,onDraw则在给到的canvas类上执行对应的具体的draw的api来实现

比较少关注到可能是canvas往下的内容,实际上onDraw的回调实际执行在view的draw方法,而这个canvas的绘制对我们来说也是上层android sdk的api,实际上他的执行前后又跟surface有关系,在系统的framework里,执行到view的draw前后分别有surface的lock和unlock调用,前者通过把canvas和当前的surface进行关联,开辟了一个共享内存(界面绘制这种高速率进程通信,共享内存的跨进程通信方式速度更快效率更高),draw的时候canvas进行了一个类似数据记录的工作,unlock的时候释放这块共享内存,并把对应的数据推送到执行绘制的surfaceFlinger,而往下再底层由谷歌自己的2d渲染框架skia来通过cpu运算执行绘制(如果有硬件加速也会由OpenGL来通过gpu执行),本人的理解大致如下图:

2.windows,surface与surfaceFlinger

window:window本身是一个抽象类,他的唯一实现类是PhoneWindow对view进行管理。和他对应的有windowManager,用来管理window的诸如添加、更新、删除操作(add、update,remove),而windowmanager执行这些操作都是由windowmanagerservice(WMS)跨进程通信实现的。大致关系如下

surface:一个window对应一个surface,surface用于承接相关的数据,对应系统渲染绘制的数据由surface承载,对应的graphicBuffer将通过WindowManager推送到surfaceFlinger进行消费执行渲染绘制

(根据源码的注释理解总感觉有点怪怪的,"surface应该由图像缓存的消费者创建,并传递给生产者来绘制进去",不过核心应该也是理解为承载一个干的buffer的数据的作用)

surfaceFlinger:本身是一个Android系统的服务进程,主要是作为数据缓冲的消费者,处理对应的surface实际绘制的效果呈现,作为系统服务会和底下的HAL硬件层交互

生产者-消费者模型:Android的图像渲染绘制大的架构上遵循一个生产者-消费者模型,即生产者对应生成图像的一个buffer数据,消费者来对这个buffer进行处理,上述我们的canvas就是生产者,常规的生产者还有MediaPlayer,OPenGL ES,Camera之类

graphicBuffer:实际是surfaceFlinger进程直接处理的数据,java的上层代码有一个对应的简单映射关系的类,核心应该还是在c++代码里,此处仅作抽象认知吧

那么,在这里把上面这堆东西合起来就会得到:

3.activity启动与window和view的关系

回到最原始的认知,从activity启动时的三个显示的回调onCreate,onStart,onResume,分别在这些方法里找对于window和view相关的处理

onCreate相关

首先针对onCreate,因为Activity的回调执行大多数都是靠SystemServer进程AMS跨进程通信到app进程的ActivityThread里调用,所以往上找create的调用可以到ActivityThread里找到activity启动的核心流程方法performLaunchActivity找到

ok,再去Activity的attach去找

显而易见,在Activity的attach方法里实例化了一个PhoneWindow对象,设定了回调,软键盘模式,ui之类的东西,最后还设置了对应的windowManager并把系统的WMS绑定了

而在onCreate的时候一般我们是传入一个xml文件并用setContentView方法传入,那么我们这个方法里面具体内容

就是一个subDecor里面找到一个content的子view,然后把我们这个xml布局作为content的子view通过LayoutInfolater加载进去,而subDecor其实就是顶层decorView根据window的属性值设置title或者toolbar之类的东西的一个子view,这里个人觉得不是很重要就不贴了

onStart相关

在ActivityThread里没找到什么对应的执行,先略过

onResume相关

老规矩,在ActivityThread里找,通过一层层往上找到源头的调用handleResumeActivity方法

可以看到是一个执行performResumeActivity的方法,如果执行碰到false返回就不往下走了(看了一下包括activity已销毁之类的),我们关注一下正常执行返回true往下走的逻辑

在一个三四个条件排除掉window的问题和不准备visible之类的异常情况后执行逻辑,首先拿到本activity的window的decorView里拿到ViewRootImpl即最顶级view的管理类,拿到windowManager的params,设置对应的键盘mode,最下面可以看到调用到activity的makeVisible方法

代码很简单,核心就是拿到实现ViewManager的实现,执行addview方法,实际windowManager的实现是framework层有个windowManagerGlobal,对应的addview逻辑是

核心就是创建ViewRootImpl以及对应的add和setView方法,那么看看setView方法

setView的逻辑非常长,那么其实我们想连接的本篇的点还是在这个requestLayout方法这里,说明我们的onResume层层下来还是从顶层view的ViewRootImpl调用到了requestLayout方法,而我们通过朴素的认知知道requestLayout会重新触发view的整套绘制流程,来都来了,看看这个具体实现

首先吸引我们注意的checkThread方法,判断当前线程是否在主线程,否则抛出异常,也就是朴素的认知里大名鼎鼎的所谓为什么我们只能在主线程更新ui。

而实际干活的scheduleTraversal则给我们的handler发送了一个同步屏障,然后通过choregrapher发送了一个traversal消息,执行的是我们的traversalRunnable绘制线程,我们看看这个线程内容,只做了doTraversal一件事情

里面也就是挪走同步屏障,执行performTraversal,这个方法具体逻辑长的眼睛疼,还是按照朴素的认知记得这里面有performMeasure,performLayout和performDraw就行吧,这里还有一点就是,由于Android系统是16ms会刷新一次屏幕,这个traversal逻辑基本每次绘制都会触发走一次,所以任何你的view都会建议不要在onDraw里新建对象,不然会高频触发造成内存抖动的情况,影响app的性能体验

上述过程有点长,再一次流程图简化:

汇总

那么回到这个问题应该怎么回答呢?

1.从图标点击启动app开始:(回答侧重启动)

手机桌面其实是一个launcher进程,图标相当于桌面列表的一个item,点击图标响应点击事件会启动这个app对应的launchActivity,这个请求会通过binder机制调用到系统的system_server进程内的AMS对象执行

AMS启动这个目标activity会通过socket通信通知zygote进程,zygote进程fork自己创建一个新的进程,同时会通过反射一个ActivityThread并执行对应的main方法

ActivityThread的main方法执行就到了app进程的逻辑, 1.main方法执行了在当前线程prepare一个looper,也就是主线程的消息循环的初始化

2.执行了activityThread的attach方法,attach方法通过绑定了applicationThread对象作为AMS与本进程的binder通信对象,还创建了gcWatcher的线程对象绑定binder(3/4的内存使用执行回收)

3.启动looper循环

之后AMS通过binder通信,调用scheduleLaunchActivity方法跨进程调用到applicationThread的对应方法,对应方法具体封装一个ActivityClientRecord发送一个message到原始的Handler对象H,H在自己的handlerMessage的回调里调用performLaunchActivity,具体通过类加载器创建该activity实例并调用attach方法,然后再执行callActivityOnCreate具体调用到onCreate回调的执行,至此到activity的onCreate执行,后面会launchActivity方法再执行handleResumeActivity,handleResumeActivity会通过windowManagerGlobal的addView通过ViewRootImpl来setView最后执行到requestLayout方法,requestLayout方法具体就是执行到doTraversal来遍历执行view树的onMeasure,onLayout和onDraw,最后通过surface把这些都合并到grafficBuffer内传到surfaceFlinger调用到硬件层HardWareComposer完成绘制

2.从硬件的一个vsync信号开始:(回答侧重渲染)

屏幕刷新率60帧的情况下相当于1秒要有60帧画面,那么平均一帧的时间是1000/60 = 16.66ms,如果涉及近年来硬件升级到带来的高刷新率,这个时间还要更少,当然如果页面没有变化的情况下也有对应逻辑控制避免CPU和GPU的资源浪费,此处不赘述

由于控制绘制的GPU的生产速度可能高于系统这个刷新的速度,当供大于求的时候,由于页面上一帧还没展示完下一帧就来了,像素由左上至右下更新变化,就会造成所谓的屏幕撕裂。自然而然,当供小于求的时候,就会产生我们常说的掉帧,英文资料称为jank。

想当然的,GPU写入的数据用一个缓存放置,屏幕刷新的数据再用一个缓存放置,各司其职,屏幕需要刷新的时候再从GPU写入的数据交互取得就好了,问题又来了,CPU计算,GPU绘制,每一帧的计算和渲染都依赖上一帧展示,如果耗时计算完成的晚了,没有跟上展示的节奏,还是会jank,基于这个问题又扩展了双缓冲机制,三缓冲机制,确保CPU、GPU,Display尽可能都有事做,保证流畅

基于vsync应运而生,顾名思义垂直同步,surfaceFlinger作为系统跟硬件交互的进程,内部虚拟了一个对象来映射硬件层的抽象理解一个脉冲信号的vsync,通过vsync信号来控制展示的节奏,vsync信号来了一面GPU开始绘制,一面上层的choreographer调用Traversal,让CPU计算

相关推荐
测试老哥3 小时前
外包干了两年,技术退步明显。。。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
ThisIsClark5 小时前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
测试19987 小时前
外包干了2年,技术退步明显....
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
Aphasia3117 小时前
一次搞懂 JS 对象转换,从此告别类型错误!
javascript·面试
GISer_Jing9 小时前
2025年前端面试热门题目——HTML|CSS|Javascript|TS知识
前端·javascript·面试·html
上海运维Q先生10 小时前
面试题整理14----kube-proxy有什么作用
运维·面试·kubernetes
开发者每周简报11 小时前
求职市场变化
人工智能·面试·职场和发展
贵州晓智信息科技15 小时前
如何优化求职简历从模板选择到面试准备
面试·职场和发展
百罹鸟15 小时前
【vue高频面试题—场景篇】:实现一个实时更新的倒计时组件,如何确保倒计时在页面切换时能够正常暂停和恢复?
vue.js·后端·面试
古木20191 天前
前端面试宝典
前端·面试·职场和发展