我正在使用Java平台为Android平台调整交互式游戏。偶尔会有绘图和交互进行垃圾收集的打嗝。通常它不到十分之一秒,但有时在非常慢的设备上可能会达到200毫秒。
我正在使用ddms探查器(Android SDK的一部分)来搜索我的内存分配来自哪里,并从我的内部绘图和逻辑循环中删除它们。
最糟糕的罪犯已经完成了短暂的循环,
for(GameObject gob : interactiveObjects)
gob.onDraw(canvas);
每次循环执行时都会分配iterator
。我现在正在为我的对象使用数组(ArrayList
)。如果我想在内部循环中使用树或哈希,我知道我需要小心甚至重新实现它们而不是使用Java Collections框架,因为我无法负担额外的垃圾收集。当我在查看优先级队列时,可能会出现这种情况。
我也无法使用Canvas.drawText
显示分数和进度。这很糟糕,
canvas.drawText("Your score is: " + Score.points, x, y, paint);
因为Strings
,char
数组和StringBuffers
将全部分配以使其正常工作。如果你有几个文本显示项目并运行框架每秒60次开始加起来,将增加你的垃圾收集打嗝。我认为这里最好的选择是保留char[]
数组并手动解码int
或double
并将字符串连接到开头和结尾。我想知道是否有更清洁的东西。
我知道必须有其他人来处理这件事。您如何处理它以及您发现在Java或Android上以交互方式运行的陷阱和最佳实践?这些gc问题足以让我错过手动内存管理,但不是很多。
答案 0 :(得分:55)
我参与过Java移动游戏...避免GC对象的最佳方法(反过来 em> kill你的游戏的性能只是为了避免在你的主游戏循环中创建它们。
没有“干净”的方式来解决这个问题,我首先举一个例子......
通常情况下,你会在(50,25),(70,32),(16,18),(98,73)屏幕上看到4个球。好吧,这是你的抽象(为了这个例子而简化):
n = 4;
int[] { 50, 25, 70, 32, 16, 18, 98, 73 }
你“弹出”第二个消失的球,你的int []变为:
n = 3
int[] { 50, 25, 98, 73, 16, 18, 98, 73 }
(注意我们甚至不关心“清理”第4球(98,73),我们只是跟踪我们留下的球数。)
遗憾地手动跟踪物体。这是如何在移动设备上大多数当前表现良好的Java游戏上完成的。
现在对于字符串,这就是我要做的事情:
BufferedImage[10]
数组中的数字0到9。BufferedImage
重新绘制BufferedImage[10]
。这为您提供了两全其美的效果:您可以重复使用 drawtext(...)字体,并且在主循环期间创建的对象完全为零(因为您也躲过了对 drawtext(...)的调用,这本身可能非常糟糕地生成,好吧,不必要的废话。)
“零对象创建绘制分数”的另一个“好处”是对字体的仔细图像缓存和重用实际上不是“手动对象分配/解除分配” ,它真的只是小心缓存。
这不是“干净”,它不是“良好的做法”,而是它在一流的手机游戏中的表现(比如说,Uniwar)。
而且速度很快。快点快。比参与创建对象的任何更快。
PS:实际上,如果你仔细看一些手机游戏,你会注意到,通常字体实际上不是系统/ Java字体,而是专门为每个游戏制作的像素完美字体(我刚给你一个如何缓存系统/ Java字体的示例,但显然您也可以缓存/重用像素完美/位图字体。答案 1 :(得分:15)
虽然这是一个2岁的问题......
避免GC滞后的唯一且最好的方法是通过静态分配所有必需的对象(包括在启动时)来避免GC本身。预先创建所有必需的对象,永远不要删除它们。使用对象池重用现有对象。
无论如何,即使您对代码进行了所有可能的优化,也可能会出现最终暂停。因为除了您的应用代码之外的任何其他内容仍在内部创建GC对象,这最终将成为垃圾。例如, Java库。即使使用简单的List
类也可以创建垃圾。 (因此应该避免)调用任何Java API都可能会产生垃圾。当您使用Java时,这些分配是不可避免的。
此外,由于Java旨在利用GC,如果您真的试图避免使用GC,则会因缺少功能而遇到麻烦。 (应避免使用List
类)因为允许 GC,所有库可能使用GC,因此虚拟/实际上没有库。我认为在基于GC的语言上避免使用GC是一种疯狂的试验。
最终,唯一可行的方法是降低到可以完全控制记忆的较低级别。如C族语言(C,C ++等)。所以去NDK。
注意强>
现在Google正在发布增量(并发?)GC,可以减少很多停顿。无论如何,增量GC意味着随着时间的推移只分配GC负载,因此如果分配不理想,您仍然会看到最终的暂停。由于较少的批处理和分配操作开销的副作用,GC性能本身也会降低。
答案 2 :(得分:7)
我构建了自己的无垃圾版String.format
,至少是这种版本。你可以在这里找到它:http://pastebin.com/s6ZKa3mJ(请原谅德国的评论)。
像这样使用:
GFStringBuilder.format("Your score is: % and your name is %").eat(score).eat(name).result
所有内容都写入char[]
数组。我必须手动(逐个数字)实现从整数到字符串的转换,以摆脱所有垃圾。
除此之外,我尽可能使用SparseArray
,因为所有Java数据结构(如HashMap
,ArrayList
等)都必须使用装箱才能处理原始类型。每次您将int
打包到Integer
时,GC都必须清除此Integer
个对象。
答案 3 :(得分:4)
如果您不想按照提议预先呈现文字,drawText
会接受任何CharSequence
,这意味着我们可以自行实施该文字:
final class PrefixedInt implements CharSequence {
private final int prefixLen;
private final StringBuilder buf;
private int value;
public PrefixedInt(String prefix) {
this.prefixLen = prefix.length();
this.buf = new StringBuilder(prefix);
}
private boolean hasValue(){
return buf.length() > prefixLen;
}
public void setValue(int value){
if (hasValue() && this.value == value)
return; // no change
this.value = value;
buf.setLength(prefixLen);
buf.append(value);
}
// TODO: Implement all CharSequence methods (including
// toString() for prudence) by delegating to buf
}
// Usage:
private final PrefixedInt scoreText = new PrefixedInt("Your score is: ");
...
scoreText.setValue(Score.points);
canvas.drawText(scoreText, 0, scoreText.length(), x, y, paint);
现在绘制得分不会导致任何分配(除非buf
的内部数组可能必须增长,并且drawText
最多可能是什么,否则可能在开头一次或两次。< / p>
答案 4 :(得分:3)
在避免GC停顿至关重要的情况下,您可以使用的一个技巧是在您知道暂停无关紧要的时刻故意触发GC。例如,如果在游戏结束时使用垃圾密集型“showScores”功能,则用户不会因显示分数屏幕和开始下一场比赛之间额外的200ms延迟而分心...所以你可以打电话分数屏幕被绘制后System.gc()
。
但是如果你采用这个技巧,你需要小心,只在GC暂停不会烦人的地方这样做。如果您担心耗尽手机的电池,请不要这样做。
不要在多用户或非交互式应用程序中执行此操作,因为这样做很可能会使应用程序整体运行速度变慢。
答案 5 :(得分:2)
关于迭代器分配,避免使用ArrayList上的迭代器很容易。而不是
for(GameObject gob : interactiveObjects)
gob.onDraw(canvas);
你可以做到
for (int i = 0; i < interactiveObjects.size(); i++) {
interactiveObjects.get(i).onDraw();
}