Android内存优化

Android内存优化

基于项目经验和其他文档参考 Author: laoXu


内存简介:

RAM(random access memory)随机存取存储器。其实就是内存。

在Java的内存分配时会涉及到以下区域:

寄存器(Registers):速度最快的存储场所,因为寄存器位于处理器内部,我们在程序中无法控制。

栈(Stack):存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中。

堆(Heap):堆内存用来存放由new创建的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收器(GC)来管理。

静态域(static field):静态存储区域就是指在固定的位置存放应用程序运行时一直存在的数据,Java在内存中专门划分了一个静态存储区域来管理一些特殊的数据变量如静态的数据变量。

常量池(constant pool):虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和floating point常量)和对其他类型,字段和方法的符号引用。

非RAM存储:硬盘等永久存储空间。

堆栈特点对比:

简单的介绍一下堆和栈的特点:

栈:当定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

堆:当堆中的new产生数组和对象超出其作用域后,它们不会被释放,只有在没有引用变量指向它们的时候才变成垃圾,不能再被使用。即使这样,所占内存也不会立即释放,而是等待被垃圾回收器收走。这也是Java比较占内存的原因。

栈:存取速度比堆要快,仅次于寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

堆:堆是一个运行时数据区,可以动态地分配内存大小,因此存取速度较慢。也正因为这个特点,堆的生存期不必事先告诉编译器,而且Java的垃圾收集器会自动收走这些不再使用的数据。

栈:栈中的数据可以共享, 它是由编译器完成的,有利于节省空间。

例如:需要定义两个变量int a = 3;int b = 3;

编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b = 3;在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况。这时,如果再让a=4;那么编译器会重新搜索栈中是否有4值,如果没有,则将4存放进来,并让a指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。

堆:例如上面栈中a的修改并不会影响到b, 而在堆中一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。

java中的四种引用类型

java引用类型不是指像int、char等这些基本的数据类型。java中的引用类型有四种:强引用、软引用、弱引用、虚引用。这四种引用类型,它们关于对象的可及性是由强到弱的。

1 强引用

最常见的强引用方式如下:

//强引用  对象类型 对象名 = new 对象构造方法();

//比如下列代码

String str = new String("abc");

在上述代码中,这个str对象就是强可及对象。强可及对象永远不会被GC回收。它宁愿被抛出OOM异常,也不会回收掉强可及对象。

清除强引用对象中的引用链如下:

String str = new String("abc");

//置空

str = null;

2 软应用

软引用方式如下:

//软引用SoftReference

SoftReference softReference = new SoftReference(str);

在上述代码中,这个str对象就是软可及对象。当系统内存不足时,软可及对象会被GC回收。

清除软引用对象中的引用链可以通过模拟系统内存不足来清除,也可以手动清除,手动清除如下:

SoftReference softReference = new SoftReference(str);

softReference.clear();

3 弱引用

弱引用方式如下:

//弱引用WeakReference

WeakReference weakReference = new WeakReference

4 虚引用

虚引用方式如下:

//虚引用PhantomReference

PhantomReference phantomReference = new PhantomReference

5 补充

  • 一个对象的可及性由最强的那个来决定。

  • System.gc()方法只会回收堆内存中存放的对象。

String str = "abc";

//弱引用WeakReference

WeakReference weakReference = new WeakReference

内存泄露的原因

对无用对象的引用一直未被释放,就会导致内存泄露。如果对象已经用不到了,但是因为疏忽,导致代码中对该无用对象的引用一直没有被清除掉,就会造成内存泄露。

比如你按back键关掉了一个Activity,那么这个Activity页面就暂时没用了。但是某个后台任务如果一直持有着对该Activity对象的引用,这个时候就会导致内存泄露。

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(重用):当第一次使用完以后,尽量给其他的使用

  4. Recycle(回收):返回资源

  5. Review(检查):回顾检查你的程序,看看设计或代码有什么不合理的地方。


