内存管理与防范手段
目录
内存管理与防范手段... 1
一.内存分配跟踪工具DDMS –>Allocation tracker 使用... 2
二.内存监测工具DDMS-->Heap. 2
三.内存分析工具MAT(MemoryAnalyzerTool) 3
1.生成.hprof文件... 4
2.使用MAT导入.hprof文件... 5
3.使用MAT的视图工具分析内存... 5
四.MAT使用实例... 5
1.生成heap dump. 7
2.用MAT分析heap dumps. 9
3.使用MAT比较heap dumps. 11
五.防范不良代码... 11
1.查询数据库没有关闭游标... 11
2.缓存 convertView.. 12
3.Bitmap对象释放内存... 13
4.释放对象的引用... 13
5.Context的使用... 14
6.线程... 17
7.其他... 20
六.优化代码... 20
1.使用自身方法(Use Native Methods)... 20
2.使用虚拟优于使用接口... 20
3.使用静态优于使用虚拟... 20
4.尽可能避免使用内在的Get、Set方法... 20
5.缓冲属性调用Cache Field Lookups. 21
6.声明Final常量... 21
7.慎重使用增强型For循环语句... 22
8.避免列举类型Avoid Enums. 23
9.通过内联类使用包空间... 23
10.避免浮点类型的使用... 24
11.一些标准操作的时间比较... 24
12.为响应灵敏性设计... 25
一.内存分配跟踪工具DDMS –>Allocation tracker 使用
运行DDMS,只需简单的选择应用进程并单击Allocation tracker标签,就会打开一个新的窗口,单击“Start Tracing”按钮;然后,让应用运行你想分析的代码。运行完毕后,单击“Get Allocations”按钮,一个已分配对象的列表就会出现第一个表格中。单击第一个表格中的任何一项,在表格二中就会出现导致该内存分配的栈跟踪信息。通过allocation tracker,不仅知道分配了哪类对象,还可以知道在哪个线程、哪个类、哪个文件的哪一行。
尽管在性能关键的代码路径上移除所有的内存分配操作不是必须的,甚至有时候是不可能的,但allocation tracker可以帮你识别代码中的一些重要问题。举例来说,许多应用中发现的一个普遍错误:每次进行绘制都创建一个新的Paint对象。将Paint的创建移到一个实例区域里,是一个能极大提高程序性能的简单举措。
二.内存监测工具DDMS-->Heap
无论怎么小心,想完全避免badcode是不可能的,此时就需要一些工具来帮助我们检查代码中是否存在会造成内存泄漏的地方。Androidtools中的DDMS就带有一个很不错的内存监测工具Heap(这里我使eclipse的ADT插件,并以真机为例,在模拟器中的情况类似)。用Heap监测应用进程使用内存情况的步骤如下:
1.启动eclipse后,切换到DDMS透视图,并确认Devices视图、Heap视图都是打开的;
2.将手机通过USB链接至电脑,链接时需要确认手机是处于“USB调试”模式,而不是作为“MassStorage”;
3.链接成功后,在DDMS的Devices视图中将会显示手机设备的序列号,以及设备中正在运行的部分进程信息;
4.点击选中想要监测的进程,比如system_process进程;
5.点击选中Devices视图界面中最上方一排图标中的“UpdateHeap”图标;
6.点击Heap视图中的“CauseGC”按钮;
7.此时在Heap视图中就会看到当前选中的进程的内存使用量的详细情况
a)点击“CauseGC”按钮相当于向虚拟机请求了一次gc操作;
b)当内存使用信息第一次显示以后,无须再不断的点击“CauseGC”,Heap视图界面会定时刷新,在对应用的不断的操作过程中就可以看到内存使用的变化;
c)内存使用信息的各项参数根据名称即可知道其意思,在此不再赘述。
如何才能知道我们的程序是否有内存泄漏的可能性呢。这里需要注意一个值:Heap视图中部有一个Type叫做dataobject,即数据对象,也就是我们的程序中大量存在的类类型的对象。在dataobject一行中有一列是“TotalSize”,其值就是当前进程中所有Java数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断:
a)不断的操作当前应用,同时注意观察dataobject的TotalSize值;
b)正常情况下TotalSize值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况,所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平;
c)反之如果代码中存在没有释放对象引用的情况,则dataobject的TotalSize值在每次GC后不会有明显的回落,随着操作次数的增多TotalSize的值会越来越大,直到到达一个上限后导致进程被kill掉。
d)此处已system_process进程为例,在我的测试环境中system_process进程所占用的内存的dataobject的TotalSize正常情况下会稳定在2.2~2.8之间,而当其值超过3.55后进程就会被kill。
三.内存分析工具MAT(MemoryAnalyzerTool)
如果使用DDMS确实发现了我们的程序中存在内存泄漏,那又如何定位到具体出现问题的代码片段,最终找到问题所在呢?如果从头到尾的分析代码逻辑,那肯定会把人逼疯,特别是在维护别人写的代码的时候。这里介绍一个极好的内存分析工具MemoryAnalyzerTool(MAT)。
MAT是一个Eclipse插件,同时也有单独的RCP客户端。官方下载地址、MAT介绍和详细的使用教程请参见:www.eclipse.org/mat,在此不进行说明了。另外在MAT安装后的帮助文档里也有完备的使用教程。在此仅举例说明其使用方法。我自己使用的是MAT的eclipse插件,使用插件要比RCP稍微方便一些。
使用MAT进行内存分析需要几个步骤,包括:生成.hprof文件、打开MAT并导入.hprof文件、使用MAT的视图工具分析内存。以下详细介绍。
1.生成.hprof文件
a) 打开eclipse并切换到DDMS透视图,同时确认Devices、Heap和logcat视图已经打开了;
b) 将手机设备链接到电脑,并确保使用“USB调试”模式链接,而不是“Mass Storage“模式;
c) 链接成功后在Devices视图中就会看到设备的序列号,和设备中正在运行的部分进程;
d) 点击选中想要分析的应用的进程,在Devices视图上方的一行图标按钮中,同时选中“Update Heap”和“Dump HPROF file”两个按钮;
e) 这是DDMS工具将会自动生成当前选中进程的.hprof文件,并将其进行转换后存放在sdcard当中,如果你已经安装了MAT插件,那么此时MAT将会自动被启用,并开始对.hprof文件进行分析;
注意:第4步和第5步能够正常使用前提是我们需要有sdcard,并且当前进程有向sdcard中写入的权限(WRITE_EXTERNAL_STORAGE),否则.hprof文件不会被生成,在logcat中会显示诸如ERROR/dalvikvm(8574):hprof:can'topen/sdcard/com.xxx.hprof-hptemp:Permissiondenied.的信息。如果我们没有sdcard,或者当前进程没有向sdcard写入的权限(如system_process),那我们可以这样做:
在当前程序中,例如framework中某些代码中,可以使用android.os.Debug中的
public static void dumpHprofData(String fileName) throws IOException方法,手动的指定.hprof文件的生成位置。例如:
xxxButton.setOnClickListener(newView.OnClickListener(){
publicvoidonClick(Viewview){
android.os.Debug.dumpHprofData("/data/temp/myapp.hprof");
......
}
}
上述代码意图是希望在xxxButton被点击的时候开始抓取内存使用信息,并保存在我们指定的位置:/data/temp/myapp.hprof,这样就没有权限的限制了,而且也无须用sdcard。但要保证/data/temp目录是存在的。这个路径可以自己定义,当然也可以写成sdcard当中的某个路径。
2.使用MAT导入.hprof文件
a) 如果是eclipse自动生成的.hprof文件,可以使用MAT插件直接打开(可能是比较新的ADT才支持);
b) 如果eclipse自动生成的.hprof文件不能被MAT直接打开,或者是使用android.os.Debug.dumpHprofData()方法手动生成的.hprof文件,则需要将.hprof文件进行转换,转换的方法:将.hprof文件拷贝到PC上的/ANDROID_SDK/tools目录下,并输入命令
hprof-conv xxx.hprof yyy.hprof,其中xxx.hprof为原始文件,yyy.hprof为转换过后的文件。转换过后的文件自动放在/ANDROID_SDK/tools目录下。OK,到此为止,.hprof文件处理完毕,可以用来分析内存泄露情况了。
c) 在Eclipse中点击Windows->OpenPerspective->Other->MemoryAnalyzer,或者打MemoryAnalyzerTool的RCP。在MAT中点击File->OpenFile,浏览并导入刚刚转换而得到的.hprof文件。
3.使用MAT的视图工具分析内存
导入.hprof文件以后,MAT会自动解析并生成报告,点击DominatorTree,并按Package分组,选择自己所定义的Package类点右键,在弹出菜单中选择Listobjects->Withincomingreferences。这时会列出所有可疑类,右键点击某一项,并选择PathtoGCRoots->excludeweak/softreferences,会进一步筛选出跟程序相关的所有有内存泄露的类。据此,可以追踪到代码中的某一个产生泄露的类。
具体的分析方法在MAT的官方网站和客户端的帮助文档中有十分详尽,使用MAT分析内存查找内存泄漏的根本思路,就是找到哪个类的对象的引用没有被释放,找到没有被释放的原因,也就可以很容易定位代码中的哪些片段的逻辑有问题了。
四.MAT使用实例
使用DDMS检查这个应用的heap使用情况。你可以使用下面的方法启动DDMS:
From Eclipse : click Window > Open Perspective>Other...>DDMS
在左边的面板选择进程com.founder.android.new.kyodo.activity,然后在工具条上边点击
heap updates按钮。
这个时候切换到DDMS的VMHeap分页。它会显示每次gc后heap内存的一些基本数据。要看第一次gc后的数据内容,点击CauseGC按钮:
1.生成heap dump
我们现在使用heap dump来追踪这个问题。点击DDMS工具条上面的Dump HPROF文件按钮,选择文件存储位置,然后在运行hprof-conv。
如果你使用ADT(它包含DDMS的插件)同时也在eclipse里面安装了MAT,点击“dump HPROF”按钮将会自动地做转换(用hprof-conv)同时会在eclipse里面打开转换后的hprof文件(它其实用MAT打开)。
将生成的.hprof文件导入到MAT中,选择Leak Suspects Report ,得到下图:
2.用MAT分析heap dumps
启动MAT然后加载刚才我们生成的HPROF文件。MAT是一个强大的工具,讲述它所有的特性超出了本文的范围,所以我只想演示一种你可以用来检测泄露的方法:直方图(Histogram)视图。它显示了一个可以排序的类实例的列表,内容包括:shallow heap(所有实例的内存使用总和),或者retained heap(所有类实例被分配的内存总和,里面也包括他们所有引用的对象)。
如果我们按照shallow heap排序,自从Android3.0(Honeycomb),Bitmap的像素数据被存储在byte数组里(之前是被存储在Dalvik的heap里),所以基于这个对象的大小来判断。
右击byte[]类然后选择List Objects>with incoming references。它会生成一个heap上的所有byte数组的列表,在列表里,我们可以按照Shallow Heap的使用情况来排序。选择并展开一个比较大的对象,它将展示从根到这个对象的路径--就是一条保证对象有效的链条。
MAT不会明确告诉我们这就是泄露,因为它也不知道这个东西是不是程序还需要的,只有程序员知道。在这个案例里面,缓存使用的大量的内存会影响到后面的应用程序,所以我们可以考虑限制缓存的大小。
3.使用MAT比较heap dumps
调试内存泄露时,有时候适时比较2个地方的heap状态是很有用的。这时你就需要生成2个单独的HPROF文件(不要忘了转换格式)。下面是一些关于如何在MAT里比较2个heapdumps的内容(有一点复杂):
a) 第一个HPROF文件(usingFile>OpenHeapDump).
b) 打开Histogram view.
c) 在Navigation Historyview里(如果看不到就从Window>NavigationHistory找).右击histogram然后选择AddtoCompareBasket.
d) 打开第二个HPROF文件然后重做步骤2和3.
e) 切换到CompareBasketview,然后点击ComparetheResults(视图右上角的红色"!"图标)。
五.防范不良代码
1.查询数据库没有关闭游标
程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。
示例代码:
Cursor cursor=getContentResolver().query(uri...);
if(cursor.moveToNext()){
......
}
修正示例代码:
Cursor cursor = null;
try{
cursor=getContentResolver().query(uri...);
if(cursor!=null && cursor.moveToNext()){
......
}
}finally{
if(cursor != null){
try{
cursor.close();
}catch(Exception e){
}
}
}
2.缓存 convertView
以构造ListView的BaseAdapter为例,在BaseAdapter中提高了方法:
public View getView(int position,Viewconvert View, ViewGroup parent)
来向ListView提供每一个item所需要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的listitem的view对象会被回收,然后被用来构造新出现的最下面的listitem。这个构造过程就是由getView()方法完成的,getView()的第二个形参View convertView就是被缓存起来的listitem的view对象(初始化时缓存中没有view对象则convertView是null)。
由此可以看出,如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大。ListView回收listitem的view对象的过程可以查看:
android.widget.AbsListView.java-->void addScrapView(Viewscrap)方法。
示例代码:
public View getView(int position,Viewconvert View,ViewGroup parent){
View view = new Xxx(...);
......
return view;
}
修正示例代码:
public View getView(int position,Viewconvert View,ViewGroup parent){
View view = null;
if(convertView != null){
view = convertView;
populate(view , getItem(position));
...
}else{
view = new Xxx(...);
...
}
return view;
}
3.Bitmap对象释放内存
有时我们会手工的操作Bitmap对象,如果一个Bitmap对象比较占内存,当它不在被使用的时候,可以调用Bitmap.recycle()方法回收此对象的像素所占用的内存,但这不是必须的,视情况而定。
4.释放对象的引用
这种情况描述起来比较麻烦,举两个例子进行说明。
示例A:
假设有如下操作
public class DemoActivity extends Activity{
......
private Handler mHandler=...
private Object obj;
public void operation(){
obj = init Obj();
...
[Mark]
mHandler.post(new Runnable(){
public void run(){
use Obj(obj);
}
});
}
}
我们有一个成员变量obj,在operation()中我们希望能够将处理obj实例的操作post到某个线程的MessageQueue中。在以上的代码中,即便是mHandler所在的线程使用完了obj所引用的对象,但这个对象仍然不会被垃圾回收掉,因为DemoActivity.obj还保有这个对象的引用。所以如果在DemoActivity中不再使用这个对象了,可以在[Mark]的位置释放对象的引用,而代码可以修改为:
......
public void operation(){
obj = init Obj();
...
final Object o= obj;
obj = null;
mHandler.post(new Runnable(){
public void run(){
useObj(o);
}
}
}
......
示例B:
假设我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息(如信号强度等),则可以在LockScreen中定义一个PhoneStateListener的对象,同时将它注册到TelephonyManager服务中。对于LockScreen对象,当需要显示锁屏界面的时候就会创建一个LockScreen对象,而当锁屏界面消失的时候LockScreen对象就会被释放掉。
但是如果在释放LockScreen对象的时候忘记取消我们之前注册的PhoneStateListener对象,则会导致LockScreen无法被垃圾回收。如果不断的使锁屏界面显示和消失,则最终会由于大量的LockScreen对象没有办法被回收而引起OutOfMemory,使得system_process进程挂掉。
总之当一个生命周期较短的对象A,被一个生命周期较长的对象B保有其引用的情况下,在A的生命周期结束时,要在B中清除掉对A的引用。
5.Context的使用
Android应用程序堆最大为16MB,至少在G1之前是这样,即便没有将这些内存用完的打算,开发者也应尽量减少内存开销以便其他应用能够在后台运行而不会被强制关闭。这样的话,Android在内存中保存的应用越多,用户在应用间的切换就越快。Android应用程序的内存泄露问题,大部分时间里,这些问题都是源自同一个错误:对Context(上下文环境)的长时间引用。
在Android上,Context用于多种操作,但最多的还是用来加载和访问资源。这也是为什么所有的Widges在其构造函数中都有一个Context参数。常规的Android应用中,有两类Context:ActivityContext和ApplicationContext,通常前者被开发者传递给需要Context的类和方法。
- @Override
- Protected void onCreate(Bundle state){
- super.onCreate(state);
- TextView label = new TextView(this);
- label.setText("Leaks are bad");
- setContentView(label);
- }
这就意味着那些视图引用了整个Activity及其所拥有的一切:一般是整个视图层和所有资源。因此,如果泄露了这类Context(这里的泄露指的是引用Context,从而阻止了GC(垃圾回收)操作),就泄露了很多内存空间。如果不小心,泄露整个Activity是非常容易的事。
在进行屏幕方向改变的时候,系统默认做法是保持状态不变的情况下,销毁当前Activity并重新创建一个新的Activity。这样做,Android会从资源文件中重新装载当前应用的UI。现在假设你写了一个带有很大一幅位图的应用,但你不想在每次屏幕旋转时都装载一次位图,最简单的做法就是将其保存在一个静态区域中:
- Private static Drawables Background;
- @Override
- Protected void onCreate(Bundle state){
- super.onCreate(state);
- TextView label = new TextView(this);
- label.setText("Leaks are bad");
- if(sBackground == null){
- sBackground=getDrawable(R.drawable.large_bitmap);
- }
- label.setBackgroundDrawable(sBackground);
- setContentView(label);
- }
这段代码执行的快,同时也很有问题:在进行第一次屏幕方向改变的时候泄露了第一个Activity所占的内存空间。当一个Drawable连接到一个视图上时,视图被设置为Drawable上的一个回调,在上面的代码片段中,这就意味着Drawable引用了TextView,而TextView又引用了ActivityContext,ActivityContext又进一步引用了更多的东西(依赖与你的代码)。
上面这段示例是最简单的泄露ActivityContext的情况,你可以到HomeScreen'sSourceCode(查看unbindDrawables()
方法)中看看我们是如何通过在Acitivity销毁时将存储Drawable的回调置为null来解决该问题的。如果再有兴趣的话,某些情况下会产生一个由泄露的Context形成的链,这很糟糕,会很快使得内存耗尽。
有两种方法可以避免ActivityContext相关的内存泄露:最明显的一种是避免在ActivityContext自身的范围之外对其进行引用。上面这段示例展示了静态引用的情况,但对内部类和外部类的隐式引用同样都是危险的。第二种解决方法是用ApplicationContext,因为该Context与应用的生命周期一样长,并不依赖Activity的生命周期。如果想拥有一个生命期足够长的object(对象),但却需要给其一个必须的Context的话,别忘了ApplicationObject。获取ApplicationContext的方法很简单:执行Context.getApplicationContext()或Activity.getApplication()。
总之,为了避免ActivityContext相关的内存泄露,记住下面几条:
a) 不要长时间引用一个ActivityContext(引用周期应与Acitivity的生命周期一样长)
b) 尝试使用ApplicationContext代替AcitivityContext
c) 在Activity中,避免使用你无法控制其生命周期的非静态的内部类,使用静态的内部类,并对Activity内部进行弱引用。就是在静态的内部类中对外部类进行弱引用,就如在ViewRoot及其W内部类中的做法那样
d) 垃圾回收(GC)无法保证内存泄露
e) 使用WeakReference代替强引用。比如可以使用WeakReference<Context> mContextRef;
该部分的详细内容也可以参考Android文档中Article部分。
6.线程
线程也是造成内存泄露的一个重要的源头。线程产生内存泄露的主要原因在于线程生命周期的不可控。我们来考虑下面一段代码。
Public class MyActivity extends Activity{
Public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
newMyThread().start();
}
Private class MyThread extends Thread{
@Override
Public void run(){
super.run();
//dosomthing
}
}
}
这段代码很平常也很简单,是我们经常使用的形式。我们思考一个问题:假设MyThread的run函数是一个很费时的操作,当我们开启该线程后,将设备的横屏变为了竖屏,一般情况下当屏幕转换时会重新创建Activity,按照我们的想法,老的Activity应该会被销毁才对,然而事实上并非如此。
由于我们的线程是Activity的内部类,所以MyThread中保存了Activity的一个引用,当MyThread的run函数没有结束时,MyThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。
有些人喜欢用Android提供的AsyncTask,但事实上AsyncTask的问题更加严重,Thread只有在run函数不结束时才出现这种内存泄露问题,然而AsyncTask内部的实现机制是运用了ThreadPoolExcutor,该类产生的Thread对象的生命周期是不确定的,是应用程序无法控制的,因此如果AsyncTask作为Activity的内部类,就更容易出现内存泄露的问题。
这种线程导致的内存泄露问题应该如何解决呢?
第一、将线程的内部类,改为静态内部类。
第二、在线程内部采用弱引用保存Context引用。
解决的模型如下:
Public abstract class WeakAsyncTask<Params,Progress,Result,WeakTarget> extends
AsyncTask<Params,Progress,Result>{
Protected WeakReference<WeakTarget> mTarget;
Public WeakAsyncTask(WeakTarget target){
mTarget = new WeakReference<WeakTarget>(target);
}
@Override
Protected final void onPreExecute(){
Final WeakTarget target=mTarget.get();
if(target != null){
this.onPreExecute(target);
}
}
@Override
Protected final Result doInBackground(Params...params){
Final WeakTarget target=mTarget.get();
if(target!=null){
return this.doInBackground(target,params);
}else{
Return null;
}
}
@Override
Protected final void onPostExecute(Result result){
Final WeakTarget target=mTarget.get();
if(target != null){
this.onPostExecute(target,result);
}
}
Protected void onPreExecute(WeakTarget target){
//Nodefaultaction
}
Protected abstract Result doInBackground(WeakTarget target,Params...params);
Protected void onPostExecute(WeakTarget target ,Result result){
//Nodefaultaction
}
}
7.其他
Android应用程序中最典型的需要注意释放资源的情况是在Activity的生命周期中,在onPause()、onStop()、onDestroy()方法中需要适当的释放资源的情况。
六.优化代码
1.使用自身方法(Use Native Methods)
当处理字符串的时候,不要犹豫,尽可能多的使用诸如String.indexOf()、String.lastIndexOf()这样对象自身带有的方法。因为这些方法使用C/C++来实现的,要比在一个java循环中做同样的事情快10-100倍。还有一点要补充说明的是,这些自身方法使用的代价要比那些解释过的方法高很多,因而,对于细微的运算,尽量不用这类方法。
2.使用虚拟优于使用接口
假设你有一个HashMap对象,你可以声明它是一个HashMap或则只是一个Map:
Java代码
Map myMap1 = new HashMap();
HashMap myMap2 = new HashMap();
哪一个更好呢?
一般来说明智的做法是使用Map,因为它能够允许你改变Map接口执行上面的任何东西,但是这种“明智”的方法只是适用于常规的编程,对于嵌入式系统并不适合。通过接口引用来调用会花费2倍以上的时间,相对于通过具体的引用进行虚拟函数的调用。
如果你选择使用一个HashMap,因为它更适合于你的编程,那么使用Map会毫无价值。假定你有一个能重构你代码的集成编码环境,那么调用Map没有什么用处,即使你不确定你的程序从哪开头。(同样,public的API是一个例外,一个好的API的价值往往大于执行效率上的那点损失)
3.使用静态优于使用虚拟
如果你没有必要去访问对象的外部,那么使你的方法成为静态方法。它会被更快的调用,因为它不需要一个虚拟函数导向表。这同时也是一个很好的实践,因为它告诉你如何区分方法的性质(signature),调用这个方法不会改变对象的状态。
4.尽可能避免使用内在的Get、Set方法
C++编程语言,通常会使用Get方法(例如 i = getCount())去取代直接访问这个属性(i=mCount)。 这在C++编程里面是一个很好的习惯,因为编译器会把访问方式设置为Inline,并且如果想约束或调试属性访问,你只需要在任何时候添加一些代码。
在Android编程中,这不是一个很不好的主意。虚方法的调用会产生很多代价,比实例属性查询的代价还要多。我们应该在外部调用时使用Get和Set函数,但是在内部调用时,我们应该直接调用。
5.缓冲属性调用Cache Field Lookups
访问对象属性要比访问本地变量慢得多。你不应该这样写你的代码:
for (int i = 0; i < this.mCount; i++)
dumpItem(this.mItems[i]);
而是应该这样写:
int count = this.mCount;
Item[] items = this.mItems;
for (int i = 0; i < count; i++)
dumpItems(items[i]);
(我们直接使用“this”表明这些是它的成员变量)
一个相似的原则就是:决不在一个For语句中第二次调用一个类的方法。
例如,下面的代码就会一次又一次地执行getCount()方法,这是一个极大地浪费相比你把它直接隐藏到一个Int变量中。
for (int i = 0; i < this.getCount(); i++)
dumpItems(this.getItem(i));
当你不止一次的调用某个实例时,直接本地化这个实例,把这个实例中的某些值赋给一个本地变量。例如:
protected void drawHorizontalScrollBar(Canvas canvas, int width, int height) {
if (isHorizontalScrollBarEnabled()) {
int size = mScrollBar.getSize(false);
if (size <= 0) {
size = mScrollBarSize;
}
mScrollBar.setBounds(0, height - size, width, height);
mScrollBar.setParams( computeHorizontalScrollRange(), computeHorizontalScrollOffset(), computeHorizontalScrollExtent(), false);
mScrollBar.draw(canvas);
}
}
这里有四次mScrollBar的属性调用,把mScrollBar缓冲到一个堆栈变量之中,四次成员属性的调用就会变成四次堆栈的访问,这样就会提高效率。对于方法同样也可以像本地变量一样具有相同的特点。
6.声明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将会使用Integer类型的42,访问strVal将使用相对节省的“字符串常量”来替代一个属性调用。
将一个类或者方法声明为“final”并不会带来任何的执行上的好处,它能够进行一定的最优化处理。例如,如果编译器知道一个Get方法不能被子类重载,那么它就把该函数设置成Inline。同时,你也可以把本地变量声明为final变量。但是,这毫无意义。作为一个本地变量,使用final只能使代码更加清晰(或者你不得不用,在匿名访问内联类时)。
7.慎重使用增强型For循环语句
增强型For循环(也就是常说的“For-each循环”)经常用于Iterable接口的继承收集接口上面。在这些对象里面,一个iterator被分配给对象去调用它的hasNext()和next()方法。在一个数组列表里面,你可以自己接的敷衍它,在其他的收集器里面,增强型的for循环将相当于iterator的使用。
尽管如此,下面的源代码给出了一个可以接受的增强型for循环的例子:
public class Foo {
int mSplat;
static Foo mArray[] = new Foo[27];
public static void zero() {
int sum = 0;
for (int i = 0; i < mArray.length; i++) {
sum += mArray[i].mSplat;
}
}
public static void one() {
int sum = 0;
Foo[] localArray = mArray;
int len = localArray.length;
for (int i = 0; i < len; i++) {
sum += localArray[i].mSplat;
}
}
public static void two() {
int sum = 0;
for (Foo a: mArray) {
sum += a.mSplat;
}
}
}
zero() 函数在每一次的循环中重新得到静态属性两次,获得数组长度一次。
one() 函数把所有的东西都变为本地变量,避免类查找属性调用 。
two() 函数使用Java语言的1.5版本中的for循环语句,编辑者产生的源代码考虑到了拷贝数组的引用和数组的长度到本地变量,是例遍数组比较好的方法,它在主循环中确实产生了一个额外的载入和储存过程(显然保存了“a”),相比函数one()来说,它有一点比特上的减慢和4字节的增长。
总结之后,我们可以得到:增强的for循环在数组里面表现很好,但是当和Iterable对象一起使用时要谨慎,因为这里多了一个对象的创建。
8.避免列举类型Avoid Enums
列举类型非常好用,当考虑到尺寸和速度的时候,就会显得代价很高,例如:
public class Foo {
public enum Shrubbery { GROUND, CRAWLING, HANGING }
}
这会转变成为一个900字节的class文件(Foo$Shrubbery.class)。第一次使用时,类的初始化要在独享上面调用方法去描述列举的每一项,每一个对象都要有它自身的静态空间,整个被储存在一个数组里面(一个叫做“$VALUE”的静态数组)。那是一大堆的代码和数据,仅仅是为了三个整数值。
Shrubbery shrub = Shrubbery.GROUND;
这会引起一个静态属性的调用,如果GROUND是一个静态的Final变量,编译器会把它当做一个常数嵌套在代码里面。
9.通过内联类使用包空间
我们看下面的类声明
public class Foo {
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);
}
private class Inner {
void stuff() {
Foo.this.doStuff(Foo.this.mValue);
}
}
}
这里我们要注意的是我们定义了一个内联类,它调用了外部类的私有方法和私有属性。这是合法的调用,代码应该会显示"Value is 27"。
问题是Foo$Inner在理论上(后台运行上)是应该是一个完全独立的类,它违规的调用了Foo的私有成员。
为了弥补这个缺陷,编译器产生了一对合成的方法:
/*package*/ static int Foo.access$100(Foo foo) {
return foo.mValue;
}
/*package*/ static void Foo.access$200(Foo foo, int value) {
foo.doStuff(value);
}
当内联类需要从外部访问“mValue”和调用“doStuff”时,内联类就会调用这些静态的方法,这就意味着你不是直接访问类成员,而是通过公共的方法来访问的。前面我们谈过间接访问要比直接访问慢,因此这是一个按语言习惯无形执行的例子。
让拥有包空间的内联类直接声明需要访问的属性和方法,我们就可以避免这个问题,哲理诗是包空间而不是私有空间。这运行的更快并且去除了生成函数前面东西。(不幸的是,它同时也意味着该属性也能够被相同包下面的其他的类直接访问,这违反了标准的面向对象的使所有属性私有的原则。同样,如果是设计公共的API你就要仔细的考虑这种优化的用法)。
10.避免浮点类型的使用
在奔腾CPU发布之前,游戏作者尽可能的使用Integer类型的数学函数是很正常的。在奔腾处理器里面,浮点数的处理变为它一个突出的特点,并且浮点数与整数的交互使用相比单独使用整数来说,前者会使你的游戏运行的更快,一般的在桌面电脑上面我们可以自由的使用浮点数。
不幸的是,嵌入式的处理器通常并不支持浮点数的处理,阴齿所有的“float”和“double”操作都是通过软件进行的,一些基本的浮点数的操作就需要花费毫秒级的时间。 即使是整数,一些芯片也只有乘法而没有除法。在这些情况下,整数的除法和取模操作都是通过软件实现。当你创建一个Hash表或者进行大量的数学运算时,这都是你要考虑的。
11.一些标准操作的时间比较
为了距离说明我们的观点,下面有一张表,包括一些基本操作所使用的大概时间。注意这些时间并不是绝对的时间,绝对时间要考虑到CPU和时钟频率。系统不同,时间的大小也会有所差别。当然,这也是一种有意义的比较方法,我们可以比叫不同操作花费的相对时间。例如,添加一个成员变量的时间是添加一个本地变量的四倍。
Action |
Time |
Add a local variable |
1 |
Add a member variable |
4 |
Call String.length() |
5 |
Call empty static native method |
5 |
Call empty static method |
12 |
Call empty virtual method |
12.5 |
Call empty interface method |
15 |
Call Iterator : next() on a HashMap |
165 |
Call put() on a HashMap |
600 |
Inflate 1 View from XML |
22,000 |
Inflate 1 LinearLayout containing 1 TextView |
25,000 |
Inflate 1 LinearLayout containing 6 View objects |
100,000 |
Inflate 1 LinearLayout containing 6 TextView objects |
135,000 |
Launch an empty activity |
3,000,000 |
12.为响应灵敏性设计
代码可能通过各种性能测试,但是当用户使用时还是会需要漫长的等待,这些就是那种响应不够灵敏的应用——它们反应迟钝,挂起或冻住周期很长,或者要花很长时间来处理输入。
在Android上,系统通过向用户显示一个称为应用无响应(ANR:Application Not Responding)的对话框来防止在一段时间内响应不够快。用户可以选择让应用继续,但是用户并不会想要每次都来处理这个对话框。因此应把你的应用设计得响应灵敏,使得系统不必显示ANR给用户。
通常地,当不能响应用户输入时系统显示一个ANR。例如,如果一个应用在IO操作(经常是网络访问)上阻塞了,那么主应用线程就会无法处理正在进行的用户输入事件。经过一段时间,系统认为应用已经挂起,向用户显示一个ANR,让用户可以选择关闭。
相同地,如果你的应用花太多的时间在构建详细的内存结构上,又或者在计算游戏的下一个动作上,系统会认为你的应用已经挂起。用上面的技术来保证这些计算是高效的一直都是很重要的,但是即使是最高效的代码运行也是需要花费时间的。
在这两种情况下,解决的办法通常就是创建一个子线程,在这个子线程上做你的大部分工作。这样让你的主线程(驱动用户接口事件循环)保持运行,并让你的代码免于被系统认为已经冻住。因为这样的线程化通常都是在类级别上来完成的,所以你可以认为响应性能问题是一个类问题(与上面描述的方法级别的性能问题)。
是什么引发了ANR?
在Android系统上,应用的响应灵敏性由Activity Manager和Window Manager system services所监控,当它监测到如下的其中一个条件时,Android就会为特定的应用显示一个ANR:
5秒内对输入事件无响应。
怎样避免ANR?
考虑到上面对ANR的定义,让我们来研究一下这是为什么会发生以及怎样最好的组织你的应用以避免ANR。
Android应用正常是运行在一个单独的(如main)线程中的,这就意味着在你应用主线程中正在做的需要花很长时间来完成的事情都能够激活ANR对话框。因为你的应用并没有给自己一个机会来处理输入事件或Intent广播。
因此任何运行在主线程中的方法应该做尽可能少的事情。特别地Activitiy在关键生命周期方法中如onCreate()和onResume()应当做尽可能少的设置。潜在地的耗时长的操作(如网络或数据库操作,或高耗费数学计算如改变位图大小)应该在子线程里面完成(或以数据库操作为例,可以通过异步请求)。尽管如此,这并不是说当等待子线程完成的过程中你的主线程必须被阻塞——你不必调用Thread.wait()或Thread.sleep(),恰恰相反,你的主线程应该为子线程提供一个Handler,以便子线程完成时可以提交回给主线程。以这种方式来设计你的应用,将会允许你的主线程一直可以响应输入,以避免由5秒钟的输入事件超时导致的ANR对话。这些做法同样应该被其它任何显示UI的线程所效仿,因为它们属于同样类型的超时。
IntentReciever执行时间的特定限制限制了它们应该做什么:在后台执行的一些琐碎的工作如保存设置或注册通知。至于其它在主线程里被调用的方法,在BroadcastReceiver中,应用应该避免潜在的长耗时操作或计算,而是应该用子线程来完成密集任务(因为BroadcastReceiver的生命周期是短暂的)。对Intent broadcast作出响应,你的应用应该启动一个Service来执行长耗时的动作。同样,你也应该避免从Intent Receiver中启动Activity,因为它会产生一个新的屏,偷走任何用户正在运行的应用的焦点。对Intent broadcast作出的响应,假如你的应用需要向用户显示什么东西,应该用Notification Manager来完成。
增强响应灵敏性
通常,在一个应用中,100到200微秒是一个让用户感觉到阻滞的阈值,因此这里有些小技巧让你用来使你的应用看起来响应更灵敏。
如果你的应用正在后台对用户输入作出响应,显示正在进行的进度(ProgressBar和ProgressDialog对此很有用)。特别是对于游戏,在子线程中做移动的计算。
如果你的应用有一个耗时的初始化过程,考虑用闪屏或尽可能快地渲染主界面并异步地填充信息。在这两种情况下你都应该表明进度正在进行,以免用户觉得你的应用被冻住了。