Android图形系统(九)-View、Canvas与Surface的关系

我们已经分析了,mWindowSession.addToDisplay 通过WMS.addWindow 我们建立了app与SurfaceFlinger服务连接。并且通过requestLayout中的relayoutWindow, app请求SurfaceFlinger创建了Surface。那么接下来,我们再分析下app的视图是如何被绘制到GraphicFrame上的。这里面牵扯到的View、Canvas与Surface的关系,用这篇文章来梳理一下。(流程走的是软件绘制流程)

之前我们讲到requestLayout,开始了view的measure、layout、draw流程,我们从performDraw开始关注下最后的视图绘制工作:

//ViewRootImpl
private void performDraw() {
      ...
      draw(fullRedrawNeeded);
      ...
}

然后看ViewRootImpl的draw方法:

//ViewRootImpl
  private void draw(boolean fullRedrawNeeded) {
       Surface surface = mSurface;
    ...
               if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                   return;
               }
    ...
   }

这里我们看到,surface 在这里被接收了,并传入了drawSoftware。

//ViewRootImpl
  private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
           boolean scalingRequired, Rect dirty) {
// Draw with software renderer.
final Canvas canvas;
canvas = mSurface.lockCanvas(dirty);  //1.获取Canvas
...
mView.draw(canvas); //2.通过Canvas绘制视图
...
surface.unlockCanvasAndPost(canvas); //3.绘制结束
}

在drawSoftware方法中,我们重点关注如上三个方法:

一、 Surface的lockCanvas函数
//Surface
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;
   }
}

其最终调用了native函数nativeLockCanvas

//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;
    //从SurfaceFlinger中申请内存buffer
   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);
   //新建了一个SkBitmap,并进行了一系列设置
   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);
    //把这个bitmap放入Canvas中
   nativeCanvas->setBitmap(bitmap);
   ...
   sp<Surface> lockedSurface(surface);
   lockedSurface->incStrong(&sRefBaseOwner);
   return (jlong) lockedSurface.get();
}

这里主要关注几个点:

1.1 surface->lock(&outBuffer, dirtyRectPtr)

调用了Surface的lock函数实际上主要是调用了Surface的dequeueBuffer,而这个函数的主要目的是从SurfaceFlinger中申请GraphicBuffer, 这个buffer是用来传递绘制的元数据的。

1.2 GraphicsJNI::getNativeCanvas(env, canvasObj)

构造一个native的Canvas对象(SKCanvas),再返回这个Canvas对象,java层的Canvas对象其实只是对SKCanvas对象的一个简单包装,所有绘制方法都是转交给SKCanvas来做。

1.3 SkBitmap bitmap

Canvas底层是通过2D图形引擎skia进行图形绘制的,SkBitmap是skia中很重要的一个类,很多画图动作涉及到SkBitmap,它封装了与位图相关的一系列操作。那么在这里,bitmap对下设置了获取的内存buffer,对上关联了Canvas ,即把这个bitmap放入了Canvas中( nativeCanvas->setBitmap(bitmap) )

总结:Surface的lockCanvas函数会通过jni调用对应的native方法,本质是通过Surface的dequeueBuffer获取一块用于存放绘制元数据的GraphicBuffer,然后构造一个SKBitmap,它是绘制的核心, 对下关联buffer,对上关联canvas。

二、mView.draw(canvas)

这其实就是通过Canvas去实现具体的绘制。
以TextView的一部分绘制代码为例:

protected void onDraw(Canvas canvas) {
...
if (dr.mShowing[Drawables.LEFT] != null) {
   canvas.save();//坐标系的原点,坐标轴方向的信息。
   canvas.translate(scrollX + mPaddingLeft + leftOffset,
                    scrollY + compoundPaddingTop +
                    (vspace - dr.mDrawableHeightLeft) / 2);
   dr.mShowing[Drawables.LEFT].draw(canvas);
   canvas.restore();//恢复Canvas之前保存的状态
  }
...
}

主要看绘制:

//Canvas.java
public void translate(float dx, float dy) {
    native_translate(mNativeCanvasWrapper, dx, dy);
}
//android_graphics_Canvas.cpp
static void translate(JNIEnv*, jobject, jlong canvasHandle, jfloat dx, jfloat dy) {
    get_canvas(canvasHandle)->translate(dx, dy);
}
//external/skia/src/core/SkCanvas.cpp
void SkiaCanvas::translate(float dx, float dy) {
    mCanvas->translate(dx, dy);
}
//external/skia/src/core/SkCanvas.cpp
void SkCanvas::translate(SkScalar dx, SkScalar dy) {
   if (dx || dy) {
       this->checkForDeferredSave();
       fMCRec->fMatrix.preTranslate(dx,dy);
       // Translate shouldn't affect the is-scale-translateness of the matrix.
      SkASSERT(fIsScaleTranslate == fMCRec->fMatrix.isScaleTranslate());
      FOR_EACH_TOP_DEVICE(device->setGlobalCTM(fMCRec->fMatrix));
     this->didTranslate(dx,dy);
  }
}

从SkCanvas.cpp的路径我们可以看出,这已经在skia绘制引擎部分,这里就不深究了。只要了解:我们通过java层Canvas封装的api调用底层SKCanvas来完成真正的绘制工作就足够了。

三、surface.unlockCanvasAndPost(canvas);
//Surface.java
public void unlockCanvasAndPost(Canvas canvas) {
        synchronized (mLock) {
            checkNotReleasedLocked();
            if (mHwuiContext != null) {
                mHwuiContext.unlockAndPost(canvas);
            } else {
                unlockSwCanvasAndPost(canvas);
            }
        }
    }

正常是调用unlockSwCanvasAndPost:

//Surface.java
private void unlockSwCanvasAndPost(Canvas canvas) {
    if (canvas != mCanvas) {
        throw new IllegalArgumentException("canvas object must be the same instance that "
                + "was previously returned by lockCanvas");
    }
    if (mNativeObject != mLockedObject) {
        Log.w(TAG, "WARNING: Surface's mNativeObject (0x" +
                Long.toHexString(mNativeObject) + ") != mLockedObject (0x" +
                Long.toHexString(mLockedObject) +")");
    }
    if (mLockedObject == 0) {
        throw new IllegalStateException("Surface was not locked");
    }
    try {
        nativeUnlockCanvasAndPost(mLockedObject, canvas);
    } finally {
        nativeRelease(mLockedObject);
        mLockedObject = 0;
    }
}

往下看native方法

//android_view_Surface.cpp
static void nativeUnlockCanvasAndPost(JNIEnv* env, jclass clazz,
        jlong nativeObject, jobject canvasObj) {
    sp<Surface> surface(reinterpret_cast<Surface *>(nativeObject));
    if (!isSurfaceValid(surface)) {
        return;
    }
    // detach the canvas from the surface
    Canvas* nativeCanvas = GraphicsJNI::getNativeCanvas(env, canvasObj);
    nativeCanvas->setBitmap(SkBitmap());
    // unlock surface
    status_t err = surface->unlockAndPost();
    if (err < 0) {
        doThrowIAE(env);
    }
}

看下如何解锁surface:

//Surface.cpp
status_t Surface::unlockAndPost()
{
    if (mLockedBuffer == 0) {
        ALOGE("Surface::unlockAndPost failed, no locked buffer");
        return INVALID_OPERATION;
    }
    int fd = -1;
    status_t err = mLockedBuffer->unlockAsync(&fd);//通过Gralloc模块,最后是操作的ioctl
    ALOGE_IF(err, "failed unlocking buffer (%p)", mLockedBuffer->handle);
    err = queueBuffer(mLockedBuffer.get(), fd);
    ALOGE_IF(err, "queueBuffer (handle=%p) failed (%s)",
            mLockedBuffer->handle, strerror(-err));
    mPostedBuffer = mLockedBuffer;
    mLockedBuffer = 0;
    return err;
}

我们看到了queueBuffer函数, 而在Surface的queueBuffer函数中调用了如下函数:

mGraphicBufferProducer->queueBuffer

这个函数最终会将BufferItem的buffer清除,通知消费者的onFrameAvailable接口。然后消费者可以根据mSlots的序号再来拿buffer。

//frameworks/native/libs/gui/BufferQueueProducer.cpp
status_t BufferQueueProducer::queueBuffer(int slot,
      const QueueBufferInput &input, QueueBufferOutput *output) {
  ...

    item.mGraphicBuffer.clear();
    item.mSlot = BufferItem::INVALID_BUFFER_SLOT;
    // Call back without the main BufferQueue lock held, but with the callback
    // lock held so we can ensure that callbacks occur in order
    {
        Mutex::Autolock lock(mCallbackMutex);
        while (callbackTicket != mCurrentCallbackTicket) {
            mCallbackCondition.wait(mCallbackMutex);
        }
        if (frameAvailableListener != NULL) {
            frameAvailableListener->onFrameAvailable(item);
        } else if (frameReplacedListener != NULL) {
            frameReplacedListener->onFrameReplaced(item);
        }
        ++mCurrentCallbackTicket;
        mCallbackCondition.broadcast();
    }
...
}

所以整个过程看起来还是比较简单的。最后把整个流程再简单总结下,View、Canvas与Surface的关系也就一目了然了:

  1. Surface通过dequeueBuffer流程(具体操作在此不多赘述)获取一块存放绘制数据的buffer。

  2. View 在onDraw的时候,通过传入的Canvas进行绘制。(这里只是一个绘制的入口而已,本文是针对requestLayout流程来讲述的,当然你单独用Canvas实现绘制也是一样的)。

  3. 调用java层的CanvasAPI,实际真正负责绘制工作的是底层的Skia引擎,这里核心类包括SKCanvas(画家)以及SKBitmap(画布),绘制好的内容放入Surface 通过dequeueBuffer获取到的GraphicBuffer。

  4. 绘制完毕后,Surface通过queueBuffer将存放好绘制数据的buffer投递到队列中,并通知SurfaceFlinger消费。

参考:
https://blog.csdn.net/kc58236582/article/details/52879698
https://blog.csdn.net/jianpan_zouni/article/details/77649271

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

推荐阅读更多精彩内容

  • 故事源于身边的人 用文字记录下点滴 词虽不算优美华丽 但字字表达了真情 今天是2017年1月28日,大年初一,好多...
    有棱角的石头阅读 166评论 2 0
  • 勿言男儿少挂牵 谁晓愁海多绵缠 拄了竹杖 披了麻裳 悠悠地随性徜徉 邀了明月 对影成双 冥冥中阑干千行 叹宦海无幄...
    兰之猗阅读 342评论 3 6
  • 我觉得代理可以出彩的有两种人,这两种人,都是可以做出相当不错成绩的人,而往往那些夹在两种类型之间的代理,反倒...
    熙熙Breathe阅读 340评论 0 2
  • A:一起爬山的人太少,没意思? B:一人一世界,得一人得世界。 欣赏一人,便欣赏全世界。 A:不热闹,无聊? B:...
    君远近阅读 99评论 0 0