Reckon (计算):

了解自己应用的内存使用情况是很有必要的。如果当内存使用过高的话就需要对其进行优化,因为更少的使用内存可以减少android系统终止我们的进程的几率,也可以提高多任务执行效率和体验效果。

我们在开发过程中或者在项目后期都是需要对项目进行优化处理的,如何优化、优化哪些地方就变得很重要了,接下来就介绍一下Android中的一些常用工具:

LeakCanary

在全球最大的开源项目网站github中,有一个非常流行的开源项目LeakCanary,它能很方便的检测到当前开发的java项目中是否存在内存泄露。它能够帮住我们分析项目中内存泄露的情况和对应的位置,使用起来也是非常的简单易用。

LeakCanary的原理简介

Android内存优化

LeakCanary 的机制如下:

  1. RefWatcher.watch()会以监控对象来创建一个KeyedWeakReference弱引用对象

  2. AndroidWatchExecutor的后台线程里,来检查弱引用已经被清除了,如果没被清除,则执行一次GC

  3. 如果弱引用对象仍然没有被清除,说明内存泄漏了,系统就导出hprof文件,保存在app的文件系统目录下

  4. HeapAnalyzerService启动一个单独的进程,使用HeapAnalyzer来分析hprof文件。它使用另外一个开源库 HAHA

  5. HeapAnalyzer通过查找KeyedWeakReference弱引用对象来查找内在泄漏

  6. HeapAnalyzer计算KeyedWeakReference所引用对象的最短强引用路径,来分析内存泄漏,并且构建出对象引用链出来。

  7. 内存泄漏信息送回给DisplayLeakService,它是运行在app进程里的一个服务。然后在设备通知栏显示内存泄漏信息。

Android Studio内存分析工具

首先在Android Studio中提供了很好的内存分析工具。这个工具就是heap工具,heap工具主要是用来检测堆内存的分配情况的。它可以导出一个hprof文件,这个是手机某个时间段的内存镜像,通过分析该文件,就可以得知堆内存的分配情况。heap工具位于Android Device Monitor中(这个Android Device Monitor在Eclipse中即DDMS界面)。

Android Studio中,在你要分析的应用已执行的前提下,通过在Tools→Android→Android Device Monitor中打开。如下图:

Android内存优化

Android内存优化

打开后,操作步骤顺序如下:

  1. 在Android Device Monitor界面中选择你要分析的应用程序的包名;

  2. 点击Update Heap来更新统计信息;

  3. 然后点击Cause GC即可查看当前堆的使用情况;

  4. 点击Dump HPROF file,准备生成hprof文件。

第4步点击后,几秒钟内会出现一个窗口提示你去保存文件,(这个应用当前的内存信息会被保存在这个hprof文件中),将文件保存在顺手的地方(比如说桌面)即可。

分析hprof文件(AS、eclipse插件MAT)

1.1通过Android Studio打开hprof文件

拿到一个hprof文件后,可以直接通过Android Studio来打开。只需将该文件拖放到Android Studio中,就打开了。

打开后选择Package Tree View,内存使用情况就是以包名分类。如下图 :

Android内存优化

在界面中找到你的应用程序的包名,打开即可看到内存的使用情况。自己写的类一目了然,我们还可以借助右侧的Analyzer Task去做一些分析,比如说分析出存在泄漏的leakActivity,如下图所示:

Android内存优化

1.2通过MAT打开hprof文件

MAT工具(Memory Analysis Tools)其实是Eclipse时代的产物,它也是用来分析 hprof 文件的,不过LeakCanary可以帮助我们自动分析,而使用MAT需要我们自己去导出并且分析hprof 文件,使用的过程有些麻烦。

首先在eclipse上安装MAT插件:

  1. 去官方网站http://www.eclipse.org/mat/downloads.php,查看最新版本的地址,当前最新地址如下:

  2. 打开eclipse,Help->Install New SoftWare, 输入上面的update site,如图:

Android内存优化

安装完成后提示重启Eclipse,重启后打开window→ open perspective,看到Memory Analysis证明安装成功。

Android内存优化

为了节省大家的时间,建议直接下载它的Stand-alone版本,免安装。

Android内存优化

MAT的使用步骤如下:

  1. 首先需要Dump出一份hprof文件,可以在android studio或者eclipse导出。方法在上一节已介绍过(heap的介绍中)

  2. 直接Dump出的hprof文件要经过转换才能被 MAT识别,Android SDK提供了这个工具 hprof-conv, 在SDK目录下(sdk/platform-tools)。Android内存优化该工具需要通过命令行来进行转换。以下为参考转换流程。

    2.1) 将导出来的hprof文件放到此目录下(sdk/platform-tools),重命令为input.hprof。

    2.2) 命令行cd 到此目录下,然后输入命令:hprof-conv input.hprof out.hprof(PS:注意空格,前一段的hprof-conv代表要执行hprof-conv工具;中间的input.hprof代表你想对这个叫input.hprof的文件进行转换;最后那段out.hprof代表你转换出来的结果文件名叫做out.hprof)

    2.3) 执行后,此目录下(sdk/platform-tools)将会出现一个新文件名为:out.hprof,它即是我们的转换后的结果文件。

  3. 打开MAT,导入我们的转换后的hprof文件(最好先将out.hprof放到一个独立的文件夹中,因为导入时MAT会在当前文件夹生成很多解析文件出来),导入完成后,先弹出如下start wizard 对话框,默认选第一个“leak suspect report”我们直接点finish。成功打开后如下图所示:

Android内存优化

  1. 在OverView页项下面,点击 Actions下的 Histogram!Android内存优化

  2. 将得到 Histogram结果,它按类名将所有的实例对象列出来。!Android内存优化

  3. 在第一行的正则表达式中输入我们demo里面的类名MainActivity!Android内存优化

  4. 选中匹配出的结果右键打开菜单选择 list objects->with incoming refs。!Android内存优化

  5. 得到该类的实例,以及展开后可以看到它的引用路径!Android内存优化Android内存优化

  6. 快速找出MainActivity实例没被释放的原因,可以右健 Path to GCRoots–>exclue all phantom/weak/soft etc. reference!Android内存优化

  7. 可以看到,MainActivity在AsyncTask中引用,没有被释放。!Android内存优化


Reckon (减少):

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

下面来看看有哪些方法可以减少内存使用:

Bitmap:

Bitmap是内存消耗大户,绝大多数的OOM崩溃都是在操作Bitmap时产生的,它在使用时会花掉较多的内存。那我们就可以考虑在应用bitmap时减少某些不必要内存的使用。

图片显示:

我们需要根据需求去加载图片的大小。

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

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

图片大小:

  • 边界压缩:一张拍出来的图片分辨率可能会很大,如果不做压缩去展示的话,会消耗大量内存,可能造成OOM,通过BitmapFactory.Options去设置inSampleSize,可以对图片进行边界的压缩,减少内存开销。(做法:先设置BitmapFactory.inJustDecodeBounds为true,然后decodeFile,这样将会只去解析图片大小等信息,避免了将原图加载进内存。拿到原图尺寸信息后,根据业务逻辑换算比例,设置inSampleSize,接着设置BitmapFactory.inJustDecodeBounds为false,最后再去decodeFile,从而实现对图片边界大小进行了压缩再展示。)
    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内存优化

图片像素:

Android中图片有四种属性,分别是:

ALPHA_8:每个像素占用1byte内存 (Alpha由8位组成,代表8位Alpha位图)

>

ARGB_4444:每个像素占用2byte内存 (由4个4位组成即16位,代表16位ARGB位图)

>

ARGB_8888:每个像素占用4byte内存 (默认)(由4个8位组成即32位,代表32位ARGB位图,图片质量最佳)

