Android内存解决方案整理

郭鹏

光阴荏苒,从我们还在android1.5上调试我们的手机经过4.x的诸多变化到现在将近十年过去了,android系统也蓬勃发展到了android8.x,越来越清晰的屏幕,越来越快的cpu,越来越大的内存在带来更酷炫的用户体验的同时并未能减轻OOM对android开发者们的困扰,几年前在开发云音乐客户端时跟踪内存占用问题,开始一直被一个问题困扰,那就是同样的屏幕分辨率情况下,我们的手机跑在4.x系统时占用的内存是跑在2.x系统占用内存的两三倍甚至更多,在组内同事的帮助下,我们进行了参考源码、网贴及建测试工程跟踪测试一系列流程,搞懂了这样一个问题,就是在android2.x下bitmap的主要内存申请(decode出来的数据部分)在native,在android4.x下该空间申请在虚拟机的heap里面,而我们在DDMMS看到的一个应用对应的heap占用时看到的只是虚拟机的heap部分,无法看到native的占用,而每个应用对虚拟机heap的占用是有严格限制的(16/24/48/64…)超出这个限制之后的内存申请会报OOM异常,而native部分据我们分析应该是所有应用程序共享的,老杨写的测试工程,一个activity通过jni调用native,native代码里申请了200M内存,在2.x和4.x上测试都可以申请成功,想减少OOM,开始我们的想法是希望在4.x上也能把bitamp的内存占用移到native里面,就是争取能够用到更多的内存,在一系列尝试失败后我们决定通过一些处理来省内存,具体有如下一些途径来实现。

1,对于资源类,即drawable下的资源文件,可能被我们在xml里面通过background或src设给image view,也可能在代码里通过setBackground,setBackgroundDrawable,setBackgroundResource之类的函数设给imageview,该部分资源的解码由系统处理,系统解码后会有缓存,也就是对于对于相同的资源设给不同的view实际的bitmap只有一份,所以不建议用BitmapFactory.decodeResource先解成bitmap,再用imageview的setImageBitmap方法设给imageview,这样会导致相同的资源产生多份bitmap
另外,切图的时候对渐变不敏感的图片尽量用小一 些的图片或.9图,因为只有decode时的内存是占在我们程序内的,屏幕绘制时拉伸之后产生的存储增加不占用我们应用的内存(可能为屏幕显存)

2,对于网络图片,该部分图片较多的时候我们经常会用listview或gridview或gallery等控件加载,listview和gridview因为item中的view会重用,滑动之后屏幕之外(因为系统策略不同,可能为1.5屏或2屏之类)的view中imageview中的资源引用会被释放,但内存回收不及时(无法预知何时被回收,手动gc一般无效),gallery因为item view不重用,释放就更不及时,该部分的内存想要及时回收,可以重写imageview(或其它view)的onDetachedFromWindow()方法,在该方法中手动recycle,但会影响滑动回去显示的速度,所以不是内存问题特别严重的应用不建议这么做,但在onDetachedFromWindow的时候取消队列里还未从网络下载的图片及队列里还未来得及解码的图片还是必要的.

3,当离开当前界面,即当前界面完全不显示了,代码上的时间点就是activity或fragment的onStop的时候,对于前一个界面占用内存的较大图片的手动回收是一个比较好的时间点,因为前一个界面没有destroy前view的引用都还在,bitmap不会被回收,这样如果界面层叠很多的话内存占用我们可以粗略计算一下:以系统默认解码参数Bitmap.Config.ARGB_8888为例,对于480800的屏幕,满一屏的图片(4808004)/(10241024)=1.46M,对于1280720的xhdpi大屏幕,满一屏的图片为(12807204)/(10241024)=3.52M,而因为decode时候的samplesize为整数,我们解出来的图片往往比显示的还要大(显示时被适当压缩),而界面如果为adapterview的话引用到往往又会超出一屏,由此,如果多开了几个图多的界面的话内存占用可想而知,很容易就会达到系统一个应用24M/32M/48M的内存限制(针对4.x系统,2.x据分析bitmap的数据部分在native申请,不统计在这一部分内存限制里面),所以对于设计上会有很多大图界面重叠的应用来说不得不在界面不可见后手动去回收一些bitmap来缓解内存压力
回收方法,在onStop时遍历view树中的imageview,满足回收条件(视具体情况而定,音乐目前是图片宽度大于屏幕宽度的1/3)的手动回收实现,以云音乐的首页发现四个fragment为例

