写在最前:

本文的思路主要借鉴了2014年AnDevCon开发人员大会的一个演讲PPT,加上把网上搜集的各种内存零散知识点进行汇总、挑选、简化后整理而成。

所以我将本文定义为一个工具类的文章。假设你在ANDROID开发中遇到关于内存问题,或者立即要參加面试,或者就是单纯的学习或复习一下内存相关知识,都欢迎阅读。(本文最后我会尽量列出所參考的文章)。




OOM:


内存泄露能够引发非常多的问题:

1.程序卡顿,响应速度慢(内存占用高时JVM虚拟机会频繁触发GC)

2.莫名消失(当你的程序所占内存越大,它在后台的时候就越可能被干掉。

反之内存占用越小,在后台存在的时间就越长)

3.直接崩溃(OutOfMemoryError)

ANDROID内存面临的问题:


1.有限的堆内存,原始仅仅有16M

2.内存大小消耗等依据设备,操作系统等级,屏幕尺寸的不同而不同

3.程序不能直接控制

4.支持后台多任务处理(multitasking)

5.执行在虚拟机之上



5R:

本文主要通过例如以下的5R方法来对ANDROID内存进行优化:


1.Reckon(计算)

首先须要知道你的app所消耗内存的情况,知己知彼才干百战不殆

2.Reduce(降低)

消耗更少的资源

3.Reuse(重用)

当第一次使用完以后,尽量给其它的使用

5.Recycle(回收)

回收资源

4.Review(检查)

回想检查你的程序,看看设计或代码有什么不合理的地方。




Reckon:


关于内存简单介绍,和Reckon(内存计算)的内容请看上一篇文章:​​ANDROID内存优化(大汇总——上)​



Reduce :


Reduce的意思就是降低。直接降低内存的使用是最有效的优化方式。

以下来看看有哪些方法能够降低内存使用:



Bitmap:


Bitmap是内存消耗大户。绝大多数的OOM崩溃都是在操作Bitmap时产生的。以下来看看几个处理图片的方法:




图片显示:

我们须要依据需求去载入图片的大小。

比如在列表中仅用于预览时载入缩略图(thumbnails )。

仅仅有当用户点击具体条目想看具体信息的时候,这时另启动一个fragment/activity/对话框等等。去显示整个图片


图片大小:





直接使用ImageView显示bitmap会占用较多资源,特别是图片较大的时候,可能导致崩溃。

 

使用BitmapFactory.Options设置inSampleSize, 这样做能够降低对系统资源的要求。 

属性值inSampleSize表示缩略图大小为原始图片大小的几分之中的一个,即假设这个值为2。则取出的缩略图的宽和高都是原始图片的1/2,图片大小就为原始大小的1/4。 

BitmapFactory.Options bitmapFactoryOptions = new BitmapFactory.Options();
bitmapFactoryOptions.inJustDecodeBounds = true;
bitmapFactoryOptions.inSampleSize = 2;
// 这里一定要将其设置回false,由于之前我们将其设置成了true
// 设置inJustDecodeBounds为true后,decodeFile并不分配空间,即,BitmapFactory解码出来的Bitmap为Null,但可计算出原始图片的长度和宽度
options.inJustDecodeBounds = false;
Bitmap bmp = BitmapFactory.decodeFile(sourceBitmap, options);



图片像素:




Android中图片有四种属性。各自是:

ALPHA_8:每一个像素占用1byte内存 

ARGB_4444:每一个像素占用2byte内存 

ARGB_8888:每一个像素占用4byte内存 (默认)

RGB_565:每一个像素占用2byte内存 




Android默认的颜色模式为ARGB_8888。这个颜色模式色彩最细腻。显示质量最高。但相同的,占用的内存也最大。

所以在对图片效果不是特别高的情况下使用RGB_565(565没有透明度属性),例如以下:



publicstaticBitmapreadBitMap(Contextcontext, intresId) {
BitmapFactory.Optionsopt = newBitmapFactory.Options();
opt.inPreferredConfig = Bitmap.Config.RGB_565;
opt.inPurgeable = true;
opt.inInputShareable = true;
//获取资源图片
InputStreamis = context.getResources().openRawResource(resId);
returnBitmapFactory.decodeStream(is, null, opt);
}





图片回收:

使用Bitmap过后,就须要及时的调用Bitmap.recycle()方法来释放Bitmap占用的内存空间。而不要等Android系统来进行释放。

以下是释放Bitmap的演示样例代码片段。