>

RGB_565:每个像素占用2byte内存 (R为5位,G为6位,B为5位,共16位,它是没有透明度的)

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应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。

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

假设我们的应用会用到大量的默认图片,而且这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生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”关键字:

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 

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

  1. picasso square开源的图片缓存

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

特点:(1)可以自动检测adapter的重用并取消之前的下载

(2)图片变换

(3)可以加载本地资源

(4)可以设置占位资源

(5)支持debug模式

  1. ImageCache 图片缓存,包含内存和Sdcard缓存

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

特点:

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

(2)包含二级缓存,可自定义文件名保存规则

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

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

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

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

  1. 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;  

}  

@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提供了ExecutorService和Executors类,我们可以应用它去建立线程池。

通常可以建立如下4种:

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

ExecutorService singleTaskExecutor =  Executors.newSingleThreadExecutor();  



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

ExecutorService limitedTaskExecutor = Executors.newFixedThreadPool(3);  



/** 所有任务都一次性开始的线程池 */  

ExecutorService allTaskExecutor = Executors.newCachedThreadPool();  



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

ExecutorService scheduledTaskExecutor = Executors.newScheduledThreadPool(3);  

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

ExecutorService singleTaskExecutor =  Executors.newSingleThreadExecutor();  



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

ExecutorService limitedTaskExecutor = Executors.newFixedThreadPool(3);  



/** 所有任务都一次性开始的线程池 */  

ExecutorService allTaskExecutor = Executors.newCachedThreadPool();  



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

ExecutorService scheduledTaskExecutor = Executors.newScheduledThreadPool(3); 

注意:

要根据情况适度使用缓存,因为内存有限。

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


Recycle(回收):

Recycle(回收),回收可以说是在内存使用中最重要的部分。因为内存空间有限,无论你如何优化,如何节省内存总有用完的时候。而回收的意义就在于去清理和释放那些已经闲置,废弃不再使用的内存资源和内存空间。

因为在Java中有垃圾回收(GC)机制,所以我们平时都不会太关注它,下面就来简单的介绍一下回收机制:

垃圾回收(GC):

Java垃圾回收器:

在C,C++或其他程序设计语言中,资源或内存都必须由程序员自行声明产生和回收,否则其中的资源将消耗,造成资源的浪费甚至崩溃。但手工回收内存往往是一项复杂而艰巨的工作。

于是,Java技术提供了一个系统级的线程,即垃圾收集器线程(Garbage Collection Thread),来跟踪每一块分配出去的内存空间,当Java 虚拟机(Java Virtual Machine)处于空闲循环时,垃圾收集器线程会自动检查每一快分配出去的内存空间,然后自动回收每一快可以回收的无用的内存块。

作用:

1.清除不用的对象来释放内存:

采用一种动态存储管理技术,它自动地释放不再被程序引用的对象,按照特定的垃圾收集算法来实现资源自动回收的功能。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用。

2.消除堆内存空间的碎片:

由于创建对象和垃圾收集器释放丢弃对象所占的内存空间,内存会出现碎片。碎片是分配给对象的内存块之间的空闲内存洞。碎片整理将所占用的堆内存移到堆的一端,JVM将整理出的内存分配给新的对象。

垃圾回收器优点:

1.减轻编程的负担,提高效率:

使程序员从手工回收内存空间的繁重工作中解脱了出来,因为在没有垃圾收集机制的时候,可能要花许多时间来解决一个难懂的存储器问题。在用Java语言编程的时候,靠垃圾收集机制可大大缩短时间。

2.它保护程序的完整性:

因此垃圾收集是Java语言安全性策略的一个重要部份。

垃圾回收器缺点:

1.占用资源时间:

Java虚拟机必须追踪运行程序中有用的对象, 而且最终释放没用的对象。这一个过程需要花费处理器的时间。

2.不可预知:

