Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android系統教程 >> 安卓省電與加速 >> Android應用程序UI硬件加速渲染的預加載資源地圖集服務(Asset Atlas Service)分析

Android應用程序UI硬件加速渲染的預加載資源地圖集服務(Asset Atlas Service)分析

編輯:安卓省電與加速

我們知道,Android系統在啟動的時候,會對一些系統資源進行預加載。這樣不僅使得應用程序在需要時可以快速地訪問這些資源,還使得這些資源能夠在不同應用程序之間進行共享。在硬件加速渲染環境中,這些預加載資源還有進一步優化的空間。Android系統提供了一個地圖集服務,負責將預加載資源合成為一個紋理上傳到GPU去,並且能夠在所有的應用程序之間進行共享。本文就詳細分析這個預加載資源地圖集服務的實現原理。

 

資源預加載是發生在Zygote進程的,然後Zygote進程fork了應用程序進程,於是就使得預加載的資源可以在Zygote進程與所有的應用程序進程進行共享。這種內存共享機制是由Linux進程創建方式決定的。也就是說,父進程fork子進程之後,只要它們都不去修改某一塊內存,那麼這塊內存就可以在父進程和子進程之間進行共享。一旦父進程或者子進程修改了某一塊內存,那麼Linux內核就會通過一種稱為COW(Copy On Wrtie)的技術為要修改的進程創建一塊內存拷貝出來,這時候被修改的內存就不再可以共享。

對於預加載資源來說,它們都是只讀的,因此就可以保證它們在Zygote進程與所有的應用程序進程進行共享。這在應用程序UI使用軟件方式渲染時可以工作得很好。但是當應用程序UI使用硬件加速渲染時,情況就發生了變化。資源一般是作為紋理來使用的。這意味著每一個應用程序都會將它要使用的預加載資源作為一個紋理上傳到GPU去,如圖1所示:

\

圖1 應用程序獨立將預加載資源上傳到GPU

因此,這種做法會浪費GPU內存。為了節省GPU內存,Android系統在System進程中運行了一個Asset Atlas Service。這個Asset Atlas Service將預加載資源合成為一個紋理,並且上傳到GPU去。應用程序進程可以向Asset Atlas Service請求上傳後的紋理,從而使得它們不需要再單獨去上傳一份,這樣就可以起到在GPU級別共享的作用,如圖2所示:

\

圖2 應用程序在GPU級別共享預加載資源

在圖2中,最右側顯示的是應用程序進程的Render Thread,它們通過Asset Atlas Service獲得已經上傳到GPU的預加載資源紋理,這樣就可以直接使用它們,而不再需要獨立上傳。

接下來,我們從Zygote進程預加載資源、System進程合成資源為紋理並且上傳到GPU,以及應用程序使用上傳後的紋理三個過程來描述預加載資源地圖集機制,以便可以更好地理解應用程序是如何做到在GPU級別共享預加載資源的。

我們首先看Zygote進程預加載資源的過程。從前面Android系統進程Zygote啟動過程的源代碼分析一文可以知道, Zygote進程在Java層的入口點為ZygoteInit類的靜態成員函數main,它的實現如下所示:

 

public class ZygoteInit {
    ......

    public static void main(String argv[]) {
        try {
            ......

            registerZygoteSocket(socketName);
            ......
            preload();
            ......

            if (startSystemServer) {
                startSystemServer(abiList, socketName);
            }

            ......
            runSelectLoop(abiList);

            ......
        } catch (MethodAndArgsCaller caller) {
            ......
        } catch (RuntimeException ex) {
            ......
        }
    }

    ......
}
這個函數定義在文件frameworks/base/core/java/com/android/internal/os/ZygoteInit.java中。

ZygoteInit類的靜態成員函數main的執行流程如下所示:

1. 調用成員函數registerZygoteSocket創建一個Server端的Socket,用來與運行在System進程中的ActivityManagerService服務通信,也就是用來接收ActivityManagerService發送過來的創建應用程序進程的請求。

2. 調用成員函數preload執行預加載資源的操作。

3. 調用成員函數startSystemServer啟動System進程。

4. 調用成員函數runSelectLoop進入一個循環中等待和處理ActivityManagerService服務發送過來的創建應用程序進程的請求。

這裡我們只關注資源預加載的過程,即ZygoteInit類的成員函數preload的實現,如下所示:

 

public class ZygoteInit {
    ......

    static void preload() {
        Log.d(TAG, "begin preload");
        preloadClasses();
        preloadResources();
        preloadOpenGL();
        preloadSharedLibraries();
        // Ask the WebViewFactory to do any initialization that must run in the zygote process,
        // for memory sharing purposes.
        WebViewFactory.prepareWebViewInZygote();
        Log.d(TAG, "end preload");
    }

    ......
}

這個函數定義在文件frameworks/base/core/java/com/android/internal/os/ZygoteInit.java中。

Zygote進程需要預加載的東西很多,包括:

1. 預加載系統類,這是通過調用ZygoteInit類的靜態成員函數preloadClasses實現的。

2. 預加載系統資源,這是通過調用ZygoteInit類的靜態成員函數preloadResources實現的。

3. 預加載Open GL資源,這是通過調用ZygoteInit類的靜態成員函數preloadOpenGL實現的。

4. 預加載一些共享庫,這是通過調用ZygoteInit類的靜態成員函數preloadSharedLibraries實現的。

5. 預加載WebView庫,這是通過調用WebViewFactory類的靜態成員函數prepareWebViewInZygote實現的。

所有的這些預加載行為都是為了實現內存共享目的的,也就是在Zygote進程和所有應用程序進程之間進行內存共享。這裡我們只關注系統資源的預加載過程,即ZygoteInit類的靜態成員函數preloadResources的實現,如下所示:

 

public class ZygoteInit {
    ......

    private static void preloadResources() {
        ......

        try {
            ......
            mResources = Resources.getSystem();
            mResources.startPreloading();
            if (PRELOAD_RESOURCES) {
                ......
                TypedArray ar = mResources.obtainTypedArray(
                        com.android.internal.R.array.preloaded_drawables);
                int N = preloadDrawables(runtime, ar);
                ......

                ar = mResources.obtainTypedArray(
                        com.android.internal.R.array.preloaded_color_state_lists);
                N = preloadColorStateLists(runtime, ar);
                ......
            }
            mResources.finishPreloading();
        } catch (RuntimeException e) {
            ......
        } finally {
            ......
        }
    }

    ......
}
這個函數定義在文件frameworks/base/core/java/com/android/internal/os/ZygoteInit.java中。

 

從這裡就可以看到,預加載的系統資源有兩類,一類是Drawable資源,另一類是Color State List資源,分別通過調用ZygoteInit類的靜態成員函數preloadDrawables和preloadColorStateLists實現。這兩類資源所包含的具體資源列表分別由frameworks/base/core/res/res/values/arrays.xml文件裡面的數組preloaded_drawables和preloaded_color_state_lists定義。

接下來我們只關注Drawable資源的預加載過程,即ZygoteInit類的靜態成員函數preloadDrawables的實現,如下所示:

 

public class ZygoteInit {
    ......

