Android官方建议,编写高效的代码的准则如下:
- 不要做冗余的工作
- 进来避免次数过多的分配内存
- 其实还需要再添加一条准则:深入理解所用语言特性和系统平台的API,具体到Android开发,熟练掌握Java语言,并对Android SDK所提供的API了如指掌。
数据结构的选择
正确的选择数据结构很重要,对Java中常见的数据结构例如ArrayList和LinkedList,HashMap和HashSet等,需要做到对他们的联系和区别有比较深入的了解,方便再编码的时候做出正确的选择,下面我们再Android开发中使用SparseArray代替HashMap为例进行说明。SparseArray是Android平台特有的稀疏数组的实现,它是Integer到Object的一个映射,再特定场合可用于替代HashMap<Integer,<E>>,提高性能。他的核心是现实二分查找算法。
//This is Arrays.binarySearch(), but dosen't do any argument validation.
static int binarySearch(int[] array, int size, int value){
int lo = 0;
int hi = size - 1;
while(lo <= hi){
final int mid = ( lo + hi) >>> 1;
final int midVal= array[mid];
if(midVal < value){
lo = mid + 1;
}else if(midVal > value){
hi = mid - 1;
}else{
return mid; // value found
}
}
return ~lo; //value not present
}
SparseArray家族目前有下面的四大类
//用于替代HashMap<Integer, Boolean> booleanMap = new HashMap<Integer, Boolean>();
SparseBooleanArray booleanArray = new SparseBooleanArray();
//用于替代HashMap<Integer, Integer> booleanMap = new HashMap<Integer, Integer>();
SparseIntArray intArray = new SparseIntArray();
//用于替代HashMap<Integer, Long> booleanMap = new HashMap<Integer, Long>();
SparseLongArray longArray = new SparseLongArray();
//用于替代HashMap<Integer, String> booleanMap = new HashMap<Integer, String>();
SparseArray<String> stringArray = new SparseArray<String>();
需要注意的几点如下:
SparseArray不是线程安全的
由于要进行二分查找,因此,SparseArray会对插入的数据按照Key值大小顺序插入。
SparseArray对删除操作做了优化,它并不会立即删除这个元素,而是通过设置标识位(DELETED)的方式,后面尝试重用。
再Android工程中运行Lint进行静态代码分析,会有一个名为AndroidLintUseSparseArrays的检查项,如果违规,它会提示
HashMap can be replaced with SparseArray
这样可以很轻松的找到工程可优化的地方。
Handler和内部类的正确用法
Android代码中设计线程通信的地方通常会使用Handler,典型的代码结构如下。
public class HandlerActivity extends Activity{
//可能引入泄露的方法
private final Handler mLeakyHandler = new Handler(){
@Override
public void handleMessage(Message msg){
// ....
}
};
}
使用Android Lint分析这段代码,会违反检查项AndroidLintHandlerLeak,得到如下提示。
This Handler calss should be static or leaks might occour
那么产生内存泄漏的原因可能是什么呢?我们知道Handler是和Looper以及MessageQueue一起工作的,再Android中,一个应用启动以后,系统默认会创建一个为主线程服务的Looper对象,该Looper对象用于处理主线程的所有Message对象,它的生命周期贯穿于整个应用的生命周期。在主线程中使用的Handler都会默认绑定到这个Looper对象。在主线程中创建Handler对象时,它会立即关联主线程Looper对象的MessageQueue,这时发送到MessageQueue中的Message对象都会持有这个Handler对象的引用,这样在Looper处理消息时才能回调到Handler的handleMessage方法。因此,如果Message还没有被处理完成,那么Handler对象也就不会被垃圾回收。
在上面的代码中,将Handler的实例声明为HandlerActivity类的内部类。在Java语言中,非静态内部匿名类会持有外部类的一个隐式的引用,这样就可能会导致外部类无法被垃圾回收。因此,最终由于MessageQueue中的Message还没有处理完成,就会持有Handler对象的引用,而非静态Handler对象会持有外部类HandlerActivity的引用,这个Activity无法被垃圾回收从而导致内存泄漏。
一个明显会引入内存泄漏的例子
public class HandlerActivity extends Activity{
//可能引入泄露的方法
private final Handler mLeakyHandler = new Handler(){
@Override
public void handleMessage(Message msg){
//.....
}
};
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
//延迟5分钟发送消息
mLeakyHandler.postDelayed(new Runnable(){
@Override
public void run(){
//......
}
}, 1000*60*5);
}
}
由于消息延迟5分子发送,因此,当用户进入这个Activity并退出以后,在消息发送并处理完成之前,这个Activity是不会被系统回收的(系统内存确实不够的情况例外)
解决方案如下:
- 在子线程中使用Handler,这时需要开发者自己创建一个Looper对象,这个Looper对象的生命周期同一般的Java对象,因此这种方法可行
- 将Handler声明为静态的内部类,前面说过,静态内部类不会持有外部类的引用,因此,也不会引起内存泄漏。经典用法的代码如下
public class HandlerActivity extends Activity{
/*
声明一个静态的Handler内部类,并持有外部类的弱引用
*/
private static class InnerHandler extends Handler{
private final WeakReference<HandlerActivity> mActivity;
public InnerHandler(HandlerActivity activity){
mActivity = new WeakReference<HandlerActivity>(activity);
}
@Override
public void handleMessage(Message msg){
HandlerActivity activity = mActivity.get();
if(activity != null){
//.....
}
}
}
private final InnerHandler mHandler = new InnerHandler(this);
/*
静态匿名内部类不会持有外部类的引用
*/
private static final Runnable sRunnable = new Runnable(){
@Override
public void run(){
//......
}
};
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
//延迟5分子发送消息
mHandler.postDelayed(sRunnable, 1000*60*5);
}
}
正确的使用Context
Context应该是每个入门Android的程序员第一个接触的概念,他代表当前上下文的环境,可以用来实现很多功能的调用,语句如下
/获取资源管理器对象,进而可以访问到例如String,color等资源
Resources resources = context.getResources();
//启动指定的Activity
context.startActivity(new Intent(this, MainActivity.class));
//获取各种系统服务
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
//获取系统文件目录
File internalDir = context.getCacheDir();
File externalDir = context.getExternalCacheDir();
//更多...
可见,正确的理解Context的概念是很重要的,虽然在应用开发中随处可见Context的使用,但并不是所有的Context实力都具有相同的功能,在使用上需要区别对待,否则会很容易引入问题。总结下面的Context的种类
Context的种类
根据Context依托的组件以及用途的不同,我们可以将Context分为如下几种。
- Application:Android应用中的默认单例类,在Activity或者Service中通过getApplication()可以获取到这个单例,通过context.getApplicationContext()可以获取到应用全局唯一的Context实例。
- Activity/Service:这两个类都是ContextWrapper的子类,在这两个类中可以通过getBaseContext()获取到他们的Context实例,不同的Activity或者Service实例,他们的Context都是独立的,不会复用。
- BroadcastReceiver:和Activity以及Service不同,BroadcastReceiver本身并不是Context的子类,而是在回调函数onReceive()中由Android框架传入一个Context实例。系统传入的这个Context实例都是经过功能裁剪的,他不能调用registerReceiver()以及bindService()这两个函数。
- ContentProvider:同样的,ContentProvider也不是context的子类,但是在创建时系统会传入一个Context实例,这样在ContentProvider中可以通过调用getContext()函数获取。如果ContentProvider和调用者处于相同的应用进程中,那么getContext()将返回应用全局唯一的Context实例。如果是其他进程调用的ContentProvider,那么ContentProvider将持有自身所在进程的Context实例。
错误使用Context导致的内存泄漏
错误的使用Context可能会导致内存泄漏,典型的例子是在实现单例模式时使用Context,如下代码是可能会导致内存泄漏的单利实现。
public class SingleInstance{
private static SingleInstance sInstance;
private SingleInstance(Context context){
mContext = context;
}
public static SingleInstance getInstance(Context context){
if(sInstance == null){
sInstance = new SingleInstance(context);
}
return sInstance;
}
}
如果使用者调用getInstance时传入的Context是一个Activity或者是Service实例,那么在应用退出之前,由于单利模式一直存在,会导致对应的Activity或者Service被单利引用,从而不会被垃圾回收,Acitivty或者Service中关联其他View或者数据结构对象也不会被释放,从而导致内存泄漏。正确的做法是使用ApplicationContext,因为它是应用唯一的,而生命周期和应用一致的,正确的单例实现如下。
public class SingleInstance{
private Context mContext;
private static SingleInstance sInstance;
private SingleInstance(Context context){
mContext = context;
}
public static SingleInstance getInstance(Context context){
if(sInstance == null){
//获取到上下文对象
sInstance = new SingleInstance(context.getApplicationContext());
}
return sInstance;
}
}
不同的Context对比
不同组件中的Context能提供的功能不尽相同,总结如下
功能 | Application | Activity | Service | BroadcastReceiver | ContentProvider |
显示Dialog | NO | YES | NO | NO | NO |
启动Activity | NO[1] | YES | NO[1] | NO[1] | NO[1] |
实现layout Inflation | NO[2] | YES | NO[2] | NO[2] | NO[2] |
启动Service | YES | YES | YES | YES | YES |
绑定Service | YES | YES | YES | YES | NO |
发送Broadcast | YES | YES | YES | YES | YES |
注册Broadcast | YES | YES | YES | YES | NO[3] |
加载资源Resource | YES | YES | YES | YES | YES |
- 其中NO[1]表示对应的组建并不是真的启动Activity,而是建议不要这么做,因为这些组件会在新的Task中创建Activity,而不是在原来的Task中。
- NO[2]标记也是表示也不建议这么做,因为在非Activity中进行Layout Inflation,会使用系统默认的主题,而不是应用中设置的主题。
- NO[3]标记表示在Android4.2及以上的系统上,如果注册的BroadcastReceiver是null时是可以的,用来获取sticky广播的当前值。
掌握Java的四种引用方式
掌握Java的四种引用类型对于写出内存使用良好的应用是很关键的
- 强引用(StrongReference):强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
- 软引用(SoftReference):如果一个对象只具有软引用,当内存空间足够时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
- 弱引用(WeakReference):从名字可以看出,弱引用势必软引用更弱的一种引用类型,只有弱引用指向对象的生命周期更短。当垃圾回收器扫描到只有弱引用的对象时,不论当前的内存空间是否不足,都会对弱引用对象进行回收。弱引用也可以和一个引用队列配合使用,当弱引用指向的对象被回收以后,Java虚拟机会将这个弱引用加入到与之关联的引用队列中。
- 虚引用(PhantomReference)"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
其他代码微优化
避免创建非必要的对象
对象的创建需要内存分配,对象的销毁需要垃圾回收,这些都会在一定程度上影响应用的性能。因此一般来说,最好是重用对象而不是在每次需要的时候去创建一个功能相同的新对象,特别是注意不要在循环中重复创建相同的对象。
1. 简介
一般来说,我们最好的做法是重用对象,而不是每次使用都new出一个新的相同功能的对象,这样做很高效,特别是对于那些不可变类来说。
2. 例子
引用书中的一个很好的例子:比如有一个Person对象,对象中有一个字段是生日,生日是个常量,它不能被更改,现在需要完成一个方法,用这个方法我们可以获取到这个人是否出生在1946到1964年之间(也就是生育高峰期(baby boomer)),你会怎么做呢?
像下面这样?:
public class Person {
private final Date birthday;
public Person(Date birthday) {
this.birthday = birthday;
}
public boolean isBabyBoomer() {
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
c.set(1946, Calendar.JANUARY, 1, 0, 0, 0);//将时间设置为1946年1月1日0时0分0秒
Date boomStart = c.getTime();
c.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Date boomEnd = c.getTime();
return birthday.compareTo(boomStart) >= 0 &&
birthday.compareTo(boomEnd) < 0;
}
}
isBabyBoomer在每次调用的时候都会创建一个Calendar 一个TimeZone和两个Date实例,这些都是不必要的,下面是改进版本:
public class Person {
private final Date birthday;
private static final Date BOOM_START;
private static final Date BOOM_END;
public Person(Date birthday) {
this.birthday = birthday;
}
static {
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
c.set(1946, Calendar.JANUARY, 1, 0, 0, 0);//将时间设置为1946年1月1日0时0分0秒
BOOM_START = c.getTime();
c.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_END = c.getTime();
}
public boolean isBabyBoomer() {
return birthday.compareTo(BOOM_START) >= 0 &&
birthday.compareTo(BOOM_END) < 0;
}
}
3. 如果想要保证性能,则请尽量使用基本数据类型。
每次自动装箱(autoboxing)都会穿件一个多余的对象,比如:
Integer value = 0;
上面这段代码就是自动装箱,每次这样做都会穿件一个多余的Integer对象。
总结
本条的重点是避免创建多余的对象,提高重用性,但是需要注意对象的重用不能是盲目的,对于对象的重用,我们应该把目光集中在那些重量级的类,或是构造方法中需要做很多初始化的类的身上。对于一些比较轻量级的类,我们应该将代码的简洁和可维护性放在跟高的优先级上。
对常量使用static final修饰
对于基本数据类型和String类型的变量,建议使用static final修饰,因为final类型的常量会在会进入静态dex文件的与初始化部分,这是对基本数据类型和String类型常量的调用不会涉及类的初始化,而是直接调用字面量。
不要将视图控件声明为static,因为View对象会引用Activity对象,当Activity退出时其对象本身无法被销毁,会造成内存溢出
避免内部的Getters/Setters
在面向对象编程中,Getters/Setters的作用主要是对外屏蔽具体的变量定义,从而达到更好封装性。而如果是在类内部还是用Getters/Setters函数访问变量的话,会降低访问的速度。根据Android官方文档,在没有JIT(Just In Time)编译器时,直接访问变量的速度是调用Getter方法的3倍;在JIT编译时,直接访问变量的速度是调用Getter方法的7倍。当然,如果你的应用中使用了ProGuard的话,那么ProGuard会对Getters/Setters进行内联擦做,从而达到直接访问的效果。
数据类型的选择
字符串拼接用StringBuilder代替String,在非并发情况下用StringBuilder代替StringBuffer,如果你对字符串的长度有大致了解,如100字符左右,可以直接new StringBuilder(128)指定初始大小,减少空间不够时的再次分配。64位类型如long double的处理比32位如int慢,final类型存储在常量区中读取效率更高
数据结构选择
常见的数据结构选择如:
ArrayList和LinkedList
- ArrayList根据index取值更快,LinkedList更占内存、随机插入删除更快速、扩容效率更高。一般推荐ArrayList。
ArrayList、HashMap、LinkedHashMap、HashSet
- hash系列数据结构查询速度更优,ArrayList存储有序元素,HashMap为键值对数据结构,LinkedHashMap可以记住加入次序的hashMap,HashSet不允许重复元素。
HashMap、WeakHashMap
- WeakHashMap中元素可在适当时候被系统垃圾回收器自动回收,所以适合在内存紧张型中使用。
Collections.synchronizedMap和ConcurrentHashMap
- ConcurrentHashMap为细分锁,锁粒度更小,并发性能更优。Collections.synchronizedMap为对象锁,自己添加函数进行锁控制更方便。
Android也提供了一些性能更优的数据类型,如SparseArray、SparseBooleanArray、SparseIntArray、Pair
Sparse系列的数据结构是为key为int情况的特殊处理,采用二分查找及简单的数组存储,加上不需要泛型转换的开销,相对Map来说性能更优
代码的重构
代码重构是一项持之以恒的工作,需要依靠团队中每一个成员来维护代码库的高质量,如何有效的进行代码重构,推荐阅读《重构-改善既有的代码设计》和《代码整洁之道》
总结:
- 了解编程语言的编译原理,使用高效编码方式从语法上提高程序性能
- 采用合理的数据结构和算法提高程序性能,决定程序性能的关键
- 采用多线程、缓存数据、延迟加载、提前加载等手段,解决严重的性能瓶颈
- 合理配置虚拟机堆内存使用上限和使用率,减少垃圾回收频率
- 合理使用native代码
- 合理配置数据库缓存类型和优化SQL语句加快读取速度,使用事务加快写入速度
- 使用工具分析性能问题,找出性能瓶颈
参考文章: