Android畫面顯示流程分析(5)

努比亞技術(shù)團(tuán)隊(duì)原創(chuàng)內(nèi)容,轉(zhuǎn)載請(qǐng)務(wù)必注明出處。

8. 應(yīng)用是如何繪圖的

目前很多游戲類應(yīng)用都是借由SurfaceView申請(qǐng)到畫布,然后自主上幀,并不依賴Vsync信號(hào), 所以本章通過幾個(gè)helloworld示例來看下應(yīng)用側(cè)是如何繪圖和上幀的。

由于java層很多接口是對(duì)C層接口的JNI封裝,這里我們只看一些C層接口的用法。下面的示例代碼為縮減篇幅把一些異常處理部分的代碼去除了,只保留了重要的部分,如果讀者需要執(zhí)行示例代碼,可以自行加入一些異常處理部分。

8.1. 無圖形庫(kù)支持下的繪圖

下面的示例中演示的是如何使用C層接口向SurfaceFlinger申請(qǐng)一塊畫布,然后不使用任何圖形庫(kù),直接修改畫布上的像素值,最后提交給SurfaceFlinger顯示。

int main()
{
    sp<ProcessState> proc(ProcessState::self());
    ProcessState::self()->startThreadPool();//在應(yīng)用和SurfaceFlinger溝通過程中要使用到binder, 所以這里要先初始化binder線程池

    sp<SurfaceComposerClient> client = new SurfaceComposerClient();//SurfaceComposerClient是SurfaceFlinger在應(yīng)用側(cè)的代表, SurfaceFlinger的接口通過它來提供
    client->initCheck();
    //先通過createSurface接口來申請(qǐng)一塊畫布,參數(shù)里包含對(duì)畫布起的名字,大小,位深信息
    sp<SurfaceControl> surfaceControl = client->createSurface(String8("Console Surface"),800, 600, PIXEL_FORMAT_RGBA_8888);

    SurfaceComposerClient::Transaction t;
    t.setLayer(surfaceControl, 0x40000000).apply();
    //通過getSurface接口獲取到Surface對(duì)象
    sp<Surface> surface = surfaceControl->getSurface();
    
    ANativeWindow_Buffer buffer;
    //通過Surface的lock方法調(diào)用到dequeueBuffer,獲取到一個(gè)BufferQueue可用的Slot
    status_t err = surface->lock(&buffer, NULL);// &clipRegin

    void* addr = buffer.bits;
    ssize_t len = buffer.stride * 4 * buffer.height;
    memset(addr, 255, len);//這里繪圖,由于我們沒有使用任何圖形庫(kù),所以這里把內(nèi)存填成255, 畫一個(gè)純色畫面
    
    surface->unlockAndPost();//這里會(huì)調(diào)用到queueBuffer,把我們繪制好的畫面提交給SurfaceFlinger

    printf("sleep...\n");
    usleep(5 * 1000 * 1000);
    
    surface.clear();
    surfaceControl.clear();
    
    printf("complete. CTRL+C to finish.\n");
    IPCThreadState::self()->joinThreadPool();
    return 0;
}

在上面的示例中,幾個(gè)關(guān)建點(diǎn)是,第一步,先創(chuàng)建出一個(gè)SurfaceComposerClient,它是我們和Surfaceflinger溝通的橋梁,第二步,通過SurfaceComposerClient的createLayer接口創(chuàng)建一個(gè)SurfaceControl,這是我們控制Surface的一個(gè)工具,第三步,從SurfaceControl的getSurface接口來獲取Surface對(duì)象,這是我們操作BufferQueue的接口。

有了Surface對(duì)象,我們可以通過Surface的lock方法來dequeueBuffer, 再通過unlockAndPost接口來queueBuffer, 循環(huán)執(zhí)行,我們就可以對(duì)畫布進(jìn)行連續(xù)繪制和提交數(shù)據(jù)了,屏幕上動(dòng)態(tài)的畫面就出來了。

所以對(duì)于SurfaceFlinger或者說對(duì)于Display系統(tǒng)底層所提供的接口主要就是這三個(gè)SurfaceComposerClient, SurfaceControl和Surface. 這里我們不妨稱其為Display系統(tǒng)接口三大件。

8.2. 有圖形庫(kù)支持下的繪圖

在上節(jié)示例中,我們并沒有去繪畫復(fù)雜的圖案,只是使用內(nèi)存填充的方式畫了一個(gè)純色畫面,在本節(jié)中我們將嘗試使用圖形庫(kù)在給定的畫布上畫一些復(fù)雜的圖案,比如畫一張圖片上去。

在上節(jié)的討論中我們知道要畫畫面出來,要拿到Display的三大件(SurfaceComposerClient, SurfaceControl和Surface),接下來拿到畫布后我們使用skia庫(kù)來畫一張圖片到屏幕上。

using namespace android;
//先寫一個(gè)函數(shù)把圖片轉(zhuǎn)成一個(gè)bitmap
static status_t initBitmap(SkBitmap* bitmap, const char* fileName) {
    if (fileName == NULL) {
        return NO_INIT;
    }
    sk_sp<SkData> data = SkData::MakeFromFileName(fileName);
    sk_sp<SkImage> image = SkImage::MakeFromEncoded(data);
     bool  result  = image->asLegacyBitmap(bitmap, SkImage::kRO_LegacyBitmapMode);
    if(!result ){
        printf("decode picture fail!");
        return NO_INIT;
    }
    return NO_ERROR;
}

int main()
{
    sp<ProcessState> proc(ProcessState::self());
    ProcessState::self()->startThreadPool();//和上一示例一樣要開啟binder線程池

    // create a client to surfaceflinger
    sp<SurfaceComposerClient> client = new SurfaceComposerClient();//三大件第一件
    client->initCheck();
    sp<SurfaceControl> surfaceControl = client->createSurface(String8("Consoleplayer Surface"),800, 600, PIXEL_FORMAT_RGBA_8888);//三大件第二件

    SurfaceComposerClient::Transaction t;
    t.setLayer(surfaceControl, 0x40000000).apply();

    sp<Surface> surface = surfaceControl->getSurface();//三大件第三件
    sp<IGraphicBufferProducer> graphicBufferProducer = surface->getIGraphicBufferProducer();

    ANativeWindow_Buffer buffer;
    status_t err = surface->lock(&buffer, NULL);//調(diào)用dequeueBuffer把buffer拿來
    
    SkBitmap* bitmapDevice = new SkBitmap;
    SkIRect* updateRect = new SkIRect;
    SkBitmap* bitmap = new SkBitmap;
    initBitmap(bitmap, "/sdcard/picture.png");//從文件讀一個(gè)bitmap出來
    
    printf("decode picture done.\n");
    ssize_t bpr = buffer.stride * bytesPerPixel(buffer.format);
    SkColorType config = convertPixelFormat(buffer.format);
    bitmapDevice->setInfo(SkImageInfo::Make(buffer.width, buffer.height, config, kPremul_SkAlphaType), bpr);
    //上面我們創(chuàng)建了另一個(gè)SkBitmap對(duì)象bitmapDevice
    if (buffer.width > 0 && buffer.height > 0) {
        bitmapDevice->setPixels(buffer.bits);//這里把幀緩沖區(qū)buffer的地址設(shè)給了bitmapDevice,這時(shí)和bitmapDevice畫東西就是在向幀緩沖區(qū)buffer畫東西
    } else {
        bitmapDevice->setPixels(NULL);
    }
    //SkRegion region;
    printf("to create canvas..\n");
    SkCanvas* nativeCanvas = new SkCanvas(*bitmapDevice);
    SkRect sr;
    sr.set(*updateRect);
    nativeCanvas->clipRect(sr);
    SkPaint paint;
    nativeCanvas->clear(SK_ColorBLACK);
    const SkRect dst = SkRect::MakeXYWH(0,0,800, 600);
    paint.setAlpha(255);
    const SkIRect src1 = SkIRect::MakeXYWH(0, 0, bitmap->width(), bitmap->height());
    printf("draw ....\n");
    nativeCanvas->drawBitmapRect((*bitmap), src1, dst, &paint);//調(diào)用SkCanvas的drawBitmapRect把圖片畫到bitmapDevice,也就是畫到了從Surface申請(qǐng)到的幀緩沖區(qū)buffer中
    
    surface->unlockAndPost();//調(diào)用queueBuffer把buffer提交給SurfaceFlinger顯示

    printf("sleep...\n");
    usleep(10 * 1000 * 1000);
    
    surface.clear();
    surfaceControl.clear();
    
    printf("test complete. CTRL+C to finish.\n");
    IPCThreadState::self()->joinThreadPool();
    return 0;
}

在上面的示例中獲取到幀緩沖區(qū)buffer的方式和上一個(gè)例子是一樣的,不同點(diǎn) 是我們把申請(qǐng)到的buffer的地址空間給到了skia庫(kù),然后我們通過skia提供的操作接口把一張圖片畫到了幀緩沖區(qū)buffer中,由此可以看出我們想使用圖形庫(kù)來操作幀緩沖區(qū)的關(guān)鍵是要把幀緩沖區(qū)buffer的地址對(duì)接到圖形庫(kù)提供的接口上。

