一、普通View生成圖片的原理
我們先來分析下從普通View中獲取圖片的方法。程式碼如下:
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()
方法。我們來研究下這個方法的實現。
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的影象。
在Android中繪製一個二維影象需要四個基本元件: 1、a Bitmap:儲存影象畫素資料(to hold the pixels) 2、a Canvas:包含一系列繪製和影象變換的方法(to host the draw calls,writing into the bitmap) 3、a drawing primitive:影象原型 (e.g. Rect, Path, text, Bitmap) 4、a paint:畫筆描述繪製顏色、風格 (to describe the colors and styles for the drawing)
一句話描述:canvas 用畫筆把影象原型繪製到bitmap上。
二、同理為啥不能從SurfaceView中獲取圖片呢?
從上分析中可以知道獲取普通View的圖形就是呼叫View的Draw方法在新的Bitmap上再繪製一次。那為啥同樣的邏輯在SurfaceView上無效呢?讓我們來看下SurfaceView
的Draw
方法的實現。
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,表示目標圖的顏色. 程式碼註釋就是重疊部分影象合成的計算公式。
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層只繪製了一個透明視窗。
看到這裡就明白了為啥從SurfaceView中獲取不到影象快取了。普通View獲取影象換成的原理是呼叫View的Draw方法在新的Bitmap上繪製一次View的內容,但是SurfaceView比較特別,它的展示內容繪製不是通過draw流程繪製的,所以我們通過這種方式獲取不到影象快取。
如果是這樣,那又會有一個疑問了,SurfaceView上展示的影象內容到底是怎麼繪製的呢,和普通View的影象繪製有什麼區別呢?
三、Android上影象渲染流程
上面程式碼以繪製文字為例,展示了在普通View和SurfaceView上繪製影象的程式碼實現。它們的共同點是都是用canvas
來繪製影象。不同的地方是普通View是從複寫的onDraw(Canvas canvas)
方法中獲取到canvas
的,而SurfaceView是從surface
中獲取canvas
來繪製的。
3.1 普通View的繪製
想要弄清楚View是怎麼繪製的得先弄明白View是怎麼建立出來的。我們先來看下View的建立流程。
Android應用開發都都知道,在Android應用中建立一個互動介面使用的四大元件之一的Activity,在Activity的onResume生命週期方法執行後介面就展示出來了。如上圖所示介面建立流程大致分三個步驟:
- 步驟一:建立Activity,這個過程會建立一個PhoneWindow例項;
- 步驟二:在Activity的
onCreate
生命週期中setContentView
設定應用開發者定義的佈局View。佈局設定的過程是委派給PhoneWindow來完成的。PhoneWindow
先建立介面根佈局,其中包括了一些系統資訊展示的區域,然後把應用開發者傳進來的應用介面放置到應用資訊展示區域。整個介面佈局形成一棵佈局樹ViewTree。 - 步驟三:在Activity的
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
上的不同區域繪製影象資料。
3.1 SurfaceView的繪製
我們再來詳細看下在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樹繪製流程中繪製的。
四、如何解決這個問題
5.1 SurfaceView內容是開發者繪製的
既然繪製工作是自己做的,那麼獲取圖片時可以模仿view.getDrawingCache()
方法實現一個SurfaceView的getDrawingCache()
方法即可。
5.2 SurfaceView顯示內容是其他模組繪製的
常見的我們將surface
設定到MediaPlayer
、MediaCodec
模組中,顯示內容由這些模組來繪製的,那麼繪製方法我們就是未知的也就實現不了類getDrawingCache()的功能。這種情況下我們可以換用TextureView
來實現。