Android性能优化(二)内存优化

        上一篇文章,我总结了一下app启动优化的一些知识。这篇文章,总结一下内存优化相关的一些知识。内存优化,相比于启动优化,可能没那么明显。为什么这么说呢?启动快慢,我们体验一次就能体验出来。而内存增长,我们操作一次,两次,三次......如果不借助什么工具或者命令的话,我们的肉眼并不能发现什么。因此,在很多情况下,我们会忽视掉app内存这一块。可能大部分程序员,知道自己的app存在内存问题,往往是因为QA的monkey test中,存在着某个oom的崩溃,或者bugly上频繁上报oom的崩溃。那这个时候,我们意识到,原来我们的app并没有想象中那么完美。

一、前言

        在正式的总结内存优化相关知识之前,我还是说一些我自己的经历。如下图所示,在这个照片中,记录的是我以前参与开发的一个功能。当我们自信心满满地填完测试用例,提测后,发现没什么大的问题。然后,QA下班跑monkey,第二天一大早,各种crash日志打包给你发过来了。打开一看,啥,我的应用跑了一晚上monkey内存增长了400M?我靠,这么牛逼!我不信,我试试。于是,我就试了试,下图就是我当年的记录:

        怎么样?你跌倒了没?崩溃了没?崩溃了,彻底崩溃了。尤其以前我们做手机,非常强调整体的内存。于是,我们抓紧联系QA:小姐姐,这个问题不大,我们一会就修复好了。说完,立马去查看代码。这块内存释放了吗?释放了。这里的对象回收了吗?回收了。这个容器清空了吗?清空了。好像没问题啊!!!但是,你自己的测试结果都在那!!!所以,我们回归到那句话,内存问题,不像是启动问题或者是一些直观的UI问题,往往容易被忽视。而且,根据代码去排查根本不好排查。一个是因为代码就是我们自己写的,再一个就是我们不可能一行一行代码去找,我们只能大体的去找一些回收函数,或者猜测可能出问题的模块。

二、三种内存问题

1、内存抖动

        记得刚工作的时候,我做好自己的工作,就会去看我导师和我主管头上挂着的BUG。记得在我主管那看到一个性能组的BUG:XXX模块XXX操作时,存在卡顿,内存图如下。我看了一下,内存图这不是很正常吗,有增加有减少,整体内存没有太大的增长啊。我于是就得出了结论:没问题。于是,我继续往下滑,看到了我主管的comment:内存抖动。啥?内存抖动?这是个啥?于是,我百度了一下,大体了解到:

        内存抖动,由于短时间内频繁的创建和销毁对象,频繁触发GC操作导致的内存问题。对象创建并用完后及时销毁,这是没问题的。但是如果频繁的创建和销毁,就会频繁的触发GC操作。其实,GC操作是非常消耗CPU的,进而会导致卡顿等问题。

        我写个demo模拟下内存抖动的情况,我在界面上放了两个Button,一个叫创建,一个叫销毁,点击创建会创建一个bitmap,点击销毁则销毁该bitmap。代码如下:

        button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                BitmapFactory.Options options = new BitmapFactory.Options();
                options.inJustDecodeBounds = false;
                mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.splash);
                Log.d("TTTT","create");
            }
        });
        button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mBitmap != null && !mBitmap.isRecycled()) {
                    Log.d("TTTT","recycle");
                    mBitmap.recycle();
                    mBitmap = null;
                }
            }
        });

        我们使用profiler监控一下内存变化,放大后,如下图所示。很明显,内存增减交错,呈现锯齿状,但由于我们有创建,有回收,整体的内存没有大幅度的增加:

2、内存泄漏

        接下来,说一下什么是内存泄漏。内存泄漏,简单来说,就是内存有申请,没释放,或者说没有完全释放,导致可用内存逐渐减少,当到达一定的程度时,也会频繁的触发GC操作。因此,内存泄漏也会导致卡顿等性能问题。

        我依然通过一个小例子,演示一下内存泄漏。我还是使用上面的例子稍微改一下。我每次点击创建都会创建一个不同大小的Bitmap。而且,我只点创建,不点销毁,代码如下:

 button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                BitmapFactory.Options options = new BitmapFactory.Options();
                options.inJustDecodeBounds = false;
                mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.splash);
                size++;
                Bitmap bitmap = Bitmap.createScaledBitmap(mBitmap, 54 * size, 96 * size, false);
                Log.d("TTTT", "create");
            }
        });

        我们使用profiler抓一下内存,我们可以看到,随着我一直点击创建,内存一直在增加,虽然也有骤降的地方,但是整体趋势是增加的。从64M一直增加到了190M,且没有停止的趋势:

3、内存溢出

        内存溢出,我接着上面的内存泄漏来说。我们从内存泄漏可以知道,内存一直申请一直增加,没有释放的趋势。当达到Android系统为每个app设置的内存阈值后,会如何?毫无疑问,会因为oom(OutOfMemoryError),导致崩溃。

        首先,我们在程序中获取一下内存阈值,很简单的代码:

Runtime runtime = Runtime.getRuntime();
Log.d("TTTT", "maxMemory:" + runtime.maxMemory());

        看一下打印的结果:

2020-03-07 22:19:24.926 4482-4482/? D/TTTT: maxMemory:201326592

        我们计算一下201326592子节是多少M:201326592/(1024^2) = 192M。也就是说,我当前手机的内存阈值是192M。当然,我们有方法可以将内存扩展为更大,在这里我先不多说。

        事实上,在上面的代码中,我已经点到崩溃了,那我接着上面的图,把后面的内存图贴一下。如下所示,可以看到,在内存在128M-192M之间的时候,已经开始频繁的触发GC操作。再往后,我们发现,内存直接降到了0,不动了。为什么?因为,他真的达到内存阈值了,他真的崩溃了。

 

         我们把鼠标移动到图的某一帧位置,可以查看到此时的内存情况,可以看到,最后崩溃前,JAVA内存占用达到了186M,总内存达到233.6M。而我们app刚启动的时候,JAVA内存只有6M,总内存46.8M,如下图所示:

                                              

        我们看一下崩溃日志,果不其然,分配内存失败,OOM:

 

三、内存优化

        首先,在总结内存优化的方法前,我们先总结一下几种内存问题的原因:

(1)对象频繁创建和销毁导致内存抖动,触发GC操作,导致卡顿甚至OOM

(2)只申请内存,没有释放内存,导致系统可用内存减少,触发GC操作,导致卡顿直至OOM

        因此,内存优化的两个方向就是:避免内存抖动和内存泄漏。那么,如何避免内存抖动和内存泄漏呢?其实,我们在上面的demo中,做了两种错误的操作。我们也可以据此给出几种避免内存抖动和内存泄漏的方法。由于,内存优化的方法太多了,而且需要根据具体的项目,我们不一一展开。在此,我总结一下我用过的以及以前从资料上查到的一些优化技巧。大家可以据此考虑一下自己的项目中,是否存在某些地方不合理。

1、Bitmap的使用

        大家可以看到,我的demo中使用的是Bitmap来演示一些错误的代码。其实,对于大多数内存泄漏问题,都跟Bitmap有关系。因为。这玩意如果使用不当,将会带来非常大的内存消耗。可能大家现在展示图片基本都使用Glide等第三方库,很少去使用BitmapFactory去解码Bitmap。但是,对于Bitmap的使用,我还是重点总结一下:

(1)使用BitmapFactory来decode一块比较大的Bitmap时,需要注意inSampleSize的计算。通常,我使用的方法是:

  • 设置inJustDecodeBounds为true,可以在不解码Bitmap的情况下获取Bitmap的实际宽高(sourceWidth,sourceHeight);
  • 选取图片显示区域的宽(objWidth)或高(objHeight)作为参考值,根据fitin算法,计算合适的inSampleSize;
  • 选取合适的解码位数,32位(ARGB8888),16位(RGB565)等,如果没有特殊需求,使用RGB565即可;
  • 根据计算得到的inSampleSize,设置inJustDecodeBounds为false,解码Bitmap。

(2)Bitmap使用完后,及时的回收,回收的方法如下:

        if (mBitmap != null && !mBitmap.isRecycled()) {
            mBitmap.recycle();
            mBitmap = null;
        }

2、static对象谨慎使用

3、cursor及时关闭

4、各种监听器的注销

5、SharedPreferences谨慎使用

6、第三方库谨慎使用

        总结一下,这篇博客从我的亲身经历出发,介绍了三个内存问题及其原因。并根据导致内存问题的几个原因,提出了一些内存优化的建议和方向。当然,我以前接触比较多的就是Bitmap,因此我花了较多的篇幅围绕Bitmap来写demo和提出建议。后面,我会逐渐的丰富和总结其他的一些内存优化注意事项。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章