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

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

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

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

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

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

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

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

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

    SurfaceComposerClient::Transaction t;
    t.setLayer(surfaceControl, 0x40000000).apply();
    //通過(guò)getSurface接口獲取到Surface對(duì)象
    sp<Surface> surface = surfaceControl->getSurface();
    
    ANativeWindow_Buffer buffer;
    //通過(guò)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);//這里繪圖,由于我們沒(méi)有使用任何圖形庫(kù),所以這里把內(nèi)存填成255, 畫(huà)一個(gè)純色畫(huà)面
    
    surface->unlockAndPost();//這里會(huì)調(diào)用到queueBuffer,把我們繪制好的畫(huà)面提交給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溝通的橋梁,第二步,通過(guò)SurfaceComposerClient的createLayer接口創(chuàng)建一個(gè)SurfaceControl,這是我們控制Surface的一個(gè)工具,第三步,從SurfaceControl的getSurface接口來(lái)獲取Surface對(duì)象,這是我們操作BufferQueue的接口。

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

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

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

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

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

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();//和上一示例一樣要開(kāi)啟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拿來(lái)
    
    SkBitmap* bitmapDevice = new SkBitmap;
    SkIRect* updateRect = new SkIRect;
    SkBitmap* bitmap = new SkBitmap;
    initBitmap(bitmap, "/sdcard/picture.png");//從文件讀一個(gè)bitmap出來(lái)
    
    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畫(huà)東西就是在向幀緩沖區(qū)buffer畫(huà)東西
    } 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把圖片畫(huà)到bitmapDevice,也就是畫(huà)到了從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ù),然后我們通過(guò)skia提供的操作接口把一張圖片畫(huà)到了幀緩沖區(qū)buffer中,由此可以看出我們想使用圖形庫(kù)來(lái)操作幀緩沖區(qū)的關(guān)鍵是要把幀緩沖區(qū)buffer的地址對(duì)接到圖形庫(kù)提供的接口上。

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

8.3. 使用OpenGL&EGL的繪圖

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

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

using namespace android;

int main()
{
    sp<ProcessState> proc(ProcessState::self());
    ProcessState::self()->startThreadPool();//同樣地開(kāi)啟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
    };
    
    //開(kāi)始初始化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ù)來(lái)一個(gè)純紅色的畫(huà)面
    glClear(GL_COLOR_BUFFER_BIT);
    
    eglSwapBuffers(display, eglSurface);//這里會(huì)調(diào)用到Surface的queueBuffer方法,提交畫(huà)好的幀緩沖區(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來(lái)間接操作幀緩沖區(qū)buffer,這些操作包括申請(qǐng)新的BufferQueue slot和提交繪制好的BufferQueue slot.

本章小結(jié)

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

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

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

image-20210922143904647.png

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

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

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

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

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

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

10. 結(jié)語(yǔ)

Android的Display系統(tǒng)是Android平臺(tái)上一個(gè)相對(duì)比較復(fù)雜的系統(tǒng),文中所述均是筆者通過(guò)閱讀源碼、閱讀網(wǎng)上其他人分享的文章、平時(shí)工作中的感悟以及在工作中向同事請(qǐng)教總結(jié)而來(lái)。限于自身的知識(shí)結(jié)構(gòu)和技術(shù)背景,未必有些理解是正確的,請(qǐng)讀者閱讀過(guò)程中多思考,多以源碼為準(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)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,382評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,038評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,853評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,616評(píng)論 6 408
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,112評(píng)論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,355評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,869評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,727評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,928評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,165評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,570評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,813評(píng)論 1 282
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,585評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,892評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容