预加载资源地图集服务(Asset Atlas Service)分析
最后更新于:2022-04-02 05:03:51
原文出处——>[Android应用程序UI硬件加速渲染的预加载资源地图集服务(Asset Atlas Service)分析](http://blog.csdn.net/luoshengyang/article/details/45831269)
我们知道,Android系统在启动的时候,会对一些系统资源进行预加载。这样不仅使得应用程序在需要时可以快速地访问这些资源,还使得这些资源能够在不同应用程序之间进行共享。在硬件加速渲染环境中,这些预加载资源还有进一步优化的空间。Android系统提供了一个地图集服务,负责将预加载资源合成为一个纹理上传到GPU去,并且能够在所有的应用程序之间进行共享。本文就详细分析这个预加载资源地图集服务的实现原理。
资源预加载是发生在Zygote进程的,然后Zygote进程fork了应用程序进程,于是就使得预加载的资源可以在Zygote进程与所有的应用程序进程进行共享。这种内存共享机制是由Linux进程创建方式决定的。也就是说,父进程fork子进程之后,只要它们都不去修改某一块内存,那么这块内存就可以在父进程和子进程之间进行共享。一旦父进程或者子进程修改了某一块内存,那么Linux内核就会通过一种称为COW(Copy On Wrtie)的技术为要修改的进程创建一块内存拷贝出来,这时候被修改的内存就不再可以共享。
对于预加载资源来说,它们都是只读的,因此就可以保证它们在Zygote进程与所有的应用程序进程进行共享。这在应用程序UI使用软件方式渲染时可以工作得很好。但是当应用程序UI使用硬件加速渲染时,情况就发生了变化。资源一般是作为纹理来使用的。这意味着每一个应用程序都会将它要使用的预加载资源作为一个纹理上传到GPU去,如图1所示:
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/005fbbe4b53f499f0d97af4e788c4c5a_730x669.png)
图1 应用程序独立将预加载资源上传到GPU
因此,这种做法会浪费GPU内存。为了节省GPU内存,Android系统在System进程中运行了一个Asset Atlas Service。这个Asset Atlas Service将预加载资源合成为一个纹理,并且上传到GPU去。应用程序进程可以向Asset Atlas Service请求上传后的纹理,从而使得它们不需要再单独去上传一份,这样就可以起到在GPU级别共享的作用,如图2所示:
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/47dd07dbfa62b1ecafa2b81d9eff2b09_717x349.png)
图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 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所示:
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/49708e7a893e5ad81717cf831bcd1576_768x170.png)
图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资源都在当前的空闲块中找到位置进行渲染,从而完成整个合成过程。关于这个合成过程的详细实现,可以参考frameworks/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中。
参数proxyPtr指向的是一个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所示:
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/a66c5e88cc527e42da2dd531007eced4_278x240.png)
图4 Android应用程序窗口UI分层绘制模式
图4显示的窗口由一个背景图、一个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数组由成员变量mMergingBatches指向。每一个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所示:
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/37136d1a618897d955d073f056a47f6b_454x221.png)
图5 文字合并渲染优化
图5的左边是一个由英文字母合并而成的纹理,类似于由预加载Drawable资源合成的地图集纹理。在绘制图5右边的文字时,每一个字母的绘制内容都是从左边合成的英文字母纹理而来的,这一点也是类以于预加载Drawable资源的渲染方式。
应用程序窗口UI的硬件加速渲染中的合并渲染优化是一个复杂的操作。复杂的原因就在于判断两个渲染操作是否可以合并在一起执行,而决定两个渲染操作是否可以合并的因素又很多。这里我们只是通过九宫图的渲染来简单说明这一过程,需要更深入了解这些合并渲染优化的执行过程,可以按照我们这里提供的流程仔细研读一下源码。
至此,我们就分析完成预加载资源地图集服务的作用以实现原理了。总结来说,它的作用就是:
1. 在GPU这一级别实现资源共享;
2. 为合并渲染优化提供了可能。
在接下来的两篇文章中,我们继续分析应用程序窗口的Display List构建过程以及渲染过程。结合这两个过程,我们再回过头来阅读这篇文章,就可以更好地了解预加载资源地图集起到的上述两个作用。敬请关注!
';