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

一、普通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(),跟蹤源碼可知最終調(diào)用到View.javabuildDrawingCacheImpl()方法。我們來研究下這個方法的實現(xiàn)。

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設(shè)置給一個Canvas,然后再調(diào)用源View的Draw方法,將圖像原型繪制到新Bitmap上。簡單說,就是通過Canvas把源View的圖像原型繪制到新Bitmap中,這樣再將新Bitmap保存起來就得到了View的圖像。

在Android中繪制一個二維圖像需要四個基本組件:
1、a Bitmap:保存圖像像素數(shù)據(jù)(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:畫筆描述繪制顏色、風(fēng)格 (to describe the colors and styles for the drawing)

一句話描述:canvas 用畫筆把圖像原型繪制到bitmap上。

二、同理為啥不能從SurfaceView中獲取圖片呢?

從上分析中可以知道獲取普通View的圖形就是調(diào)用View的Draw方法在新的Bitmap上再繪制一次。那為啥同樣的邏輯在SurfaceView上無效呢?讓我們來看下SurfaceViewDraw方法的實現(xiàn)。

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方法及其簡單,就上面這幾行代碼。關(guān)鍵代碼就這行canvas.drawColor(0, PorterDuff.Mode.CLEAR);源碼中注釋已經(jīng)解釋了這行代碼的作用,就是在View層打一個洞露出View層下面的東西。從下面?zhèn)渥⒖梢钥吹绞褂?code>PorterDuff.Mode.CLEAR模式drawColor就是繪制全透明。

PorterDuff.Mode 我的理解就是兩張圖片重疊的部分圖像合成模式。下面是PorterDuff.Mode的部分源碼。
Sa:全稱為Source alpha,表示源圖的Alpha通道;
Sc:全稱為Source color,表示源圖的顏色;
Da:全稱為Destination alpha,表示目標(biāo)圖的Alpha通道;
Dc:全稱為Destination color,表示目標(biāo)圖的顏色.
代碼注釋就是重疊部分圖像合成的計算公式。

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方法最終調(diào)用了super.draw(canvas),實際調(diào)用View的onDraw方法來繪制View的內(nèi)容,但是我們看SurfaceView的源碼發(fā)現(xiàn)它沒有實現(xiàn)onDraw方法。也就是說在普通View遞歸繪制過程中,SurfaceView在View層只繪制了一個透明窗口。

看到這里就明白了為啥從SurfaceView中獲取不到圖像緩存了。普通View獲取圖像換成的原理是調(diào)用View的Draw方法在新的Bitmap上繪制一次View的內(nèi)容,但是SurfaceView比較特別,它的展示內(nèi)容繪制不是通過draw流程繪制的,所以我們通過這種方式獲取不到圖像緩存。

如果是這樣,那又會有一個疑問了,SurfaceView上展示的圖像內(nèi)容到底是怎么繪制的呢,和普通View的圖像繪制有什么區(qū)別呢?

三、Android上圖像渲染流程

在View和SurfaceView上繪制文字

上面代碼以繪制文字為例,展示了在普通View和SurfaceView上繪制圖像的代碼實現(xiàn)。它們的共同點是都是用canvas來繪制圖像。不同的地方是普通View是從復(fù)寫的onDraw(Canvas canvas)方法中獲取到canvas的,而SurfaceView是從surface中獲取canvas來繪制的。

3.1 普通View的繪制

想要弄清楚View是怎么繪制的得先弄明白View是怎么創(chuàng)建出來的。我們先來看下View的創(chuàng)建流程。


Android界面創(chuàng)建過程

Android應(yīng)用開發(fā)都都知道,在Android應(yīng)用中創(chuàng)建一個交互界面使用的四大組件之一的Activity,在Activity的onResume生命周期方法執(zhí)行后界面就展示出來了。如上圖所示界面創(chuàng)建流程大致分三個步驟:

  • 步驟一:創(chuàng)建Activity,這個過程會創(chuàng)建一個PhoneWindow實例;
  • 步驟二:在Activity的onCreate生命周期中setContentView設(shè)置應(yīng)用開發(fā)者定義的布局View。布局設(shè)置的過程是委派給PhoneWindow來完成的。PhoneWindow先創(chuàng)建界面根布局,其中包括了一些系統(tǒng)信息展示的區(qū)域,然后把應(yīng)用開發(fā)者傳進(jìn)來的應(yīng)用界面放置到應(yīng)用信息展示區(qū)域。整個界面布局形成一棵布局樹ViewTree。
  • 步驟三:在Activity的onResume生命周期中將ViewTree添加到WMS中,WMS通過ViewRootImpl來觸發(fā)ViewTree的遞歸測量、布局和繪制的流程。這個過程完成后界面就展示出來了。

