Mobile Development 12 min read

Unlock Android GPU Memory: Master startTrimMemory to Reduce App Kills

Android apps often get killed due to high memory usage, especially from GPU caches; this article explains the Android drawing system architecture, how bitmap rendering creates GPU memory leaks, and demonstrates using WindowManagerGlobal.startTrimMemory to clear those caches while outlining common pitfalls and best practices.

Tencent TDS Service
Tencent TDS Service
Tencent TDS Service
Unlock Android GPU Memory: Master startTrimMemory to Reduce App Kills

Android apps are frequently terminated because of excessive memory consumption, with GPU cache buildup being a major contributor.

1. Overview of Android Rendering System

The framework can be captured by twelve key relationships: an Activity maps to a Window (including system windows like notifications); each Window owns a Surface that manages a GraphicBuffer accessible by both GPU and CPU; a Window contains a tree of Views, with the topmost View being the DecorView that shares a single Surface; SurfaceView (or GLSurfaceView) has its own independent Surface and rendering thread; rendering of Views with hardware acceleration occurs on a Surface via the hwui library, which maintains several GPU caches to avoid re‑uploading textures; composition of Surfaces is performed by SurfaceFlinger; each rendering cycle is triggered by a VSync signal (~16.6 ms); GLSurfaceView can render independently of VSync, but its Surface still undergoes composition by SurfaceFlinger; after drawing, the application calls eglSwapBuffers to return the GraphicBuffer to SurfaceFlinger, which then displays it and releases the buffer for reuse; view animations rely on hardware layers to avoid redrawing sub‑views, improving performance; modern Android devices share memory between CPU and GPU, so heavy GPU usage reduces memory available to the CPU and influences the Low‑Memory Killer policy.

2. GPU Cache from Canvas drawBitmap (GPU Memory Leak)

When a bitmap is drawn, the Canvas (with hardware acceleration) becomes a GLES20RecordingCanvas, whose parent is GLES20Canvas. The drawBitmap call ultimately invokes hwui’s OpenGLRenderer::drawBitmap, which stores the bitmap as a texture in a TextureCache.

@Override
public void drawBitmap(Bitmap bitmap, float left, float top, Paint paint) {
    throwIfCannotDraw(bitmap);
    int modifiers = paint != null ? setupModifiers(bitmap, paint) : MODIFIER_NONE;
    try {
        final int nativePaint = paint == null ? 0 : paint.mNativePaint;
        nDrawBitmap(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, left, top, nativePaint);
    } finally {
        if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers);
    }
}

The native side is implemented in android_view_GLES20Canvas.cpp:

static void android_view_GLES20Canvas_drawBitmap(JNIEnv* env, jobject clazz,
    OpenGLRenderer* renderer, SkBitmap* bitmap, jbyteArray buffer, float left,
    float top, SkPaint* paint) {
    JavaHeapBitmapRef bitmapRef(env, bitmap, buffer);
    renderer->drawBitmap(bitmap, left, top, paint);
}

The renderer’s method:

status_t OpenGLRenderer::drawBitmap(SkBitmap* bitmap, float left, float top, SkPaint* paint) {
    const float right = left + bitmap->width();
    const float bottom = top + bitmap->height();
    if (quickReject(left, top, right, bottom)) {
        return DrawGlInfo::kStatusDone;
    }
    mCaches.activeTexture(0);
    Texture* texture = getTexture(bitmap);
    if (!texture) return DrawGlInfo::kStatusDone;
    const AutoTexture autoCleanup(texture);
    if (CC_UNLIKELY(bitmap->getConfig() == SkBitmap::kA8_Config)) {
        drawAlphaBitmap(texture, left, top, paint);
    } else {
        drawTextureRect(left, top, right, bottom, texture, paint);
    }
}

hwui’s TextureCache keeps these textures; if the cache is full, the oldest entries are evicted. When bitmaps are not reused, each distinct bitmap creates a new GPU texture, leading to memory growth even if the Java bitmap objects are recycled.

Texture* TextureCache::get(SkBitmap* bitmap) {
    Texture* texture = mCache.get(bitmap);
    if (!texture) {
        if (bitmap->width() > mMaxTextureSize || bitmap->height() > mMaxTextureSize) {
            ALOGW("Bitmap too large to be uploaded into a texture (%dx%d, max=%dx%d)",
                  bitmap->width(), bitmap->height(), mMaxTextureSize, mMaxTextureSize);
            return NULL;
        }
        const uint32_t size = bitmap->rowBytes() * bitmap->height();
        if (size < mMaxSize) {
            while (mSize + size > mMaxSize) {
                mCache.removeOldest();
            }
        }
        texture = new Texture();
        texture->bitmapSize = size;
        generateTexture(bitmap, texture, false);
        if (size < mMaxSize) {
            mSize += size;
            mCache.put(bitmap, texture);
        } else {
            texture->cleanup = true;
        }
    } else if (bitmap->getGenerationID() != texture->generation) {
        generateTexture(bitmap, texture, true);
    }
    return texture;
}

Thus, even diligent bitmap recycling may not free GPU memory because the TextureCache is not automatically cleared.

3. How the System Releases GPU Caches

The system clears these caches via ActivityManagerService when an app is switched or backgrounded. AMS calls trimApplication, which eventually invokes WindowManagerGlobal.startTrimMemory with a trim level.

static void startTrimMemory(int level) {
    if (sEgl == null || sEglConfig == null) return;
    Gl20RendererEglContext managedContext = (Gl20RendererEglContext) sEglContextStorage.get();
    if (managedContext == null) {
        return;
    } else {
        usePbufferSurface(managedContext.getContext());
    }
    if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) {
        GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_FULL);
    } else if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
        GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_MODERATE);
    }
}
GLES20Canvas.flushCaches

forwards to Caches::flush, which clears various caches depending on the flush mode (Full, Moderate, Layers).

void Caches::flush(FlushMode mode) {
    if (mode > kFlushMode_Layers) {
        tasks.stop();
    }
    switch (mode) {
        case kFlushMode_Full:
            textureCache.clear();
            patchCache.clear();
            dropShadowCache.clear();
            gradientCache.clear();
            fontRenderer->clear();
            fboCache.clear();
            dither.clear();
            // fall through
        case kFlushMode_Moderate:
            fontRenderer->flush();
            textureCache.flush();
            pathCache.clear();
            // fall through
        case kFlushMode_Layers:
            layerCache.clear();
            renderBufferCache.clear();
            break;
    }
    clearGarbage();
}

Calling startTrimMemory at appropriate moments can free GPU memory, but aggressive flushing may degrade rendering performance until the next VSync redraw.

Summary: Invoking WindowManagerGlobal.startTrimMemory helps release GPU caches and lower memory pressure, but it should be used judiciously to avoid temporary performance loss.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Mobile DevelopmentAndroidhwuiGPU MemorystartTrimMemory
Tencent TDS Service
Written by

Tencent TDS Service

TDS Service offers client and web front‑end developers and operators an intelligent low‑code platform, cross‑platform development framework, universal release platform, runtime container engine, monitoring and analysis platform, and a security‑privacy compliance suite.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.