    private static int preloadDrawables(VMRuntime runtime, TypedArray ar) {
        int N = ar.length();
        for (int i=0; i       這個函數定義在文件frameworks/base/core/java/com/android/internal/os/ZygoteInit.java中。

 

ZygoteInit類的靜態成員函數preloadDrawables通過調用靜態成員變量mResources指向的一個Resources對象的成員函數getDrawable來依次讀取由參數ar描述的一系列Drawable資源。ZygoteInit類的靜態成員變量mResources指向的Resources對象就是用來描述系統資源的,從前面Android資源管理框架(Asset Manager)簡要介紹和學習計劃這個系列的文章可以知道,當我們調用它的成員函數getXXX來獲取指定的資源時,如果該資源還沒有加載,那麼就會被加載。

這些被預加載的Drawable將會被運行在System進程裡面的Asset Atlas Service合成一個地圖集,並且作為紋理上傳到GPU去,因此,接下來我們就繼續分析Asset Atlas Service的實現。

前面提到,System進程是由Zygote進程啟動的。System進程啟動之後,就會加載系統服務,其中就包括Asset Atlas Service,如下所示:

 

public final class SystemServer {
    ......

    public static void main(String[] args) {
        new SystemServer().run();
    }
    
    ......

    private void run() {
        ......

        Looper.prepareMainLooper();

        ......

        // Start services.
        try {
            ......
            startOtherServices();
        } catch (Throwable ex) {
            ......
        }

        ......

        // Loop forever.
        Looper.loop();
        ......
    }

    ......

   private void startOtherServices() {
        ......

        AssetAtlasService atlas = null;
        ......

        if (mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL) {
            ......

            if (!disableNonCoreServices) {
                try {
                    ......
                    atlas = new AssetAtlasService(context);
                    ServiceManager.addService(AssetAtlasService.ASSET_ATLAS_SERVICE, atlas);
                } catch (Throwable e) {
                    .......
                }
            }
        
            ......
        }

        ......
    }

    ......
}
這三個函數定義在文件frameworks/base/services/java/com/android/server/SystemServer.java中。

 

Asset Atlas Service是一個非系統核心服務,當設備啟動在非工廠模式,並且在沒有禁用非系統核心服務的條件下,就會啟動Asset Atlas Service。

Asset Atlas Service的啟動過程如下所示:

 

public class AssetAtlasService extends IAssetAtlas.Stub {
    ......

    public AssetAtlasService(Context context) {
        ......

        ArrayList bitmaps = new ArrayList(300);
        int totalPixelCount = 0;

        // We only care about drawables that hold bitmaps
        final Resources resources = context.getResources();
        final LongSparseArray drawables = resources.getPreloadedDrawables();

        final int count = drawables.size();

        ......

        for (int i = 0; i < count; i++) {
            final Bitmap bitmap = drawables.valueAt(i).getBitmap();
            if (bitmap != null && bitmap.getConfig() == Bitmap.Config.ARGB_8888) {
                bitmaps.add(bitmap);
                totalPixelCount += bitmap.getWidth() * bitmap.getHeight();
            }
        }

        // Our algorithms perform better when the bitmaps are first sorted
        // The comparator will sort the bitmap by width first, then by height
        Collections.sort(bitmaps, new Comparator() {
            @Override
            public int compare(Bitmap b1, Bitmap b2) {
                if (b1.getWidth() == b2.getWidth()) {
                    return b2.getHeight() - b1.getHeight();
                }
                return b2.getWidth() - b1.getWidth();
            }
        });

        // Kick off the packing work on a worker thread
        new Thread(new Renderer(bitmaps, totalPixelCount)).start();
    }

    ......
}

這個函數定義在文件frameworks/base/services/core/java/com/android/server/AssetAtlasService.java。

Asset Atlas Service的構造函數首先是獲得預加載的Drawable資源,並且按照寬度和高度從大到小的順序保存在一個Bitmap數組列表中,接著再將這個Bitmap數組列表封裝在一個Renderer對象中,最後將這個Renderer對象post到一個新創建的線程去處理,即在新創建的線程中調用Renderer類的成員函數run,如下所示:

 

public class AssetAtlasService extends IAssetAtlas.Stub {
    ......

    private GraphicBuffer mBuffer;
    ......

    private class Renderer implements Runnable {
        private final ArrayList mBitmaps;
        private final int mPixelCount;
        ......

        Renderer(ArrayList bitmaps, int pixelCount) {
            mBitmaps = bitmaps;
            mPixelCount = pixelCount;
        }

        ......

        @Override
        public void run() {
            Configuration config = chooseConfiguration(mBitmaps, mPixelCount, mVersionName);
            ......

            if (config != null) {
                mBuffer = GraphicBuffer.create(config.width, config.height,
                        PixelFormat.RGBA_8888, GRAPHIC_BUFFER_USAGE);

                if (mBuffer != null) {
                    Atlas atlas = new Atlas(config.type, config.width, config.height, config.flags);
                    if (renderAtlas(mBuffer, atlas, config.count)) {
                        mAtlasReady.set(true);
                    }
                }
            }
        }

        ......
    }

    ......
}
這個函數定義在文件frameworks/base/services/core/java/com/android/server/AssetAtlasService.java。

 

Renderer類的成員函數run首先調用另外一個成員函數chooseConfiguration計算出將所有預加載的Drawable資源合成在一張圖片中所需要的最小寬度和高度值。有了這兩個值之後,就可以創建一塊Graphic Buffer了。這個Graphic Buffer保存在外部類AssetAtlasService的成員變量mBuffer中。

Renderer類的成員函數run接著再調用另外一個成員函數renderAtlas將所有預加載的Drawable資源渲染在前面創建的Graphic Buffer中,從形成一個預加載Drawable資源地圖集,實際上就是將預加載Drawable資源合成在一張大的圖片中。這個合成的過程要借助於Atlas類來完成,因此Renderer類的成員函數run會將一個Atlas對象傳遞給成員函數renderAtlas。

Renderer類的成員函數renderAtlas的實現如下所示:

 

public class AssetAtlasService extends IAssetAtlas.Stub {
    ......

    private static final boolean DEBUG_ATLAS_TEXTURE = false;
    ......

    private long[] mAtlasMap;
    ......

    private class Renderer implements Runnable {
        ......

        private long mNativeBitmap;
        ......

        private boolean renderAtlas(GraphicBuffer buffer, Atlas atlas, int packCount) {
            // Use a Source blend mode to improve performance, the target bitmap
            // will be zero'd out so there's no need to waste time applying blending
            final Paint paint = new Paint();
            paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));

            // We always render the atlas into a bitmap. This bitmap is then
            // uploaded into the GraphicBuffer using OpenGL to swizzle the content
            final Canvas canvas = acquireCanvas(buffer.getWidth(), buffer.getHeight());
            if (canvas == null) return false;

            final Atlas.Entry entry = new Atlas.Entry();

            mAtlasMap = new long[packCount * ATLAS_MAP_ENTRY_FIELD_COUNT];
            long[] atlasMap = mAtlasMap;
            int mapIndex = 0;

            boolean result = false;
            try {
                final long startRender = System.nanoTime();
                final int count = mBitmaps.size();

                ......

                for (int i = 0; i < count; i++) {
                    final Bitmap bitmap = mBitmaps.get(i);
                    if (atlas.pack(bitmap.getWidth(), bitmap.getHeight(), entry) != null) {
                        ......

                        canvas.save();
                        canvas.translate(entry.x, entry.y);
                        if (entry.rotated) {
                            canvas.translate(bitmap.getHeight(), 0.0f);
                            canvas.rotate(90.0f);
                        }

                        canvas.drawBitmap(bitmap, 0.0f, 0.0f, null);
                        canvas.restore();
                        atlasMap[mapIndex++] = bitmap.mNativeBitmap;
                        atlasMap[mapIndex++] = entry.x;
                        atlasMap[mapIndex++] = entry.y;
                        atlasMap[mapIndex++] = entry.rotated ? 1 : 0;
                    }
                }

                ......
                if (mNativeBitmap != 0) {
                    result = nUploadAtlas(buffer, mNativeBitmap);
                }

                ......

            } finally {
                releaseCanvas(canvas);
            }

            return result;
        }

        private Canvas acquireCanvas(int width, int height) {
            if (DEBUG_ATLAS_TEXTURE) {
                ......
            } else {
                Canvas canvas = new Canvas();
                mNativeBitmap = nAcquireAtlasCanvas(canvas, width, height);
                return canvas;
            }
        }

        private void releaseCanvas(Canvas canvas) {
            if (DEBUG_ATLAS_TEXTURE) {
                ......
            } else {
                nReleaseAtlasCanvas(canvas, mNativeBitmap);
            }
        }

        ......
    }

    ......
}

 

這個函數定義在文件frameworks/base/services/core/java/com/android/server/AssetAtlasService.java。

Renderer類的成員函數renderAtlas將所有預加載的Drawable資源合成在一張圖片的過程如下所示:

1. 調用成員函數acquireCanvas創建一個Canvas,這個Canvas封裝了一個SkBitmap,這個SkBitmap是通過調用外部類AssetAtlasService的JNI成員函數nAcquireAtlasCanvas在Native層創建的,它的地址保存在成員變量mNativeBitmap中。

2. 對於每一個預加載的Drawable資源,首先通過參數atlas指向的一個Atlas對象的成員函數pack計算出它們在合成的一張大的圖片中的位置和旋轉角度。有了這些信息之後,就可以將每一個預加載的Drawable資源渲染在前面創建的Canvas中。同時,每一個預加載的Drawable資源對應的Bitmap對象的地址值,以及渲染在Canvas中的位置和旋轉信息,還會記錄在外部類AssetAtlasService的成員變量mAtlasMap描述的一個long數組中。記錄的這些信息在使用合成的圖片時是很重要的。首先,通過對應的Bitmap對象的地址值可以知道一個Drawable資源是否位於合成的圖片中。其次,確定一個Drawable資源在合成的圖片之後,通過位置和旋轉信息可以准確地在合成的圖片中訪問到Drawable資源的內容。後面我們分析應用程序進程使用這個合成的圖片的時候,就會更清楚地看到上述記錄的信息是如何使用的。

3. 將所有預加載的Drawable資源都渲染在前面創建的Canvas之後,接下來就再通過調用外部類AssetAtlasService的JNI成員函數nUploadAtlas將該Canvas封裝的SkBitmap的內容作為一個紋理上傳到GPU中。上傳到GPU的紋理可以通過前面創建的Graphic Buffer來訪問。

4. 將Canvas封裝的SkBitmap上傳到GPU之後,前面創建的Canvas就不需要了,因此就可以調用外部類AssetAtlasService的JNI成員函數releaseCanvas釋放它占用的資源,實際上就是釋放它封裝的SkBitmap占用的內存。

接下來我們簡單描述一下將預加載Drawable資源合成一張圖片的算法,如圖3所示:

\

圖3 將預加載Drawable資源合成一張圖片的算法示意圖

開始的時候,整個圖片當作一個空閒塊C0,如圖3(a)所示。接下來將Drawable B1對應的Bitmap渲染在空閒C0的左上角位置,如圖3(b)所示。剩下的空閒位置按照垂直或者水平方向劃分為C1和C2兩塊,如圖3(c)和圖3(d)所示。再接下來的其它Drawable資源對應的Bitmap,例如B2,將在C1或者C2空閒塊找到合適的位置進行渲染。假設選擇的空閒塊是C1,那麼C1剩余的空閒位置又繼續按照上述過程進行劃分。依次類推,每一個Drawable資源都在當前的空閒塊中找到位置進行渲染,從而完成整個合成過程。關於這個合成過程的詳細實現,可以參考rameworks/base/graphics/java/android/graphics/Atlas.java文件中的Atlas類的實現。

預加載Drawable資源合成到一張大的圖片之後,就可以作為紋理上傳到GPU了,這是通過外部類AssetAtlasService的成員函數nUploadAtlas來實現的。AssetAtlasService的成員函數nUploadAtlas是一個JNI函數,由Native層的函數com_android_server_AssetAtlasService_upload實現,如下所示:

 

static jboolean com_android_server_AssetAtlasService_upload(JNIEnv* env, jobject,
        jobject graphicBuffer, jlong bitmapHandle) {

    SkBitmap* bitmap = reinterpret_cast(bitmapHandle);
    // The goal of this method is to copy the bitmap into the GraphicBuffer
    // using the GPU to swizzle the texture content
    sp buffer(graphicBufferForJavaObject(env, graphicBuffer));

    if (buffer != NULL) {
        EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
        ......

        EGLint major;
        EGLint minor;
        if (!eglInitialize(display, &major, &minor)) {
            ......
        }

        // We're going to use a 1x1 pbuffer surface later on
        // The configuration doesn't really matter for what we're trying to do
        EGLint configAttrs[] = {
                EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
                EGL_RED_SIZE, 8,
                EGL_GREEN_SIZE, 8,
                EGL_BLUE_SIZE, 8,
                EGL_ALPHA_SIZE, 0,
                EGL_DEPTH_SIZE, 0,
                EGL_STENCIL_SIZE, 0,
                EGL_NONE
        };
        EGLConfig configs[1];
        EGLint configCount;
        if (!eglChooseConfig(display, configAttrs, configs, 1, &configCount)) {
            ......
        }
        ......

        // These objects are initialized below but the default "null"
        // values are used to cleanup properly at any point in the
        // initialization sequence
        GLuint texture = 0;
        EGLImageKHR image = EGL_NO_IMAGE_KHR;
        EGLSurface surface = EGL_NO_SURFACE;
        EGLSyncKHR fence = EGL_NO_SYNC_KHR;

        EGLint attrs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE };
        EGLContext context = eglCreateContext(display, configs[0], EGL_NO_CONTEXT, attrs);
        ......

        // Create the 1x1 pbuffer
        EGLint surfaceAttrs[] = { EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE };
        surface = eglCreatePbufferSurface(display, configs[0], surfaceAttrs);
        ......

        if (!eglMakeCurrent(display, surface, surface, context)) {
            ......
        }

        // We use an EGLImage to access the content of the GraphicBuffer
        // The EGL image is later bound to a 2D texture
        EGLClientBuffer clientBuffer = (EGLClientBuffer) buffer->getNativeBuffer();
        EGLint imageAttrs[] = { EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE };
        image = eglCreateImageKHR(display, EGL_NO_CONTEXT,
                EGL_NATIVE_BUFFER_ANDROID, clientBuffer, imageAttrs);
        ......

        glGenTextures(1, &texture);
        glBindTexture(GL_TEXTURE_2D, texture);
        glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image);
        ......

        // Upload the content of the bitmap in the GraphicBuffer
        glPixelStorei(GL_UNPACK_ALIGNMENT, bitmap->bytesPerPixel());
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, bitmap->width(), bitmap->height(),
                GL_RGBA, GL_UNSIGNED_BYTE, bitmap->getPixels());
        ......

        // The fence is used to wait for the texture upload to finish
        // properly. We cannot rely on glFlush() and glFinish() as
        // some drivers completely ignore these API calls
        fence = eglCreateSyncKHR(display, EGL_SYNC_FENCE_KHR, NULL);
        ......

        // The flag EGL_SYNC_FLUSH_COMMANDS_BIT_KHR will trigger a
        // pipeline flush (similar to what a glFlush() would do.)
        EGLint waitStatus = eglClientWaitSyncKHR(display, fence,
                EGL_SYNC_FLUSH_COMMANDS_BIT_KHR, FENCE_TIMEOUT);
        ......
    }

    return JNI_FALSE;
}
這個函數定義在文件frameworks/base/services/core/jni/com_android_server_AssetAtlasService.cpp中。

 

參數bitmapHandle描述的是一個SkBitmap對象,這個SkBitmap對象就是前面用來渲染Drawable資源的Canvas底層封裝的SkBitmap對象,因此它裡面包含的內容就是預加載Drawable資源合成後的圖片。另外一個參數graphicBuffer指向的是之前創建的一個Graphic Buffer,它是在GPU分配的。現在要做的事情就是將參數bitmapHandle描述的SkBitmap的內容上傳到參數graphicBuffer描述的Graphic Buffer中。

上傳的過程如下所示:

1. 調用eglGetDisplay、eglInitialize、eglChooseConfig、eglCreateContext、eglCreatePbufferSurface和eglMakeCurrent等egl函數初始化一個Open GL環境。

2. 調用glGenTextures、glBindTexture和glEGLImageTargetTexture2DOES等gl函數創建一個紋理,並且指定參數graphicBuffer描述的Graphic Buffer作為該紋理的儲存。

3. 調用glPixelStorei和glTexSubImage2D等gl函數將參數bitmapHandle描述的SkBitmap的內容上傳到前面創建的紋理中去,也就是上傳到參數graphicBuffer描述的Graphic Buffer中去。

4. 調用eglCreateSyncKHR和eglClientWaitSyncKHR等egl函數等待上傳操作完成。

這樣,當函數com_android_server_AssetAtlasService_upload執行完畢,預加載的Drawable資源合成的圖片就作為紋理上傳到GPU去了,並且這個紋理可以通過一個Graphic Buffer來訪問。這個Graphic Buffer可以通過Asset Atlas Service提供的接口getBuffer來獲得,如下所示:

 

public class AssetAtlasService extends IAssetAtlas.Stub {
    ......

    private GraphicBuffer mBuffer;
    ......

    private long[] mAtlasMap;
    ......

    @Override
    public GraphicBuffer getBuffer() throws RemoteException {
        return mAtlasReady.get() ? mBuffer : null;
    }

    @Override
    public long[] getMap() throws RemoteException {
        return mAtlasReady.get() ? mAtlasMap : null;
    }

    ......
}
這個函數定義在文件frameworks/base/services/core/java/com/android/server/AssetAtlasService.java。

 

注意,為了能夠訪問到每一個預加載Drawable資源的內容,只獲得它們合成在的Graphic Buffer還不足夠,我們還必須知道每一個預加載Drawable資源在Graphic Buffer中的位置和旋轉等輔助信息,因此,Asset Atlas Service還提供了另外一個接口getMap來獲得上述輔助信息。

預加載Drawable資源合成在的圖片就是我們前面提到的地圖集,接下來我們繼續分析應用程序進程是如何使用它們的。在前面Android應用程序UI硬件加速渲染環境初始化過程分析一文中提到,Android應用程序進程在初始Open GL渲染上下文時,會通過一個AtlasInitializer類的成員函數init來初始化預加載資源地圖集,如下所示:

 

public class ThreadedRenderer extends HardwareRenderer {
    ......

    private static class AtlasInitializer {
        ......
     
        private boolean mInitialized = false;
        ......

        synchronized void init(Context context, long renderProxy) {
            if (mInitialized) return;
            IBinder binder = ServiceManager.getService("assetatlas");
            ......

            IAssetAtlas atlas = IAssetAtlas.Stub.asInterface(binder);
            try {
                if (atlas.isCompatible(android.os.Process.myPpid())) {
                    GraphicBuffer buffer = atlas.getBuffer();
                    if (buffer != null) {
                        long[] map = atlas.getMap();
                        if (map != null) {
                            ......
                            nSetAtlas(renderProxy, buffer, map);
                            mInitialized = true;
                        }
                        ......
                    }
                }
            } catch (RemoteException e) {
                ......
            }
        }

        ......
    }

    ......
}

這個函數定義在文件frameworks/base/core/java/android/view/ThreadedRenderer.java中。

AtlasInitializer類的成員函數init首先是獲得Asset Atlas Service的Binder代理接口,接著再調用這個接口的成員函數isCompatible判斷當前進程的父進程與Asset Atlas Service運行在的進程的父進程是不是一樣的。如果是一樣的,那麼才會進一步調用Asset Atlas Service的Binder代理接口的成員函數getBuffer和getMap獲得預加載資源的地圖集信息。

這裡之所以要做這樣的判斷,是為了保證當前進程和Asset Atlas Service運行在的進程是可以共享預加載Drawable資源的。當前進程是一個Android應用程序進程,它是由Zygote進程fork出來的,而Asset Atlas Service運行在的進程即為System進程,它也是由Zygote進程fork出來的。因此,它們的父進程是一樣的。

最後,AtlasInitializer類的成員函數init調用另外一個成員函數nSetAtlas將獲得的預加載資源地圖集信息設置到當前進程的Render Thread中去。AtlasInitializer類的成員函數nSetAtlas是一個JNI函數,它由Native層的函數android_view_ThreadedRenderer_setAtlas實現,如下所示:

 

static void android_view_ThreadedRenderer_setAtlas(JNIEnv* env, jobject clazz,
        jlong proxyPtr, jobject graphicBuffer, jlongArray atlasMapArray) {
    sp buffer = graphicBufferForJavaObject(env, graphicBuffer);
    jsize len = env->GetArrayLength(atlasMapArray);
    if (len <= 0) {
        ALOGW("Failed to initialize atlas, invalid map length: %d", len);
        return;
    }
    int64_t* map = new int64_t[len];
    env->GetLongArrayRegion(atlasMapArray, 0, len, map);

    RenderProxy* proxy = reinterpret_cast(proxyPtr);
    proxy->setTextureAtlas(buffer, map, len);
}
這個函數定義在文件frameworks/base/core/jni/android_view_ThreadedRenderer.cpp中。

 

參數proxPtr指向的是一個RenderProxy對象,從前面Android應用程序UI硬件加速渲染環境初始化過程分析一文可以知道,這個RenderProxy對象是應用程序進程的Main Thread用來與Render Thread進通通信的,這裡通過調用它的成員函數setTextureAtlas將預加載資源地圖集信息傳遞給Render Thread。

RenderProxy類的成員函數setTextureAtlas的實現如下所示:

 

CREATE_BRIDGE4(setTextureAtlas, RenderThread* thread, GraphicBuffer* buffer, int64_t* map, size_t size) {
    CanvasContext::setTextureAtlas(*args->thread, args->buffer, args->map, args->size);
    args->buffer->decStrong(0);
    return NULL;
}

void RenderProxy::setTextureAtlas(const sp& buffer, int64_t* map, size_t size) {
    SETUP_TASK(setTextureAtlas);
    args->thread = &mRenderThread;
    args->buffer = buffer.get();
    args->buffer->incStrong(0);
    args->map = map;
    args->size = size;
    post(task);
}
這個函數定義在文件frameworks/base/libs/hwui/renderthread/RenderProxy.cpp中。

 

RenderProxy類的成員函數setTextureAtlas通過宏SETUP_TASK將預加載資源地圖集信息封裝在一個Task中,並且通過調用另外一個成員函數post將該Task添加到Render Thread的Task Queue中,最終該Task將在Render Thread中調用由宏CREATE_BRIDGE4定義的函數setTextureAtlas進行處理。

宏CREATE_BRIDGE4定義的函數setTextureAtlas主要就是調用CanvasContext類的靜態成員函數setTextureAtlas來接收預加載資源地圖集信息,它的實現如下所示:

 

void CanvasContext::setTextureAtlas(RenderThread& thread,
        const sp& buffer, int64_t* map, size_t mapSize) {
    thread.eglManager().setTextureAtlas(buffer, map, mapSize);
}
這個函數定義在文件frameworks/base/libs/hwui/renderthread/CanvasContext.cpp。

 

CanvasContext類的靜態成員函數setTextureAtlas首先是獲得在Render Thread中創建的一個EglManager對象,接著再調用這個EglManager對象的成員函數setTextureAtlas來處理參數buffer和map描述的預加載資源地圖集信息。

EglManager類的成員函數setTextureAtlas的實現如下所示:

 

void EglManager::setTextureAtlas(const sp& buffer,
        int64_t* map, size_t mapSize) {
    ......

    mAtlasBuffer = buffer;
    mAtlasMap = map;
    mAtlasMapSize = mapSize;

    if (hasEglContext()) {
        ......
        initAtlas();
    }
}
這個函數定義在文件frameworks/base/libs/hwui/renderthread/EglManager.cpp中。

 

EglManager類的成員函數setTextureAtlas首先是將預加載資源地圖集信息記錄在成員變量mAtlasBuffer、mAtlasMap和mAtlasMapSize中,接著再調用另外一個成員函數hasEglContext判斷Render Thread的Open GL渲染上下文是否已經創建。如果已經創建,那麼就會調用成員函數initAtlas對前面記錄下來的預加載資源地圖集信息進行初始化。

從前面Android應用程序UI硬件加速渲染環境初始化過程分析一文可以知道,當Java層的ThreadedRenderer對象創建時,Render Thread的Open GL渲染上下文還沒有創建,因此這時候就不可以對預加載資源地圖集信息進行初始化。

從前面Android應用程序UI硬件加速渲染環境初始化過程分析一文還可以知道,等到Render Thread的Open GL渲染上下文創建時,CanvasContext類的成員函數setSurface會被調用來綁定當前激活的窗口。CanvasContext類的成員函數setSurface在調用的過程中,又會調用EglManager類的成員函數createSurface將當前激活的窗口封裝為一個EGL Surface,作為Open GL的渲染Surface。

EglManager類的成員函數createSurface的實現如下所示:

 

EGLSurface EglManager::createSurface(EGLNativeWindowType window) {
    initialize();
    EGLSurface surface = eglCreateWindowSurface(mEglDisplay, mEglConfig, window, NULL);
    ......
    return surface;
}
這個函數定義在文件frameworks/base/libs/hwui/renderthread/EglManager.cpp中。

 

EglManager類的成員函數createSurface在調用函數eglCreateWindowSurface將參數window描述的窗口封裝成一個EGL Surface之前,會先調用另外一個成員函數initialize來在當前線程中初始化一個Open GL渲染上下文。

EglManager類的成員函數initialize的實現如下所示:

 

void EglManager::initialize() {
    ......

    mEglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    LOG_ALWAYS_FATAL_IF(mEglDisplay == EGL_NO_DISPLAY,
            "Failed to get EGL_DEFAULT_DISPLAY! err=%s", egl_error_str());

    EGLint major, minor;
    LOG_ALWAYS_FATAL_IF(eglInitialize(mEglDisplay, &major, &minor) == EGL_FALSE,
            "Failed to initialize display %p! err=%s", mEglDisplay, egl_error_str());

    ......

    createContext();
    .......
    initAtlas();
}
這個函數定義在文件frameworks/base/libs/hwui/renderthread/EglManager.cpp中。

 

從這裡就可以看到,EglManager類的成員函數initialize為當前線程創建好Open GL渲染上下文之後,就會調用另外一個成員函數initAtlas去初始化前面已經記錄下來的預加載資源地圖集信息。

EglManager類的成員函數initAtlas的實現如下所示:

 

void EglManager::initAtlas() {
    if (mAtlasBuffer.get()) {
        Caches::getInstance().assetAtlas.init(mAtlasBuffer, mAtlasMap, mAtlasMapSize);
    }
}
這個函數定義在文件frameworks/base/libs/hwui/renderthread/EglManager.cpp中。

 

Render Thread通過一個Caches類來管理一些Open GL對象信息,例如在在渲染過程要重復使用的一些Texture和FBO等對象。同時,Caches類也通過一個類型為AssetAtlas的成員變量assetAtlas來管理預加載資源地圖集信息。因此, EglManager類的成員函數initAtlas就會調用Caches類的成員變量assetAtlas指向的一個AssetAtlas對象的成員函數init來負責執行初始化預加載資源地圖集的工作。

AssetAtlas類的成員函數init的實現如下所示:

 

void AssetAtlas::init(sp buffer, int64_t* map, int count) {
    ......

    mImage = new Image(buffer);

    if (mImage->getTexture()) {
        Caches& caches = Caches::getInstance();

        mTexture = new Texture(caches);
        mTexture->id = mImage->getTexture();
        mTexture->width = buffer->getWidth();
        mTexture->height = buffer->getHeight();

        createEntries(caches, map, count);
    } 

    ......
}
這個函數定義在文件frameworks/base/libs/hwui/AssetAtlas.cpp中。

 

AssetAtlas類的成員函數init首先是根據參數buffer指向的一個Graphic Buffer生成一個Open GL紋理。如果生成成功,再創建一個Texture對象來描述這個Open GL紋理的ID、寬度和高度等信息,並且將該Texture對象保存在AssetAtlas類的成員變量mTexture中。接著再調用另外一個成員函數createEntries為預加載資源地圖集包含的每一個Drawable資源創建描述信息。

我們首先看AssetAtlas類的成員函數init根據參數buffer指向的一個Graphic Buffer生成Open GL紋理的過程,這是創建一個Image對象來完成的,因此我們接下來分析Image類的構造函數的實現,如下所示:

 

Image::Image(sp buffer) {
    // Create the EGLImage object that maps the GraphicBuffer
    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    EGLClientBuffer clientBuffer = (EGLClientBuffer) buffer->getNativeBuffer();
    EGLint attrs[] = { EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE };

    mImage = eglCreateImageKHR(display, EGL_NO_CONTEXT,
            EGL_NATIVE_BUFFER_ANDROID, clientBuffer, attrs);

    if (mImage == EGL_NO_IMAGE_KHR) {
        ALOGW("Error creating image (%#x)", eglGetError());
        mTexture = 0;
    } else {
        // Create a 2D texture to sample from the EGLImage
        glGenTextures(1, &mTexture);
        Caches::getInstance().bindTexture(mTexture);
        glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, mImage);

        GLenum status = GL_NO_ERROR;
        while ((status = glGetError()) != GL_NO_ERROR) {
            ALOGW("Error creating image (%#x)", status);
        }
    }
}
這個函數定義在文件frameworks/base/libs/hwui/Image.cpp。

 

我們首先明確,參數buffer指向的一個Graphic Buffer是在Asset Atlas Service運行在的System進程創建的,它是預加載資源地圖集紋理的儲存。也就是說,這個Graphic Buffer裡面包含了預加載資源地圖集的內容。現在Image類的構造函數將這個Graphic Buffer封裝成一個EGLImageKHR對象,並且將該EGLImageKHR對象作為當前進程中的一個紋理來使用。這就相當於是說,在當前進程中創建的紋理與前面在System進程創建的預加載資源地圖集紋理背後對應的儲存都是一樣的,因此就達到了在GPU中共享預加載資源的目的。這種共享機制的一個關鍵技術點是通過Graphic Buffer來在不同進程之間傳遞紋理儲存,從而實現共享。

回到AssetAtlas類的成員函數init中,接下來我們繼續分析預加載資源地圖集包含的每一個Drawable資源的描述信息的創建過程,即AssetAtlas類的成員函數createEntries的實現,如下所示:

 

void AssetAtlas::createEntries(Caches& caches, int64_t* map, int count) {
    const float width = float(mTexture->width);
    const float height = float(mTexture->height);

    for (int i = 0; i < count; ) {
        SkBitmap* bitmap = reinterpret_cast(map[i++]);
        // NOTE: We're converting from 64 bit signed values to 32 bit
        // signed values. This is guaranteed to be safe because the "x"
        // and "y" coordinate values are guaranteed to be representable
        // with 32 bits. The array is 64 bits wide so that it can carry
        // pointers on 64 bit architectures.
        const int x = static_cast(map[i++]);
        const int y = static_cast(map[i++]);
        bool rotated = map[i++] > 0;

        // Bitmaps should never be null, we're just extra paranoid
        if (!bitmap) continue;

        const UvMapper mapper(
                x / width, (x + bitmap->width()) / width,
                y / height, (y + bitmap->height()) / height);

        Texture* texture = new DelegateTexture(caches, mTexture);
        texture->id = mTexture->id;
        texture->blend = !bitmap->isOpaque();
        texture->width = bitmap->width();
        texture->height = bitmap->height();

        Entry* entry = new Entry(bitmap, x, y, rotated, texture, mapper, *this);
        texture->uvMapper = &entry->uvMapper;

        mEntries.add(entry->bitmap, entry);
    }
}
這個函數定義在文件frameworks/base/libs/hwui/AssetAtlas.cpp中。

 

預加載資源地圖集包含的每一個Drawable資源的描述信息記錄在參數map描述的一個int64_t數組中。從前面的分析可以知道,每一個Drawable資源在這個int64_t數組中占據著4個描述信息,分別是底層對應的SkBitmap對象的地址值、在合成的地圖集圖片中的偏移位置和旋轉情況。

其中,最重要的就是Drawable資源在合成的地圖集圖片中的偏移位置。我們知道,紋理坐標是被歸一化處理的,也就是它的取值范圍是[0, 1]。但是參數map描述的int64_t數組記錄的Drawable資源偏移位置不是歸一化處理的。因此,如果我們要在前面創建的地圖集紋理中准確地訪問到指定的Drawable資源,就必須要將參數map描述的int64_t數組記錄的Drawable資源偏移位置進行歸一化處理。處理的過程很簡單,只要我們知道一個Drawable資源合成前的大小和合成在地圖集圖片的位置,以及合成的地圖集圖片的大小,就可以計算得到它在前面創建的地圖集紋理的歸一化偏移位置。計算得到的歸一化偏移位置保存在一個UvMapper對象中。

為了方便描述一個Drawable資源在地圖集紋理對應的那部分內容,AssetAtlas類的成員函數createEntries為每一個Drawable資源創建一個委托紋理對象(DelegateTexture)。也就是說,通過這個委托紋理對象能夠在地圖集紋理中訪問到對應的Drawable資源占據的那部分內容。這個委托紋理對象記錄了地圖集紋理的ID,以及對應的Drawable資源對應的SkBitmap對象的寬度、高度和透明信息。其中,透明信息是一個很重要的信息,後面在渲染這些Drawable資源時將會使用到。這一點我們後面再分析。

現在,上面收集到的每一個預加載的Drawable資源的描述信息都記錄在一個Entry對象中,並且這個Entry對象會以預加載的Drawable資源對應的SkBitmap對象的地址值為Key值,保存在AssetAtlas類的成員變量mEntries描述的一個KeyedVector中。這樣,以後給出一個Drawable資源對應的SkBitmap對象,我們就可以快速地通過這個KeyedVector查詢得知它是否是一個預加載的Drawable資源。如果是一個加載的Drawable資源的話,那麼在渲染它的時候,就直接到地圖集紋理去訪問它的內容就行了,而不用為它單獨創建一個紋理。

例如,假設我們在UI上將一個預加載的Drawable資源作為一個Bitmap來使用,那麼這個Bitmap最終會通過OpenGLRenderer類的成員函數drawBitmap進行渲染,如下所示:

 

status_t OpenGLRenderer::drawBitmap(const SkBitmap* bitmap, const SkPaint* paint) {
    if (quickRejectSetupScissor(0, 0, bitmap->width(), bitmap->height())) {
        return DrawGlInfo::kStatusDone;
    }

    mCaches.activeTexture(0);
    Texture* texture = getTexture(bitmap);
    if (!texture) return DrawGlInfo::kStatusDone;
    const AutoTexture autoCleanup(texture);

    if (CC_UNLIKELY(bitmap->colorType() == kAlpha_8_SkColorType)) {
        drawAlphaBitmap(texture, 0, 0, paint);
    } else {
        drawTextureRect(0, 0, bitmap->width(), bitmap->height(), texture, paint);
    }

    return DrawGlInfo::kStatusDrew;
}
這個函數定義在文件frameworks/base/libs/hwui/OpenGLRenderer.cpp中。

 

參數bitmap描述的就是預加載的Drawable資源底層對應的SkBitmap對象。在調用OpenGLRenderer類的成員函數drawAlphaBitmap或者drawTextureRect來渲染該Drawable資源之前,我們首先要為它創建一個紋理,這是通過調用OpenGLRenderer類的成員函數getTexture來實現的,它的實現如下所示:

 

Texture* OpenGLRenderer::getTexture(const SkBitmap* bitmap) {
    Texture* texture = mCaches.assetAtlas.getEntryTexture(bitmap);
    if (!texture) {
        return mCaches.textureCache.get(bitmap);
    }
    return texture;
}

這個函數定義在文件frameworks/base/libs/hwui/OpenGLRenderer.cpp中。

OpenGLRenderer類的成員函數getTexture首先是通過調用成員變量mCaches指向的一個Caches對象的成員變量assetAtlas描述的一個AssetAtlas對象的成員函數getEntryTexture檢查參數bitmap描述的是否是地圖集紋理包含的一個Drawable資源。如果是的話,那麼就會得到一個對應的紋理;否則的話,就需要為參數bitmap描述的資源創建一個新的紋理,這是通過調用成員變量mCaches指向的一個Caches對象的成員變量textureCache描述的一個TextureCache對象的成員函數get實現的。

TextureCache類會緩存那些曾經使用的紋理,當調用它的成員函數get的時候,它首先檢查參數描述的SkBitmap在緩存中是否已經有對應的紋理了。如果有的話,就將該紋理返回給調用者;否則的話,就會為該Bitmap創建一個新的紋理,然後再返回給調用者。

這裡我們只關注參數bitmap描述的是地圖集紋理包含的一個Drawable資源的情況,這時候調用AssetAtlas類的成員函數getEntryTexture就會獲得一個不為NULL的紋理,如下所示:

 

Texture* AssetAtlas::getEntryTexture(const SkBitmap* bitmap) const {
    ssize_t index = mEntries.indexOfKey(bitmap);
    return index >= 0 ? mEntries.valueAt(index)->texture : NULL;
}
這個函數定義在文件frameworks/base/libs/hwui/AssetAtlas.cpp中。

 

從前面的分析可以知道,當參數bitmap描述的SkBitmap是地圖集紋理包含的一個Drawable資源時,在AssetAtlas類的成員變量mEntries描述一個KeyedVector中就可以找到一個對應的Entry對象,並且通過這個Entry對象的成員變量texture可以獲得一個對應的紋理。

從上面這個例子我們就可以看出預加載資源地圖集是如何在Zygote進程、System進程以及應用程序進程之間進行GPU級別的紋理共享的。事實上,預加載資源地圖集紋理的作用遠不止於此。後面我們分析Render Thread渲染窗口UI的時候,可以看到一個Open GL繪制命令合並渲染優化的操作。通過合並若干個Open GL繪制命令為一個Open GL繪制命令,可以減少Open GL渲染管線的狀態切換操作,從而提高渲染的效率。預加載資源地圖集紋理為這種合並渲染優化提供了可能,接下來我們就分析這種合並渲染優化的實現原理。

我們知道,Android應用程序窗口UI的視圖是樹形結構的。在渲染的時候,先繪制父視圖的UI,再繪制子視圖的UI。我們可以把這種繪制模式看作是分層的,即先繪制背後的層,再繪制前面的層,如圖4所示:

\

圖4 Android應用程序窗口UI分層繪制模式

圖4顯示的窗口由g一個背景圖、一個ActionBar、一個Button和一個應用程序圖標組成,它們按照從前到後的順序排列。其中,ActionBar和Button都是由背景和文字組成的,它們使用的背景圖均為一個預加載的Drawable資源,並且是按照九宮圖方式繪制。

按照我們前面描述的分層繪制模式,圖4顯示的窗口UI按照A、B、C、D、E和F的順序進行繪制,每一次繪制對應的都是一個Open GL繪制命令。但是實際上,有些繪制命令是可以進行合並的。例如,ActionBar和Button的背景圖,它們使用的都是預加載的Drawable資源,並且這些資源已經合成為一個地圖集紋理上傳到了GPU去了。如果可以將這兩個背景圖的繪制合並成一個Open GL命令,那麼就可以Open GL渲染管線的狀態切換次數,提高渲染效率。

注意,這種合並操作並不是總能執行的。例如,假設在圖4中,介於ActionBar和Button之間應用程序圖標不僅與ActionBar重疊,還與Button重疊,那麼ActionBar和Button的背景就不可以進行合並繪制。這意味著兩個繪制命令是否能夠進行合並是由許多因素決定的。

後面我們在分析應用程序窗口的Display List構建和渲染過程就會看到,圖4顯示的A、B、C、D、E和F繪制操作都是對應一個Draw Op。我們可以將Draw Op看作是一個繪制命令,它們按照前後順序保存在應用程序窗口的Display List中。為了能夠實現合並,這些Draw Op不是馬上被執行,而是先通過一個Deferred Display List進行重排,將可以合並的Draw Op先進行合並,然後再對它們進行合並。重排的算法就是依次將原來保存在應用程序窗口的Display List的Draw Op添加到Deferred Display List中去。在添加的過程中,如果發現後一個Draw Op可以與前一個Draw Op進行合並,那麼就對它們進行合並。

