Android优化总结
一、使用正确的FOR循环
for ( int i=0, n=list.size();i<n; i++)
{
...//在Arraylist中使用最为高效
}
for (Object obj : list)
{
...//在除Arraylist以外的结构,例如collection中使用高效
}
for (int i=0;i<list.size(); i++)
{
...//最低效
}
二、避免创建不必要的对象
1.避免创建重复对象
for (int i=0,n=list.size(); i<n; i++)
{
new Foo();
obj.setXXX(list.get(i));
....
}
在不影响功能的情况下,可以使用
Foo obj = newFoo();
for (int i=0,n=list.size(); i<n; i++)
{
obj.setXXX(list.get(i));
....
}
因为每一次new的代价都是很高的,因为可能会调用GC。
2.使用对象池
最佳办法是程序员自己管理每一个对象的生命周期。我的理解是,自建对象池管理对象。
对象池化的基本思路是:将用过的对象保存起来,等下一次需要这种对象的时候,再拿出来重复使用,从而在一定程度上减少频繁创建对象所造成的开销。用于充当保存对象的“容器”的对象,被称为“对象池”。
利:
1.对象池能避免重复创建多余的对象,尤其是开销较大的对象;
2.对于一些重量级的对象,使用对象池统一管理其生命周期,可以减少许多不必要的性能损耗;例如数据库连接池的使用。
弊:
因为维护对象池也要造成一定开销。对生成时开销不大的对象进行池化,反而可能会出现“维护对象池的开销”大于“生成新对象的开销”,从而使性能降低的情况。
3.消除过期对象引用
由于JAVA的GC会自动回收内存,确实方便了许多,但是在更多时候,对于无意识的内存泄漏情况非常多,而且很难检查。案例:
public class Stack
{
private Object[] element;
privat int size = 0;
public Stack(int ca)
{
this.elements = new Object[ca];
}
public void push(Object e)
{//这里还需要判断是否越界,略
element[size++] = e;
}
pop()
{
if ( size == 0)
return;
return elements[--size];
}
}
内存泄漏”。随着不断的使用,性能会逐渐降低,在极端情况下,会导致OOM。
我们发现,在pop()的时候,虽然实现了功能,但是出栈的对象永远不会被回收,栈内部维护着这些对象的过期引用,即使我们不在需要他它。在JDK内部,出栈的源码是这样写的:
public synchronized Epop() {
if (elementCount == 0){
throw newEmptyStackException();
}
final int index =--elementCount;
final E obj = (E)elementData[index];
elementData[index] = null;
modCount++;
return obj;
}
这就是一个典型的无意识泄漏。
再比如,我曾经以为回收一个Arraylist的内存是通过:Arraylist.clear(),其实源码是:
@Override public void clear() {
if (size != 0) {
Arrays.fill(array,0, size, null);
size = 0;
modCount++;
}
}
又是一个无意识泄漏,应该是list= null回收内存。
三、正确使用String对象
privatefinal String MICRO_REG_CREATE = "CREATE TABLE IF NOT EXISTS "
+ MicroMsgRegDao.TB_NAME +" ("
.....
这是QQ通讯录创建SQL表的一段代码,由于直接只用“+”运算构造字符串,我们额外创建了许多不必要的对象,在许多其他使用中,也从来不会注意这点,并试图去优化他,我们可以使用StringBilder或者StringBuffer
private final
String MICRO_REG_CREATE = newStringBuilder("CREATE TABLE IF NOT EXISTS")
append(MicroMsgRegDao.TB_NAME).append("(")
append(MicroMsgRegDao.COLUMN_ID)
append(" INTEGER PRIMARY KEY,")........
PS:
String ab = "a" + "b";
相当于
String a = "a"; //第一个对象
String b = "b"; //第二个对象
String c = a+b; //第三个对象
String ab = c;
因此创建了许多不必要的对象。
为连接n个字符串而重复地使用字符串连接操作符,要求n的平方级时间。
StringBuilder和StringBuffer功能完全相同,唯一不同是StringBuffer是线程安全的,而StringBuilder是非线程安全的。
知识拓展
StringBuffer sb = new StringBuffer()
这行代码生成一个默认StringBuffer对象,默认它能储存16个char值,当我们不停的向其中添加char时,超过16个char的时候,sb会自动扩展,按照一下方法:
1.创建一个能容纳50个char的数组,并持有它;
2.将数据拷贝到新的数组中。
当添加的char达到51时,对象按此方式继续增长,带来了不必要的消耗。那么我们在初始化时,可以预估最终需要的字符串长度,使用
StringBuffer sb = new StringBuffer(52);
可以避免两次无效的创建复制操作。
需要知道的是,Arraylist也是使用相同的机制做到动态增加,所以,如果我们可以在ArrayList初始化的时候,指定其最初长度,可以带来一定的效率增长。
同时ArrayList 和Vector的区别在于,ArrayList线程不安全,所以高效,Vector线程安全,所以低效。结合使用场合,建议使用ArrayList。
四、对常量使用Static Final修饰符
对于不需要改变的常量类型,尽可能的使用static final 声明以提高其性能。同时,对于不需要继承的Class,也可以用final修饰,可以极大的提高运行效率。
五、在私有内部内中,考虑用包访问权限替代私有访问权限
主要是针对这种情况的写法
public class Foo {
//将private改为protected可以带来效率提升
private class Inner {
void stuff() {
Foo.this.doStuff(Foo.this.mValue);
}
}
private int mValue;
public void run() {
Inner in= new Inner();
mValue = 27;
in.stuff();
}
private void doStuff(int value) {
System.out.println("Value is " + value);
}
}
对于内部类,在访问外部私有成员变量时,语法上是正确的,虚拟机在执行过程中会认为其非法,会生成几个综合方法来桥接这些代码,造成性能低下。
六、避免使用枚举
枚举变量非常方便,但不幸的是它会牺牲执行的速度和并大幅增加文件体积。
所以,尽量使用静态常量代替枚举变量。
关于Android内存泄漏
在终端设备中,由于内存有限,如果我们使用内存不当,将会照成设备运行越来越缓慢,而由于使用JAVA语言,许多童鞋忽略了内存泄漏这块,总结一下需要注意那些地方可能造成内存泄漏的。
一、造成内存泄漏的原因
由于JAVA的内存是由GC管理的,所以对程序员来说是透明的。可以通过System.gc()申请回收内存,这一接口作用是建议GC回收内存,注意,仅仅是建议回收,是否回收内存仍由GC决定,同时,建议任何时候都不要使用System.gc()。
GC的工作原理:我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从main进程开始执行,那么该图就是以main进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被GC回收。
很显然,Obj2会被回收,Obj1不会。
现在,给出一个案例:
class A
{
private B b;
publicA(B b)
{
this.b= b;
}
}
class B
{
privateA a;
publicvoid setA(A a)
{
this.a= a;
}
}
public static void main(String args[]) {
Bb = new B();
A a = new A(b);
b.setA(a);
}
这段代码演示典型JAVA内存泄漏的原理。类A引用了一个B的实例,同时这个实例又引用了A,形成闭合的环。对于这种环,即使外部已经没有引用了,但是JVM仍然无法回收内存,它认为A和B一直是存在外部引用的,内存永远得不到释放。并且,这种错误只能通过程序员review才能发现出来,非常隐蔽。
修改方法:
只需要在释放内存的时候,手动设置任意一个引用无效,切断环,即可。这里我们可以调用
b.setA(null);
二、释放对象的引用
Frankywang曾提到一个QQ内存泄漏的BUG,我查了一些资料,做出了记录,方便大家理解。
Android上 ,Context可以用于很多操作,但是大部分时候是用来加载以及使用资源。这就是为什么所有的widgets在他们的构造函数中接受一个Context参数。在一般的android应用中,你通常有两种Context:分别是Activity和Application。通常的,当我们的类和方法需要使用到context时,我们传递的是Activity这个context,比如:
private static DrawablesBackground;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
TextView label = new TextView(this);
if(sBackground == null) {
sBackground =getDrawable(R.drawable.large_bitmap);
}
label.setBackgroundDrawable(sBackground);
setContentView(label);
}
这段代码,看起来没有问题,实际上泄漏了整个Activity。当屏幕方向改变时(横竖屏切换),系统默认销毁当前Activity并创建一个新的activity同时保持原有状态。它泄漏了第一个activity,这个在第一次屏幕改变时被创建的activity。当一个Drawable被关联到一个view上,这个view就相当于在drawable上设置的一个回调。在上面的代码片段中,注意sBackground是一个static 变量。这表示丫的命很长。然而,drawable有一个TextView的引用,而这个TextView又拥有一个activity的引用(Context),activity依次引用了几乎所有的东西。于是这个Activity就这样泄漏了。
三、查询数据库没有关闭游标
这种情况应该比较少,非常明显的泄漏。
Cursor cursor =getContentResolver().query(uri);
if (cursor.moveToNext())
{
...
}
改:
Cursor cursor = null;
try
{
cursor = getContentResolver().query(uri);
if ( ..... )
{}
}catch(Exception e){
} finally{
if( cursor!= null)
try{
cursor.close();
}cach(Exceptione){
}
四、构造Adapter时,没有使用缓存的convertView
初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的View对象,同事ListView会将这些View对象缓存起来。当向下滚动ListView时,原先位于最上面的List item的View对象会被回收,然后被用来构造新出现的最下面的list item。这个构造过程是由getView()方法来完成的,getView()的第二个参数View convertView就是被缓存起来的view对象。
案例:
public View getView(int position, ViewconvertView, ViewGroup parent){
View view = new XXView();
...
return view;
}
正确写法:
public View getView(int position, ViewconvertView, ViewGroup parent){
View view = null;
if( convertView != null){
view = convertView;
}else{
view =new XXView();
}
...
return view;
}
五、Bitmap对象在不使用时候调用recycle()释放内存
由于Bitmap比较占用内存,所以当它不使用的时候,建议调用Bitmap.recycle()方法回收此对象,否则,可能造成Out Of Memory异常。当然,recycle()并不是必须的,建议使用。
六、使用缓存
Frankywang交流时提到,在使用缓存的过程中,实现支持弱引用的HashMap,在HashMap中的键值没有任何外部引用的时候,允许JVM回收不必要的元素,我查阅了一下QQ通讯录的源码,确实有这个实现。同时,JDK实际上已经为我们封装了这个实现,具体API是:
java.util.WeakHashMap
我们在使用缓存时,可以直接调用WeakHashMap代替HashMap:在缓存的条目过期之后,它们会自动删除。
七、使用单例
Frankywang交流是提到单例写法问题。单例是我们经常需要使用到的,我收集几种主流的单例写法,分析其特点。
单例一:
public class Foo
{
private static Foo foo = null;
private Foo(){}
public stataic Foo getFoo(){
if (foo == null )
foo = new Foo();
returnfoo;
}
}
分析:这是我个人最常用的写法,有安全隐患。假设线程A调用getFoo(),执行语句 foo = newFoo(); 如果在初始化的过程中,另外一个线程B同样调用getFoo(),那么这个方法看到对象被部分初始化的部分,导致灾难性的后果。
单例二:
public class Foo
{
private static Foo foo = null;
private Foo(){}
public stataic Foo getFoo(){
if ( foo == null ){
synchronized(Foo.class)
{
if( foo == null)
foo = new Foo();
}
}
return foo;
}
}
分析:同样是在上种情况下,如果被共享的变量包含一个原语值,而不是一个对象引用,则它可以正常工作,否则不能正常工作。(我认为应该都可以工作,不知道原因,求解答),参见《effect java》 第二条
单例三:
public class Foo
{
private static final Foo foo = new Foo();
private Foo(){}
public stataic Foo getFoo(){ return foo;}
}
分析:这是最佳写法,但是违反了迟缓初始化的规定,我会尽量使用这种写法。
单例四:
public class Foo
{
private Foo(){}
class FooHolder
{
static final Foo foo = new Foo();
}
public stataic Foo getFoo(){
return FooHolder.foo;
}
}
分析:最优美的写法,规避了各种风险。缺点在于,它不能用于实例域,只能用户抽象域。
Android中的性能分析
献上KM美文两篇,非常实用的技巧,建议大家都看看。
《Android性能分析工具之TraceView》
http://km.oa.com/group/571/articles/show/98630?kmref=search
《Android 应用程序内存泄漏的分析》
http://km.oa.com/group/571/articles/show/97711?kmref=search