工作中遇到的Android内存优化问题(2)

Android内存泄漏问题,大部分都是对象(Activity,Fragment..)不能被回收,对象中的资源占用内存引起。还有一些包括cursor未关闭,文件流未关闭等(这些可以用StrictMode设置进行排查)。对象之所以不能被回收主要有两个原因一个是对象被别的类静态引用,另一个是多线程下,其中一个延时线程(主要是网络请求)持有的此对象,而此线程不结束,就不会释放对象引用。

 查询内存泄漏的工具,除了我在上一篇说的抓Heap用工具分析外,还可以用LeakCanary (https://github.com/square/leakcanary),这个工具太好了,它让我们知道了工具不是越复杂越好,它用法简单,功能却不简单,几乎可以检测到大部分内存泄漏问题。

首先来看一个例子,此例子是Android 5.1之前EditText存在的一个内存泄漏的bug,此bug谷歌在Android5.1的这条提交(893d6fe48d37f71e683f722457bea646994a10bf)上已经修复了。但是在5.1之前的版本还会有这个问题,不过Android6.0有一个 InputMethodManager 的内存泄漏问题,和这个问题很类似,因此我们来看一看EditText这个泄漏问题的分析方法,对于分析其他类似问题有帮助。

来看一下例子,这个例子很简单只有一个EditText和一个ListView,ListView里面放了几张图片

public class MainActivity extends Activity {

    EditText mEditText;
    List<Integer> mContentList;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mEditText = (EditText)findViewById(R.id.edit_text);
        ListView listView = (ListView)findViewById(R.id.img_list_view);

        mContentList = new ArrayList<>();
        mContentList.add(R.drawable.g1);
        mContentList.add(R.drawable.g2);
        mContentList.add(R.drawable.g3);
        mContentList.add(R.drawable.g4);

        ImageAdapter imageAdapter = new ImageAdapter(this,mContentList);
        listView.setAdapter(imageAdapter);

    }

    class ImageAdapter extends BaseAdapter{
        Context context;
        List<Integer> contentList;
        public ImageAdapter(Context context,List<Integer> contentList){
            this.context = context;
            this.contentList = contentList;
        }

        @Override
        public int getCount() {
            return contentList.size();
        }

        @Override
        public Object getItem(int position) {
            return contentList.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            if(convertView == null){
                convertView = new ImageView(context);
                convertView.setLayoutParams(new AbsListView.LayoutParams(300,100));
            }
            ((ImageView)convertView).setImageResource(contentList.get(position));
            return convertView;
        }
    }
}


这点代码怎么看都没有内存泄漏的代码,但是用Android Studio看一下内存占用 当进入应用时内存占用如图


占用了43M,当退出应用,并点击 进行强制gc回收时,内存占用如下图

还是43M(5.1以上系统没有此问题),从这就可以看出是内存泄漏了。现在我们就用上一篇里用的MAT工具分析一下(不清楚的可以看一下上一篇文章(工作中遇到的Android内存优化问题(1)))。



从图中可以看出是byte[]占用过多,我们可以确认就是ListView里面加载的图片。进一步右键byte[],点击with incoming references,点开如图


我们可以定位到ListView,ListView没有被回收,我们可以知道一定是MainActivity没有被回收,你一直往下展开也可以找到MainActivity,我们也可以在Histogram最上方搜索MainAcvitity,如图搜索到了MainActivity



右键此对象List Objects -- > List incoming reference 紧接着右键 --> Path to GC Roots --> exclude all phantom/weak/soft etc. (具体操作请查看上一篇),得到如图的结果



可以清楚的看到是sCached导致了内存泄漏,从Android 源码中我们可以看到 sCached的定义 private static final TextLine[] sCached = new TextLine[3];

定义为了一个静态变量,从上图中我们可以看到是此变量中的 第一元素【0】,是一个TextLine,它的成员变量mSpanned....到最后持有了一个MainActivity,导致MainActivity无法

释放。前面我说过谷歌在android5.1中修复了,下图是谷歌修复的提交代码




我们可以看过在TextLine调用 TextLine recycle(TextLine tl) 回收时并没有把所有变量置为null,因此导致了mSpanned下持有了我们的MainActivity对象,引起了内存泄漏。

此问题既然是Android源码本身的问题,那怎么解决了,一种方法是在 http://stackoverflow.com/questions/30397356/android-memory-leak-on-textview-leakcanary-leak-can-be-ignored/30397901#30397901 里面的一个回答中通过反射进行设置

public static class Utils {
    private static final Field TEXT_LINE_CACHED;

    static {
        Field textLineCached = null;
        try {
            textLineCached = Class.forName("android.text.TextLine").getDeclaredField("sCached");
            textLineCached.setAccessible(true);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        TEXT_LINE_CACHED = textLineCached;
    }

    public static void clearTextLineCache() {
        // If the field was not found for whatever reason just return.
        if (TEXT_LINE_CACHED == null) return;

        Object cached = null;
        try {
            // Get reference to the TextLine sCached array.
            cached = TEXT_LINE_CACHED.get(null);
        } catch (Exception ex) {
            //
        }
        if (cached != null) {
            // Clear the array.
            for (int i = 0, size = Array.getLength(cached); i < size; i ++) {
                Array.set(cached, i, null);
            }
        }
    }

    private Utils() {}
}

当然你如果不想用反射,你也可以用另一种方法,既然Activity不能被释放,那我们可以自己释放Activity中的资源,也就是在Activity的onDestroy()里将listVIew的Adapter置为空

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mListView.setAdapter(null);
        mListView = null;
    }

此时再查看内存,可以看到内存大部分已经释放,问题已不严重



下一篇我们来介绍一下LeakCanary,上面我们说的那个问题因为是Android源码问题,LeakCanary已经过滤掉了此问题,因此用它查不到上面的内存泄漏

demo 源码 http://download.csdn.net/detail/u011291205/9625759







评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值