在android平臺(tái)上,我們通常不會(huì)直接使用CPU去繪圖,通常是調(diào)用opengl或其他圖形庫(kù)去指揮GPU去做這些繪圖的事情,那么又是如何使用opengl庫(kù)來完成繪圖的呢?

8.3. 使用OpenGL&EGL的繪圖

由上面第二個(gè)例子可知,要想使用一個(gè)圖形庫(kù)來向幀緩沖區(qū)buffer繪圖的關(guān)建是要把對(duì)應(yīng)的buffer給到圖形庫(kù), 我們知道opengl是一套設(shè)備無關(guān)的api接口,它和平臺(tái)是無關(guān)的,所以和Surface接口的任務(wù)是由EGL庫(kù)來完成的,幀緩沖區(qū)buffer要和EGL庫(kù)對(duì)接。

在hwui繪圖中是以如下結(jié)構(gòu)對(duì)接的:

image-20210904123515681.png

首先EGL庫(kù)會(huì)提供一個(gè)EGLSurface的對(duì)象,這個(gè)對(duì)象是對(duì)三大件中的Surface的一個(gè)封裝,它本身與幀提交相關(guān)部分提供了兩個(gè)接口:dequeue/queue,分別對(duì)應(yīng)Surface的dequeueBuffer和queueBuffer.

下面我們通過一個(gè)示例來看下它在C層是如何使用和與三大件對(duì)接的:

using namespace android;

int main()
{
    sp<ProcessState> proc(ProcessState::self());
    ProcessState::self()->startThreadPool();//同樣地開啟binder線程池

    // create a client to surfaceflinger
    sp<SurfaceComposerClient> client = new SurfaceComposerClient();//三大件第一件
    client->initCheck();
    sp<SurfaceControl> surfaceControl = client->createSurface(String8("Consoleplayer Surface"),800, 600, PIXEL_FORMAT_RGBA_8888);//三大件第二件

    SurfaceComposerClient::Transaction t;
    t.setLayer(surfaceControl, 0x40000000).apply();

    sp<Surface> surface = surfaceControl->getSurface();//三大件第三件
    
    // initialize opengl and egl
    const EGLint attribs[] = {
            EGL_RED_SIZE,   8,
            EGL_GREEN_SIZE, 8,
            EGL_BLUE_SIZE,  8,
            EGL_DEPTH_SIZE, 0,
            EGL_NONE
    };
    
    //開始初始化EGL庫(kù)
    EGLint w, h;
    EGLSurface eglSurface;
    EGLint numConfigs;
    EGLConfig config;
    EGLContext context;
    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    
    eglInitialize(display, 0, 0);
    eglChooseConfig(display, attribs, &config, 1, &numConfigs);
    eglSurface = eglCreateWindowSurface(display, config, surface.get(), NULL);//創(chuàng)建eglSurface(對(duì)Surface的一個(gè)封裝)
    context = eglCreateContext(display, config, NULL, NULL);
    eglQuerySurface(display, eglSurface, EGL_WIDTH, &w);
    eglQuerySurface(display, eglSurface, EGL_HEIGHT, &h);

    if (eglMakeCurrent(display, eglSurface, eglSurface, context) == EGL_FALSE)//會(huì)調(diào)用dequeue以獲取幀緩沖區(qū)buffer
        return NO_INIT;

    glShadeModel(GL_FLAT);
    glDisable(GL_DITHER);
    glDisable(GL_SCISSOR_TEST);
    //draw red 
    glClearColor(255,0,0,1);//這里用opengl庫(kù)來一個(gè)純紅色的畫面
    glClear(GL_COLOR_BUFFER_BIT);
    
    eglSwapBuffers(display, eglSurface);//這里會(huì)調(diào)用到Surface的queueBuffer方法,提交畫好的幀緩沖區(qū)數(shù)據(jù)


    printf("sleep...\n");
    usleep(10 * 1000 * 1000);
    
    surface.clear();
    surfaceControl.clear();
    
    printf("test complete. CTRL+C to finish.\n");
    IPCThreadState::self()->joinThreadPool();
    return 0;
}

在上面的例子中我們看到了opengl&egl庫(kù)對(duì)幀緩沖區(qū)buffer的使用方式,首先和8.1的示例中一樣從三大件中獲取的幀緩沖區(qū)操作接口,只是這里我們不再直接使用該接口,而是把Surface對(duì)象給到EGL庫(kù),由EGL庫(kù)去使用它,我們使用opengl 的api來間接操作幀緩沖區(qū)buffer,這些操作包括申請(qǐng)新的BufferQueue slot和提交繪制好的BufferQueue slot.

本章小結(jié)

