安卓應用一般都害怕自己被殺。內存占用高是被殺的重要原因之中的一個。所以大家都想盡各種招數應對,但效果都一般。
但有一招:
WindowManagerGlobal.getInstance().startTrimMemory(TRIM_MEMORY_COMPLETE);
差點兒沒有人提及。這段時間tos的實戰,在通知欄和桌面都有嘗試,發現效果還不錯,但要掌握好這個函數的使用方法。須要細致理解背后的原理,畢竟這個調用相當於在局部時間內讓應用的一系列GPU緩存被清理。相當於硬件加速失效。
文章分三大部分,第一大部分用簡單的方式描寫敘述安卓繪制系統框架。第二大部分說明繪制過程中GPU產生緩存的原因。
第三大部分說明startTrimMemory可以清理的GPU緩存以及一些誤區。
(一)簡單介紹安卓繪制系統框架
安卓繪制系統比較復雜。網上非常多文章講得非常細,但不easy抓住核心要點,事實上我們僅僅要抓到12個關鍵的相應關系和概念,就能夠掌握清晰基本框架,對debug和性能優化都有價值。
1)一個activity相應一個window。當然。沒有activity耶能夠有window,比方通知欄,window大家都知道。有各種屬性。比方層次。位置等等
2)一個window相應一個surface。surface事實上就是一個對graphic buffer進行管理的對象
3)surface的創建是請求surfaceflinger完畢的。事實上相應的是一塊graphicbuffer,gpu和cp都能訪問到
4)window上能夠有非常多的view,能夠是一棵view的tree。對於activity來說,頂部的view就是DecorView,activity上全部的view都相應同一個surface
5)相比activity里的view。surfaceview(glsurfaceview)會有自己獨立的surface。有自己獨立的處理線程。與activity的surface不是同一個
6)activity的view的繪制(打開硬件加速的情況下),事實上就是在一個surface上的繪制,終於通過hwui這個so完畢,這是在應用端進行的。不是在surfaceflinger這一側。hwui是硬件繪制的關鍵庫。最關鍵的是hwui里有一系列GPU緩存,避免在繪制的時候又一次再上傳圖片紋理等GPU繪制相關的數據
7)各個surface另一個合成的過程。這是在surfaceflinger中完畢的
8)每一次activity的view的繪制和surface的合成,都是通過vsync信號觸發的,vsync每16.6毫秒觸發一次
9)surfaceview(glsurfaceview)的繪制能夠不通過vsync來同步,自己的線程獨立控制節奏,可是繪制之后的surface的合成。由surfaceflinger統一進行
10)應用側的surface。不管是view還是surface view相應的,繪制完成之后。通過eglwapbuffer的方法,將graphicbuffer queue回給surfaceflinger(surfaceflinger合成完成之后,會上屏,之后會釋放出來。讓應用側能夠又一次使用這些buffer)
11)view做動畫的時候,假設子view沒有刷新,子view的ondraw能夠不被觸發,這是動畫過程性能高效的一個關鍵點。以view的hardware layer緩存總體做動畫就可以,在view做動畫的時候假設觸發了子view的又一次繪制,繪制效率就會減少
12) 眼下主流安卓手機。GPU和CPU會共享內存。GPU占用內存多了。留給CPU的就會對應降低,每一個進程GPU占用的內存,也會被統計到各個進程的總內存其中,會影響到low memory killer的策略
另外一張圖大致也能夠反映出上面的12個關鍵描寫敘述的部分體系結構
(二)canvas 繪制bitmap 導致的GPU緩存(俗稱GPU內存泄漏)
大家肯定感興趣,一個bitmap。是怎樣繪制到屏幕上的view的繪制代碼里會觸發canvas.drawBitmap,硬件加速打開的話。canvas事實上就是GLES20RecordingCanvas,GLES20RecordingCanvas的父類是GLES20Canvas。
我們看看GLES20Canvas的GLES20Canvas::DrawBitmap的代碼:
@Override public void drawBitmap(Bitmap bitmap, float left, float top, Paint paint) { throwIfCannotDraw(bitmap); // Shaders are ignored when drawing bitmaps 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); } } |
GLES20Canvas相應的native代碼是android_view_GLES20Canvas.cpp,android_view_GLES20Canvas_drawBitmap 就是nDrawBitmap的詳細實現。
static void android_view_GLES20Canvas_drawBitmap(JNIEnv* env, jobject clazz, OpenGLRenderer* renderer, SkBitmap* bitmap, jbyteArray buffer, float left, float top, SkPaint* paint) { // This object allows the renderer to allocate a global JNI ref to the buffer object. JavaHeapBitmapRef bitmapRef(env, bitmap, buffer); renderer->drawBitmap(bitmap, left, top, paint); } |
這里已經非常明白。canvas的drawbitmap事實上調用的就是hwui里的OpenGLRenderer的drawBitmap,我們看看里面做了什么事情。
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有TextureCache對象,將繪制的bitmap緩存在gpu紋理里,這樣下次假設有反復的。就能夠直接使用來進行繪制,避免再次上傳紋理。
假設TextureCache里沒有相關bitmap的緩存,TextureCache就會創建bitmap的紋理緩存,假設緩存空間不夠了,TextureCache就會移除最老的bitmap的緩存,釋放空間給新的bitmap做緩存。
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(); // Don't even try to cache a bitmap that's bigger than the cache if (size < mMaxSize) { while (mSize + size > mMaxSize) { mCache.removeOldest(); } } texture = new Texture(); texture->bitmapSize = size; generateTexture(bitmap, texture, false); if (size < mMaxSize) { mSize += size; TEXTURE_LOGD("TextureCache::get: create texture(%p): name, size, mSize = %d, %d, %d", bitmap, texture->id, size, mSize); if (mDebugEnabled) { ALOGD("Texture created, size = %d", size); } mCache.put(bitmap, texture); } else { texture->cleanup = true; } } else if (bitmap->getGenerationID() != texture->generation) { generateTexture(bitmap, texture, true); } return texture; } |
有意思的是TextureCache怎樣知道是同一個bitmap,這個依賴於LRUCache,TextureCache里的成員變量mCache,這個LRUCache中,bitmap相當於是key。這意味着什么?意味着假設你的bitmap沒有復用,每次對象都不一樣的話,必定會在gpu空間產生一份拷貝。
即使你是一位優秀的android開發。很注意回收bitmap,gpu空間依舊會有占用,由於在bitmap的回收函數中。並沒有對主動清除TextureCache的調用。
當一個canvas重復被觸發繪制的時候。內存監測工具依舊能夠發現內存泄漏,GPU的緩存不斷上漲就是一個非常有可能的原因。
那系統什么時候能夠釋放?
(三)系統怎樣釋放GPU緩存
系統會在什么時候釋放這些GPU緩存呢?通常是在ActivityManagerService(AMS)里。當應用切換的時候。AMS就會觸發trimApplication函數。trimApplication調用的updateOomAdjLocked里會有例如以下的清除緩存的過程:
這個能夠看出:
-
系統會在某個時候清除hwui里申請的GPU緩存
2.在后台時間越久的進程越easy被清理。排在最后的能夠被深度清理,詳細代碼在hardwarerender.java里:
static void startTrimMemory(int level) { if (sEgl == null || sEglConfig == null) return; Gl20RendererEglContext managedContext = (Gl20RendererEglContext) sEglContextStorage.get(); // We do not have OpenGL objects 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); } } |
GLES20的flushCaches本質上還是調用了hwui的Caches.cpp的操作函數Caches::flush(FlushMode mode)
void Caches::flush(FlushMode mode) { FLUSH_LOGD("Flushing caches (mode %d)", mode); // We must stop tasks before clearing caches 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(); } |
GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_FULL) 相應的是kFlushMode_Full,這個清理的程度最深
GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_MODERATE)相應的是kFlushMode_Moderate
GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_LAYERS)相應的是kFlushMode_Layers
關於kFlushMode_Layers,我們要小心。
當我們往windowmanager里addview之后,假設做了removeView。並不會釋放view里的texture cache,可是會觸發GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_LAYERS),清除layer cache。在之前的工作中,團隊曾有討論,覺得removeView能夠充分釋放GPU緩存,這個結論是不准確的。近期有位同學研究的非常深入,他的demo和源代碼走讀證明了removeView僅僅會釋放layer cache,並沒有觸發紋理緩存的回收,這意味着什么?意味通知系統動態addView->顯示 ->removeView的過程依舊會導致GPU內存逐步上漲。系統剩余內存越來越少的情況,直到系統AMS觸發startTrimMemory后,內存才會被回收一些。
總結一下:應用開發人員調用startTrimMemory會幫助app或者系統很多其它的釋放內存,降低內存壓力,可是調用的位置和時機要謹慎,由於清除了緩存。在下一次繪制(vsync的下一個信號到來)的時候繪制效率不會非常高。