從上面流程圖可以看出界面繪制是從ViewRootImpl中開始觸發(fā)的。來看下精簡后的performTraversals方法。

frameworks\base\core\java\android\view\ViewRootImpl.java

private void performTraversals() {
    ...
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
    performLayout(lp, mWidth, mHeight);
    ...
    performDraw();
    ...
}

就是我們熟知的measure - layout - draw流程。今天我們主要關(guān)心View的繪制,我們來看下Draw的流程,主要看下在View的Draw方法中傳遞進(jìn)來Canvas對象是怎么產(chǎn)生的。

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);
        ...
        // 這里就調(diào)用到View里了,平時復(fù)寫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屬性,當(dāng)界面繪制時,就調(diào)用mSurface.lockCanvas方法獲取一個Canvas對象傳遞個View遞歸繪制。ViewRootImpl簡易類圖如下。

ViewRootImpl類圖

Canvas: 封裝了一系列繪制的方法;
Surface: 圖像數(shù)據(jù)保存區(qū)。

通過下面的Surface的源碼可以看到mSurface.lockCanvas實際就是Canvas設(shè)置了一個Bitmap。而后的View遞歸繪制就是在Surface創(chuàng)建的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設(shè)置Bitmap
    nativeCanvas->setBitmap(bitmap);

    sp<Surface> lockedSurface(surface);
    lockedSurface->incStrong(&sRefBaseOwner);
    return (jlong) lockedSurface.get();
}

到這里普通View的繪制就算是跑通了。一個PhoneWindow實例就對應(yīng)一個界面,以它通過樹形結(jié)構(gòu)組織Views,把根View設(shè)置到ViewRootImpl實例中,ViewRootImpl實例和根部局實例是一一對應(yīng)的,ViewRootImpl接收系統(tǒng)消息來后通過根部局觸發(fā)遞歸繪制。我們的界面像素數(shù)據(jù)保存在Surface中,這個Surface就是在ViewRootImpl中創(chuàng)建的。

view繪制

從上面圖可以看出雖然各個view都有自己的onDraw方法,但是他們使用的canvas是同一個對象,實際上他們是在同一個surface上的不同區(qū)域繪制圖像數(shù)據(jù)。

3.1 SurfaceView的繪制

我們再來詳細(xì)看下在SurfaceView上繪制文字的過程。在SurfaceView這個繪制場景中我們屢一下前面講到圖像繪制的四要素,圖像原型就是我們需要繪制的文字、畫筆就是繪制是創(chuàng)建的paint實例、繪制方法就是canvas對象的drawText方法、像素承載容器就是surface。

SurfaceView類圖

從上圖可以看出在SurfaceView繪制過程中有兩個surface。一個是繼承自普通View繪制流程從ViewRootImpl傳遞出來的mSurface1,另一個是SurfaceView自己的屬性mSurface2。在View數(shù)遞歸繪制過程中,SurfaceView只在mSurface1上繪制了一個透明區(qū)域,沒有繪制任何實質(zhì)的內(nèi)容。真正SurfaceView展示的內(nèi)容是直接操作mSurface2來繪制的。也就是說SurfaceView顯示內(nèi)容更新不需要走View樹遞歸繪制的過程,直接操作自己私有的mSurface2即可,這也是為什么我們可以通過非UI線程來更新SurfaceView顯示內(nèi)容的原因。

SurfaceView繪制

到這里我們SurfaceView的繪制流程也清楚了。到這里文章標(biāo)題的疑問就比較好回答了。從普通view中獲取圖像的方法view.getDrawingCache()實質(zhì)是調(diào)用View樹繪制的方法在新的bitmap上再繪制一次圖像原型。但是SurfaceView的展示圖像卻不是在View樹繪制流程中繪制的。

四、如何解決這個問題

5.1 SurfaceView內(nèi)容是開發(fā)者繪制的

既然繪制工作是自己做的,那么獲取圖片時可以模仿view.getDrawingCache()方法實現(xiàn)一個SurfaceView的getDrawingCache()方法即可。

5.2 SurfaceView顯示內(nèi)容是其他模塊繪制的

常見的我們將surface設(shè)置到MediaPlayerMediaCodec模塊中,顯示內(nèi)容由這些模塊來繪制的,那么繪制方法我們就是未知的也就實現(xiàn)不了類getDrawingCache()的功能。這種情況下我們可以換用TextureView來實現(xiàn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容