垃圾收集器线程虽然是作为低优先级的线程运行,但在系统可用内存量过低的时候,它可能会突发地执行来挽救内存资源。当然其执行与否也是不可预知的。

3.不确定性:

不能保证一个无用的对象一定会被垃圾收集器收集,也不能保证垃圾收集器在一段Java语言代码中一定会执行。

同样也没有办法预知在一组均符合垃圾收集器收集标准的对象中,哪一个会被首先收集。

4.不可操作

垃圾收集器不可以被强制执行,但程序员可以通过调用System. gc方法来建议执行垃圾收集器。

垃圾回收算法:

1.引用计数(Reference Counting)

比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。

2.标记-清除(Mark-Sweep)

此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。

3.复制(Copying)

此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不过出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。

4.标记-整理(Mark-Compact)

此算法结合了 “标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象 “压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

5.增量收集(Incremental Collecting)

实施垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么原因JDK5.0中的收集器没有使用这种算法的。

6.分代(Generational Collecting)

基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。

finalize():

每一个对象都有一个finalize方法,这个方法是从Object类继承来的

当垃圾回收确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。

Java 技术允许使用finalize方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。一旦垃圾回收器准备好释放对象占用的空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。

简单的说finalize方法是在垃圾收集器删除对象之前对这个对象调用的

System.gc():

我们可以调用System.gc方法,建议虚拟机进行垃圾回收工作(注意,是建议,但虚拟机会不会这样干,我们也无法预知!)

下面来看一个例子来了解finalize()和System.gc()的使用:

public class TestGC {  

    public TestGC() {}  



    //当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。  

    protected void finalize() {  

        System.out.println("我已经被垃圾回收器回收了...");  

    }  



    public static void main(String [] args) {  

        TestGC gc = new TestGC();  

        gc = null;    

        // 建议虚拟机进行垃圾回收工作  

        System.gc();  

    }  

}  

如上面的例子所示,大家可以猜猜重写的finalize方法会不会执行?

答案是:不一定!

因为无论是设置gc的引用为null还是调用System.gc()方法都只是”建议”垃圾回收器进行垃圾回收,但是最终所有权还在垃圾回收器手中,它会不会进行回收我们无法预知!

资源的回收:

刚才讲了一堆理论的东西,下面来点实际能用上的,资源的回收:

Thread(线程)回收:

线程中涉及的任何东西GC都不能回收(Anything reachable by a thread cannot be GC’d ),所以线程很容易造成内存泄露。

如下面代码所示:

Thread t = new Thread() {  

    public void run() {  

        while (true) {  

            try {  

                Thread.sleep(1000);  

                System.out.println("thread is running...");  

            } catch (InterruptedException e) {  



            }  

        }  

    }  

};  

t.start();  

t = null;  

System.gc(); 

如上在线程t中每间隔一秒输出一段话,然后将线程设置为null并且调用System.gc方法。

最后的结果是线程并不会被回收,它会一直的运行下去。

因为运行中的线程是称之为垃圾回收根(GC Roots)对象的一种,不会被垃圾回收。当垃圾回收器判断一个对象是否可达,总是使用垃圾回收根对象作为参考点。

Cursor(游标)回收:

Cursor是Android查询数据后得到的一个管理数据集合的类,在使用结束以后。应该保证Cursor占用的内存被及时的释放掉,而不是等待GC来处理。并且Android明显是倾向于编程者手动的将Cursor close掉,因为在源代码中我们发现,如果等到垃圾回收器来回收时,会给用户以错误提示。

所以我们使用Cursor的方式一般如下:

Cursor cursor = null;  

try {  

    cursor = mContext.getContentResolver().query(uri,null, null,null,null);  

    if(cursor != null) {  

        cursor.moveToFirst();  

        //do something  

    }  

} catch (Exception e) {  

    e.printStackTrace();  

} finally {  

    if (cursor != null) {  

        cursor.close();  

    }  

}  