1.记状态:保存listview当前的数据及滑动的位置等必要信息
public void onStop() {
super.onStop(); Log.e("FindListFragment", "type:" + mType + " recycleImage"); clearList(); NeteaseMusicUtils.recycleAllImage(mList); }

private List mListData = new ArrayList();

public void clearList(){
    if(mAdapter != null){
        currStatus.savePosition(mList);
        mList.setAdapter(null);
    }
} 

private class ListViewStatus {
// 当前状态 int offset; int limit; int itemPosition; int topPosition; private PageValue hasMore = new PageValue();

    public ListViewStatus(int limit, boolean hasMore) {
        this.limit = limit;
        this.hasMore.setHasMore(hasMore);
    }

    public void savePosition(ListView list){
        if(list == null)return;
        itemPosition = list.getFirstVisiblePosition();
        topPosition = list.getChildAt(0) == null?0:list.getChildAt(0).getTop();
    }

    public void loadPosition(ListView list){
        if(list == null)return;
        list.setSelectionFromTop(itemPosition, topPosition);
    }
}

2.遍历,找到view树中的所有加载网络图片的ImageView(封装后为NetImageView)
private static void recycleImage(ViewGroup viewGroup){
for(int i = 0; i < viewGroup.getChildCount(); i++){ View child = viewGroup.getChildAt(i); if(child instanceof NetImageView){ NeteaseMusicUtils.recycleImageView((ImageView)child); } if(child instanceof ViewGroup){ recycleImage((ViewGroup) child); } } }

3.回收
public static void recycleImageView(ImageView imageview) {
if (imageview == null || !(imageview instanceof NetImageView) || !((NetImageView) imageview).isNetImage()) { return; } ImageLoaderManager.cancelLoadImage(imageview); Drawable drawable = imageview.getDrawable(); if (drawable != null && drawable instanceof BitmapDrawable) { Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); imageview.setImageDrawable(null); if (bitmap != null && !bitmap.isRecycled() && bitmap.getWidth() > NeteaseMusicApplication.getInstance().getResources().getDisplayMetrics().widthPixels / 3) { bitmap.recycle(); bitmap = null; } } }

4.onResume的恢复
public void onResume() {
super.onResume(); recoverList(); }

public void recoverList(){
if(mAdapter != null){ mList.setAdapter(mAdapter); currStatus.loadPosition(mList); mFillEmptyFooter.requestLayout(); } }

之前对于resource的手动回收发现有时候会有些问题,就是界面stop后没有到resume,回收的图片可能还被绘制,网络图片目前没有发现该情况(该情况为测试统计结果,从源码上得不到支持。。),加之如果手动回收resource后resume时需要自己解码,然后用setImageBitmap重新设回去,会存在第1点所说的资源解码后多份的问题,所以目前不建议手动回收resource。 对于回收的资源可能在不需要显示的时候被绘制导致Canvas: trying to use a recycled bitmap异常,作为一个折衷的办法,可以重写imagview,重写ondraw方法,捕掉该异常,作为最后一道避免崩溃的防线,虽然可能会有图片绘制不出来的风险(到目前为止测试中未发现),但我们觉得偶尔的图片绘制不出来比起经常由于OOM导致应用程序无法继续运行来说宁可选择前者,所谓的两害相较取其轻吧。。

代码如下: @Override protected void onDraw(Canvas canvas) { try { super.onDraw(canvas); } catch (RuntimeException e) { e.printStackTrace(); } }

为什么会发生内存泄漏:

为了判断Java中是否有内存泄露,我们首先必须了解Java是如何管理(堆)内存的。Java的内存管理就是对象的分配和释放问题。在Java中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不需要通过调用函数来释放内存,但它只能回收无用并且不再被其它对象引用的那些对象所占用的空间。

Java的内存垃圾回收机制是从程序的根对象(如静态对象/寄存器/栈上指向的堆内存对象等)开始检查引用链,当遍历一遍后得到上述这些无法回收的对象和他们所引用的对象链,组成无法回收的对象集合,而其他孤立对象(集)就作为垃圾回收。GC为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。 如果某些无用对象,被根对象引用而无法被gc回收,则产生内存泄漏。 目前只总结了这些,欢迎浏览过这篇文档的同学的补充