// 先推断是否已经回收
if(bitmap != null && !bitmap.isRecycled()){
// 回收而且置为null
bitmap.recycle();
bitmap = null;
}
System.gc();



捕获异常:

经过上面这些优化后还会存在报OOM的风险。所以以下须要一道最后的关卡——捕获OOM异常:

Bitmap bitmap = null;
try {
// 实例化Bitmap
bitmap = BitmapFactory.decodeFile(path);
} catch (OutOfMemoryError e) {
// 捕获OutOfMemoryError,避免直接崩溃
}
if (bitmap == null) {
// 假设实例化失败 返回默认的Bitmap对象
return defaultBitmapMap;
}





改动对象引用类型:


引用类型:

引用分为四种级别,这四种级别由高到低依次为:强引用>软引用>弱引用>虚引用。

强引用(strong reference)

如:Object object=new Object()。object就是一个强引用了。

当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠任意回收具有强引用的对象来解决内存不足问题。

软引用(SoftReference)

仅仅有内存不够时才回收,经常使用于缓存;当内存达到一个阀值,GC就会去回收它;

弱引用(WeakReference)   

弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中。一旦发现了仅仅具有弱引用的对象,无论当前内存空间足够与否。都会回收它的内存。 

虚引用(PhantomReference)   

"虚引用"顾名思义,就是形同虚设,与其它几种引用都不同。虚引用并不会决定对象的生命周期。假设一个对象仅持有虚引用,那么它就和没有不论什么引用一样。在不论什么时候都可能被垃圾回收。  


软引用和弱引用的应用实例:

注意:对于SoftReference(软引用)或者WeakReference(弱引用)的Bitmap缓存方案,如今已经不推荐使用了。自Android2.3版本号(API Level 9)開始,垃圾回收器更着重于对软/弱引用的回收。所以以下的内容能够选择忽略。

在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大并且声明周期较长的对象时候,能够尽量应用软引用和弱引用技术。

以下以使用软引用为例来具体说明(弱引用的使用方式与软引用是类似的):

如果我们的应用会用到大量的默认图片,并且这些图片非常多地方会用到。如果每次都去读取图片,因为读取文件须要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,须要的时候直接从内存中读取。可是。因为图片占用内存空间比較大,缓存非常多图片须要非常多的内存,就可能比較easy发生OutOfMemory异常。这时。我们能够考虑使用软引用技术来避免这个问题发生。

首先定义一个HashMap,保存软引用对象。

private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

再来定义一个方法,保存Bitmap的软引用到HashMap。


public void addBitmapToCache(String path) {
// 强引用的Bitmap对象
Bitmap bitmap = BitmapFactory.decodeFile(path);
// 软引用的Bitmap对象
SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
// 加入该对象到Map中使其缓存
imageCache.put(path, softBitmap);
}

获取的时候。能够通过SoftReference的get()方法得到Bitmap对象。

public Bitmap getBitmapByPath(String path) {
// 从缓存中取软引用的Bitmap对象
SoftReference<Bitmap> softBitmap = imageCache.get(path);
// 推断是否存在软引用
if (softBitmap == null) {
return null;
}
// 取出Bitmap对象。假设因为内存不足Bitmap被回收。将取得空
Bitmap bitmap = softBitmap.get();
return bitmap;
}

使用软引用以后。在OutOfMemory异常发生之前。这些缓存的图片资源的内存空间能够被释放掉的,从而避免内存达到上限。避免Crash发生。

须要注意的是,在垃圾回收器对这个Java对象回收前,SoftReference类所提供的get方法会返回Java对象的强引用,一旦垃圾线程回收该Java对象之后,get方法将返回null。所以在获取软引用对象的代码中,一定要推断是否为null。以免出现NullPointerException异常导致应用崩溃。

究竟什么时候使用软引用,什么时候使用弱引用呢?

个人觉得,假设仅仅是想避免OutOfMemory异常的发生,则能够使用软引用。假设对于应用的性能更在意,想尽快回收一些占用内存比較大的对象,则能够使用弱引用。

还有就是能够依据对象是否常常使用来推断。假设该对象可能会常常使用的,就尽量用软引用。假设该对象不被使用的可能性更大些,就能够用弱引用。

另外。和弱引用功能类似的是WeakHashMap。WeakHashMap对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的回收,回收以后,其条目从映射中有效地移除。WeakHashMap使用ReferenceQueue实现的这样的机制。

其它小tips:



对常量使用static final修饰符

让我们来看看这两段在类前面的声明:

static int intVal = 42;

static String strVal = "Hello, world!";

编译器会生成一个叫做clinit的初始化类的方法,当类第一次被使用的时候这种方法会被运行。

方法会将42赋给intVal,然后把一个指向类中常量表 的引用赋给strVal。

当以后要用到这些值的时候,会在成员变量表中查找到他们。 以下我们做些改进,使用“final”keyword:

static final int intVal = 42;

static final String strVal = "Hello, world!";

如今,类不再须要clinit方法,由于在成员变量初始化的时候。会将常量直接保存到类文件里。用到intVal的代码被直接替换成42。而使用strVal的会指向一个字符串常量,而不是使用成员变量。

将一个方法或类声明为final不会带来性能的提升。可是会帮助编译器优化代码。举例说,假设编译器知道一个getter方法不会被重载。那么编译器会对其採用内联调用。

你也能够将本地变量声明为final。相同,这也不会带来性能的提升。

使用“final”仅仅能使本地变量看起来更清晰些(可是也有些时候这是必须的。比方在使用匿名内部类的时候)。


静态方法取代虚拟方法

假设不须要訪问某对象的字段,将方法设置为静态,调用会加速15%到20%。这也是一种好的做法,由于你能够从方法声明中看出调用该方法不须要更新此对象的状态。


降低不必要的全局变量

尽量避免static成员变量引用资源耗费过多的实例,比方Context


由于Context的引用超过它本身的生命周期,会导致Context泄漏。所以尽量使用Application这样的Context类型。 你能够通过调用Context.getApplicationContext()或 Activity.getApplication()轻松得到Application对象。 




避免创建不必要的对象

最常见的样例就是当你要频繁操作一个字符串时,使用StringBuffer取代String。

对于全部全部基本类型的组合:int数组比Integer数组好。这也概括了一个基本事实,两个平行的int数组比 (int,int)对象数组性能要好非常多。

整体来说,就是避免创建短命的暂时对象。降低对象的创建就能降低垃圾收集。进而降低对用户体验的影响。


避免内部Getters/Setters

在Android中,虚方法调用的代价比直接字段訪问高昂很多。通常依据面向对象语言的实践。在公共接口中使用Getters和Setters是有道理的,但在一个字段常常被訪问的类中宜採用直接訪问。














避免使用浮点数

通常的经验是,在Android设备中,浮点数会比整型慢两倍。


使用实体类比接口好

如果你有一个HashMap对象,你能够将它声明为HashMap或者Map:

Map map1 = new HashMap();
HashMap map2 = new HashMap();


哪个更好呢?

依照传统的观点Map会更好些,由于这样你能够改变他的详细实现类。仅仅要这个类继承自Map接口。传统的观点对于传统的程序是正确的,可是它并不适合嵌入式系统。调用一个接口的引用会比调用实体类的引用多花费一倍的时间。假设HashMap全然适合你的程序。那么使用Map就没有什么价值。

假设有些地方你不能确定,先避免使用Map。剩下的交给IDE提供的重构功能好了。(当然公共API是一个例外:一个好的API经常会牺牲一些性能)


避免使用枚举

枚举变量很方便,但不幸的是它会牺牲运行的速度和并大幅添加文件体积。

使用枚举变量能够让你的API更出色,并能提供编译时的检查。所以在通常的时候你毫无疑问应该为公共API选择枚举变量。

可是当性能方面有所限制的时候。你就应该避免这样的做法了。

for循环

訪问成员变量比訪问本地变量慢得多。如以下一段代码:


for(int i =0; i < this.mCount; i++)  {}


永远不要在for的第二个条件中调用不论什么方法,如以下一段代码:


for(int i =0; i < this.getCount(); i++) {}


对上面两个样例最好改为:


int count = this.mCount; / int count = this.getCount();
for(int i =0; i < count; i++) {}

在java1.5中引入的for-each语法。编译器会将对数组的引用和数组的长度保存到本地变量中。这对訪问数组元素很好。 可是编译器还会在每次循环中产生一个额外的对本地变量的存储操作(如以下样例中的变量a)。这样会比普通循环多出4个字节。速度要略微慢一些:


for (Foo a : mArray) {
sum += a.mSplat;
}


了解并使用类库

选择Library中的代码而非自己重写。除了通常的那些原因外,考虑到系统空暇时会用汇编代码调用来替代library方法,这可能比JIT中生成的等价的最好的Java代码还要好。

当你在处理字串的时候,不要吝惜使用String.indexOf()String.lastIndexOf()等特殊实现的方法。这些方法都是使用C/C++实现的,比起Java循环快10到100倍。

System.arraycopy方法在有JIT的Nexus One上。自行编码的循环快9倍。

android.text.format包下的Formatter类,提供了IP地址转换、文件大小转换等方法;DateFormat类,提供了各种时间转换。都是很高效的方法。

TextUtils类。对于字符串处理Android为我们提供了一个简单有用的TextUtils类,假设处理比較简单的内容不用去思考正則表達式最好还是试试这个在android.text.TextUtils的类

高性能MemoryFile类。非常多人抱怨Android处理底层I/O性能不是非常理想,假设不想使用NDK则能够通过MemoryFile类实现高性能的文件读写操作。MemoryFile适用于哪些地方呢?对于I/O须要频繁操作的。主要是和外部存储相关的I/O操作。MemoryFile通过将 NAND或SD卡上的文件,分段映射到内存中进行改动处理。这样就用快速的RAM取代了ROM或SD卡,性能自然提高不少,对于Android手机而言同一时候还降低了电量消耗。该类实现的功能不是非常多,直接从Object上继承,通过JNI的方式直接在C底层运行。



Reuse:




Reuse重用,降低内存消耗的重要手段之中的一个。


核心思路就是将已经存在的内存资源又一次使用而避免去创建新的,最典型的使用就是缓存(Cache)池(Pool)




Bitmap缓存:





Bitmap缓存分为两种:

一种是内存缓存。一种是硬盘缓存。

内存缓存(LruCache):

以牺牲宝贵的应用内存为代价,内存缓存提供了高速的Bitmap訪问方式。系统提供的LruCache类是很适合用作缓存Bitmap任务的,它将近期被引用到的对象存储在一个强引用的LinkedHashMap中,而且在缓存超过了指定大小之后将近期不常使用的对象释放掉。

注意:曾经有一个很流行的内存缓存实现是SoftReference(软引用)或者WeakReference(弱引用)的Bitmap缓存方案,然而如今已经不推荐使用了。自Android2.3版本号(API Level 9)開始。垃圾回收器更着重于对软/弱引用的回收,这使得上述的方案相当无效。


硬盘缓存(DiskLruCache):

一个内存缓存对加速訪问近期浏览过的Bitmap非常有帮助。可是你不能局限于内存中的可用图片。

GridView这样有着更大的数据集的组件能够非常轻易消耗掉内存缓存。

你的应用有可能在运行其它任务(如打电话)的时候被打断,而且在后台的任务有可能被杀死或者缓存被释放。一旦用户又一次聚焦(resume)到你的应用,你得再次处理每一张图片。

在这样的情况下,硬盘缓存能够用来存储Bitmap并在图片被内存缓存释放后减小图片载入的时间(次数)。当然,从硬盘载入图片比内存要慢,而且应该在后台线程进行,由于硬盘读取的时间是不可预知的。

注意:假设訪问图片的次数很频繁,那么ContentProvider可能更适合用来存储缓存图片。比如Image Gallery这种应用程序。

很多其它关于内存缓存和硬盘缓存的内容请看Google官方教程​​https://developer.android.com/develop/index.html​





图片缓存的开源项目:


对于图片的缓存如今都倾向于使用开源项目,这里我列出几个我搜到的:





1. Android-Universal-Image-Loader 图片缓存

眼下使用最广泛的图片缓存。支持主流图片缓存的绝大多数特性。

项目地址:https://github.com/nostra13/Android-Universal-Image-Loader

 

2. picasso square开源的图片缓存

项目地址:https://github.com/square/picasso

特点:(1)能够自己主动检測adapter的重用并取消之前的下载

(2)图片变换

(3)能够载入本地资源

(4)能够设置占位资源

(5)支持debug模式

 

3. ImageCache 图片缓存。包括内存和Sdcard缓存

项目地址:https://github.com/Trinea/AndroidCommon

特点:

(1)支持预取新图片,支持等待队列

(2)包括二级缓存。可自己定义文件名称保存规则

(3)可选择多种缓存算法(FIFO、LIFO、LRU、MRU、LFU、MFU等13种)或自己定义缓存算法

(4)可方便的保存及初始化恢复数据

(5)支持不同类型网络处理

(6)可依据系统配置初始化缓存等




4. Android 网络通信框架Volley


项目地址:https://android.googlesource.com/platform/frameworks/volley


我们在程序中须要和网络通信的时候。大体使用的东西莫过于AsyncTaskLoader。HttpURLConnection,AsyncTask,HTTPClient(Apache)等。在2013年的Google I/O公布了Volley。Volley是Android平台上的网络通信库。能使网络通信更快,更简单,更健壮。





特点:


(1)JSON,图像等的异步下载;


(2)网络请求的排序(scheduling)


(3)网络请求的优先级处理


(4)缓存


(5)多级别取消请求


(6)和Activity和生命周期的联动(Activity结束时同一时候取消全部网络请求)





Adapter适配器




在Android中Adapter使用十分广泛,特别是在list中。所以adapter是数据的 “集散地” ,所以对其进行内存优化是非常有必要的。


以下算是一个标准的使用模版:


主要使用convertView和ViewHolder来进行缓存处理




@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder vHolder = null;
//假设convertView对象为空则创建新对象,不为空则复用
if (convertView == null) {
convertView = inflater.inflate(..., null);
// 创建 ViewHodler 对象
vHolder = new ViewHolder();
vHolder.img= (ImageView) convertView.findViewById(...);
vHolder.tv= (TextView) convertView.findViewById(...);
// 将ViewHodler保存到Tag中(Tag能够接收Object类型对象。所以不论什么东西都能够保存在当中)
convertView.setTag(vHolder);
} else {
//当convertView不为空时。通过getTag()得到View
vHolder = (ViewHolder) convertView.getTag();
}
// 给对象赋值。改动显示的值
vHolder.img.setImageBitmap(...);
vHolder.tv.setText(...);
return convertView;
}
//将显示的View 包装成类
static class ViewHolder {
TextView tv;
ImageView img;
}








池(PooL)




对象池:

对象池使用的基本思路是:将用过的对象保存起来,等下一次须要这样的对象的时候。再拿出来反复使用,从而在一定程度上降低频繁创建对象所造成的开销。 并不是全部对象都适合拿来池化――由于维护对象池也要造成一定开销。

对生成时开销不大的对象进行池化,反而可能会出现“维护对象池的开销”大于“生成新对象的开销”,从而使性能降低的情况。

可是对于生成时开销可观的对象,池化技术就是提高性能的有效策略了。




线程池:



线程池的基本思想还是一种对象池的思想。开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程运行调度由池管理器来处理。当有线程任务时,从池中取一个,运行完毕后线程对象归池,这样能够避免重复创建线程对象所带来的性能开销。节省了系统的资源。


比方:一个应用要和网络打交道。有非常多步骤须要訪问网络,为了不堵塞主线程,每一个步骤都创建个线程,在线程中和网络交互,用线程池就变的简单,线程池是对线程的一种封装,让线程用起来更加简便,仅仅须要创一个线程池,把这些步骤像任务一样放进线程池,在程序销毁时仅仅要调用线程池的销毁函数就可以。

java提供了ExecutorServiceExecutors类,我们能够应用它去建立线程池。

通常能够建立例如以下4种:


/** 每次仅仅运行一个任务的线程池 */
ExecutorService singleTaskExecutor = Executors.newSingleThreadExecutor();

/** 每次运行限定个数个任务的线程池 */
ExecutorService limitedTaskExecutor = Executors.newFixedThreadPool(3);

/** 全部任务都一次性開始的线程池 */
ExecutorService allTaskExecutor = Executors.newCachedThreadPool();

/** 创建一个可在指定时间里运行任务的线程池,亦可反复运行 */
ExecutorService scheduledTaskExecutor = Executors.newScheduledThreadPool(3);



很多其它关于线程池的内容我推荐这篇文章:http://www.xuanyusong.com/archives/2439




注意:



要依据情况适度使用缓存,由于内存有限。


能保存路径地址的就不要存放图片数据。不常常使用的尽量不要缓存。不用时就清空。







写在最后:


我准备将文章分为上、中、下三部分。如今已经所有完毕:

内存简单介绍。Recoken(计算)请看ANDROID内存优化(大汇总——上)

Reduce(降低),Reuse(重用) 请看:ANDROID内存优化(大汇总——中)

Recycle(回收), Review(检查) 请看:ANDROID内存优化(大汇总——全)


写这篇文章的目的就是想弄一个大汇总,将零散的内存知识点总结一下,假设有错误、不足或建议都希望告诉我。


參考文章:

解析Android开发优化之:软引用与弱引用的应用(http://www.jb51.net/article/36627.htm)

205520/archive/2013/02/17/2914190.html)

Android研究院之应用开发线程池的经典使用(http://www.xuanyusong.com/archives/2439)