咱们先来分析下从普通View中获取图片的方法。代码以下:java
public Bitmap getBitmapFromView(View view){
if (view == null) {
return null;
}
view.setDrawingCacheEnabled(true);
Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache());
view.setDrawingCacheEnabled(false);
view.destroyDrawingCache();
return bitmap;
}
复制代码
上面是从普通view获取图像的方法,核心API是view.getDrawingCache()
,跟踪源码可知最终调用到View.java
的buildDrawingCacheImpl()
方法。咱们来研究下这个方法的实现。android
frameworks\base\core\java\android\view\View.java
private void buildDrawingCacheImpl() {
Bitmap bitmap = Bitmap.createBitmap(mResources.getDisplayMetrics(), width, height, quality);
Canvas canvas = new Canvas(bitmap);
final int restoreCount = canvas.save();
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
draw(canvas);
}
canvas.restoreToCount(restoreCount);
}
复制代码
上面是我精简后的方法,能够很清晰的看到普通View生成图像的原理就是,生成一个新的Bitmap,把这个新的Bitmap设置给一个Canvas,而后再调用源View的Draw方法,将图像原型绘制到新Bitmap上。简单说,就是经过Canvas把源View的图像原型绘制到新Bitmap中,这样再将新Bitmap保存起来就获得了View的图像。canvas
在Android中绘制一个二维图像须要四个基本组件: 一、a Bitmap:保存图像像素数据(to hold the pixels) 二、a Canvas:包含一系列绘制和图像变换的方法(to host the draw calls,writing into the bitmap) 三、a drawing primitive:图像原型 (e.g. Rect, Path, text, Bitmap) 四、a paint:画笔描述绘制颜色、风格 (to describe the colors and styles for the drawing)缓存
一句话描述:canvas 用画笔把图像原型绘制到bitmap上。bash
从上分析中能够知道获取普通View的图形就是调用View的Draw方法在新的Bitmap上再绘制一次。那为啥一样的逻辑在SurfaceView上无效呢?让咱们来看下SurfaceView
的Draw
方法的实现。ide
frameworks\base\core\java\android\view\SurfaceView.java
@Override
public void draw(Canvas canvas) {
if (mWindowType != WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
// draw() is not called when SKIP_DRAW is set
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) {
// punch a whole in the view-hierarchy below us
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
}
super.draw(canvas);
}
复制代码
SurfaceView的Draw方法及其简单,就上面这几行代码。关键代码就这行canvas.drawColor(0, PorterDuff.Mode.CLEAR);
源码中注释已经解释了这行代码的做用,就是在View层打一个洞露出View层下面的东西。从下面备注能够看到使用PorterDuff.Mode.CLEAR
模式drawColor
就是绘制全透明。布局
PorterDuff.Mode 个人理解就是两张图片重叠的部分图像合成模式。下面是PorterDuff.Mode的部分源码。 Sa:全称为Source alpha,表示源图的Alpha通道; Sc:全称为Source color,表示源图的颜色; Da:全称为Destination alpha,表示目标图的Alpha通道; Dc:全称为Destination color,表示目标图的颜色. 代码注释就是重叠部分图像合成的计算公式。ui
frameworks\base\graphics\java\android\graphics\PorterDuff.java
public enum Mode {
/** [0, 0] */
CLEAR (0),
/** [Sa, Sc] */
SRC (1),
/** [Da, Dc] */
DST (2),
/** [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] */
SRC_OVER (3),
...
}
复制代码
Draw方法最终调用了super.draw(canvas)
,实际调用View的onDraw
方法来绘制View的内容,可是咱们看SurfaceView
的源码发现它没有实现onDraw
方法。也就是说在普通View递归绘制过程当中,SurfaceView在View层只绘制了一个透明窗口。spa
看到这里就明白了为啥从SurfaceView中获取不到图像缓存了。普通View获取图像换成的原理是调用View的Draw方法在新的Bitmap上绘制一次View的内容,可是SurfaceView比较特别,它的展现内容绘制不是经过draw流程绘制的,因此咱们经过这种方式获取不到图像缓存。线程
若是是这样,那又会有一个疑问了,SurfaceView上展现的图像内容究竟是怎么绘制的呢,和普通View的图像绘制有什么区别呢?
上面代码以绘制文字为例,展现了在普通View和SurfaceView上绘制图像的代码实现。它们的共同点是都是用canvas
来绘制图像。不一样的地方是普通View是从复写的onDraw(Canvas canvas)
方法中获取到canvas
的,而SurfaceView是从surface
中获取canvas
来绘制的。
想要弄清楚View是怎么绘制的得先弄明白View是怎么建立出来的。咱们先来看下View的建立流程。
Android应用开发都都知道,在Android应用中建立一个交互界面使用的四大组件之一的Activity,在Activity的onResume生命周期方法执行后界面就展现出来了。如上图所示界面建立流程大体分三个步骤:
onCreate
生命周期中setContentView
设置应用开发者定义的布局View。布局设置的过程是委派给PhoneWindow来完成的。PhoneWindow
先建立界面根布局,其中包括了一些系统信息展现的区域,而后把应用开发者传进来的应用界面放置到应用信息展现区域。整个界面布局造成一棵布局树ViewTree。onResume
生命周期中将ViewTree添加到WMS中,WMS经过ViewRootImpl
来触发ViewTree的递归测量、布局和绘制的流程。这个过程完成后界面就展现出来了。从上面流程图能够看出界面绘制是从ViewRootImpl
中开始触发的。来看下精简后的performTraversals
方法。
frameworks\base\core\java\android\view\ViewRootImpl.java
private void performTraversals() {
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
...
}
复制代码
就是咱们熟知的measure - layout - draw
流程。今天咱们主要关心View的绘制,咱们来看下Draw的流程,主要看下在View的Draw方法中传递进来Canvas
对象是怎么产生的。
frameworks\base\core\java\android\view\ViewRootImpl.java
final Surface mSurface = new Surface();
private void performDraw() {
...
mIsDrawing = true;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
try {
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
...
}
private void draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
if (!surface.isValid()) {
return;
}
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
...
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
...
// Draw with software renderer.
final Canvas canvas;
try {
canvas = mSurface.lockCanvas(dirty);
...
// 这里就调用到View里了,平时复写View的onDraw(Canvas canvas)方法绘制图像时用到的canvas就是这里传递下去的。
mView.draw(canvas);
...
} finally {
try {
surface.unlockCanvasAndPost(canvas);
} catch (IllegalArgumentException e) {
Log.e(mTag, "Could not unlock surface", e);
mLayoutRequested = true;
return false;
}
}
return true;
}
复制代码
从上述源码能够看到ViewRootImpl
有一个Surface
属性,当界面绘制时,就调用mSurface.lockCanvas
方法获取一个Canvas
对象传递个View递归绘制。ViewRootImpl
简易类图以下。
Canvas: 封装了一系列绘制的方法; Surface: 图像数据保存区。
经过下面的Surface
的源码能够看到mSurface.lockCanvas
实际就是Canvas设置了一个Bitmap。然后的View递归绘制就是在Surface建立的Bitmap上绘制。
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;
}
}
复制代码
frameworks\base\core\jni\android_view_Surface.cpp
static jlong nativeLockCanvas(JNIEnv* env, jclass clazz, jlong nativeObject, jobject canvasObj, jobject dirtyRectObj) {
sp<Surface> surface(reinterpret_cast<Surface *>(nativeObject));
ANativeWindow_Buffer outBuffer;
status_t err = surface->lock(&outBuffer, dirtyRectPtr);
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) {
bitmap.setPixels(outBuffer.bits);
} else {
// be safe with an empty bitmap.
bitmap.setPixels(NULL);
}
Canvas* nativeCanvas = GraphicsJNI::getNativeCanvas(env, canvasObj);
// 给Canvas设置Bitmap
nativeCanvas->setBitmap(bitmap);
sp<Surface> lockedSurface(surface);
lockedSurface->incStrong(&sRefBaseOwner);
return (jlong) lockedSurface.get();
}
复制代码
到这里普通View的绘制就算是跑通了。一个PhoneWindow
实例就对应一个界面,以它经过树形结构组织Views,把根View设置到ViewRootImpl
实例中,ViewRootImpl
实例和根部局实例是一一对应的,ViewRootImpl
接收系统消息来后经过根部局触发递归绘制。咱们的界面像素数据保存在Surface
中,这个Surface
就是在ViewRootImpl中建立的。
onDraw
方法,可是他们使用的canvas是同一个对象,实际上他们是在同一个
surface
上的不一样区域绘制图像数据。
咱们再来详细看下在SurfaceView上绘制文字的过程。在SurfaceView这个绘制场景中咱们屡一下前面讲到图像绘制的四要素,图像原型就是咱们须要绘制的文字、画笔就是绘制是建立的paint实例、绘制方法就是canvas对象的drawText方法、像素承载容器就是surface。
从上图能够看出在SurfaceView
绘制过程当中有两个surface
。一个是继承自普通View绘制流程从ViewRootImpl
传递出来的mSurface1
,另外一个是SurfaceView
本身的属性mSurface2
。在View数递归绘制过程当中,SurfaceView只在mSurface1
上绘制了一个透明区域,没有绘制任何实质的内容。真正SurfaceView
展现的内容是直接操做mSurface2
来绘制的。也就是说SurfaceView
显示内容更新不须要走View树递归绘制的过程,直接操做本身私有的mSurface2
便可,这也是为何咱们能够经过非UI线程来更新SurfaceView
显示内容的缘由。
到这里咱们SurfaceView的绘制流程也清楚了。到这里文章标题的疑问就比较好回答了。从普通view中获取图像的方法view.getDrawingCache()
实质是调用View树绘制的方法在新的bitmap上再绘制一次图像原型。可是SurfaceView的展现图像却不是在View树绘制流程中绘制的。
既然绘制工做是本身作的,那么获取图片时能够模仿view.getDrawingCache()
方法实现一个SurfaceView的getDrawingCache()
方法便可。
常见的咱们将surface
设置到MediaPlayer
、MediaCodec
模块中,显示内容由这些模块来绘制的,那么绘制方法咱们就是未知的也就实现不了类getDrawingCache()的功能。这种状况下咱们能够换用TextureView
来实现。