有一种情况下,我们不能直接将Cursor关闭掉,这就是在CursorAdapter中应用的情况,但是注意,CursorAdapter在Acivity结束时并没有自动的将Cursor关闭掉,因此,你需要在onDestroy函数中,手动关闭。

@Override    

protected void onDestroy() {          

    if (mAdapter != null && mAdapter.getCurosr() != null) {    

        mAdapter.getCursor().close();    

    }    

    super.onDestroy();     

}    

Receiver(接收器)回收

调用registerReceiver()后未调用unregisterReceiver().

当我们Activity中使用了registerReceiver()方法注册了BroadcastReceiver,一定要在Activity的生命周期内调用unregisterReceiver()方法取消注册

也就是说registerReceiver()和unregisterReceiver()方法一定要成对出现,通常我们可以重写Activity的onDestory()方法:

@Override    

protected void onDestroy() {    

      this.unregisterReceiver(receiver);    

      super.onDestroy();    

}    

Stream/File(流/文件)回收:

主要针对各种流,文件资源等等如:

InputStream/OutputStream,SQLiteOpenHelper,SQLiteDatabase,Cursor,文件,I/O,Bitmap图片等操作等都应该记得显示关闭。

和之前介绍的Cursor道理类似,就不多说了。


Review(回顾,检查):

Review(回顾,检查),大家都知道Code Review的重要性。而这里我说的Review和Code Review差不多,主要目的就是检查代码中存在的不合理和可以改进的地方。

Code Review(代码检查):

Code Review主要检查代码中存在的一些不合理或可以改进优化的地方,大家可以参考之前写的Reduce,Reuse和Recycle都是侧重讲解这方面的。

UI Review(视图检查):

Android对于视图中控件的布局渲染等会消耗很多的资源和内存,所以这部分也是我们需要注意的。

减少视图层级:

减少视图层级可以有效的减少内存消耗,因为视图是一个树形结构,每次刷新和渲染都会遍历一次。

hierarchyviewer:

想要减少视图层级首先就需要知道视图层级,所以下面介绍一个SDK中自带的一个非常好用的工具hierarchyviewer。

你可以在下面的地址找到它:your sdk path\sdk\tools

Android内存优化

如上图大家可以看到,hierarchyviewer可以非常清楚的看到当前视图的层级结构,并且可以查看视图的执行效率(视图上的小圆点,绿色表示流畅,黄色和红色次之),所以我们可以很方便的查看哪些view可能会影响我们的性能从而去进一步优化它。

hierarchyviewer还提供另外一种列表式的查看方式,可以查看详细的屏幕画面,具体到像素级别的问题都可以通过它发现。

ViewStub标签

此标签可以使UI在特殊情况下,直观效果类似于设置View的不可见性,但是其更大的意义在于被这个标签所包裹的Views在默认状态下不会占用任何内存空间。

include标签

可以通过这个标签直接加载外部的xml到当前结构中,是复用UI资源的常用标签。

merge标签

它在优化UI结构时起到很重要的作用。目的是通过删减多余或者额外的层级,从而优化整个Android Layout的结构。

布局用Java代码比写在XML中快

一般情况下对于Android程序布局往往使用XML文件来编写,这样可以提高开发效率,但是考虑到代码的安全性以及执行效率,可以通过Java代码执行创建,虽然Android编译过的XML是二进制的,但是加载XML解析器的效率对于资源占用还是比较大的,Java处理效率比XML快得多,但是对于一个复杂界面的编写,可能需要一些套嵌考虑,如果你思维灵活的话,使用Java代码来布局你的Android应用程序是一个更好的方法。

重用系统资源:

1. 利用系统定义的id

比如我们有一个定义ListView的xml文件,一般的,我们会写类似下面的代码片段。

<ListView  

    android:id="@+id/mylist"  

    android:layout_width="fill_parent"  

    android:layout_height="fill_parent"/>  