本章我們通過三個(gè)示例程序了解了下display部分給應(yīng)用層設(shè)計(jì)的接口,了解到了通過三大件可以拿到幀緩沖區(qū)buffer, 之后應(yīng)用如何作畫就是應(yīng)用層的事情了,應(yīng)用可以選擇不使用圖形庫(kù),也可以選擇圖形庫(kù)讓cpu來作畫,也可以使用像opengl&egl這樣的庫(kù)來指揮GPU來作畫。

9. 應(yīng)用畫面更新總結(jié)

通過以上章節(jié)的了解,APP的畫面要顯示到屏幕上大致上要經(jīng)過如下圖所示系統(tǒng)組件的處理:

image-20210922143904647.png

首先App向SurfaceFlinger申請(qǐng)畫布(通過dequeueBuffer接口),SurfaceFlinger內(nèi)部有一個(gè)BufferQueue的管理實(shí)體,它會(huì)分配一個(gè)GraphicBuffer給到APP, App拿到buffer后調(diào)用圖形庫(kù)向這塊buffer內(nèi)繪畫。

APP繪畫完成后使用向SurfaceFlinger提交繪制完成的buffer(通過queueBuffer接口), 當(dāng)然這時(shí)候的繪制完成只是說在CPU側(cè)繪制完成,此時(shí)GPU可能還在該buffer上作畫,所以這時(shí)向SurfaceFlinger提交數(shù)據(jù)的同時(shí)還會(huì)帶上一個(gè)acquireFence,使用接下來使用該buffer的人能知道什么時(shí)候buffer使用完畢了。

SurfaceFlinger收到應(yīng)用提交的幀緩沖區(qū)buffer后是在下一個(gè)vsync-sf信號(hào)來時(shí)做處理,首先遍歷所有的Layer, 找到哪些Layer有上幀, 通過acquireBuffer把Buffer拿出來,通知給HWC Service去參與合成, 最后調(diào)用HWC Service的presentDisplay接口來告知HWC Service SurfaceFlinger的工作已完成。

HWC Service收到合成任務(wù)后開始合成數(shù)據(jù),在SurfaceFlinger調(diào)用presetDisplay時(shí)會(huì)去調(diào)用DRM接口DRMAtomicReq::Commit通知kernel可以向DDIC發(fā)送數(shù)據(jù)了.

如果有TE信號(hào)來提示已進(jìn)入消隱區(qū),這時(shí)DRM驅(qū)動(dòng)會(huì)馬上開始通過DSI總線向DDIC傳輸數(shù)據(jù),與此同時(shí)Panel的Disp Scan也在進(jìn)行中,傳輸完成后這幀畫面就完整地顯示到了屏幕上。

至此,一幀畫面的更新過程就完成了,我們這里講了這么久的一個(gè)復(fù)雜的過程,其實(shí)在高刷手機(jī)上一秒鐘要重復(fù)做100多次!_

10. 結(jié)語

Android的Display系統(tǒng)是Android平臺(tái)上一個(gè)相對(duì)比較復(fù)雜的系統(tǒng),文中所述均是筆者通過閱讀源碼、閱讀網(wǎng)上其他人分享的文章、平時(shí)工作中的感悟以及在工作中向同事請(qǐng)教總結(jié)而來。限于自身的知識(shí)結(jié)構(gòu)和技術(shù)背景,未必有些理解是正確的,請(qǐng)讀者閱讀過程中多思考,多以源碼為準(zhǔn),文中所述請(qǐng)僅做參考。文中有不正確的地方也歡迎大家批評(píng)指正。

特別感謝如下作者的知識(shí)分享:

作者: ariesjzj 題目:《Android中的GraphicBuffer同步機(jī)制-Fence》 地址:https://blog.csdn.net/jinzhuojun/article/details/39698317

作者:-Yaong- 題目:《linux GPU上多個(gè)buffer間的同步之ww_mutex、dma_fence的使用 筆記》地址https://www.cnblogs.com/yaongtime/p/14332526.html

作者:lyf 題目《android graphic(16)—fence(簡(jiǎn)化)》 地址:https://zhuanlan.zhihu.com/p/68782817

作者:何小龍 題目:《LCD顯示異常分析——撕裂(tear effect)》 地址:https://blog.csdn.net/hexiaolong2009/article/details/79319512?spm=1001.2014.3001.5501

作者:迅猛一只虎 題目:《LCD timing 時(shí)序參數(shù)總結(jié)》 地址:https://blog.csdn.net/wending1986/article/details/106837597

作者:kerneler_ 題目:《LCD屏?xí)r序分析》 地址:https://blog.csdn.net/skyflying2012/article/details/8553893

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

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

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