Android App优化这个问题,我相信是Android开发者一个永恒的话题。本篇文章也不例外,也是来说解一下Android内存优化。那么本篇文章有什么不一样呢? 本篇文章主要是从最基础的Android系统内存管理方面出发再到App优化方法,让你能更加清楚地理解、处理Android内存优化问题,下面进入正题。android
一般状况下,一个APP就是一个进程或者说是一个虚拟机。也就是说咱们一个APP运行的时候那么就有一个单独的进程在运行。可是也有不少的大公司在Mainfest指定process进程名字,因此会看到一个APP对应多个进程的状况。算法
咱们用实际的代码来演示看一下: 这是我运行的一个App的包名:数组
咱们在Windows上看一下他的进程:缓存
zygote进程是由init进程启动起来,在Android中,zygote是整个系统建立新进程的核心进程,换句话说就是zygote进程是android的孵化进程也就是父进程。bash
经过命令 dumpsys meminfo + 进程名字,能够获取具体信息:网络
Pss Total : 当前使用物理内存的大小数据结构
Heap Size : 堆空间app
Heap Alloc : 分配多少堆空间框架
Heap Free :空闲堆空间ide
通常来讲:Heap Size = Heap Alloc + Heap Free
Native Heap:指的JIN开发所占的堆空间 Dalvik Heap : 虚拟机的堆空间 Dalvik Other : 虚拟机其余所占空间 stack : 堆栈占多少
其余还有不少的有用信息,就不一一解释了,感兴趣的能够多去了解这方面的知识,我这里就主要说一下咱们常常内存泄漏主要在:Pss Total 中的TOTAL不断的变大就能够看出内存泄漏
GC就是垃圾收集器,只有在Heap剩余空间不够的时候才会触发垃圾回收。
Java的垃圾回收机制就是你在开发的时候不用去关注内存是否去释放,这个是一个优势,可是也有缺点就是当前的变量不使用了,放在一边,只有当内存不够的时候才会触发GC去回收这些不使用的内存。为何说是个缺点呢?
**由于在GC触发垃圾回收的时候,全部的线程都会被暂停,此时就会咱们常常出现的卡顿现象。
首先咱们要知道一个理论:每一个APP分配的内存最大限制,是随着设备的不一样而改变的。所以,咱们才须要咱们去管理咱们的内存,有一点要明白的就是系统分配的内存,通常状况下是确定够使用的,若是出现OOM这种状况,那么一定是你的APP优化的不够好。
最常说的吃内存的: 高清图片,如今的手机拍照动不动就是以M为单位。可是就咱们目前的开发来讲,大多数人使用的是Glide、Picasso的框架,其实都是框架给咱们处理了管理图片的问题。
为何要限制内存?
假如咱们每一个App都不限制内存的大小,那么各自的APP都无论理,让内存一直增大,Android系统是容许多个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这参数的意义:
1.当你的app在后台时:
TRIM_MEMORY_COMPLETE :当前进程在LRU列表的尾部,若是没有足够的内存,它将很快被杀死。这时候你应该释听任何不影响app运行的资源。
TRIM_MEMORY_MODERATE :当前进程在LRU列表的中部,若是系统进一步须要内存,你的进程可能会被杀死。
TRIM_MEMORY_BACKGROUND:当前进程在LRU列表的头部,虽然你的进程不会被高优杀死,可是系统已经开始准备杀死LRU列表中的其余进程了, 所以你应该尽可能的释放可以快速回复的资源,以保证当用户返回你的app时能够快速恢复。 。
2.当你的app的可见性改变时:
TRIM_MEMORY_UI_HIDDEN:当前进程的界面已经不可见,这时是释放UI相关的资源的好时机。
3.当你的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());
}
复制代码
2.Android studio 3.1 工具 Android profiler
这里写图片描述
这种方式是方便查看的,直接在下方点击 Android profiler就能够了,方便快捷
3.DDMS
打开DDMS
这也是看咱们的APP的内存使用状况,标记了的假如你的APP在运行的时候 data object和 class object 不断的变化,那就说明你的应用可能有内存泄露了,这个时候你就须要检查一下。
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());
}
复制代码
获得的用 “+”连接的结果:
耗时2.6s
用stringBuilder的结果:
耗时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个对象。
这一块就是咱们的内存抖动的状况。
分析一下缘由:
咱们在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.单例形成的内存泄露
建立一个单例
Activity获取单例对象,并将Activity传入单例中:
咱们假设这样一个场景,咱们打开应用,而后点手机返回,等待一段时间假设10s,这样就会形成内存泄露。
为何会形成内存泄露呢?
AppManager appManager=AppManager.getInstance(this) 这句传入的是Activity的Context,咱们都知道,Activty是间接继承于Context的,当这Activity退出时,Activity应该被回收, 可是单例中又持有它的引用,致使Activity回收失败,形成内存泄漏。 像这种状况咱们应该怎么避免呢? 咱们将传入的this改为getApplicationContext(),由于咱们Application的生命周期是和APP的生命周期一直的因此就不存在内存泄露的问题。
如今咱们的APP基本上都会有图片显示,那么有图片显示必然就会出现图片的优化问题,若是处理不得当就会出现OOM。
1.什么是OOM?
咱们程序申请须要10485776byte太大了,虚拟机没法知足咱们,羞愧的shutdown自杀了
2.为何会有OOM?
由于android系统的app的每一个进程或者每一个虚拟机有个最大内存限制,若是申请的内存资源超过这个限制,系统就会抛出OOM错误。跟整个设备的剩余内存没太大关系。好比比较早的android系统的一个虚拟机最多16M内存,当一个app启动后,虚拟机不停的申请内存资源来装载图片,当超过内存上限时就出现OOM。 这一小节说的图片优化OOM,为何说图片会形成OOM呢?由于咱们在网络请求加载图片的时候,咱们要申请内存来装载图片,而后咱们的一张图片本来1M,可是下载下来以后转换成Bitmap显示到咱们的控件的话,那么咱们的Bitmap此时的大小估计是好几M,会翻好几倍。当你下载多了,不注意回收这些Bitmap的话,就会形成OOM。
总结有一下三种状况:
解决加载图片出现OOM有几种方法:
为何这块咱们没有细讲,主要是由于咱们如今的图片加载主要都是使用这框架Glide、Picasso、Fresco 来加载图片,咱们如今就像是傻瓜似的操做,直接传入个Url就行了,图片的优化问题框架已经给我作的很好了,无需咱们考虑那么多。若是说有必要的话,我以后能够来一篇框架的加载图片原理,源码解析,若有须要的能够在后台留言。
原创不易,若是以为写得好,扫码关注一下点个赞,是我最大的动力。
关注我,必定会有意想不到的东西等你: 天天专一分享Android、JAVA干货
备注:程序圈LT