为啥从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(),跟踪源码可知最终调用到View.javabuildDrawingCacheImpl()方法。我们来研究下这个方法的实现。

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上无效呢?让我们来看下SurfaceViewDraw方法的实现。

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上绘制文字

上面代码以绘制文字为例,展示了在普通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树绘制流程中绘制的。

四、如何解决这个问题

5.1 SurfaceView内容是开发者绘制的

既然绘制工作是自己做的,那么获取图片时可以模仿view.getDrawingCache()方法实现一个SurfaceView的getDrawingCache()方法即可。

5.2 SurfaceView显示内容是其他模块绘制的

常见的我们将surface设置到MediaPlayerMediaCodec模块中,显示内容由这些模块来绘制的,那么绘制方法我们就是未知的也就实现不了类getDrawingCache()的功能。这种情况下我们可以换用TextureView来实现。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容