排列你的数据
再一次,封装的原则将被破坏。让我们假设你的应用存储一些排列的记录,每个记录包含两个字段:一个id和一个value。一个面向对象的方法是定义一个Record类,如Listing 4-12所示。
Listing 4-12 Record类
public class Record {
private final short id;
private final short value;
// 可能会更多
public Record(short id, short value) {
this.id = id;
this.value = value;
}
public final short getId() {
return id;
}
public final short getValue() {
return value;
}
public void doSomething() {
// 在这里坐一些事情
}
}
既然定义了Record,你的应用可以简单的分配一个array,在array中保存这些record,并且提供额外的方法,如Listing 4-13所示。
Listing 4-13 保存Records
public class MyRecords {
private Record[] records;
int nbRecords;
public MyRecords (int size) {
records = new Record[size];
}
public int addRecord (short id, short value) {
int index;
if (nbRecords < records.length) {
index = nbRecords;
records[nbRecords] = new Record(id, value);
nbRecords++;
} else {
index = -1;
}
return index;
}
public void deleteRecord (int index) {
if (index < 0) {
// 在这里抛出exception - 无效的参数
}
if (index < nbRecords) {
nbRecords--;
records[index] = records[nbRecords];
records[nbRecords] = null; // 不要忘记释放引用
}
}
public int sumValues (int id) {
int sum = 0;
for (int i=0; i<nbRecords; i++) {
Record r = records[i];
if (r.getId() == id) {
sum += r.getValue();
}
}
return sum;
}
public void doSomethingWithAllRecords () {
for (int i=0; i<nbRecords; i++) {
records[i].doSomething();
}
}
}
所有的这些将导致一个漂亮干净的设计。然而,直到你实际运行代码你才会发现缺陷:
(1) 在每次添加record到array的时候将会创建一个新的对象。尽管每个对象是轻量级的,内存分配始终是消耗,可以被避免。
(2) 如果id和value是public的,避免调用getId()和getValue()
如果允许你修改Record类,使id和value是public的是明显的缺陷。sumValues()的实现将被稍微修改,如Listing 4-14所示。
Listing 4-14 修改sumValues
public int sumValues (int id) {
int sum = 0;
for (Record r : records) {
if (r.id == id) {
sum += r.value;
}
}
return sum;
}
然而,这样根本没有减少分配数量;record对象仍然需要被创建,因为record要被添加到array。
NOTE:你可以在C/C++容易的避免分配,但是在Java所有的对象实际上引用,需要使用new操作符创建。
既然所有的对象是在heap中分配的,你可以仅仅在array中存储对象的引用,你可以修改MyRecords类去使用一个short的array去移除所有的分配。修改的类在Listing 4-15中给出。
Listing 4-15 使用Short Array修改MyRecords类
public class MyRecords {
private short[] records;
int nbRecords;
public MyRecords (int size) {
records = new short[size * 2];
}
public int addRecord (short id, short value) {
int index;
if (nbRecords < records.length) {
index = nbRecords;
records[nbRecords * 2] = id;
records[nbRecords * 2 + 1] = value;
nbRecords++;
} else {
index = -1;
}
return index;
}
public void deleteRecord (int index) {
if (index < 0) {
// 在这里抛异常 - 无效参数
}
if (index < nbRecords) {
nbRecords--;
records[index*2] = records[nbRecords * 2];
records[index*2 + 1] = records[nbRecords*2 + 1];
}
}
public int sumValues (int id) {
int sum = 0;
for (int i=0; i<nbRecords; i++) {
if (records[i*2] == id) {
sum += records[i*2 + 1];
}
}
return sum;
}
public void soSomethingWithAllRecords () {
Record r = new Record(0, 0);
for (int i=0;i<nbRecords; i++) {
r.id = records[i*2];
r.value = records[i*2 + 1];
r.doSomething();
}
}
}
让我们想象,稍后,你发现这些关于MyRecords类是如何使用的事情:
(1) sumValues() 被调用次数远远多于doSomethingWillAllRecords()
(2) 仅仅几个record使用同一个id
换句话说,id字段比value字段读的次数更多。给出额外的一段信息,你可能想到如下的方案去改善性能:使用两个array而不是一个,保存所有足够接近的id,当在sumValues()顺序遍历id array时最大化cache hit,。第一个array仅包含record id,第二个array包含仅仅record value。结果,当sumValues()运行两次,更多的record id在cache中被找到,因为许多record id被存储在一个cache line中。
新的MyRecords的实现在Listing 4-16中给出。
Listing 4-16 使用两个Array修改MyRecords类
public class MyRecords {
private short[] recordIds; // 第一个array仅存储id
private short[] recordValues; // 第二个array保存value
int nbRecords;
public MyRecords (int size) {
recordIds = new short[size];
recordValues = new short[size];
}
public int addRecord (short id, short value) {
int index;
if (nbRecords < recordIds.length) {
index = nbRecords;
recordIds[nbRecords] = id;
recordValues[nbRecords] = value;
} else {
index = -1;
}
return index;
}
public void deleteRecord (int index) {
if (index < 0) {
// 抛出异常-无效参数
}
if (index < nbRecords) {
nbRecords--;
recordIds[index] = recordIds[nbRecords];
recordValues[index] = recordValues[nbRecords];
}
}
public int sumValues (int id) {
int sum = 0;
for (int i=0; i<nbRecords; i++) {
if (recordIds[i] == id) {
sum += recordValues[i]; // 如果id匹配,我们只读value
}
}
return sum;
}
public void doSomethingWithAllRecords () {
Record r = new Record(0, 0);
for (int i=0; i<nbRecords;i++) {
r.id = recordIds[i];
r.value = recordValues[i];
r.doSomething();
}
}
}
你经常没有机会去实现这种优化。比如,上面的listing假设doSomething()不会去修改Record对象,并假设MyRecords不会提供任何方法去从array获取Record对象。如果这些假设不成立的话,Listing 4-15和4-16的实现不再会和Listing 4-13等同。
记住直到你了解你的代码是如何被使用的前你不能合适的优化你的代码。再一次,遵循实事求是的态度:不要在知道你要尝试去解决什么问题前去优化,因为一种优化模式可能会对其他的有负面的影响。
垃圾回收
Java的一个巨大的好处是垃圾回收。当对象不再使用的时候,GC释放(或者回收)内存。比如,在Listing 4-15中doSomethingWithAllRecords()分配的Record对象当方法返回后,对GC来说是可被消除的,这个对象的引用不再存在。有两个非常重要的事情要记住:
(1) Memory leak可能仍然存在
(2) 使用垃圾收集器去帮助你去管理内存,因为它不仅仅释放不再使用的内存
内存泄露
当对象不再被引用内存可以被回收,当对象的引用仍然存在的话可能发生内存泄露。一个经典的例子是Android文档中当屏幕旋转(比如,从竖屏到横屏),整个Activity对象被泄露。这个特殊的实例很容易复现,而且是非常严重的。因为一个Activity对象使用很多内存(经常包含更多对象的引用)。没有简单的方案去避免内存泄露,然而Android提供给你工具和API去发现他们。
Eclipse的DDMS视图的Heap和Allocation Tracker分别允许你跟踪内存使用和内存分配。再一次,这些工具不会告诉你内存泄露在哪里(如果有的话),但是你可以使用它们去分析应用内存使用,你的应用如果有泄露,很有希望找出。
TIP:使用Eclipse Memory Analyzer更好分析你的内存使用。你可以从http://www.eclipse.org/mat去下载。
Android 2.3定义了StrictMode类,可以很好的帮助你发现潜在的内存泄露。尽管在Android2.3中StrictMode的虚拟机策略仅允许你发现SQLite对象(比如cursor)没有被关闭,StrictMode的VM策略在Android3.0或者以上允许你发现如下的潜在泄露:
(1) Activity泄露
(2) 其他对象的泄露
(3) 当对象没有被关闭发生的泄露(看Android文档的完整的类列表实现的Closeable接口)
NOTE:StrictMode类在Android2.3引入(API 9),额外的VM策略和线程策略功能在Android3.0引入(API 11)。比如,Honeycomb的StrictMode线程策略支持在违例发生的时候屏幕闪烁。
Listing 4-17给出了如何去使用StrictMode类去检测你的应用中的内存泄露。你需要在你的开发过程中和测试过程中开启这个功能,当发布应用的时候关闭它。
Listing 4-17 使用StrictMode
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
builder.detectLeakedSqlLiteObjects();
if (VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
builder.detectActivityLeaks().detectLeakedClosableObjects();
}
// 或者你可以简单的调用builder.detectAll()
// 处理方式
builder.penaltyLog(); // 存在其他处理方式 (e.g. penaltyDeath())并且可以联合
StrictMode.VmPolicy vmp = builder.build();
StrictMode.setVmPolicy(vmp);
}
}
在这个特殊的实例中,当一个可关闭的对象(SQLite对象或者其他的)没有被关闭,StrictMode检测到一个违反,仅仅记录下这个violation。验证这个行为,你可以简单的查找一个数据库,不关闭返回的cursor,比如修改Listing 1-25给出的代码。
涉及到StrictMode,建议你简单的使用detectAll(),允许你的应用在将来的安卓版本利用StrictMode的新的功能。