这里我们定义了一个ListView,定义它的id是”@+id/mylist”。实际上,如果没有特别的需求,就可以利用系统定义的id,类似下面的样子。

<ListView  

    android:id="@android:id/list"  

    android:layout_width="fill_parent"  

    android:layout_height="fill_parent"/>  

在xml文件中引用系统的id,只需要加上“@android:”前缀即可。如果是在Java代码中使用系统资源,和使用自己的资源基本上是一样的。不同的是,需要使用android.R类来使用系统的资源,而不是使用应用程序指定的R类。这里如果要获取ListView可以使用android.R.id.list来获取。

2. 利用系统的图片资源

这样做的好处,一个是美工不需要重复的做一份已有的图片了,可以节约不少工时;另一个是能保证我们的应用程序的风格与系统一致。

3. 利用系统的字符串资源

如果使用系统的字符串,默认就已经支持多语言环境了。如上述代码,直接使用了@android:string/yes和@android:string/no,在简体中文环境下会显示“确定”和“取消”,在英文环境下会显示“OK”和“Cancel”。

4. 利用系统的Style

假设布局文件中有一个TextView,用来显示窗口的标题,使用中等大小字体。可以使用下面的代码片段来定义TextView的Style。

<TextView  

        android:id="@+id/title"  

        android:layout_width="wrap_content"  

        android:layout_height="wrap_content"  

        android:textAppearance="?android:attr/textAppearanceMedium" />  

其中android:textAppearance=”?android:attr/textAppearanceMedium”就是使用系统的style。需要注意的是,使用系统的style,需要在想要使用的资源前面加“?android:”作为前缀,而不是“@android:”。

5. 利用系统的颜色定义

除了上述的各种系统资源以外,还可以使用系统定义好的颜色。在项目中最常用的,就是透明色的使用。

android:background ="@android:color/transparent" 

除了上面介绍的以外还有很多其他Android系统本身自带的资源,它们在应用中都可以直接使用。具体的,可以进入android-sdk的相应文件夹中去查看。例如:可以进入androidsdk\platforms\android-8\data\res,里面的系统资源就一览无余了。

开发者需要花一些时间去熟悉这些资源,特别是图片资源和各种Style资源,这样在开发过程中,能重用的尽量重用,而且有时候使用系统提供的效果可能会更好。

其他小tips:

  1. 分辨率适配-ldpi,-mdpi, -hdpi配置不同精度资源,系统会根据设备自适应,包括drawable, layout,style等不同资源。

  2. 尽量使用dp(density independent pixel)开发,不用px(pixel)。

  3. 多用wrap_content, match_parent

  4. 永远不要使用AbsoluteLayout

  5. 使用9patch(通过~/tools/draw9patch.bat启动应用程序),png格式

  6. 将Acitivity中的Window的背景图设置为空。getWindow().setBackgroundDrawable(null);android的默认背景是不是为空。

  7. View中设置缓存属性.setDrawingCache为true。

Desgin Review(设计检查):

Desgin Review主要侧重检查一下程序的设计是否合理,包括框架的设计,界面的设计,逻辑的设计(其实这些东西开发之前就应该想好了)。

框架设计:

是否定义了自己的Activity和fragment等常用控件的基类去避免进行重复的工作

是否有完善的异常处理机制,即使真的出现OOM也不会直接崩溃导致直接退出程序

界面设计:

  1. 在视图中加载你所需要的,而不是你所拥有。因为用户不可能同时看到所有东西。最典型的例子就是ListView中的滑动加载。

  2. 如果数据特别大,此时应该暗示用户去点击加载,而不是直接加载。

  3. 合理运用分屏,转屏等,它是个双刃剑,因为它即可以使程序更加美观功能更加完善,但也相应增加了资源开销。

逻辑设计:

避免子类直接去控制父类中内容,可以使用监听等方式去解决

注:以上是在工作和学习中积累的经验,可能有一些不足之处需要完善。