Android 系统性能优化(55)---Android 性能优化之内存优化

时间:2021-07-26 03:53:11

Android 性能优化之内存优化


前言


Android App优化这个问题,我相信是Android开发者一个永恒的话题。本篇文章也不例外,也是来讲解一下Android内存优化。那么本篇文章有什么不同呢?

本篇文章主要是从最基础的Android系统内存管理方面出发再到App优化方法,让你能更加清楚地理解、处理Android内存优化问题,下面进入正题。


Android内存的管理方式


Android系统分配和回收方式


通常情况下,一个APP就是一个进程或者说是一个虚拟机。也就是说我们一个APP运行的时候那么就有一个单独的进程在运行。但是也有很多的大公司在Mainfest指定process进程名字,所以会看到一个APP对应多个进程的情况。


我们用实际的代码来演示看一下:

这是我运行的一个App的包名:


Android 系统性能优化(55)---Android 性能优化之内存优化

我们在Windows上看一下他的进程:

Android 系统性能优化(55)---Android 性能优化之内存优化

UID表示:用户

PID表示:进程ID

PPID表示:进程父ID

CMD表示:名字

可以看一下我们的App运行的进程在上面可以找到

Android 系统性能优化(55)---Android 性能优化之内存优化

进程ID:12768  进程父ID:1509

通过父ID我们可以找到:

Android 系统性能优化(55)---Android 性能优化之内存优化

我们通过这张图可以清楚的看到一个我们熟悉的名字:zygote

这个是什么呢?


zygote进程是由init进程启动起来,在Android中,zygote是整个系统创建新进程的核心进程,换句话说就是zygote进程是android的孵化进程也就是父进程。


通过命令 dumpsys meminfo + 进程名字,可以获取具体信息:


Android 系统性能优化(55)---Android 性能优化之内存优化

简单介绍下我们需要知道:

Pss Total  :  当前使用物理内存的大小

Heap Size  :  堆空间

Heap Alloc  :  分配多少堆空间

Heap Free  :空闲堆空间

一般来说:Heap Size = Heap Alloc + Heap Free


Native Heap:指的JIN开发所占的堆空间

Dalvik Heap : 虚拟机的堆空间

Dalvik Other : 虚拟机其他所占空间

           stack : 堆栈占多少


其他还有很多的有用信息,就不一一解释了,感兴趣的可以多去了解这方面的知识,我这里就主要说一下我们经常内存泄漏主要在:Pss Total 中的TOTAL不断的变大就可以看出内存泄漏


GC就是垃圾收集器,只有在Heap剩余空间不够的时候才会触发垃圾回收。


Java的垃圾回收机制就是你在开发的时候不用去关注内存是否去释放,这个是一个优点,但是也有缺点就是当前的变量不使用了,放在一边,只有当内存不够的时候才会触发GC去回收这些不使用的内存。为什么说是个缺点呢?


因为在GC触发垃圾回收的时候,所有的线程都会被暂停,此时就会我们经常出现的卡顿现象。


APP内存限制机制


首先我们要知道一个理论:每个APP分配的内存最大限制,是随着设备的不同而改变的。因此,我们才需要我们去管理我们的内存,有一点要明白的就是系统分配的内存,一般情况下是肯定够使用的,如果出现OOM这种情况,那么必定是你的APP优化的不够好。


最常说的吃内存的:

  • 高清图片,现在的手机拍照动不动就是以M为单位。但是就我们目前的开发来说,大多数人使用的是Glide、Picasso的框架,其实都是框架给我们处理了管理图片的问题。



为什么要限制内存?


假如我们每个App都不限制内存的大小,那么各自的APP都不管理,让内存一直增大,Android系统是允许多个APP同时运行的,总的空间是固定的,最终导致结果必然有些APP没有内存可以分配。



  • 切换后台是APP清理机制


    APP切换的时候采用的是LRU  Cache这种算法。


什么是LRU Cache算法


LRU Cache是一个Cache置换算法,含义是“最近最少使用”,当Cache满(没有空闲的cache块)时,把满足“最近最少使用”的数据从Cache中置换出去,并且保证Cache中第一个数据是最近刚刚访问的。由“局部性原理”,这样的数据更有可能被接下来的程序访问。


        切换到实际场景就是,我们APP切换的时候会把刚刚访问的放在第一个。当我们内存不足的时候我们就会置换出最近最少使用、或者最久未使用的。

而最近使用的APP,最不容易被清理掉。


    当我们的应用要被清理掉的时候,或者是我们的内存出现不够的时候,我们的APP中会回调一个方法

@Override
public void onTrimMemory(int level) {
   
super.onTrimMemory(level);
}


我们解释一下Level这参数的意义:


当你的app在后台时:

TRIM_MEMORY_COMPLETE :当前进程在LRU列表的尾部,如果没有足够的内存,它将很快被杀死。这时候你应该释放任何不影响app运行的资源。

TRIM_MEMORY_MODERATE :当前进程在LRU列表的中部,如果系统进一步需要内存,你的进程可能会被杀死。

TRIM_MEMORY_BACKGROUND:当前进程在LRU列表的头部,虽然你的进程不会被高优杀死,但是系统已经开始准备杀死LRU列表中的其他进程了, 因此你应该尽量的释放能够快速回复的资源,以保证当用户返回你的app时可以快速恢复。                   。


当你的app的可见性改变时:

TRIM_MEMORY_UI_HIDDEN:当前进程的界面已经不可见,这时是释放UI相关的资源的好时机。


当你的app正在运行时:

TRIM_MEMORY_RUNNING_CRITICAL:虽然你的进程不会被杀死,但是系统已经开始准备杀死其他的后台进程了,这时候你应该释放无用资源以防止性能下降。下一个阶段就是调用"onLowMemory()"来报告开始杀死后台进程了,特别是状况已经开始影响到用户。

TRIM_MEMORY_RUNNING_LOW:虽然你的进程不会被杀死,但是系统已经开始准备杀死其他的后台进程了,你应该释放不必要的资源来提供系统性能,否则会 影响用户体验。

TRIM_MEMORY_RUNNING_MODERATE:系统已经进入了低内存的状态,你的进程正在运行但是不会被杀死。


我们可以用过这个Level的参数来判断当前APP的情况,来优化内存。


  • 监控内存的几种演示方法


1.在我们的代码中动态打印出我们的内存大小


private void printMemorySize() {
       ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
       //以M为单位  当前APP最大限制内存
       int memoryClass = activityManager.getMemoryClass();
       //通过在Manifest <application>标签中largeHeap属性的值为"true"  为应用分配的最大的内存
       int largeMemoryClass = activityManager.getLargeMemoryClass();
       StringBuilder stringBuilder = new StringBuilder();
       stringBuilder.append("memoryClass===" + memoryClass+"\n")
               .append("largeMemoryClass===" + largeMemoryClass);

       Logger.e(stringBuilder.toString());


       //以M为单位输出当前可用总的Memory大小
       float totalMemory = Runtime.getRuntime().totalMemory() * 1.0f / (1014 * 1024);
       // 以M为单位输出当前空闲的Memory大小
       float freeMemory = Runtime.getRuntime().freeMemory() * 1.0f / (1024 * 1024);
       // 以M为单位输出虚拟机限制的最大内存
       float maxMemory = Runtime.getRuntime().maxMemory() * 1.0f / (1024 * 1024);

       StringBuilder builder = new StringBuilder();
       builder.append("totalMemory==" + totalMemory + "\n")
               .append("freeMemory==" + freeMemory + "\n")
               .append("maxMemory==" + maxMemory + "\n");

       Logger.e(builder.toString());
   }


Android 系统性能优化(55)---Android 性能优化之内存优化


通过上面的代码和图我们就可以清楚的知道我们的应用的内存的情况。

这种方法也是最复杂的方法,需要代码去打印,下面两种是我们的Android studio提供的工具。



2.Android studio 3.1 工具 Android profiler


Android 系统性能优化(55)---Android 性能优化之内存优化


这种方式是方便查看的,直接在下方点击 Android profiler就可以了,方便快捷



3.DDMS


打开DDMS


Android 系统性能优化(55)---Android 性能优化之内存优化


这也是看我们的APP的内存使用情况,标记了的假如你的APP在运行的时候 data object和 class object 不断的变化,那就说明你的应用可能有内存泄露了,这个时候你就需要检查一下。


Android内存优化


数据结构的优化


1.字符串拼接


我们都知道我们的如果使用string 的 “+”方式来拼接字符串,会产生字符串中间内存块,这些内存块是无用的,造成内存浪费,这种方式低效、而且耗时。我们就是实际看看:


int length = 20;
       int rawLength = 300;
       int[][] intMatrix = new int[length][rawLength];
       for (int i = 0; i < length; i++) {
           for (int j = 0; j < rawLength; j++) {
               intMatrix[i][j] = ran.nextInt();
           }
       }

初始化一个两维的矩阵,得到随机数。

/**
    * 用StringBuilder连接起来
    */

   private void strBuild() {
       StringBuilder builder = null;
       Log.e("test", "builder start:");
       for (int i = 0; i < length; i++) {
           for (int j = 0; j < rawLength; j++) {
               builder.append(intMatrix[i][j]+"").append(",");
           }
           Log.e("test", "builder:" + i);

       }
       Log.e("test", "add finish:" + builder.toString().length());
   }

   /**
    * 字符串用 “+” 连接起来
    */

   private void strAdd() {
       String str = null;

       Log.e("test", "add start:");
       for (int i = 0; i < length; i++) {
           for (int j = 0; j < rawLength; j++) {
               str = str + intMatrix[i][j];
               str = str + ",";
           }
           Log.e("test", "add:" + i);

       }
       Log.e("test", "add finish:" + str.length());
   }


得到的用 “+”链接的结果:


Android 系统性能优化(55)---Android 性能优化之内存优化


耗时2.6s


用stringBuilder的结果:


Android 系统性能优化(55)---Android 性能优化之内存优化


耗时0.06s


这就可以看出我们的String和StringBuilder的使用效率的对比了。


2.替换HashMap


还有值得一提的就是JAVA里面的HashMap,这个使用的效率是不高的,我们要用ArrayMap、SparseArray替换。


3.内存抖动


 内存都用的主要原因是我们内存变量的使用不当造成的


/**
    * 试验内存抖动
    */

   private void doChurn() {
       Log.e("test", "doChurn start: ");
       int len = 10;
       int rawLen = 450000;
       for (int i = 0; i < rawLen; i++) {
           String[] strings = new String[len];
           for (int j = 0; j < len; j++) {
               strings[j] = String.valueOf(ran.nextInt());
           }
           Log.e("test", "doChurn : " + i);
       }
       Log.e("test", "doChurn end: ");
   }


重点就是在创建string数组那里,是放在第一个for循环里面,rawLen=450000,因此会创建450000个对象。


Android 系统性能优化(55)---Android 性能优化之内存优化


这一块就是我们的内存抖动的情况。


分析一下原因:


我们在for循环里面创建了45000个string对象,然后再里面添加了数据之后就没有使用了,当创建的对象达到内存限制的时候就会触发GC回收,接下来又创建,又回收,这样就导致了内存抖动的情况。


内存的复用


  • 复用系统自带的资源


  • ListView/GridView中的ConvertView的复用,当然我们现在ListView和GridView使用已经很少了,都被RecyclerView给取代了


  • 我们在自定义View的要避免在onDraw中去创建对象,因为onDraw方法会经常执行



内存泄露


内存泄露已经是老生常谈了,但是我们还是要举一些简单的例子让大家知道怎样会造成内存泄露。


什么是内存泄露?


内存泄露:由于你代码的问题,导致某一块内存虽然已经不使用了,但是依然被其他的东西(对象或者其他)引用着,使得GC没发对它回收。


所以内存泄露会导致APP剩余可用的Heap越来越少,频繁触发GC。


1.内部内造成的内存泄露



/**
    * 创建一个线程
    */

   private class MyThread extends Thread {

       @Override
       public void run() {
           try {
               //休眠5分钟
               Thread.sleep(1000 * 60 * 5);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
   }


上面这个是一个Activity的内部内,每次启动这个activity都会开启这个线程。点击按钮开启这个Activity 触发线程休眠 5min,然后按返回键,再点击按钮开启这个Activity触发线程休眠5min,就这样依次反复操作多次。我们5min中内可以重复这样的N次操作,我们的操作会频繁的触发GC回收,但是由于我们的线程还在运行,这个内部类是默认持有外部类对象,因此这个Activity就不会被回收,就造成了内存泄露。


内部内又分很多种,静态内部类、非静态内部类、匿名内部类,这些内部类我们都应该注意不要长时间引用Activity。



2.单例造成的内存泄露


创建一个单例

Android 系统性能优化(55)---Android 性能优化之内存优化

Activity获取单例对象,并将Activity传入单例中:


Android 系统性能优化(55)---Android 性能优化之内存优化


我们假设这样一个场景,我们打开应用,然后点手机返回,等待一段时间假设10s,这样就会造成内存泄露。


为什么会造成内存泄露呢?


AppManager appManager=AppManager.getInstance(this)

这句传入的是Activity的Context,我们都知道,Activty是间接继承于Context的,当这Activity退出时,Activity应该被回收, 但是单例中又持有它的引用,导致Activity回收失败,造成内存泄漏。


像这种情况我们应该怎么避免呢?  我们将传入的this改成getApplicationContext(),因为我们Application的生命周期是和APP的生命周期一致的所以就不存在内存泄露的问题。


Android 图片优化之OOM


现在我们的APP基本上都会有图片显示,那么有图片显示必然就会出现图片的优化问题,如果处理不得当就会出现OOM。


什么是OOM?


我们程序申请需要10485776byte太大了,虚拟机无法满足我们,羞愧的shutdown自杀了


为什么会有OOM?


因为android系统的app的每个进程或者每个虚拟机有个最大内存限制,如果申请的内存资源超过这个限制,系统就会抛出OOM错误。跟整个设备的剩余内存没太大关系。比如比较早的android系统的一个虚拟机最多16M内存,当一个app启动后,虚拟机不停的申请内存资源来装载图片,当超过内存上限时就出现OOM。



这一小节说的图片优化OOM,为什么说图片会造成OOM呢?因为我们在网络请求加载图片的时候,我们要申请内存来装载图片,然后我们的一张图片原本1M,但是下载下来之后转换成Bitmap显示到我们的控件的话,那么我们的Bitmap此时的大小估计是好几M,会翻好几倍。当你下载多了,不注意回收这些Bitmap的话,就会造成OOM。


总结有一下三种情况:


  • 直接加载 超大尺寸 图片;

  • 图片加载后 未及时释放

  • 在页面中,同时加载 非常多 的图片;


解决加载图片出现OOM有几种方法:


  • 对图片进行裁剪之后再加载图片。

  • 采用LruCache来缓存图片

  • 对图片进行适当的缩小之后再加载显示



为什么这块我们没有细讲,主要是因为我们现在的图片加载主要都是使用这框架Glide、Picasso、Fresco 来加载图片,我们现在就像是傻瓜似 的操作,直接传入个Url就好了,图片的优化问题框架已经给我做的很好了,无需我们考虑那么多。如果说有必要的话,我之后可以来一篇框架的加载图片原理,源码解析,如有需要的可以在后台留言。