Deferred Display List內部維護了一個Hash Map數組,用來描述哪些Draw Op是如何合並的,如下所示:

 

class DeferredDisplayList {
   ......

    enum OpBatchId {
        kOpBatch_None = 0, // Don't batch
        kOpBatch_Bitmap,
        kOpBatch_Patch,
        kOpBatch_AlphaVertices,
        kOpBatch_Vertices,
        kOpBatch_AlphaMaskTexture,
        kOpBatch_Text,
        kOpBatch_ColorText,

        kOpBatch_Count, // Add other batch ids before this
    };
 
    ......

    /**
     * Maps the mergeid_t returned by an op's getMergeId() to the most recently seen
     * MergingDrawBatch of that id. These ids are unique per draw type and guaranteed to not
     * collide, which avoids the need to resolve mergeid collisions.
     */
    TinyHashMap mMergingBatches[kOpBatch_Count];
    
    ......
}
這個類定義在文件frameworks/base/libs/hwui/DeferredDisplayList.h中。

 

Deferred Display List內部維護的Hash Map數組由成員變量mMerginBatches指向。每一個Draw Op都有一個Batch ID和一個Merge ID。只有Batch ID和Merge ID相同的兩個Draw Op才有可能被合並。我們這裡說可能,是因為即使是Batch ID和Merge ID相同的兩個Draw Op,如果它們之間存在一個具有不同Batch ID和Merge ID的Draw Op,並且繪制區域與它們重疊,那麼就不能進行合並。

我們可以將Batch ID看作是一個用來決定兩個Draw Op是否能夠進行合並的一級Key。例如,九宮圖繪制的Draw Op的Batch ID定義為kOpBatch_Patch,而文字繪制的Draw Op的Batch ID定義為kOpBatch_Text。通過這個一級Key,我們可以快速地確認兩個Draw Op是否能夠進行合並。這很顯然,不同的Batch ID的Draw Op不能進行合並。

Merge ID是一個用來決定兩個Draw Op是否能夠進行合並的二級Key。兩個Draw Op的Batch ID雖然相同,它們也未必能夠進行合並。例如,對於兩個kOpBatch_Patch Draw Op,如果一個使用了預加載的Drawable資源,另一個使用的不是預加載的Drawable資源,那麼就不能進行合並。或者兩者使用的都是預加載的Drawable資源,但是其中一個是透明的,另一個是不透明的。

可以合並的Draw Op維護在一個Draw Batch中。具體的維護過程是在將Display List的Draw Op轉移到Defered Display List的過程進行的,如下所示:

1. 以Draw Op的Batch ID為索引,在Merging Batch數組中找到對應的Hash Map;

2. 以Draw Op的Merge ID為索引,在上一步找到的Hash Map中找到對應的Draw Batch;

3. 判斷正在處理的Draw Op與它對應的Draw Batch的已經有的Draw Op是否能夠進行合並,如果能夠進行合並,就將它放進對應的Draw Batch去。

最後,就以Draw Batch為單位進行繪制,這樣就可以使得可以進行合並的Draw Op可以進行合並渲染。

現在的一個問題,就是如何獲得一個Draw Op的Batch ID和Merge ID。以九宮圖繪制為例,它對應的Draw Op是一個DrawPatchOp,如下所示:

 

class DrawPatchOp : public DrawBoundedOp {
public:
    DrawPatchOp(const SkBitmap* bitmap, const Res_png_9patch* patch,
            float left, float top, float right, float bottom, const SkPaint* paint)
            : DrawBoundedOp(left, top, right, bottom, paint),
            mBitmap(bitmap), mPatch(patch), mGenerationId(0), mMesh(NULL),
            mAtlas(Caches::getInstance().assetAtlas) {
        mEntry = mAtlas.getEntry(bitmap);
        ......
    };
    ......
    virtual void onDefer(OpenGLRenderer& renderer, DeferInfo& deferInfo,
            const DeferredDisplayState& state) {
        deferInfo.batchId = DeferredDisplayList::kOpBatch_Patch;
        deferInfo.mergeId = getAtlasEntry() ? (mergeid_t) mEntry->getMergeId() : (mergeid_t) mBitmap;
        deferInfo.mergeable = state.mMatrix.isPureTranslate() &&
                OpenGLRenderer::getXfermodeDirect(mPaint) == SkXfermode::kSrcOver_Mode;
        deferInfo.opaqueOverBounds = isOpaqueOverBounds(state) && mBitmap->isOpaque();
    }

private:
    const SkBitmap* mBitmap;
    const Res_png_9patch* mPatch;

    ......

    const AssetAtlas& mAtlas;
    ......
    AssetAtlas::Entry* mEntry;
};
這個類定義在文件frameworks/base/libs/hwui/DisplayListOp.h中。

 

其中,成員變量mBitmap和mPatch描述的是九宮圖繪制使用的是SkBitmap和九宮圖信息。如果使用的SkBitmap是一個預加載的Drawable資源,那麼就可以在我們前面描述用來描述預加載資源地圖集紋理的AssetAtlas類中獲得一個Entry,保存在成員變量mEntry中。

在將一個Draw Op從Display List轉移到Defered Display List的過程中,會通過調用這個Draw Op的成員函數onDefer來獲得合並相關的信息。例如,通過調用DrawPatchOp類的成員函數onDefer可以獲得一個九宮圖繪制的合並信息。這些合並信息通過一個DeferInfo對象來描述。

DeferInfo類有四個成員變量batchId、mergeId、mergeable和opaqueOverBounds。其中,第一個成員變量batchId和第二個mergeId描述的就是Batch ID和Merge ID。第三個成員變量mergeable描述的是Draw Op是否可以進行合並,這與當前的窗口渲染狀態以及Draw Op本身的渲染屬性有關。例如,對於九宮圖的Draw Op來說,只有在當前窗口的變換矩陣只包含平移變換以及Draw Op使用的畫筆的繪制模式為SkXfermode::kSrcOver_Mode時,才可以進行合並。第四個成員變量描述的是Draw Op是否會完全遮擋了排列在它前面的Draw Op。如果完全遮擋了,那麼排列在它前面的Draw Op實際上是不需要進行繪制的。這又是一個渲染優化操作。

回到九宮圖繪制這個Draw Op中,它的Batch ID的確定很簡單,固定為kOpBatch_Patch。對於Merge ID,就相比復雜一些了。前面我們提到,這與九宮圖使用的SkBitmap有關,而且還與這個SkBitmap透明與否有關。

如果九宮圖使用的SkBitmap是一個預加載的Drawable資源,那麼它就有一個對應的Entry對象,通過調用這個Entry對象的成員函數getMergeId可以獲得我們需要的Merge ID。如果九宮圖使用的SkBitmap不是一個預加載的Drawable資源,那麼就用它使用的SkBitmap的地址作為Merge ID。很顯然,一個使用了預加載Drawable資源和一個不使用預加載Drawable資源的九宮圖繪制的Merge ID是不一樣的,而且兩個使用了不同的非預加載Drawable資源的九宮圖繪制的Merge ID也是不一樣的。

接下來我們再來看Entry類的成員函數getMergeId返回來的Merge ID是什麼,如下所示:

 

class AssetAtlas {
    ......

    struct Entry {
        ......
 
        Texture* texture;
        ......
 
        const AssetAtlas& atlas; 
        ......

        /**
         * Unique identifier used to merge bitmaps and 9-patches stored
         * in the atlas.
         */
        const void* getMergeId() const {
            return texture->blend ? &atlas.mBlendKey : &atlas.mOpaqueKey;
        }
 
        ......
    };

    ......

    const bool mBlendKey;
    const bool mOpaqueKey;
    
    ......
}
這個函數定義在文件frameworks/base/libs/hwui/AssetAtlas.h。

 

