一 概述
在上一篇文章 Android图形显示系统2 图像消费者 中,我们详细地讲解了图像消费者,我们已经了解了 Android 中的图像元数据是如何被 SurfaceFlinger,HWComposer 或者 OpenGL ES 消费的,那么,图像元数据又是怎么生成的呢?这一篇文章就来详细介绍 Android 中的图像生产者—— SKIA,OPenGL ES,Vulkan,他们是 Android 中最重要的三支画笔。
二 Skia
Skia 是谷歌开源的一款跨平台的 2D 图形引擎,目前谷歌的 Chrome 浏览器、Android、Flutter、以及火狐浏览器、火狐操作系统和其它许多产品都使用它作为图形引擎,它作为 Android 系统第三方软件,放在 external/skia/ 目录下。虽然 Android 从 4.0 开始默认开启了硬件加速,但不代表 Skia 的作用就不大了,其实 Skia 在 Android 中的地位是越来越重要了,从 Android 8 开始,我们可以选择使用 Skia 进行硬件加速,Android 9 开始就默认使用 Skia 来进行硬件加速。Skia 的硬件加速主要是通过 copybit 模块调用 OpenGL 或者 SKia 来实现。
由于 Skia 的硬件加速也是通过 Copybit 模块调用的 OpenGL 或者 Vulkan 接口,所以我们这儿只说说 Skia 通过 cpu 绘制的,也就是软绘的方式。还是老规则,先看看 Skia 要如何使用
2.1 如何使用Skia
OpenGL ES 的使用要配合 EGL,需要初始化 Display,surface,context 等,用法还是比较繁琐的,Skia 在使用上就方便很多了。掌握 Skia 绘制三要素:画板 SkCanvas、画纸 SkBitmap、画笔 SkPaint,我们就能很轻松的用 Skia 来绘制图形。
下面详细的解释 Skia 的绘图三要素
1.SkBitmap 用来存储图形数据,它封装了与位图相关的一系列操作
SkBitmap bitmap = new SkBitmap();
//设置位图格式及宽高
bitmap->setConfig(SkBitmap::kRGB_565_Config,800,480);
//分配位图所占空间
bitmap->allocPixels();
2.SkCanvas 封装了所有画图操作的函数,通过调用这些函数,我们就能实现绘制操作。
//使用前传入bitmap
SkCanvas canvas(bitmap);
//移位,缩放,旋转,变形操作
translate(SkiaScalar dx, SkiaScalar dy);
scale(SkScalar sx, SkScalar sy);
rotate(SkScalar degrees);
skew(SkScalar sx, SkScalar sy);
//绘制操作
drawARGB(u8 a, u8 r, u8 g, u8 b....) //给定透明度以及红,绿,兰3色,填充整个可绘制区域。
drawColor(SkColor color...) //给定颜色color, 填充整个绘制区域。
drawPaint(SkPaint& paint) //用指定的画笔填充整个区域。
drawPoint(...)//根据各种不同参数绘制不同的点。
drawLine(x0, y0, x1, y1, paint) //画线,起点(x0, y0), 终点(x1, y1), 使用paint作为画笔。
drawRect(rect, paint) //画矩形,矩形大小由rect指定,画笔由paint指定。
drawRectCoords(left, top, right, bottom, paint),//给定4个边界画矩阵。
drawOval(SkRect& oval, SkPaint& paint) //画椭圆,椭圆大小由oval矩形指定。
//……其他操作
3.SkPaint 用来设置绘制内容的风格,样式,颜色等信息
setAntiAlias: 设置画笔的锯齿效果。
setColor: 设置画笔颜色
setARGB: 设置画笔的a,r,p,g值。
setAlpha: 设置Alpha值
setTextSize: 设置字体尺寸。
setStyle: 设置画笔风格,空心或者实心。
setStrokeWidth: 设置空心的边框宽度。
getColor: 得到画笔的颜色
getAlpha: 得到画笔的Alpha值。
我们看一个完整的使用 Demo
void draw() {
SkBitmap bitmap = new SkBitmap();
//设置位图格式及宽高
bitmap->setConfig(SkBitmap::kRGB_565_Config,800,480);
//分配位图所占空间
bitmap->allocPixels();
//使用前传入bitmap
SkCanvas canvas(bitmap);
//定义画笔
SkPaint paint1, paint2, paint3;
paint1.setAntiAlias(true);
paint1.setColor(SkColorSetRGB(255, 0, 0));
paint1.setStyle(SkPaint::kFill_Style);
paint2.setAntiAlias(true);
paint2.setColor(SkColorSetRGB(0, 136, 0));
paint2.setStyle(SkPaint::kStroke_Style);
paint2.setStrokeWidth(SkIntToScalar(3));
paint3.setAntiAlias(true);
paint3.setColor(SkColorSetRGB(136, 136, 136));
sk_sp<SkTextBlob> blob1 =
SkTextBlob::MakeFromString("Skia!", SkFont(nullptr, 64.0f, 1.0f, 0.0f));
sk_sp<SkTextBlob> blob2 =
SkTextBlob::MakeFromString("Skia!", SkFont(nullptr, 64.0f, 1.5f, 0.0f));
canvas->clear(SK_ColorWHITE);
canvas->drawTextBlob(blob1.get(), 20.0f, 64.0f, paint1);
canvas->drawTextBlob(blob1.get(), 20.0f, 144.0f, paint2);
canvas->drawTextBlob(blob2.get(), 20.0f, 224.0f, paint3);
}
这个 Demo 的效果如下:
了解了 Skia 如何使用,我们接着看两个场景:Skia 进行软件绘制,Flutter 界面绘制
2.2 Skia进行软件绘制
在上一篇文章中我们讲了通过使用 OpenGL 渲染的硬件绘制方式,这里会接着讲使用 Skia 渲染的软件绘制方式,虽然 Android 默认开启了硬件加速,但是由于硬件加速会有耗电和内存的问题,一些系统应用和常驻应用依然是使用的软件绘制的方式,软绘入口还是在 draw 方法中。
//文件-->/frameworks/base/core/java/android/view/ViewRootImpl.java
private void performDraw() {
......
draw(fullRedrawNeeded);
......
}
private void draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
if (!surface.isValid()) {
return;
}
......
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
if (mAttachInfo.mThreadedRenderer != null &&
mAttachInfo.mThreadedRenderer.isEnabled()) {
......
//硬件渲染
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
} else {
......
//软件渲染
if (!drawSoftware(surface, mAttachInfo, xOffset,
yOffset, scalingRequired, dirty)) {
return;
}
}
}
......
}
......
}
我们来看看 drawSoftware 函数的实现
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
// Draw with software renderer.
final Canvas canvas;
......
canvas = mSurface.lockCanvas(dirty);
......
mView.draw(canvas);
......
surface.unlockCanvasAndPost(canvas);
......
return true;
}
drawSoftware 函数的流程主要为三步:
- 通过 mSurface.lockCanvas 获取 Canvas
- 通过 draw 方法,将根 View 及其子 View 遍历绘制到 Canvas 上
- 通过 surface.unlockCanvasAndPost 将绘制内容提交给 surfaceFlinger 进行合成
2.2.1 Lock Surface
我们先来看第一步,这个 Canvas 对应着 Native 层的 SkCanvas。
//文件-->/frameworks/base/core/java/android/view/Surface.java
public Canvas lockCanvas(Rect inOutDirty)
throws Surface.OutOfResourcesException, IllegalArgumentException {
synchronized (mLock) {
checkNotReleasedLocked();
if (mLockedObject != 0) {
throw new IllegalArgumentException("Surface was already locked");
}
mLockedObject = nativeLockCanvas(mNativeObject, mCanvas, inOutDirty);
return mCanvas;
}
}
lockCanvas 函数中通过 JNI 函数 nativeLockCanvas,创建 Nativce 层的 Canvas,nativeLockCanvas 的入参 mNativeObject 对应着 Native 层的 Surface,关于 Surface 和 Buffer 的知识,在下一篇图形缓冲区中会详细简介,这里不做太多介绍。我们直接看 nativeLockCanvas 的实现。
static jlong nativeLockCanvas(JNIEnv* env, jclass clazz,
jlong nativeObject, jobject canvasObj, jobject dirtyRectObj) {
sp<Surface> surface(reinterpret_cast<Surface *>(nativeObject));
if (!isSurfaceValid(surface)) {
doThrowIAE(env);
return 0;
}
Rect dirtyRect(Rect::EMPTY_RECT);
Rect* dirtyRectPtr = NULL;
if (dirtyRectObj) {
dirtyRect.left = env->GetIntField(dirtyRectObj, gRectClassInfo.left);
dirtyRect.top = env->GetIntField(dirtyRectObj, gRectClassInfo.top);
dirtyRect.right = env->GetIntField(dirtyRectObj, gRectClassInfo.right);
dirtyRect.bottom = env->GetIntField(dirtyRectObj, gRectClassInfo.bottom);
dirtyRectPtr = &dirtyRect;
}
ANativeWindow_Buffer outBuffer;
//关键点1,获取用来存储图形绘制的buffer
status_t err = surface->lock(&outBuffer, dirtyRectPtr);
if (err < 0) {
const char* const exception = (err == NO_MEMORY) ?
OutOfResourcesException :
"java/lang/IllegalArgumentException";
jniThrowException(env, exception, NULL);
return 0;
}
SkImageInfo info =
SkImageInfo::Make(outBuffer.width, outBuffer.height,
convertPixelFormat(outBuffer.format),
outBuffer.format == PIXEL_FORMAT_RGBX_8888
? kOpaque_SkAlphaType : kPremul_SkAlphaType,
GraphicsJNI::defaultColorSpace());
SkBitmap bitmap;
ssize_t bpr = outBuffer.stride * bytesPerPixel(outBuffer.format);
bitmap.setInfo(info, bpr);
if (outBuffer.width > 0 && outBuffer.height > 0) {
//将上一个buffer里的图形数据复制到当前bitmap中
bitmap.setPixels(outBuffer.bits);
} else {
// be safe with an empty bitmap.
bitmap.setPixels(NULL);
}
//关键点2,创建一个SKCanvas
Canvas* nativeCanvas = GraphicsJNI::getNativeCanvas(env, canvasObj);
//关键点3,给SKCanvas设置Bitmap
nativeCanvas->setBitmap(bitmap);
//如果指定了脏区,则设定脏区的区域
if (dirtyRectPtr) {
nativeCanvas->clipRect(dirtyRect.left, dirtyRect.top,
dirtyRect.right, dirtyRect.bottom, SkClipOp::kIntersect);
}
if (dirtyRectObj) {
env->SetIntField(dirtyRectObj, gRectClassInfo.left, dirtyRect.left);
env->SetIntField(dirtyRectObj, gRectClassInfo.top, dirtyRect.top);
env->SetIntField(dirtyRectObj, gRectClassInfo.right, dirtyRect.right);
env->SetIntField(dirtyRectObj, gRectClassInfo.bottom, dirtyRect.bottom);
}
sp<Surface> lockedSurface(surface);
lockedSurface->incStrong(&sRefBaseOwner);
return (jlong) lockedSurface.get();
}
nativeLockCanvas 主要做了这几件事情:
- 通过 surface->lock 函数获取绘制用的 Buffer
- 根据 Buffer 信息创建 SkBitmap
- 根据 SkBitmap,创建并初始化 SkCanvas
通过 nativeLockCanvas,我们就创建好 SkCanvas 了,并且设置了可以绘制图形的 SkBitmap,此时我们就可以通过 SkCanvas 往 bitmap 里面绘制图形,mView.draw() 函数,就做了这件事情。
2.2.2 绘制
我们接着看看 View 中的 draw() 函数
//文件-->/frameworks/base/core/java/android/view/View.java
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque =
(privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
int saveCount;
//1,绘制背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// 2,绘制当前view的图形
if (!dirtyOpaque) onDraw(canvas);
// 3,绘制子view的图形
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
//4,绘制decorations,如滚动条,
// 前景等 Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// 5,绘制焦点的高亮
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
......
}
draw 函数中做了这几件事情:
- 绘制背景
- 绘制当前 view
- 遍历绘制子 view
- 绘制前景
我们可以看看 Canvas 里的绘制方法,这些绘制方法都是 JNI 方法,并且一一对应着 SkCanvas 中的绘制方法
//文件-->/frameworks/base/graphics/java/android/graphics/Canvas.java
......
private static native void nDrawBitmap(long nativeCanvas, ......);
private static native void nDrawColor(long nativeCanvas, ......);
private static native void nDrawPaint(long nativeCanvas, ......);
private static native void nDrawPoint(long canvasHandle, ......);
private static native void nDrawPoints(long canvasHandle, ......);
private static native void nDrawLine(long nativeCanvas, ......);
private static native void nDrawLines(long canvasHandle, ......);
private static native void nDrawRect(long nativeCanvas, ......);
private static native void nDrawOval(long nativeCanvas, ......);
private static native void nDrawCircle(long nativeCanvas, ......);
private static native void nDrawArc(long nativeCanvas, ......);
private static native void nDrawRoundRect(long nativeCanvas, ......);
......
2.2.3 Post Surface
软件绘制的最后一步,通过 surface.unlockCanvasAndPost 将绘制内容提交给 surfaceFlinger 合成,将绘制出来的图形提交给 SurfaceFlinger,然后 SurfaceFlinger 作为消费者处理图形后,我们的界面就显示出来了。
public void unlockCanvasAndPost(Canvas canvas) {
synchronized (mLock) {
checkNotReleasedLocked();
if (mHwuiContext != null) {
mHwuiContext.unlockAndPost(canvas);
} else {
unlockSwCanvasAndPost(canvas);
}
}
}
private void unlockSwCanvasAndPost(Canvas canvas) {
if (canvas != mCanvas) {
throw new IllegalArgumentException(
"canvas object must be the same instance that "
+ "was previously returned by lockCanvas");
}
if (mNativeObject != mLockedObject) {
Log.w(TAG, "WARNING: Surface's mNativeObject (0x" +
Long.toHexString(mNativeObject) + ") != mLockedObject (0x" +
Long.toHexString(mLockedObject) +")");
}
if (mLockedObject == 0) {
throw new IllegalStateException("Surface was not locked");
}
try {
nativeUnlockCanvasAndPost(mLockedObject, canvas);
} finally {
nativeRelease(mLockedObject);
mLockedObject = 0;
}
}
这里调用了 Native 函数 nativeUnlockCanvasAndPost,我们接着往下看。
static void nativeUnlockCanvasAndPost(JNIEnv* env, jclass clazz,
jlong nativeObject, jobject canvasObj) {
sp<Surface> surface(reinterpret_cast<Surface *>(nativeObject));
if (!isSurfaceValid(surface)) {
return;
}
// detach the canvas from the surface
Canvas* nativeCanvas = GraphicsJNI::getNativeCanvas(env, canvasObj);
nativeCanvas->setBitmap(SkBitmap());
// unlock surface
status_t err = surface->unlockAndPost();
if (err < 0) {
doThrowIAE(env);
}
}
在这里,surface->unlockAndPost() 函数就会将 Skia 绘制出来的图像传递给 SurfaceFlinger 进行合成。通过 skia 进行软件绘制的流程已经讲完了,至于如何通过 Surface 获取缓冲区,在缓冲区绘制完数据后,surface->unlockAndPost() 又如何通知 SurfaceFlinger,这一点在下一篇文章的图形缓冲区中会详细的讲解。
可以看到,Skia 软件绘制的流程比硬件绘制要简单很多,我们接着看看 Skia 进行 Flutter 绘制。
2.3 Skia进行Flutter的界面绘制
在讲解 Flutter 如何通过 Skia 生产图像之前,先简单介绍一下 Flutter,Flutter 的架构分为 Framework层,Engine 层和 Embedder 三层。
- Framework 层使用 dart 语言实现,包括 UI,文本,图片,按钮等 Widgets,渲染,动画,手势等
- Engine 使用 C++ 实现,主要包括渲染引擎 Skia,Dart 虚拟机和文字排版 Text 等模块
- Embedder 是一个嵌入层,通过该层把 Flutter 嵌入到各个平台上去,Embedder 的主要工作包括渲染 Surface 设置, 线程设置,以及插件等
了解了 Flutter 的架构,我们接着了解 Flutter 显示一个界面的流程。我们知道在 Android 中,显示一个界面需要将 XML 界面布局解析成 ViewGroup,然后再经过测量 Measure,布局 Layout 和绘制 Draw 的流程。Flutter 和 Android 的显示不太一样,它会将通过 Dart 语言编写的 Widget 界面布局转换成 ElementTree 和 Render ObjectTree。ElementTree 相当于是 ViewGroup,Render ObjectTree 相当于是经过 Measure 和 Layout 流程之后的 ViewGroup。这种模式在很多场景上都有使用,比如 Webview,在渲染界面时,也会创建一颗 Dom 树,render 树和 RenderObject,这样的好处是可以通过 Diff 比较改变过的组件,然后渲染时,只对改变过的组件做渲染,同时对跨平台友好,可以通过这种树的形式来抽象出不同平台的公共部分。
讲完了上面两个背景,我们直接来看 Flutter 是如何使用 Skia 来绘制界面的。
下面是一个 Flutter 页面的 Demo
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
这个页面是一个 WidgetTree,相当于我们 Activity 的 xml,widget 树会转换成 ElementTree 和 RenderObjectTree,我们看看入口函数 runApp 时如何进行树的转换的。
//文件-->/packages/flutter/lib/src/widgets
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();
}
void scheduleAttachRootWidget(Widget rootWidget) {
Timer.run(() {
attachRootWidget(rootWidget);
});
}
void attachRootWidget(Widget rootWidget) {
_readyToProduceFrames = true;
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget,
).attachToRenderTree(buildOwner,
renderViewElement as RenderObjectToWidgetElement<RenderBox>);
}
接着看 attachToRenderTree 函数
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner,
[RenderObjectToWidgetElement<T> element]) {
if (element == null) {
owner.lockState(() {
element = createElement(); //创建rootElement
element.assignOwner(owner); //绑定BuildOwner
});
owner.buildScope(element, () { //子widget的初始化从这里开始
element.mount(null, null); // 初始化子Widget前,先执行rootElement的mount方法
});
} else {
...
}
return element;
}
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
_dirty = false;
}
从代码中可以看到,Widget 都被转换成了 Element,Element 接着调用了 mount 方法,在 mount 方法中,可以看到 Widget 又被转换成了 RenderObject,此时 Widget Tree 的 ElementTree 和 RenderObject 便都生成完了。
前面提到了 RenderObject 类似于经过了 Measure 和 Layout 流程的 ViewGroup,RenderObject 的 Measure 和 Layout 就不在这儿说了,那么还剩一个流程 Draw 流程,同样是在 RenderObject 中进行的,它的入口在 RenderObject 的 paint 函数中。
// 绘制入口,从 view 根节点开始,逐个绘制所有子节点
@override
void paint(PaintingContext context, Offset offset) {
if (child != null)
context.paintChild(child, offset);
}
可以看到,RenderObject 通过 PaintingContext 来进行了图形的绘制,我们接着来了解一下PaintingContext 是什么。
//文件-->/packages/flutter/lib/src/rendering/object.dart
import 'dart:ui' as ui show PictureRecorder;
class PaintingContext extends ClipContext {
@protected
PaintingContext(this._containerLayer, this.estimatedBounds)
final ContainerLayer _containerLayer;
final Rect estimatedBounds;
PictureLayer _currentLayer;
ui.PictureRecorder _recorder;
Canvas _canvas;
@override
Canvas get canvas {
if (_canvas == null)
_startRecording();
return _canvas;
}
void _startRecording() {
_currentLayer = PictureLayer(estimatedBounds);
_recorder = ui.PictureRecorder();
_canvas = Canvas(_recorder);
_containerLayer.append(_currentLayer);
}
void stopRecordingIfNeeded() {
if (!_isRecording)
return;
_currentLayer.picture = _recorder.endRecording();
_currentLayer = null;
_recorder = null;
_canvas = null;
}
可以看到,PaintingContext 是绘制的上下文,前面讲 OpenGL 进行硬件加速时提到的 CanvasContext,它也是绘制的上下文,里面封装了 Skia,Opengl 或者 Vulkan 的渲染管线。这里的 PaintingContext 则封装了 Skia。
我们可以通过 CanvasContext 的 get canvas 函数获取 Canvas,它调用了 _startRecording 函数,函数中创建了 PictureRecorder 和 Canvas,这两个类都是位于 dart:ui 库中,dart:ui 位于 engine 层,在前面架构中提到,Flutter 分为 Framewrok,Engine 和 embened 三层,Engine 中包含了 Skia,dart 虚拟机和 Text。dart:ui 就是位于 Engine 层的。
我们接着去 Engine 层的代码看看 Canvas 的实现。
//文件-->engine-master\lib\ui\canvas.dart
Canvas(PictureRecorder recorder, [ Rect? cullRect ]) :
assert(recorder != null) { // ignore: unnecessary_null_comparison
if (recorder.isRecording)
throw ArgumentError('"recorder" must not already be '+
'associated with another Canvas.');
_recorder = recorder;
_recorder!._canvas = this;
cullRect ??= Rect.largest;
_constructor(recorder, cullRect.left, cullRect.top,
cullRect.right, cullRect.bottom);
}
void _constructor(PictureRecorder recorder,
double left,
double top,
double right,
double bottom) native 'Canvas_constructor';
这里 Canvas 调用了 Canvas_constructor 这一个 native 方法,我们接着看这个 native 方法的实现。
//文件-->engine-master\lib\ui\painting\engine.cc
static void Canvas_constructor(Dart_NativeArguments args) {
UIDartState::ThrowIfUIOperationsProhibited();
DartCallConstructor(&Canvas::Create, args);
}
fml::RefPtr<Canvas> Canvas::Create(PictureRecorder* recorder,
double left,
double top,
double right,
double bottom) {
if (!recorder) {
Dart_ThrowException(
ToDart("Canvas constructor called with non-genuine PictureRecorder."));
return nullptr;
}
fml::RefPtr<Canvas> canvas = fml::MakeRefCounted<Canvas>(
recorder->BeginRecording(SkRect::MakeLTRB(left, top, right, bottom)));
recorder->set_canvas(canvas);
return canvas;
}
Canvas::Canvas(SkCanvas* canvas) : canvas_(canvas) {}
可以看到,这里通过 PictureRecorder->BeginRecording 创建了 SkCanvas,这其实是 SkCanvas 的另外一种使用方式,这里我简单的介绍一个 demo。
Picture createSolidRectanglePicture(
Color color, double width, double height)
{guanjiandguanjiand
PictureRecorder recorder = PictureRecorder();
Canvas canvas = Canvas(recorder);
Paint paint = Paint();
paint.color = color;
canvas.drawRect(Rect.fromLTWH(0, 0, width, height), paint);
return recorder.endRecording();
}
这个 demo 的效果如下图,它创建 Skia 的方式就和 Flutter 创建 Skia 的方式是一样的。
此时,我们的 SkCanvas 创建好了,并且直接通过 PaintingContext 的 get canvas 函数就能获取到,那么获取到 SkCanvas 后直接调用 Canvas 的绘制 api,就可以将图像绘制出来了。
Flutter 界面显示的全流程是比较复杂的,Flutter 完全是自建的一套图像显示流程,无法通过 Android 的 SurfaceFlinger 进行图像合成,也无法使用 Android 的 Gralloc 模块分配图像缓冲区,所以它需要有自己的图像生产者,有自己的图形消费者,也有自己的图形缓冲区,这里面就有非常多的流程,比如如何接收 VSync,如何处理及合成 Layer,如何创建图像缓冲区,这里只是对 Flutter 的图像生产者的部分做了一个初步的介绍,关于 Flutter 更深入一步的细节,就不在这里继续讲解了。后面我会专门写一系列文章来详细讲解 Flutter。
三 Vulkan
与 OpenGL 相比,Vulkan 可以更详细的向显卡描述你的应用程序打算做什么,从而可以获得更好的性能和更小的驱动开销,作为 OpenGL 的替代者,它设计之初就是为了跨平台实现的,可以同时在 Windows、Linux 和 Android 开发。甚至在 Mac OS 系统上运行。Android 在 7.0 开始,便增加了对 Vulkan 的支持,Vulkan 一定是未来的趋势,因为它比 OpenGL 的性能更好更强大。下面我们就了解一下,如何使用 Vulkan 来生产图像。
3.1 如何使用Vulkan
Vulkan 的使用和 OpenGL 类似,同样是三步:初始化,绘制,提交 buffer。下面来看一下具体的流程:
1.初始化 Vulkan 实例,物理设备和任务队列以及 Surface
- 创建 Instances 实例
VkInstanceCreateInfo instance_create_info = {
VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
nullptr,
0,
&application_info,
0,
nullptr,
static_cast<uint32_t>(desired_extensions.size()),
desired_extensions.size() > 0 ? &desired_extensions[0] : nullptr
};
VkInstance inst;
VkResult result = vkCreateInstance( &instance_create_info, nullptr, &inst );
- 初始化物理设备,也就是我们的显卡设备,Vulkna 的设计是支持多 GPU 的,这里选择第一个设备就行了
uint32_t extensions_count = 0;
VkResult result = VK_SUCCESS;
//获取所有可用物理设备,并选择第一个
result = vkEnumerateDeviceExtensionProperties( physical_device, nullptr,
&extensions_count, &available_extensions[0]);
if( (result != VK_SUCCESS) ||
(extensions_count == 0) ) {
std::cout << "Could not get the number of device extensions." << std::endl;
return false;
}
- 获取 queue,Vulkan 的所有操作,从绘图到上传纹理,都需要将命令提交到队列中
uint32_t queue_families_count = 0;
//获取队列簇,并选择第一个
queue_families.resize( queue_families_count );
vkGetPhysicalDeviceQueueFamilyProperties( physical_device,
&queue_families_count, &queue_families[0] );
if( queue_families_count == 0 ) {
std::cout << "Could not acquire properties of queue families." << std::endl;
return false;
}
- 初始化逻辑设备,在选择要使用的物理设备之后,我们需要设置一个逻辑设备用于交互
VkResult result = vkCreateDevice( physical_device, &device_create_info,
nullptr, &logical_device );
if( (result != VK_SUCCESS) ||
(logical_device == VK_NULL_HANDLE) ) {
std::cout << "Could not create logical device." << std::endl;
return false;
}
return true;
- 上述初始完毕后,接着初始化 Surface,然后我们就可以使用 Vulkan 进行绘制了
#ifdef VK_USE_PLATFORM_WIN32_KHR
//创建WIN32的surface,如果是Android,需要使用VkAndroidSurfaceCreateInfoKHR
VkWin32SurfaceCreateInfoKHR surface_create_info = {
VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR,
nullptr,
0,
window_parameters.HInstance,
window_parameters.HWnd
};
VkResult result = vkCreateWin32SurfaceKHR(
instance, &surface_create_info, nullptr, &presentation_surface );
2.通过 vkCmdDraw 函数进行图像绘制
void vkCmdDraw(
//在Vulkan中,像绘画命令、内存转换等操作并不是直接通过方法调用去完成的
// 而是需要把所有的操作放在Command Buffer里
VkCommandBuffer commandBuffer,
uint32_t vertexCount, //顶点数量
uint32_t instanceCount, // 要画的instance数量,没有:置1
uint32_t firstVertex,// vertex buffer中第一个位置 和 vertex Shader 里gl_vertexIndex 相关。
uint32_t firstInstance);// 同firstVertex 类似。
3.提交 buffer
if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) {
throw std::runtime_error("failed to submit draw command buffer!");
}
我在这里比较浅显的介绍了 Vulkan 的用法,但上面介绍的只是 Vulkan 的一点皮毛,Vulkan 的使用比 OpenGL 要复杂的很多,机制也复杂很多,如果想进一步了解 Vulkan 还是得专门去深入研究。虽然只介绍了一点皮毛,但已经可以让我们去了解 Vulkan 这一图像生产者,是如何在 Android 系统中生产图像的,下面就来看看吧。
3.2 Vulkan进行硬件加速
在前面讲 OpenGL 进行硬件加速时,提到了 CanvasContext,它会根据渲染的类型选择不同的渲染管线,Android 是通过 Vulkan 或者还是通过 OpenGL 渲染,主要是 CanvasContext 里选择的渲染管线的不同。
CanvasContext* CanvasContext::create(RenderThread& thread,
bool translucent, RenderNode* rootRenderNode, IContextFactory* contextFactory) {
auto renderType = Properties::getRenderPipelineType();
switch (renderType) {
case RenderPipelineType::OpenGL:
return new CanvasContext(thread, translucent, rootRenderNode, contextFactory,
std::make_unique<OpenGLPipeline>(thread));
case RenderPipelineType::SkiaGL:
return new CanvasContext(thread, translucent, rootRenderNode, contextFactory,
std::make_unique<skiapipeline::SkiaOpenGLPipeline>(thread));
case RenderPipelineType::SkiaVulkan:
return new CanvasContext(thread, translucent, rootRenderNode, contextFactory,
std::make_unique<skiapipeline::SkiaVulkanPipeline>(thread));
default:
LOG_ALWAYS_FATAL("canvas context type %d not supported", (int32_t) renderType);
break;
}
return nullptr;
}
我们这里直接看 SkiaVulkanPipeline。
//文件->/frameworks/base/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
SkiaVulkanPipeline::SkiaVulkanPipeline(renderthread::RenderThread& thread)
: SkiaPipeline(thread), mVkManager(thread.vulkanManager()) {}
SkiaVulkanPipeline 的构造函数中初始化了 VulkanManager,VulkanManager 是对 Vulkan 使用的封装,和前面讲到的 OpenGLPipeline 中的 EglManager 类似。我们看一下 VulkanManager 的初始化函数。
//文件-->/frameworks/base/libs/hwui/renderthread/VulkanManager.cpp
void VulkanManager::initialize() {
if (hasVkContext()) {
return;
}
auto canPresent = [](VkInstance, VkPhysicalDevice, uint32_t) { return true; };
mBackendContext.reset(GrVkBackendContext::Create(vkGetInstanceProcAddr,
vkGetDeviceProcAddr, &mPresentQueueIndex, canPresent));
//……
}
初始化函数中我们主要关注 GrVkBackendContext::Create 方法。
// Create the base Vulkan objects needed by the GrVkGpu object
const GrVkBackendContext* GrVkBackendContext::Create(uint32_t* presentQueueIndexPtr,
CanPresentFn canPresent,
GrVkInterface::GetProc getProc) {
//……
const VkInstanceCreateInfo instance_create = {
VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, // sType
nullptr, // pNext
0, // flags
&app_info, // pApplicationInfo
(uint32_t) instanceLayerNames.count(), // enabledLayerNameCount
instanceLayerNames.begin(), // ppEnabledLayerNames
(uint32_t) instanceExtensionNames.count(), // enabledExtensionNameCount
instanceExtensionNames.begin(), // ppEnabledExtensionNames
};
ACQUIRE_VK_PROC(CreateInstance, VK_NULL_HANDLE, VK_NULL_HANDLE);
//1,创建Vulkan实例
err = grVkCreateInstance(&instance_create, nullptr, &inst);
if (err < 0) {
SkDebugf("vkCreateInstance failed: %d\n", err);
return nullptr;
}
uint32_t gpuCount;
//2,查询可用物理设备
err = grVkEnumeratePhysicalDevices(inst, &gpuCount, nullptr);
if (err) {
//……
}
//……
gpuCount = 1;
//3,选择物理设备
err = grVkEnumeratePhysicalDevices(inst, &gpuCount, &physDev);
if (err) {
//……
}
//4,查询队列簇
uint32_t queueCount;
grVkGetPhysicalDeviceQueueFamilyProperties(physDev, &queueCount, nullptr);
if (!queueCount) {
//……
return nullptr;
}
SkAutoMalloc queuePropsAlloc(queueCount * sizeof(VkQueueFamilyProperties));
// now get the actual queue props
VkQueueFamilyProperties* queueProps = (VkQueueFamilyProperties*)queuePropsAlloc.get();
//5,选择队列簇
grVkGetPhysicalDeviceQueueFamilyProperties(physDev, &queueCount, queueProps);
//……
// iterate to find the graphics queue
const VkDeviceCreateInfo deviceInfo = {
VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, // sType
nullptr, // pNext
0, // VkDeviceCreateFlags
queueInfoCount, // queueCreateInfoCount
queueInfo, // pQueueCreateInfos
(uint32_t) deviceLayerNames.count(), // layerCount
deviceLayerNames.begin(), // ppEnabledLayerNames
(uint32_t) deviceExtensionNames.count(), // extensionCount
deviceExtensionNames.begin(), // ppEnabledExtensionNames
&deviceFeatures // ppEnabledFeatures
};
//6,创建逻辑设备
err = grVkCreateDevice(physDev, &deviceInfo, nullptr, &device);
if (err) {
SkDebugf("CreateDevice failed: %d\n", err);
grVkDestroyInstance(inst, nullptr);
return nullptr;
}
auto interface =
sk_make_sp<GrVkInterface>(getProc, inst, device, extensionFlags);
if (!interface->validate(extensionFlags)) {
SkDebugf("Vulkan interface validation failed\n");
grVkDeviceWaitIdle(device);
grVkDestroyDevice(device, nullptr);
grVkDestroyInstance(inst, nullptr);
return nullptr;
}
VkQueue queue;
grVkGetDeviceQueue(device, graphicsQueueIndex, 0, &queue);
GrVkBackendContext* ctx = new GrVkBackendContext();
ctx->fInstance = inst;
ctx->fPhysicalDevice = physDev;
ctx->fDevice = device;
ctx->fQueue = queue;
ctx->fGraphicsQueueIndex = graphicsQueueIndex;
ctx->fMinAPIVersion = kGrVkMinimumVersion;
ctx->fExtensions = extensionFlags;
ctx->fFeatures = featureFlags;
ctx->fInterface.reset(interface.release());
ctx->fOwnsInstanceAndDevice = true;
return ctx;
}
可以看到,GrVkBackendContext::Create 中所作的事情就是初始化 Vulkan,初始化的流程和前面介绍如何使用 Vulkan 中初始化流程都是一样的,这些都是通用的流程。
初始化完成,我们接着看看 Vulkan 如何绑定 Surface,只有绑定了 Surface,我们才能使用 Vulkan 进行图像绘制。
//文件-->/frameworks/base/libs/hwui/renderthread/VulkanManager.cpp
VulkanSurface* VulkanManager::createSurface(ANativeWindow* window) {
initialize();
if (!window) {
return nullptr;
}
VulkanSurface* surface = new VulkanSurface();
VkAndroidSurfaceCreateInfoKHR surfaceCreateInfo;
memset(&surfaceCreateInfo, 0, sizeof(VkAndroidSurfaceCreateInfoKHR));
surfaceCreateInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR;
surfaceCreateInfo.pNext = nullptr;
surfaceCreateInfo.flags = 0;
surfaceCreateInfo.window = window;
VkResult res = mCreateAndroidSurfaceKHR(mBackendContext->fInstance, &surfaceCreateInfo, nullptr,
&surface->mVkSurface);
if (VK_SUCCESS != res) {
delete surface;
return nullptr;
}
SkDEBUGCODE(VkBool32 supported; res = mGetPhysicalDeviceSurfaceSupportKHR(
mBackendContext->fPhysicalDevice, mPresentQueueIndex,
surface->mVkSurface, &supported);
// All physical devices and queue families on Android must be capable of
// presentation with any
// native window.
SkASSERT(VK_SUCCESS == res && supported););
if (!createSwapchain(surface)) {
destroySurface(surface);
return nullptr;
}
return surface;
}
可以看到,这个创建了 VulkanSurface,并绑定了 ANativeWindow,ANativeWindow 是 Android 的原生窗口,在前面介绍 OpenGL 进行硬件渲染时,也提到过 createSurface 这个函数,它是在 performDraw 被执行的,在这里就不重复说了。
接下来就是调用 Vulkan 的 api 进行绘制的图像的流程
bool SkiaVulkanPipeline::draw(const Frame& frame, const SkRect& screenDirty,
const SkRect& dirty, const FrameBuilder::LightGeometry& lightGeometry,
LayerUpdateQueue* layerUpdateQueue, const Rect& contentDrawBounds,
bool opaque, bool wideColorGamut, const BakedOpRenderer::LightInfo& lightInfo,
const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler) {
sk_sp<SkSurface> backBuffer = mVkSurface->getBackBufferSurface();
if (backBuffer.get() == nullptr) {
return false;
}
SkiaPipeline::updateLighting(lightGeometry, lightInfo);
renderFrame(*layerUpdateQueue, dirty, renderNodes, opaque,
wideColorGamut, contentDrawBounds, backBuffer);
layerUpdateQueue->clear();
// Draw visual debugging features
if (CC_UNLIKELY(Properties::showDirtyRegions ||
ProfileType::None != Properties::getProfileType())) {
SkCanvas* profileCanvas = backBuffer->getCanvas();
SkiaProfileRenderer profileRenderer(profileCanvas);
profiler->draw(profileRenderer);
profileCanvas->flush();
}
// Log memory statistics
if (CC_UNLIKELY(Properties::debugLevel != kDebugDisabled)) {
dumpResourceCacheUsage();
}
return true;
}
最后通过 swapBuffers 提交绘制内容
void VulkanManager::swapBuffers(VulkanSurface* surface) {
if (CC_UNLIKELY(Properties::waitForGpuCompletion)) {
ATRACE_NAME("Finishing GPU work");
mDeviceWaitIdle(mBackendContext->fDevice);
}
SkASSERT(surface->mBackbuffers);
VulkanSurface::BackbufferInfo* backbuffer =
surface->mBackbuffers + surface->mCurrentBackbufferIndex;
GrVkImageInfo* imageInfo;
SkSurface* skSurface = surface->mImageInfos[backbuffer->mImageIndex].mSurface.get();
skSurface->getRenderTargetHandle((GrBackendObject*)&imageInfo,
SkSurface::kFlushRead_BackendHandleAccess);
// Check to make sure we never change the actually wrapped image
SkASSERT(imageInfo->fImage == surface->mImages[backbuffer->mImageIndex]);
// We need to transition the image to VK_IMAGE_LAYOUT_PRESENT_SRC_KHR and make sure that all
// previous work is complete for before presenting. So we first add the necessary barrier here.
VkImageLayout layout = imageInfo->fImageLayout;
VkPipelineStageFlags srcStageMask = layoutToPipelineStageFlags(layout);
VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
VkAccessFlags srcAccessMask = layoutToSrcAccessMask(layout);
VkAccessFlags dstAccessMask = VK_ACCESS_MEMORY_READ_BIT;
VkImageMemoryBarrier imageMemoryBarrier = {
VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, // sType
NULL, // pNext
srcAccessMask, // outputMask
dstAccessMask, // inputMask
layout, // oldLayout
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, // newLayout
mBackendContext->fGraphicsQueueIndex, // srcQueueFamilyIndex
mPresentQueueIndex, // dstQueueFamilyIndex
surface->mImages[backbuffer->mImageIndex], // image
{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1} // subresourceRange
};
mResetCommandBuffer(backbuffer->mTransitionCmdBuffers[1], 0);
VkCommandBufferBeginInfo info;
memset(&info, 0, sizeof(VkCommandBufferBeginInfo));
info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
info.flags = 0;
mBeginCommandBuffer(backbuffer->mTransitionCmdBuffers[1], &info);
mCmdPipelineBarrier(backbuffer->mTransitionCmdBuffers[1], srcStageMask, dstStageMask, 0, 0,
nullptr, 0, nullptr, 1, &imageMemoryBarrier);
mEndCommandBuffer(backbuffer->mTransitionCmdBuffers[1]);
surface->mImageInfos[backbuffer->mImageIndex].mImageLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
// insert the layout transfer into the queue and wait on the acquire
VkSubmitInfo submitInfo;
memset(&submitInfo, 0, sizeof(VkSubmitInfo));
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.waitSemaphoreCount = 0;
submitInfo.pWaitDstStageMask = 0;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &backbuffer->mTransitionCmdBuffers[1];
submitInfo.signalSemaphoreCount = 1;
// When this command buffer finishes we will signal this semaphore so that we know it is now
// safe to present the image to the screen.
submitInfo.pSignalSemaphores = &backbuffer->mRenderSemaphore;
// Attach second fence to submission here so we can track when the command buffer finishes.
mQueueSubmit(mBackendContext->fQueue, 1, &submitInfo, backbuffer->mUsageFences[1]);
// Submit present operation to present queue. We use a semaphore here to make sure all rendering
// to the image is complete and that the layout has been change to present on the graphics
// queue.
const VkPresentInfoKHR presentInfo = {
VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, // sType
NULL, // pNext
1, // waitSemaphoreCount
&backbuffer->mRenderSemaphore, // pWaitSemaphores
1, // swapchainCount
&surface->mSwapchain, // pSwapchains
&backbuffer->mImageIndex, // pImageIndices
NULL // pResults
};
mQueuePresentKHR(mPresentQueue, &presentInfo);
surface->mBackbuffer.reset();
surface->mImageInfos[backbuffer->mImageIndex].mLastUsed = surface->mCurrentTime;
surface->mImageInfos[backbuffer->mImageIndex].mInvalid = false;
surface->mCurrentTime++;
}
这些流程都和 OpenGL 是一样的,初始化,绑定 Surface,绘制,提交,所以就不细说了,对 Vulkan 有兴趣的,可以深入的去研究。至此 Android 中的另一个图像生产者 Vulkan 生产图像的流程也讲完了。
四 总结
OpenGL,Skia,Vulkan 都是跨平台的图形生产者,我们不仅仅可以在 Android 设备上使用,我们也可以在 IOS 设备上使用,也可以在 Windows 设备上使用,使用的流程基本和上面一致,但是需要适配设备的原生窗口和缓冲,所以掌握了 Android 是如何绘制图像的,我们也具备了掌握其他任何设备上是如何绘制图像的能力。
在随后的两篇文章中,我们会介绍 Android 图像渲染原理的最后一部分:图像缓冲区。这三部分如果都能掌握,我们基本就掌握了 Android 中图像绘制原理了。