上一篇文章,讲到了很多Android应用开发中需要注意的性能和内存方面的技巧。这一篇文章就是从smali指令级来分析性能优化和内存优化的问题。
如何解决界面启动时间开销大的问题
我们在编写Android应用的时候,很多情况下会遇到界面启动时间过长的问题,用户体验非常的不好。所以我们在编写代码的时候,一定要多加注意如何提高界面的启动时间。下面会讲到几个优化界面启动开销的技巧。
1.类的加载开销
当一个类的静态方法或者静态属性被调用或者类被实例化得时候,虚拟机首先做的第一件事情就是DexClassLoader将类的class文件加载到虚拟机,而加载到虚拟机的过程会触发class文件中clinit函数的执行。我们看下面一段代码
package com.example.smalidemo.foreach; import java.util.ArrayList; import java.util.List; public class OnInitTest { public static final String INIT_STRING = "initstring_fantasy"; public static String INIT2_STRING = "for initstring_fantasy 2"; private String INIT3_STRING = "initstring_fantasyh 3"; public static final int INT_1 = 100; public static int INIT_INT = 10000; public static List list = new ArrayList(); public static final ArrayList<AppBean> mAppListOnInit = new ArrayList<AppBean>(); public static ArrayList mAppList = null; private static String STRING_ARRAY[] = { "jpg", "mp5", "mp4" }; private static final String FINAL_STRING_ARRAY[] = { "pdf", "txt", "exe" };
}
反编译后的smali文件
.class public Lcom/example/smalidemo/foreach/OnInitTest; .super Ljava/lang/Object; .source "OnInitTest.java" # static fields .field private static final FINAL_STRING_ARRAY:[Ljava/lang/String; = null .field public static INIT2_STRING:Ljava/lang/String; = null .field public static INIT_INT:I = 0x0 .field public static final INIT_STRING:Ljava/lang/String; = "initstring_fantasy" .field public static final INT_1:I = 0x64 .field private static STRING_ARRAY:[Ljava/lang/String; .field public static list:Ljava/util/List; .field public static mAppList:Ljava/util/ArrayList; .field public static final mAppListOnInit:Ljava/util/ArrayList; .annotation system Ldalvik/annotation/Signature; value = { "Ljava/util/ArrayList", "<", "Lcom/example/smalidemo/foreach/AppBean;", ">;" } .end annotation .end field # instance fields .field private INIT3_STRING:Ljava/lang/String; # direct methods .method static constructor <clinit>()V .locals 10 .prologue const/16 v9, 0x2710 const/4 v8, 0x3 const/4 v7, 0x2 const/4 v6, 0x1 const/4 v5, 0x0 .line 8 const-string v3, "for initstring_fantasy 2" sput-object v3, Lcom/example/smalidemo/foreach/OnInitTest;->INIT2_STRING:Ljava/lang/String; .line 11 sput v9, Lcom/example/smalidemo/foreach/OnInitTest;->INIT_INT:I .line 12 new-instance v3, Ljava/util/ArrayList; invoke-direct {v3}, Ljava/util/ArrayList;-><init>()V sput-object v3, Lcom/example/smalidemo/foreach/OnInitTest;->list:Ljava/util/List; .line 13 new-instance v3, Ljava/util/ArrayList; invoke-direct {v3}, Ljava/util/ArrayList;-><init>()V sput-object v3, Lcom/example/smalidemo/foreach/OnInitTest;->mAppListOnInit:Ljava/util/ArrayList; .line 14 const/4 v3, 0x0 sput-object v3, Lcom/example/smalidemo/foreach/OnInitTest;->mAppList:Ljava/util/ArrayList; .line 15 new-array v3, v8, [Ljava/lang/String; const-string v4, "jpg" aput-object v4, v3, v5 const-string v4, "mp5" aput-object v4, v3, v6 const-string v4, "mp4" aput-object v4, v3, v7 sput-object v3, Lcom/example/smalidemo/foreach/OnInitTest;->STRING_ARRAY:[Ljava/lang/String; .line 16 new-array v3, v8, [Ljava/lang/String; const-string v4, "pdf" aput-object v4, v3, v5 const-string v4, "txt" aput-object v4, v3, v6 .line 17 const-string v4, "exe" aput-object v4, v3, v7 .line 16 sput-object v3, Lcom/example/smalidemo/foreach/OnInitTest;->FINAL_STRING_ARRAY:[Ljava/lang/String; .line 20 const/4 v1, 0x0 .local v1, i:I move v2, v1 .line 21 .end local v1 #i:I .local v2, i:I :goto_0 add-int/lit8 v1, v2, 0x1 .end local v2 #i:I .restart local v1 #i:I if-lt v2, v9, :cond_0 .line 25 return-void .line 22 :cond_0 new-instance v0, Lcom/example/smalidemo/foreach/AppBean; invoke-direct {v0}, Lcom/example/smalidemo/foreach/AppBean;-><init>()V .line 23 .local v0, bean:Lcom/example/smalidemo/foreach/AppBean; sget-object v3, Lcom/example/smalidemo/foreach/OnInitTest;->mAppListOnInit:Ljava/util/ArrayList; invoke-virtual {v3, v0}, Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z move v2, v1 .end local v1 #i:I .restart local v2 #i:I goto :goto_0 .end method在上面得Java文件中,我们声明了一些变量,其中包括字符串,整数,字符串数组,其中有些声明了final,一些没有声明final。我们可以看到声明final的字符串和整数,在编译后的文件中已经成为了常量,而没有声明final的变量,我们可以看到在声明的地方,仍然是类型默认值 string的默认值是null,而int的默认值是0。所以当DexClassLoader加载class文件的时候,会在<clinit>函数里面,对以上的静态变量初始化值。我们可以看到,声明的若干个静态变量,在编译后的smali文件中,成为了更多的smali指令。
提高DexClassLoader加载class的速度,就是要提高class中<clinit>函数的执行速度。
通过以上的分析我们可以总结出以下几个规则:
声明静态的变量,一定要添加final的声明 (在编译器变量就被常量代替,就不会再类加载的时候消耗CPU时间)
2.类的创建实例开销
一个class文件中除了静态变量外,还有很多全局非静态变量。而我们在声明全局变量的时候,都会为全局变量赋值。
private String INIT3_STRING = "initstring_fantasyh 3";
# instance fields .field private INIT3_STRING:Ljava/lang/String;
.method public constructor <init>()V .locals 1 .prologue .line 6 invoke-direct {p0}, Ljava/lang/Object;-><init>()V .line 9 const-string v0, "initstring_fantasyh 3" iput-object v0, p0, Lcom/example/smalidemo/foreach/OnInitTest;->INIT3_STRING:Ljava/lang/String; .line 6 return-void .end method
提高类创建实例的过程,就是优化<init>函数执行速度的过程。我们最好的做法就是在声明全局变量的地方,赋给默认值,在函数中真正要用的时候,再进行初始化。
无论我们是静态全局变量还是非静态全局变量,在类加载和实例化的过程中,都会对这些变量,进行赋值。如果我们在声明的时候赋了值,那么在init函数中赋值,否则也会执行一条赋值null或者0的指令。因此尽量少的声明全局变量是优化的一大准则。(因为只要声明了一个全局变量就会在类加载或者初始化的时候执行一条指令。)
以上两点都做好了优化,相信能为界面的显示速度提高不少。
但是当我们需要在代码中定义一些常量的集合或者数组的时候,如何避免这两个过程开销大的问题呢。比如在开发中,我们需要定义一个静态的全局数组。如果定义在一个在Activity的onCreate中就实例化的类中,肯定会对Activity的启动时间消耗不少。针对这种问题如何做好优化呢?我们可以将这种静态的数组重构到另一个class中,在使用数组的时候,我们可以直接调用这个类中的静态数组。这样就避免了在Activity初始化的流程中,就初始化那么多数组的问题。
延迟初始化是我们在优化Activity启动时间的一个很有力的技巧。在不修改算法和逻辑结构的基础上,通过延迟初始化也能达到一定程度的优化。
通过以上的分析可以总结以下几条规则:
1.尽可能少的声明全局变量
2.声明静态变量一定要final声明
3.对于开销大的静态模块或者全局非静态模块,可以重构到另外一个类里,达到延迟初始化的作用。