一 概述
在上一篇文章 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