优化安卓应用内存的神奇方法以及背后的原理,一般人我不告诉他

蹉跎莫遣韶光老,人生唯有读书好。这篇文章主要讲述优化安卓应用内存的神奇方法以及背后的原理,一般人我不告诉他相关的知识,希望能为你提供帮助。
安卓应用一般都害怕自己被杀。内存占用高是被杀的重要原因之中的一个。所以大家都想尽各种招数应对,但效果都一般。


但有一招:

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的研发以及虚拟现实技术的研发,在安卓操作系统,多媒体技术上有不错的积累,开发公司级课件《深入安卓省电十大困惑》。





























































    推荐阅读