<a name="简介"></a>
简介
性能优化是开发任何软件比较重要和困难的一点,特别是在android应用上,受硬件和系统的限制比较大,随着整个系统的不断发展,性能问题就越发突出,在移动端的直接体验就是app卡顿臃肿。
现代计算机科学鼻祖曾说过“过早的优化是万恶之源”。我觉得在移动端优化不是过早或者过晚的问题,因为android性能问题大多是由于内存泄漏和不能很好的利用和规避android系统一些特性造成的。工作中按照这些技巧来编写程序,就能尽可能的避免让app越发臃肿的问题。
接下来就从内存的管理、高性能的编码、合理使用android特性以及内存的分析及工具四个方面探讨android性能优化问题。
<a name="内存的管理和分析"></a>
内存的管理和分析
<a name="当界面不可见时释放内存"></a>
当界面不可见时释放内存
程序中怎么样知道界面已是或将是不可见了呢?Activity中有两个回调方法:onStop()和onTrimMemory()方法。
onStop()方法当用从A Activity跳转到B Activity时,就会回调A的onStop()方法,这时候可以在onStop方法中进行取消网络连接、注销广播接收器、关闭数据库、关闭计时器等操作。如下所示:
protected void onStop() {
super.onStop();
//注销广播接收器
unregisterReceiver(mBroadcastReceiver);
//取消网络请求
NetManager.cancleRequest(this);
}
onTrimMemory()方法当用户已经离开我们的程序时回调,此时可以进行UI相关资源的回收,比如本页面使用的大图片。如下所示:
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
switch (level) {
case TRIM_MEMORY_UI_HIDDEN:
ImageLoader.getInstance().clearMemory();****
break;
}
}
<a name="当内存紧张时释放内存"></a>
当内存紧张时释放内存
onTrimMemory()方法还有很多种其它类型的回调,可以在手机内存降低的时候及时通知我们。我们应该根据回调中传入的级别来去决定如何释放应用程序的资源。此外,当程序正常运行和在缓存中时,回调的等级是不同的,应该分开分析:
-
程序正常运行时
TRIM_MEMORY_RUNNING_MODERATE 表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经有点低了,系统可能会开始根据LRU缓存规则来去杀死进程了。
TRIM_MEMORY_RUNNING_LOW 表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经非常低了,我们应该去释放掉一些不必要的资源以提升系统的性能,同时这也会直接影响到我们应用程序的性能
TRIM_MEMORY_RUNNING_CRITICAL 表示应用程序仍然正常运行,但是系统已经根据LRU缓存规则杀掉了大部分缓存的进程了。这个时候我们应当尽可能地去释放任何不必要的资源,不然的话系统可能会继续杀掉所有缓存中的进程,并且开始杀掉一些本来应当保持运行的进程,比如说后台运行的服务。
-
程序缓存状态时
TRIM_MEMORY_BACKGROUND 表示手机目前内存已经很低了,系统准备开始根据LRU缓存来清理进程。这个时候我们的程序在LRU缓存列表的最近位置,是不太可能被清理掉的,但这时去释放掉一些比较容易恢复的资源能够让手机的内存变得比较充足,从而让我们的程序更长时间地保留在缓存当中,这样当用户返回我们的程序时会感觉非常顺畅,而不是经历了一次重新启动的过程。
TRIM_MEMORY_MODERATE 表示手机目前内存已经很低了,并且我们的程序处于LRU缓存列表的中间位置,如果手机内存还得不到进一步释放的话,那么我们的程序就有被系统杀掉的风险了。
TRIM_MEMORY_COMPLETE 表示手机目前内存已经很低了,并且我们的程序处于LRU缓存列表的最边缘位置,系统会最优先考虑杀掉我们的应用程序,在这个时候应当尽可能地把一切可以释放的东西都进行释放。
<a name="避免在Bitmap上浪费内存"></a>
避免在Bitmap上浪费内存
OOM问题大多由于加载图片造成,因为一些图片加载时如果不注意,很可能瞬间突破内存界限,造成OOM,所以如果应用中涉及到图片加载时,应多注意以下几点:
合理读取Bitmap
ImageView加载图片时应该像素数对应,比如ImageView的大小为120dp,那么所加载的图片大小应为240像素;如果在一个小的Imageview上显示高分辨率的图片没有视觉上的好处。-
高效加载大图
参考: http://blog.csdn.net/guolin_blog/article/details/9316683#
-
使用图片缓存技术
参考: http://blog.csdn.net/guolin_blog/article/details/9316683#
<a name="使用优化过的数据集合"></a>
使用优化过的数据集合
Android API当中提供了一些优化过后的数据集合工具类,如SparseArray,SparseBooleanArray,以及LongSparseArray等,使用这些API可以让我们的程序更加高效。传统Java API中提供的HashMap工具类会相对比较低效,因为它需要为每一个键值对都提供一个对象入口,而SparseArray就避免掉了基本数据类型转换成对象数据类型的时间,此外更重要的是提高了内存效率。
<a name="知晓内存的开支情况"></a>
知晓内存的开支情况
我们还应当清楚我们所使用语言的内存开支和消耗情况,并且在整个软件的设计和开发当中都应该将这些信息考虑在内。可能有一些看起来无关痛痒的写法,结果却会导致很大一部分的内存开支,例如:
- 使用枚举通常会比使用静态常量要消耗两倍以上的内存,在Android开发当中我们应当尽可能地不使用枚举。
- 任何一个Java类,包括内部类、匿名类,都要占用大概500字节的内存空间。
- 任何一个类的实例要消耗12-16字节的内存开支,因此频繁创建实例也是会一定程序上影响内存的。
- 在使用HashMap时,即使你只设置了一个基本数据类型的键,比如说int,但是也会按照对象的大小来分配内存,大概是32字节,而不是4字节。因此最好的办法就是像上面所说的一样,使用优化过的数据集合。
<a name="尽量避免使用依赖注入框架"></a>
尽量避免使用依赖注入框架
Android工程当中使用依赖注入框架,比如说像Guice或者RoboGuice等,因为它们可以简化一些复杂的编码操作,但是这些框架为了要搜寻代码中的注解,通常都需要经历较长的初始化过程,并且还可能将一些你用不到的对象也一并加载到内存当中。这些用不到的对象会一直占用着内存空间,可能要过很久之后才会得到释放,相较之下,也许多敲几行看似繁琐的代码才是更好的选择。
<a name="使用ProGuard简化代码"></a>
使用ProGuard简化代码
ProGuard这个工具来混淆代码,但是除了混淆之外,它还具有压缩和优化代码的功能。ProGuard会对我们的代码进行检索,删除一些无用的代码,并且会对类、字段、方法等进行重命名,重命名之后的类、字段和方法名都会比原来简短很多,这样的话也就对内存的占用变得更少了。
<a name="高性能的编码"></a>
高性能的编码
编码的优化只能算是"微优化",不会有显著的性能提升,但平时写代码时注意一些,然后就会潜移默化在微观层次上提升程序的性能,而且代码也更加专业。
<a name="避免创建不必要的对象"></a>
避免创建不必要的对象
创建对象从来都不应该是一件随意的事情,因为创建一个对象就意味着垃圾回收器需要回收一个对象,而这两步操作都是需要消耗时间的。虽说创建一个对象的代价确实非常小,GC操作时的停顿时间也变得难以察觉,但是这些理由都不足以让我们可以肆意地创建对象,需要创建的对象我们自然要创建,但是不必要的对象我们就应该尽量避免创建。下面列举一些可以避免创建对象的场景:
如果我们有一个需要拼接的字符串,那么可以优先考虑使用StringBuffer或者StringBuilder来进行拼接,而不是加号连接符,因为使用加号连接符会创建多余的对象,拼接的字符串越长,加号连接符的性能越低。
在没有特殊原因的情况下,尽量使用基本数据类来代替封装数据类型,int比Integer要更加高效,其它数据类型也是一样。
当一个方法的返回值是String的时候,通常可以去判断一下这个String的作用是什么,如果我们明确地知道调用方会将这个返回的String再进行拼接操作的话,可以考虑返回一个StringBuffer对象来代替,因为这样可以将一个对象的引用进行返回,而返回String的话就是创建了一个短生命周期的临时对象。
<a name="静态优于抽象"></a>
静态优于抽象
如果并不需要访问一个对象中的某些字段,只是想调用它的某个方法来去完成一项通用的功能,那么可以将这个方法设置成静态方法,这会让调用的速度提升15%-20%,同时也不用为了调用这个方法而去专门创建对象了,这样还满足了上面的一条原则。另外这也是一种好的编程习惯,因为我们可以放心地调用静态方法,而不用担心调用这个方法后是否会改变对象的状态(静态方法内无法访问非静态字段)。
<a name="对常量使用static_final修饰符"></a>
对常量使用static final修饰符
static int intVal = 42;
static String strVal = "Hello, world!";
编译器会对上述代码生成初始化方法<clinit>主要用来第一次创建时对字段进行赋值,之后访问字段时采取字段搜索的方式访问。如果使用final修饰如下:
static final int intVal = 42;
static final String strVal = "Hello, world!";
经过这样修改之后,定义类就不再需要一个<clinit>方法了,因为所有的常量都会在dex文件的初始化器当中进行初始化。而调用时会用一种相对轻量级的字符串常量方式,而不是字段搜寻的方式。
<a name="多使用系统封装好的API"></a>
多使用系统封装好的API
Java语言当中其实给我们提供了非常丰富的API接口,我们在编写程序时如果可以使用系统提供的API就应该尽量使用,系统提供的API完成不了我们需要的功能时才应该自己去写,因为使用系统的API在很多时候比我们自己写的代码要快得多,它们的很多功能都是通过底层的汇编模式执行的。
比如说String类当中提供的好多API都是拥有极高的效率的,像indexOf()方法和一些其它相关的API,虽说我们通过自己编写算法也能够完成同样的功能,但是效率方面会和这些方法差的比较远。这里举个例子,如果我们要实现一个数组拷贝的功能,使用循环的方式来对数组中的每一个元素一一进行赋值当然是可行的,但是如果我们直接使用系统中提供的System.arraycopy()方法将会让执行效率快9倍以上
<a name="避免在内部调用Getters/Setters方法"></a>
避免在内部调用Getters/Setters方法
我们直接访问某个字段可能要比通过getters方法来去访问这个字段快3到7倍。不过我们肯定不能仅仅因为效率的原因就将封装的技巧给抛弃了,编写代码还是要按照面向对象思维的,但是我们可以在能优化的地方进行优化,比如说避免在内部调用getters/setters方法。如下:
public class Calculate {
private int one = 1;
private int two = 2;
public int getOne() {
return one;
}
public int getTwo() {
return two;
}
public int getSum() {
return getOne() + getTwo();
}
}
改成:
public class Calculate {
private int one = 1;
private int two = 2;
......
public int getSum() {
return one + two;
}
}
<a name="合理使用android特性"></a>
合理使用android特性
合理使用android的特性,也会更加实际性的优化应用,例如布局优化、合理使用Activity生命周期等
<a name="重用布局文件"></a>
重用布局文件
使用<include>和<merge>这两个非常有用的标签,避免布局重写重用。
<include>标签可以允许在一个布局当中引入另外一个布局,那么比如说我们程序的所有界面都有一个公共的部分,这个时候最好的做法就是将这个公共的部分提取到一个独立的布局文件当中,然后在每个界面的布局文件当中来引用这个公共的布局。
<merge>标签是作为<include>标签的一种辅助扩展来使用的,它的主要作用是为了防止在引用布局文件时产生多余的布局嵌套。大家都知道,Android去解析和展示一个布局是需要消耗时间的,布局嵌套的越多,那么解析起来就越耗时,性能也就越差,因此我们在编写布局文件时应该让嵌套的层数越少越好。
<a name="仅在需要时才加载布局"></a>
仅在需要时才加载布局
有的时候我们会遇到这样的场景,就是某个布局当中的元素非常多,但并不是所有元素都一起显示出来的,而是普通情况下只显示部分常用的元素,而那些不常用的元素只有在用户进行特定操作的情况下才会显示出来.例如使用ViewStub标签。
<a name="合理利用Activity生命周期"></a>
合理利用Activity生命周期
- 因为从onCreate()-onStart()期间Activity一直处于不可见状态,此时应尽量避免复杂的耗时操作,比如数据逻辑处理和UI渲染活动,已加快Activity的启动速度
- 从A Actitivity跳转到B Activity时,因为A的onPause()先于B Acitivity的onResume方法执行,所以在onPause方法中尽量避免耗时操作。
<a name="内存的分析及工具使用"></a>
内存的分析及工具使用
虽然有很多方法避免内存泄漏,但难免没有漏网之鱼,所以合理的分析内存使用情况是在后续优化时非常重要的一个方法。接下来首先了解系统GC的过程,然后,了解几种查找内存泄漏的工具。
<a name="了解系统GC"></a>
了解系统GC
GC全称是Garbage Collection,也就是所谓的垃圾回收。Android系统会在适当的时机触发GC操作,一旦进行GC操作,就会将一些不再使用的对象进行回收。当一个对象不存在其他对象对他有引用时,就会被GC掉。
那么什么时候会触发GC操作呢?这个通常都是由系统去决定的,我们一般情况下都不需要主动通知系统应该去GC了(虽然我们确实可以这么做,下面会讲到),但是我们仍然可以去监听系统的GC过程,以此来分析我们应用程序当前的内存状态。那么怎样才能去监听系统的GC过程呢?其实非常简单,系统每进行一次GC操作时,都会在LogCat中打印一条日志,我们只要去分析这条日志就可以了,日志的基本格式如下所示:
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <Pause_time>
首先第一部分GC_Reason,这个是触发这次GC操作的原因,一般情况下一共有以下几种触发GC操作的原因:
- GC_CONCURRENT: 当我们应用程序的堆内存快要满的时候,系统会自动触发GC操作来释放内存
- GC_FOR_MALLOC: 当我们的应用程序需要分配更多内存,可是现有内存已经不足的时候,系统会进行GC操作来释放内存。
- GC_HPROF_DUMP_HEAP: 当生成HPROF文件的时候,系统会进行GC操作,关于HPROF文件我们下面会讲到。
- GC_EXPLICIT: 这种情况就是我们刚才提到过的,主动通知系统去进行GC操作,比如调用System.gc()方法来通知系统。或者在DDMS中,通过工具按钮也是可以显式地告诉系统进行GC操作的。
接下来第二部分Amount_freed,表示系统通过这次GC操作释放了多少内存。
然后Heap_stats中会显示当前内存的空闲比例以及使用情况(活动对象所占内存 / 当前程序总内存)。
最后Pause_time表示这次GC操作导致应用程序暂停的时间。
<a name="DDMS查看内存使用及检测内存泄漏"></a>
DDMS查看内存使用及检测内存泄漏
如果我们想要更加清楚地实时知晓当前应用程序的内存使用情况,只通过日志就有些力不从心了,我们需要通过DDMS中提供的工具来实现。
打开DDMS界面,在左侧面板中选择你要观察的应用程序进程,然后点击Update Heap按钮,接着在右侧面板中点击Heap标签,之后不停地点击Cause GC按钮来实时地观察应用程序内存的使用情况即可,如下图所示:
接着继续操作我们的应用程序,然后继续点击Cause GC按钮,如果你发现反复操作某一功能会导致应用程序内存持续增高而不会下降的话,那么就说明这里很有可能发生内存泄漏了。
<a name="使用Eclipse_Memory_Analyzer(MAT)定位内存泄漏的位置"></a>
使用Eclipse Memory Analyzer(MAT)定位内存泄漏的位置
通过GC日志以及DDMS工具这两种方式,可以比较轻松地发现应用程序中是否存在内存泄露的现象。但如果真的出现了内存泄露,我们应该怎么定位到具体是哪里出的问题呢?这就需要借助一个内存分析工具了,叫做Eclipse Memory Analyzer(MAT)。下载地址: :http://eclipse.org/mat/downloads.php(适用于studio)
<a name="使用lint对代码进行优化"></a>
使用lint对代码进行优化
我们总希望发布的apk文件越小越好,不希望资源文件没有用到的图片资源也被打包进apk,不希望应用中使用了高于minSdk的api,也不希望AndroidManifest文件存在异常,lint就能解决我们的这些问题。Android lint是在ADT 16提供的新工具,它是一个代码扫描工具,能够帮助我们识别代码结构存在的问题,主要包括:
- 布局性能(以前是 layoutopt工具,可以解决无用布局、嵌套太多、布局太多)
- 未使用到资源
- 不一致的数组大小
- 国际化问题(硬编码)
- 图标的问题(重复的图标,错误的大小)
- 可用性问题(如不指定的文本字段的输入型)
- manifest文件的错误