层次的底部是Linux,它提供基本的系统功能,如进程管理,内存管理,设备管理,如:相机,键盘,显示器等内核处理的事情。体系结构第三个部分叫做Java虚拟机,是一种专门设计和优化的AndroidDalvik虚拟机。应用程序框架层使用Java类形式的应用程序提供了许多的更高级别的服务。允许应用程序开发人员在其应用程序中使用这些服务。应用在最上层,即所有的Android应用程序。一般我们编写的应用程序只被安装在这层。应用的例子如:浏览器,游戏等。
ui生成就是把代码中产生的view和在xml文件配置的view,经过measure,layout,dewa形成一个完整的view树,并调用系统方法进行绘制。Measure用深度优先原则递归得到所有视图(View)的宽、高;Layout用深度优先原则递归得到所有视图(View)的位置;到这里就得到view的在窗口中的布局。Draw目前Android支持了两种绘制方式:软件绘制(CPU)和硬件加速(GPU),会通过系统方法把要绘制的view合成到不同的缓冲区上
最初的ui配置
构建成内存中的viewtree
SurfaceFlinger把缓存区数据渲染到屏幕,由于是两个不同的进程,所以使用Android的匿名共享内存SharedClient缓存需要显示的数据来达到目的。SurfaceFlinger把缓存区数据渲染到屏幕(流程如下图所示),主要是驱动层的事情,这里不做太多解释。
绘制过程首先是CPU准备数据,通过Driver层把数据交给CPU渲染,其中CPU主要负责Measure、Layout、Record、Execute的数据计算工作,GPU负责Rasterization(栅格化)、渲染。由于图形API不允许CPU直接与GPU通信,而是通过中间的一个图形驱动层(GraphicsDriver)来连接这两部分。图形驱动维护了一个队列,CPU把displaylist添加到队列中,GPU从这个队列取出数据进行绘制,最终才在显示屏上显示出来。
调用layoutSubviews方法;调用addSubview:方法;
通过drawRect绘制视图;绘制string(字符串);
每个UIView都有CALayer,同时图层有一个像素存储空间,存放视图;调用-setNeedsDisplay的时候,仅会设置图层为dirty。当一个视图第一次或者某部分需要更新的时候iOS系统总是会去请求drawRect:方法。
以下是触发视图更新的一些操作:
视图系统都会自动触发重新绘制。对于自定义视图,就必须重写drawRect:方法去执行所有绘制。视图第一次展示的时候,iOS系统会传递正方形区域来表示这个视图绘制的区域。
在调用drawRect:方法之后,视图就会把自己标记为已更新,然后等待下一次视图更新被触发。
解码图片;图片格式转换;
打包layers并发送到渲染server;
递归提交子树的layers;
web通常需要将所需要的html,css,js都下载下来,并进行解析执行后才进行渲染,然后是绘制过程,先来看下前期工作
一个渲染流程会划分很多子阶段,整个处理过程叫渲染流水线,流水线可分为如下几个子阶段:构建DOM树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。每个阶段都经过输入内容-->处理过程-->输出内容三个部分。
我们有DOM树和DOM树中元素的样式,那么接下来就需要计算出DOM树中可见元素的几何位置,我们把这个计算过程叫做布局。根据元素的可见信息构建出布局树。
合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。
WebView实际上是一个ViewGroup,并将后端的具体实现抽象为WebViewProvider,而WebViewChromium正是一个提供基于Chromium的具体实现类。
再回到WebView的情况。当WebView部件发生内容更新时,例如页面加载完毕,CSS动画,或者是滚动、缩放操作导致页面内容更新,同样会在WebView触发invalidate方法,随后在视图系统的统筹安排下,WebView.onDraw方法会被调用,最后实际上调用了AwContents.onDraw方法,它会请求对应的native端对象执行OnDraw方法,将页面的内容更新绘制到WebView对应的Canvas上去。
draw()先得到一块Buffer,这块Buffer是由SurfaceFlinger负责管理的。
然后调用view的draw(canvas)当viewdraw完后,调用surface.java的unlockAndPostCanvas().
将包含有当前view内容的Buffer传给SurfaceFlinger,SurfaceFlinger将所有的Buffer混合后传给FrameBuffer.至此和native原有的view渲染就是一样的了。
RN的AndroidBridge和IOSBridge是两端通信的桥梁,是由一个转译的桥梁实现的不同语言的通信,得以实现单靠JS就调用移动端原生APi
Java->Js:Java通过注册表调用到CatalystInstance实例,通过jni,调用到javascriptCore,传递给调用BatchedBridge.js,根据参数{moduleID,methodID}require相应Js模块执行。
Js->Java:JS不主动传递数据调用Java。在需要调用调Java模块方法时,会把参数{moduleID,methodID}等数据存在MessageQueue中,等待Java的事件触发,再把MessageQueue中的{moduleID,methodID}返回给Java,再根据模块注册表找到相应模块处理。
JS开发者只需要开发各个组件对象,监听组件事件,然后利用framework接口调用render方法渲染组件。
而实际上,JS也是单线程事件循环,不管是API调用,virturalDOM同步,还是系统事件监听,都是异步事件,采用Observer(观察者)模式监听JAVA层事件,JAVA层会把JS关心的事件通过bridge直接使用javascriptCore的接口执行固定的脚本,比如"requrire(test_module).test_methode(test_args)"。此时,UImainthread相当于workthread,把系统事件或者用户事件往JS层抛,同时,JS层也不断调用模块API或者UI组件,驱动JAVA层完成实际的View渲染。JS开发者只需要监听JS层framework定义的事件即可
首先回顾一下当前Bridge的运行过程。当我们写了类似下面的React源码。
UIManager.createView([343,"RCTView",31,{"backgroundColor":-16181,"width":200,"height":200}])通过Bridge发到ShadowThread。ShadowTread接收到这条信息后,先反序列化,形成Shadowtree,然后传给Yoga,形成原生布局信息。接着又通过Bridge传给UIthread。UIthread拿到消息后,同样先反序列化,然后根据所给布局信息,进行绘制。
从上面过程可以看到三个线程的交互都是要通过Bridge,因此瓶颈也就在此。
另外JSI与React无关,可以用在任何JS引擎(V8,Hermes)。有了JSI,JS和Native就可以直接通信了,调用过程如下:JS->JSI->C++->ObjectC/Java
生产环境中Dart通过AOT编译成对应平台的指令,同时Flutter基于跨平台的Skia图形库自建了渲染引擎,最大程度地保证了跨平台渲染的一致性
Flutterframework主要是使用dart语言来编写的。framework从下到上,我们有最基础的foundational包,和构建在其上的animation,painting和gestures。
再上面就是rendering层,rendering为我们提供了动态构建可渲染对象树的方法,通过这些方法,我们可以对布局进行处理。接着是widgetslayer,它是rendering层中对象的组合,表示一个小挂件。
Widgets是Flutter中用户界面的基础。你在flutter界面中能够观察到的用户界面,都是Widgets。大的Widgets又是由一个个的小的Widgets组成,这样就构成了Widgets的层次依赖结构,在这种层次结构中,子Widgets可以共享父Widgets的上下文环境。在Flutter中一切皆可为Widget。
举例,这个Containerks控件里的child,color,Text都是Widget。
渲染就是将上面我们提到的widgets转换成用户肉眼可以感知的像素的过程。Flutter代码会直接被编译成使用Skia进行渲染的原生代码,从而提升渲染效率。
代码首先会形成widgets树如下,这些widget在build的过程中,会被转换为elementtree,其中ComponentElement是其他Element的容器,而RenderObjectElement是真正参与layout和渲染的element。。一个element和一个widget对应。然后根据elementrtree中需要渲染的元素形成RenderTree,flutter仅会重新渲染需要被重新绘制的element,每次widget变化时element会比较前后两个widget,只有当某一个位置的Widget和新Widget不一致,才会重新创建Element和widget。最后还会一个layertree,表示绘制的图层。
四棵树有各自的功能
Flutter绘制流程
在UIThread构建出四棵树,并在Engine生成Scene,最后提交给RasterThread,对LayerTree做光栅化合成上屏。
整个调度过程是生产者消费者模型,UIThread负责生产flutter::LayerTree,RasterThread负责消费flutter::LayerTree。
Mobile平台上面每一个Engine实例启动的时候会为UI,GPU,IORunner各自创建一个新的线程。所有Engine实例共享同一个PlatformRunner和线程。
IORunner的主要功能是从图片存储(比如磁盘)中读取压缩的图片格式,将图片数据进行处理为GPURunner的渲染做好准备
Dart是单线程的,但是采用了EventLoop机制,也就是不断循环等待消息到来并处理。在Dart中,实际上有两个队列,一个事件队列(EventQueue),另一个则是微任务队列(MicrotaskQueue)。在每一次事件循环中,Dart总是先去第一个微任务队列中查询是否有可执行的任务,如果没有,才会处理后续的事件队列的流程。
isolate机制尽管Dart是基于单线程模型的,但为了进一步利用多核CPU,将CPU密集型运算进行隔离,Dart也提供了多线程机制,即Isolate。每个Isolate都有自己的EventLoop与Queue,Isolate之间不共享任何资源,只能依靠消息机制通信,因此也就没有资源抢占问题。Isolate通过发送管道(SendPort)实现消息通信机制。我们可以在启动并发Isolate时将主Isolate的发送管道作为参数传给它,这样并发Isolate就可以在任务执行完毕后利用这个发送管道给我们发消息。
如果需要在启动一个Isolate执行某项任务,Isolate执行完毕后,发送消息告知我们。如果Isolate执行任务时,同时需要依赖主Isolate给它发送参数,执行完毕后再发送执行结果给主Isolate这样的双向通信,让并发Isolate也回传一个发送管道即可。
Weex里页面的渲染过程和浏览器的渲染过程类似,整体可以分为【创建前端组件】->【构建VirtualDOM】->【生成“真实”DOM】->【发送渲染指令】->【绘制原生UI】这五个步骤。前两个步骤发生在前端框架中,第三和第四个步骤在JSFramework中处理,最后一步是由原生渲染引擎实现的。页面渲染的大致流程如下
最上层的前端生态还是没变的,应该还是以vue的响应式编程为主。
2.0多了js和c++的直接调用,减少js引擎和布局引擎的通讯开销。
Weex2.0重写了渲染层的实现,不再依赖系统UI,改成依赖统一的图形库Skia自绘渲染,和Flutter原理很像,我们也直接复用了FlutterEngine的部分代码。底层把Weex对接过的各种原生能力、三方扩展模块都原样接入。对于上层链路,理论上讲业务JS代码、Vue/Rax、JSFramework都是不需要修改的。在JS引擎层面也做了一些优化,安卓上把JavaScriptCore换成了QuickJS,用bytecode加速二次打开性能,并且结合Weexjsbundle的特点做针对性的优化。
渲染引擎通用的渲染管线可以简化为【执行脚本】-->【构建节点】-->【布局/绘制】-->【合成/光栅化】--【上屏】这几个步骤。Weex里的节点构建逻辑主要在JS线程里执行,提交一颗静态节点树到UI线程,UI线程计算布局和绘制属性,生成LayerStack提交到GPU线程。
支持AOT模式,拔高了性能上限;活跃的开源社区,降低项目推进的风险;支持多语言,拓宽开发者群体。
WebAssembly(又名wasm)是一种高效的,低级别的编程语言。它让我们能够使用JavaScript以外的语言(例如C,C++,Rust或其他)编写程序,然后将其编译成WebAssembly,进而生成一个加载和执行速度非常快的Web应用程序
WebAssembly是基于堆栈的虚拟机的二进制指令格式,它被设计为编程语言的可移植编译目标。目前很多语言都已经将WebAssembly作为它的编译目标了。
可以看到是采用类前端的开发方式,限定一部分css能力。最后编译为WebAssembly,打包成wasmbundle。在进行aot编译城不同架构下的机器码。
可以看到bundle加载过程中,会执行UI区域的不同的生命周期函数。然后在渲染过程则是从virtualdomtree转化到widgettree,然后直接通过skia渲染库直接进行渲染。
KMP:KotlinMultiplatformprojects使用一份kotlin代码在不同平台上运行
KMM:KotlinMultiplatformMobile一个用于跨平台移动应用程序的SDK。使用KMM,可以构建多平台移动应用程序并在Android和iOS应用程序之间共享核心层和业务逻辑等方面。开发人员可以使用单一代码库并通过共享数据层和业务逻辑来实现功能。其实就是把一份逻辑代码编译为多个平台的产物编译中间产物,在不同平台的边缘后端下转为不同的变异后端产物,在不同平台下运行。
IR全称是intermediaterepresentation,表示编译过程中的中间信息,由编译器前端对源码分析后得到,随后会输入到后端进一步编译为机器码
IR可以有一系列的表现方式,由高层表示逐渐下降(lowering)到低层
我们所讨论的KotlinIR是抽象语法树结构(AST),是比较高层的IR表示类型。
有了完备的IR,就可以利用不同的后端,编出不同的目标代码,比如JVM的字节码,或者运行在iOS的机器码,这样就达到了跨端的目的