Entry類的成員變量texture描述的是一個預加載Drawable資源對應的Texture。前面在分析AssetAtlas類的成員函數createEntries時提到,這個Texture對象僅僅是一個委托對象,用來描述一個預加載Drawable資源在地圖集紋理對應的那一部分內容。並且這個這個Texture對象有一個成員變量blend,用來描述一個預加載Drawable資源是透明還是不透明的。透明意味著在渲染時需要與當前已經繪制的內容進行合並,而不透明在渲染時直接覆蓋在已經繪制的內容上就可以了。

從Entry類的成員函數getMergeId就可以看出,一個使用了預加載Drawable資源的九宮圖繪制的Merge ID取決於預加載Drawable資源的透明度信息。如果是透明的,那麼對應的九宮圖繪制的Merge ID就是AssetAtlas類的成員變量mBlendkey的地址;否則的話,就是AssetAtlas類的成員變量mOpaqueKey的地址。

以上就是一個九宮圖繪制的Batch ID和Merge ID的確定過程。接下來我們仍然是以九宮圖繪制為例,說明若干個九宮圖繪制合並為一個九宮圖繪制的過程。

前面提到,可以合並的Draw Op最終會保存在一個Draw Batch中。對於九宮圖繪制這個Drawp Op來說,它保存在的一個Draw Batch的具體類型為MergingDrawBatch,通過調用它的成員函數replay就可以執行保存它裡面的九宮圖繪制命令,如下所示:

 

class MergingDrawBatch : public DrawBatch {
    ......

    virtual status_t replay(OpenGLRenderer& renderer, Rect& dirty, int index) {
        ......
        if (mOps.size() == 1) {
            return DrawBatch::replay(renderer, dirty, -1);
        }

        ......

        DrawOp* op = mOps[0].op;
        ......

        status_t status = op->multiDraw(renderer, dirty, mOps, mBounds);

        ......

        return status;
    }

    ......
}
這個函數定義在文件frameworks/base/libs/hwui/DeferredDisplayList.cpp中。

 

一個MergingDrawBatch包含的所有Draw Op保存在父類DrawBatch的成員變量mOps中。如果只有一個Draw Op,那就按常規繪制就行了,這通過調用父類DrawBatch的成員函數replay實現。DrawBatch類的成員函數replay又是通過調用Draw Op的成員函數applyDraw來執行實際的渲染操作的。

如果一個MergingDrawBatch包含有有若干個Draw Op,那麼就只會調用第一個Draw Op的成員函數multiDraw進行繪制,同時傳遞給這個成員函數的參數還包括其余的Draw Op。在我們這個例子中,調用的就是DrawPatchOp類的成員函數multiDraw,它的實現如下所示:

 

class DrawPatchOp : public DrawBoundedOp {
    ......

    virtual status_t multiDraw(OpenGLRenderer& renderer, Rect& dirty,
            const Vector& ops, const Rect& bounds) {
        const DeferredDisplayState& firstState = *(ops[0].state);
        renderer.restoreDisplayState(firstState, true); // restore all but the clip

        // Batches will usually contain a small number of items so it's
        // worth performing a first iteration to count the exact number
        // of vertices we need in the new mesh
        uint32_t totalVertices = 0;
        for (unsigned int i = 0; i < ops.size(); i++) {
            totalVertices += ((DrawPatchOp*) ops[i].op)->getMesh(renderer)->verticesCount;
        }

        const bool hasLayer = renderer.hasLayer();

        uint32_t indexCount = 0;

        TextureVertex vertices[totalVertices];
        TextureVertex* vertex = &vertices[0];

        // Create a mesh that contains the transformed vertices for all the
        // 9-patch objects that are part of the batch. Note that onDefer()
        // enforces ops drawn by this function to have a pure translate or
        // identity matrix
        for (unsigned int i = 0; i < ops.size(); i++) {
            DrawPatchOp* patchOp = (DrawPatchOp*) ops[i].op;
            const DeferredDisplayState* state = ops[i].state;
            const Patch* opMesh = patchOp->getMesh(renderer);
            uint32_t vertexCount = opMesh->verticesCount;
            if (vertexCount == 0) continue;

            // We use the bounds to know where to translate our vertices
            // Using patchOp->state.mBounds wouldn't work because these
            // bounds are clipped
            const float tx = (int) floorf(state->mMatrix.getTranslateX() +
                    patchOp->mLocalBounds.left + 0.5f);
            const float ty = (int) floorf(state->mMatrix.getTranslateY() +
                    patchOp->mLocalBounds.top + 0.5f);

            // Copy & transform all the vertices for the current operation
            TextureVertex* opVertices = opMesh->vertices;
            for (uint32_t j = 0; j < vertexCount; j++, opVertices++) {
                TextureVertex::set(vertex++,
                        opVertices->x + tx, opVertices->y + ty,
                        opVertices->u, opVertices->v);
            }

            // Dirty the current layer if possible. When the 9-patch does not
            // contain empty quads we can take a shortcut and simply set the
            // dirty rect to the object's bounds.
            if (hasLayer) {
                if (!opMesh->hasEmptyQuads) {
                    renderer.dirtyLayer(tx, ty,
                            tx + patchOp->mLocalBounds.getWidth(),
                            ty + patchOp->mLocalBounds.getHeight());
                } else {
                    const size_t count = opMesh->quads.size();
                    for (size_t i = 0; i < count; i++) {
                        const Rect& quadBounds = opMesh->quads[i];
                        const float x = tx + quadBounds.left;
                        const float y = ty + quadBounds.top;
                        renderer.dirtyLayer(x, y,
                                x + quadBounds.getWidth(), y + quadBounds.getHeight());
                    }
                }
            }

            indexCount += opMesh->indexCount;
        }

        return renderer.drawPatches(mBitmap, getAtlasEntry(),
                &vertices[0], indexCount, getPaint(renderer));
    }

    ......
};
這個函數定義在文件frameworks/base/libs/hwui/DisplayListOp.h。

 

由於可以合並渲染的九宮圖繪制使用的都是同一個紋理,即預加載地圖集紋理,因此我們只要收集好各個九宮圖繪制的紋理坐標,就可以一次性地調用參數renderer描述的一個OpenGLRenderer對象的成員函數drawPatches進行渲染即可,而不需要分開來對這些九宮圖繪制進行渲染,這樣就實現了合並渲染優化。

這樣,我們就通過九宮圖繪制說明了在應用程序窗口UI的硬件加速渲染中。合並渲染優化的執行過程,從中就可以看到預加載資源地圖集所起到的作用。類似的可以進行合並渲染優化的繪制還有文字繪制。例如,在前面的圖4中,ActionBar和Button的文字也是可以合並在一起進行渲染的,如圖5所示:

\

圖5 文字合並渲染優化

圖5的左邊是一個由英文字母合並而成的紋理,類似於由預加載Drawable資源合成的地圖集紋理。在繪制圖5右邊的文字時,每一個字母的繪制內容都是從左邊合成的英文字母紋理而來的,這一點也是類以於預加載Drawable資源的渲染方式。

應用程序窗口UI的硬件加速渲染中的合並渲染優化是一個復雜的操作。復雜的原因就在於判斷兩個渲染操作是否可以合並在一起執行,而決定兩個渲染操作是否可以合並的因素又很多。這裡我們只是通過九宮圖的渲染來簡單說明這一過程,需要更深入了解這些合並渲染優化的執行過程,可以按照我們這裡提供的流程仔細研讀一下源碼。

至此,我們就分析完成預加載資源地圖集服務的作用以實現原理了。總結來說,它的作用就是:

1. 在GPU這一級別實現資源共享;

2. 為合並渲染優化提供了可能。

在接下來的兩篇文章中,我們繼續分析應用程序窗口的Display List構建過程以及渲染過程。結合這兩個過程,我們再回過頭來閱讀這篇文章,就可以更好地了解預加載資源地圖集起到的上述兩個作用。敬請關注!更多的信息也可以關注老羅的新浪微博:http://weibo.com/shengyangluo。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved