android内存优化

写在最前:html

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

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


OOM:git


内存泄露能够引起不少的问题:github

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。 

[java] view plaincopyprint?

  1. BitmapFactory.Options bitmapFactoryOptions = new BitmapFactory.Options();  

  2. bitmapFactoryOptions.inJustDecodeBounds = true;  

  3. bitmapFactoryOptions.inSampleSize = 2;  

  4. // 这里必定要将其设置回false,由于以前咱们将其设置成了true    

  5. // 设置inJustDecodeBounds为true后,decodeFile并不分配空间,即,BitmapFactory解码出来的Bitmap为Null,但可计算出原始图片的长度和宽度    

  6. options.inJustDecodeBounds = false;  

  7. 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没有透明度属性),以下:

[java] view plaincopyprint?

  1. publicstaticBitmapreadBitMap(Contextcontext, intresId) {  

  2.     BitmapFactory.Optionsopt = newBitmapFactory.Options();  

  3.     opt.inPreferredConfig = Bitmap.Config.RGB_565;  

  4.     opt.inPurgeable = true;  

  5.     opt.inInputShareable = true;  

  6.     //获取资源图片   

  7.     InputStreamis = context.getResources().openRawResource(resId);  

  8.     returnBitmapFactory.decodeStream(is, null, opt);  

  9. }  


图片回收:

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

下面是释放Bitmap的示例代码片断。

[java] view plaincopyprint?

  1. // 先判断是否已经回收  

  2. if(bitmap != null && !bitmap.isRecycled()){  

  3.     // 回收而且置为null  

  4.     bitmap.recycle();  

  5.     bitmap = null;  

  6. }  

  7. System.gc();  


捕获异常:

通过上面这些优化后还会存在报OOM的风险,因此下面须要一道最后的关卡——捕获OOM异常:

[java] view plaincopyprint?

  1. Bitmap bitmap = null;  

  2. try {  

  3.     // 实例化Bitmap  

  4.     bitmap = BitmapFactory.decodeFile(path);  

  5. catch (OutOfMemoryError e) {  

  6.     // 捕获OutOfMemoryError,避免直接崩溃  

  7. }  

  8. if (bitmap == null) {  

  9.     // 若是实例化失败 返回默认的Bitmap对象  

  10.     return defaultBitmapMap;  

  11. }  



修改对象引用类型:


引用类型:

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

强引用(strong reference)
如:Object object=new Object(),object就是一个强引用了。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具备强引用的对象来解决内存不足问题。

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

弱引用(WeakReference)   

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


虚引用(PhantomReference)   

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


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

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

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

下面以使用软引用为例来详细说明(弱引用的使用方式与软引用是相似的):

假设咱们的应用会用到大量的默认图片,并且这些图片不少地方会用到。若是每次都去读取图片,因为读取文件须要硬件操做,速度较慢,会致使性能较低。因此咱们考虑将图片缓存起来,须要的时候直接从内存中读取。可是,因为图片占用内存空间比较大,缓存不少图片须要不少的内存,就可能比较容易发生OutOfMemory异常。这时,咱们能够考虑使用软引用技术来避免这个问题发生。

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

[java] view plaincopyprint?

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

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

[java] view plaincopyprint?

  1. public void addBitmapToCache(String path) {  

  2.        // 强引用的Bitmap对象  

  3.        Bitmap bitmap = BitmapFactory.decodeFile(path);  

  4.        // 软引用的Bitmap对象  

  5.        SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);  

  6.        // 添加该对象到Map中使其缓存  

  7.        imageCache.put(path, softBitmap);  

  8.    }  

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

[java] view plaincopyprint?

  1. public Bitmap getBitmapByPath(String path) {  

  2.         // 从缓存中取软引用的Bitmap对象  

  3.         SoftReference<Bitmap> softBitmap = imageCache.get(path);  

  4.         // 判断是否存在软引用  

  5.         if (softBitmap == null) {  

  6.             return null;  

  7.         }  

  8.         // 取出Bitmap对象,若是因为内存不足Bitmap被回收,将取得空  

  9.         Bitmap bitmap = softBitmap.get();  

  10.         return bitmap;  

  11.     }  

使用软引用之后,在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”关键字:

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循环

访问成员变量比访问本地变量慢得多,以下面一段代码:

[java] view plaincopyprint?

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

永远不要在for的第二个条件中调用任何方法,以下面一段代码:

[java] view plaincopyprint?

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

对上面两个例子最好改成:

[java] view plaincopyprint?

  1. int count = this.mCount; / int count = this.getCount();  

  2. for(int i =0; i < count; i++)  {}  

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

[java] view plaincopyprint?

  1. for (Foo a : mArray) {  

  2.     sum += a.mSplat;  

  3. }  


了解并使用类库

选择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来进行缓存处理

[java] view plaincopyprint?

  1. @Override  

  2. public View getView(int position, View convertView, ViewGroup parent) {  

  3.     ViewHolder vHolder = null;  

  4.     //若是convertView对象为空则建立新对象,不为空则复用    

  5.     if (convertView == null) {  

  6.         convertView = inflater.inflate(..., null);  

  7.         // 建立 ViewHodler 对象    

  8.         vHolder = new ViewHolder();  

  9.         vHolder.img= (ImageView) convertView.findViewById(...);  

  10.         vHolder.tv= (TextView) convertView.findViewById(...);  

  11.         // 将ViewHodler保存到Tag中(Tag能够接收Object类型对象,因此任何东西均可以保存在其中)  

  12.         convertView.setTag(vHolder);  

  13.     } else {  

  14.         //当convertView不为空时,经过getTag()获得View    

  15.         vHolder = (ViewHolder) convertView.getTag();  

  16.     }  

  17.     // 给对象赋值,修改显示的值    

  18.     vHolder.img.setImageBitmap(...);  

  19.     vHolder.tv.setText(...);  

  20.     return convertView;  

  21. }  

  22. //将显示的View 包装成类    

  23. static class ViewHolder {  

  24.     TextView tv;  

  25.     ImageView img;  

  26. }  


池(PooL)

对象池:

对象池使用的基本思路是:将用过的对象保存起来,等下一次须要这种对象的时候,再拿出来重复使用,从而在必定程度上减小频繁建立对象所形成的开销。 并不是全部对象都适合拿来池化――由于维护对象池也要形成必定开销。对生成时开销不大的对象进行池化,反而可能会出现“维护对象池的开销”大于“生成新对象的开销”,从而使性能下降的状况。可是对于生成时开销可观的对象,池化技术就是提升性能的有效策略了。


线程池:

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

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


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

一般能够创建以下4种:

[java] view plaincopyprint?

  1. /** 每次只执行一个任务的线程池 */  

  2. ExecutorService singleTaskExecutor =  Executors.newSingleThreadExecutor();  

  3.   

  4. /** 每次执行限定个数个任务的线程池 */  

  5. ExecutorService limitedTaskExecutor = Executors.newFixedThreadPool(3);  

  6.   

  7. /** 全部任务都一次性开始的线程池 */  

  8. ExecutorService allTaskExecutor = Executors.newCachedThreadPool();  

  9.   

  10. /** 建立一个可在指定时间里执行任务的线程池,亦可重复执行 */  

  11. ExecutorService scheduledTaskExecutor = Executors.newScheduledThreadPool(3);  


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

注意:

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

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

相关文章
相关标签/搜索