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

安卓应用一般都害怕自己被杀,内存占用高是被杀的重要原因之一,所以大家都想尽各种招数应对,但效果都一般。

但有一招:

WindowManagerGlobal.getInstance().startTrimMemory(TRIM_MEMORY_COMPLETE);

几乎没有人提及。这段时间的实战,发现效果还不错,但要掌握好这个函数的用法,需要仔细理解背后的原理,毕竟这个调用相当于在局部时间内让应用的一系列GPU缓存被清理,相当于硬件加速失效。

文章分三大部分,第一大部分用简单的方式描述安卓绘制系统框架,第二大部分说明绘制过程中GPU产生缓存的原因。第三大部分说明startTrimMemory能够清理的GPU缓存以及一些误区。

(一)简介安卓绘制系统框架

安卓绘制系统比较复杂,网上很多文章讲得很细,但不容易抓住核心要点,其实我们只要抓到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.在后台时间越久的进程越容易被清理,排在最后的可以被深度清理,具体代码在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的研发以及虚拟现实技术的研发,在安卓操作系统,多媒体技术上有不错的积累,开发公司级课件《深入安卓省电十大困惑》。

腾讯Bugly 最专业的质量跟踪平台

精神哥、小萝莉,为您定期分享应用崩溃解决方案

原文发布于微信公众号 - 腾讯Bugly(weixinBugly)

原文发表时间:2015-08-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏非著名程序员

Android:一个高效的UI才是一个拉风的UI

开篇 Android是一个运行在移动终端上的操作系统,跟传统PC最大的不同所在就是移动终端的资源紧缺问题“比较”明显,当然对于一些屌丝机型,应该用“非常“来形容...

28090
来自专栏编程思想之路

Android中View研究自学之路 Android6.0源码分析之View(一)Android6.0源码分析之View(二)

Android中View研究自学之路 http://blog.csdn.net/zrf1335348191/article/details/54171263 ...

22470
来自专栏极客慕白的成长之路

Web前端开发推荐阅读书籍、学习课程下载

学校里没有前端的课程,那如何学习JavaScript,又如何使自己成为一个合格的前端工程师呢?

59460
来自专栏技术总结

UIKit Dynamics 置身真实世界

270100
来自专栏MixLab科技+设计实验室

从网易《初心》H5里学到的一些

这篇文章可以作为之前写的一篇《技能之H5》的补充知识。 1、拖放 HTML5 标准的组成部分。 在 HTML5 中,拖放是标准的一部分,任何元素都能够拖放。 ...

36960
来自专栏大数据文摘

手把手 | 教你爬下100部电影数据:R语言网页爬取入门指南

29070
来自专栏Android机动车

Android的16ms和垂直同步以及三重缓存

手机屏幕是由许多的像素点组成的,每个像素点通过显示不同的颜色最终屏幕呈现各种各样的图像。手机系统的类型和手机硬件的不同导致UI的流畅性体验个不一致。

24820
来自专栏天天P图攻城狮

android视频系列:视频解码篇--android上视频播放的实现

前言 要开始正儿八经地写视频系列文章了。思来想去,从播放器入手,再合适不过了。视频文件,只有播放出来,才显示出了意义;只有播放出来,才暴露出各种问题。先理解播放...

521110
来自专栏张高兴的博客

使用 Babylon.js 在 HTML 页面加载 3D 对象

25150
来自专栏python小白到大牛

用 Python 实现打飞机,让子弹飞吧!

安装好 pygame 在第一次使用 pygame 的时候,pyCharm 会自动 install pygame。

26730

扫码关注云+社区

领取腾讯云代金券