1. 程式人生 > >優化安卓應用內存的神奇方法以及背後的原理,一般人我不告訴他

優化安卓應用內存的神奇方法以及背後的原理,一般人我不告訴他

hvr 通知 lib easy rgb ger eset canvas 統計

安卓應用一般都害怕自己被殺。內存占用高是被殺的重要原因之中的一個。所以大家都想盡各種招數應對,但效果都一般。


但有一招:


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裏會有例如以下的清除緩存的過程:


技術分享


這個能夠看出:

  1. 系統會在某個時候清除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的下一個信號到來)的時候繪制效率不會非常高。


作者簡單介紹

技術分享 黃石柱 MIG智能平臺產品部終端開發組副總監10年的移動端軟件研發經驗,4年騰訊終端開發經驗。在騰訊主導設計研發tita(tos前身),魅拍等多款產品,眼下正在深入tos的研發以及虛擬現實技術的研發,在安卓操作系統,多媒體技術上有不錯的積累,開發公司級課件《深入安卓省電十大困惑》。


優化安卓應用內存的神奇方法以及背後的原理,一般人我不告訴他