爲啥從SurfaceView中獲取不到圖片?

1、普通View生成圖片的原理

咱們先來分析下從普通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.javabuildDrawingCacheImpl()方法。咱們來研究下這個方法的實現。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

2、同理爲啥不能從SurfaceView中獲取圖片呢?

從上分析中能夠知道獲取普通View的圖形就是調用View的Draw方法在新的Bitmap上再繪製一次。那爲啥一樣的邏輯在SurfaceView上無效呢?讓咱們來看下SurfaceViewDraw方法的實現。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的圖像繪製有什麼區別呢?

3、Android上圖像渲染流程

在View和SurfaceView上繪製文字

上面代碼以繪製文字爲例,展現了在普通View和SurfaceView上繪製圖像的代碼實現。它們的共同點是都是用canvas來繪製圖像。不一樣的地方是普通View是從複寫的onDraw(Canvas canvas)方法中獲取到canvas的,而SurfaceView是從surface中獲取canvas來繪製的。

3.1 普通View的繪製

想要弄清楚View是怎麼繪製的得先弄明白View是怎麼建立出來的。咱們先來看下View的建立流程。

Android界面建立過程

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簡易類圖以下。

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中建立的。

view繪製
從上面圖能夠看出雖然各個view都有本身的 onDraw方法,可是他們使用的canvas是同一個對象,實際上他們是在同一個 surface上的不一樣區域繪製圖像數據。

3.1 SurfaceView的繪製

咱們再來詳細看下在SurfaceView上繪製文字的過程。在SurfaceView這個繪製場景中咱們屢一下前面講到圖像繪製的四要素,圖像原型就是咱們須要繪製的文字、畫筆就是繪製是建立的paint實例、繪製方法就是canvas對象的drawText方法、像素承載容器就是surface。

SurfaceView類圖

從上圖能夠看出在SurfaceView繪製過程當中有兩個surface。一個是繼承自普通View繪製流程從ViewRootImpl傳遞出來的mSurface1,另外一個是SurfaceView本身的屬性mSurface2。在View數遞歸繪製過程當中,SurfaceView只在mSurface1上繪製了一個透明區域,沒有繪製任何實質的內容。真正SurfaceView展現的內容是直接操做mSurface2來繪製的。也就是說SurfaceView顯示內容更新不須要走View樹遞歸繪製的過程,直接操做本身私有的mSurface2便可,這也是爲何咱們能夠經過非UI線程來更新SurfaceView顯示內容的緣由。

SurfaceView繪製

到這裏咱們SurfaceView的繪製流程也清楚了。到這裏文章標題的疑問就比較好回答了。從普通view中獲取圖像的方法view.getDrawingCache()實質是調用View樹繪製的方法在新的bitmap上再繪製一次圖像原型。可是SurfaceView的展現圖像卻不是在View樹繪製流程中繪製的。

4、如何解決這個問題

5.1 SurfaceView內容是開發者繪製的

既然繪製工做是本身作的,那麼獲取圖片時能夠模仿view.getDrawingCache()方法實現一個SurfaceView的getDrawingCache()方法便可。

5.2 SurfaceView顯示內容是其餘模塊繪製的

常見的咱們將surface設置到MediaPlayerMediaCodec模塊中,顯示內容由這些模塊來繪製的,那麼繪製方法咱們就是未知的也就實現不了類getDrawingCache()的功能。這種狀況下咱們能夠換用TextureView來實現。

相關文章
相關標籤/搜索