安卓UI线程堆栈大小限制是多少,如何解决?

26

当我的视图层次结构被绘制时,我遇到了java.lang.StackOverflowErrors

at android.view.View.draw(View.java:6880)
at android.view.ViewGroup.drawChild(ViewGroup.java:1646)
at android.view.ViewGroup.dispatchDraw(ViewGroup.java:1373)
at android.view.View.draw(View.java:6883)
at android.view.ViewGroup.drawChild(ViewGroup.java:1646)
at android.view.ViewGroup.dispatchDraw(ViewGroup.java:1373)
...

研究指出,我的视图层次结构对Android来说太深了。使用Hierarchy Viewer,我可以看到我的最长嵌套是19个视图!

我的应用程序有点像Google Play商店应用程序(带有刷卡选项卡)。每个选项卡都是一个嵌套的片段,在片段视图页中 - 使用v4支持和HoloEverywhere。显然,这就是为什么我的层次结构变得有点疯狂。

我的问题:

  1. 真正的堆栈大小限制是多少?我找不到测量UI线程堆栈大小的方法。互联网上有些传言说是8KB,但是否有一种方法可以在某些样本设备上准确地测量?

  2. 堆栈大小限制是否随着OS版本而改变?相同的层次结构在4.0.3设备上不会崩溃,但在2.3.3设备上会崩溃(硬件相同)。为什么会这样?

  3. 除了手动优化层次结构之外,还有其他解决方案吗?我找不到增加UI线程的非常小堆栈的方法。抱歉,但60-100个堆栈帧限制就是一个笑话。

  4. 假设在#3上没有神奇的解决方案,是否有任何建议应该在哪里进行核心层次结构优化?

  5. 疯狂的想法 - 我注意到每个视图层都会添加大约3个函数调用(View.draw,ViewGroup.dispatchDraw,ViewGroup.drawChild)。也许我可以创建自己的ViewGroup实现(自定义布局),在draw()期间浪费更少的堆栈?


类似于我疯狂想法#5的想法:http://stackoverflow.com/questions/6705425/fixing-the-stackoverflow-error/14885190#14885190 - talkol
澄清一下,寻找堆栈大小的原因是估计市场规模存在问题。由于操作系统的新版本似乎具有更大的堆栈,我想找出我容易崩溃的地方,并看看是否能够应对。 - talkol
你真的应该解决那个19级嵌套视图层次结构(它并没有真正证明自己的必要性)。看看我能不能忍受它,但是怎么忍受呢?你只是让设备崩溃(还有你的应用评分)吗?Gingerbread占据了大约40%的市场,再加上劣质的硬件将导致相当一部分的市场无法满足你的需求(更糟糕的是,你可能会遇到随机设备崩溃)。 - user
好的,将19优化到更少当然是有代价的。我应该降低到多低呢?我想得到某种度量标准,这样我就可以精确地计算我想要玩得有多安全。随意说最高15级对我来说有点太过了。 - talkol
小的堆栈大小对使用递归的数据序列化来说是一个更大的问题。 - ballzak
显示剩余2条评论
5个回答

28

我认为主线程的堆栈由JVM控制 - 在Android的情况下是Dalvik JVM。如果我没有弄错,相关的常量可以在 dalvik/vm/Thread.h 文件中的 #define kDefaultStackSize 找到。

通过 dalvik 源代码历史记录查找堆栈大小:

那么你能有多少个嵌套视图呢:

无法准确说出。假设堆栈大小如上所述,这完全取决于最深度嵌套中有多少函数调用(以及每个函数需要多少变量等)。似乎问题的区域是在绘制视图时,从 android.view.ViewRoot.draw() 开始。每个视图都会调用其子视图的绘制方法,并且它会一直延伸到最深的嵌套层级。

我会至少在所有边界组中出现的设备上进行经验测试。使用模拟器似乎可以得到准确的结果(尽管我只将本机x86模拟器与真实设备进行了比较).

请记住,对实际小部件/布局实现的优化也可能影响此结果。话虽如此,我认为大多数栈空间都被每个布局层次结构吃掉,每个层次结构添加约3个嵌套函数调用:draw() -> dispatchDraw() -> drawChild(),并且这个设计从2.3-4.2没有太大变化。


2
在Android 4.4中,根据http://developer.android.com/guide/practices/verifying-apps-art.html,它是32KB。 - Omer van Kloeten
根据我的经验,一些设备制造商会更改堆栈大小。我建议检查应用程序是否可以在较小的堆栈大小上运行。您可以为模拟器设置堆栈大小:https://medium.com/appunite-edu-collection/how-to-find-stackoverflowerrors-99264e9709f8 - Jacek Marchwicki

2
我建议您查看这个这个,它们可以解决您的许多问题,并帮助您更好地理解为什么不需要在UI线程中使用较大的堆栈。希望对您有所帮助!

1
我不知道栈大小限制是多少,而且坦白说,我认为查找这个限制可能没有什么用。正如你的第二个建议所表明的那样,它很可能取决于设备上存在的Android版本和/或Dalvik VM的版本。
至于优化布局,一些选项包括:
1. 使用RelativeLayout而不是嵌套ViewGroups,特别是在LinearLayouts内部嵌套LinearLayouts,以帮助打平您的视图层次结构。这不是一个万能的解决方案;事实上,嵌套RelativeLayout可能会影响性能(因为RelativeLayout总是会measure()两次,因此将它们嵌套会对测量阶段产生指数效应)。
2. 使用自定义Views/ViewGroups,根据你的第五个问题。我听说过有几个应用程序这样做,甚至我认为一些Google应用程序也是这样做的。
3. 如果您在视图层次结构中发现任何无用的子级,则可以尝试在某些布局中使用<merge>标记(然而,我自己并没有发现它们有什么用处)。

1
搜索堆栈大小背后的原因是估计市场规模,这是有问题的。由于操作系统的新版本似乎具有更大的堆栈,我想找出我容易崩溃的地方,并看看是否能够应对。 - talkol

0
疯狂的想法5 - 也许不那么疯狂。我向你解释这个理论,你可以尝试实现它。假设我们有3个嵌套视图A > B > C。不要将C嵌套在B中,而是将其嵌套在D(某个不相关的视图)中,当B要绘制自己时,他需要调用B.draw()。当然,你可能会遇到布局问题。但是可以找到解决方案。

这会如何帮助呢?你期望在移动视图时一切都保持不变吗? - user
@Luksprog 我曾经做过类似的事情。但我的视图是全屏大小的。我只使用了画布缓冲区,在不同的视图上重新绘制画布源图像。这样我甚至都不需要添加它们。 - Ilya Gazman
有趣的是,我认为@Babibu建议的是打破嵌套,并将一些视图放置在另一个与B无关但大小相同的顶级视图D下。然后当B需要绘制自己时,我们可以获取D的画布内容。我的理解正确吗?这很有趣,但我也想知道事件怎么办...需要以某种方式转发它们。 - talkol
我还在另一个回答中添加了对我的原始疯狂想法5的另一种解释,请告诉我您的想法。 - talkol

0

关于我疯狂想法5的更好解释。我并不是说这是个好主意 :) 但我想澄清一下如何实现它。

我们将为基本布局(LinearLayout、FrameLayout、RelativeLayout)创建自己的实现,并使用它们代替原始布局。

新布局将扩展原始布局,但覆盖draw()函数。

原始绘制函数调用dispatchDraw(),后者调用drawChild(),然后您才能进入子元素的draw()。这意味着每个嵌套中的draw()都会增加3个函数调用。我们将尝试手动将其最小化为1个函数调用。

这是令人讨厌的部分。您是否熟悉inline函数?理论上,我们尝试使对dispatchDraw()drawChild()的调用内联。由于Java实际上不支持内联,最好的方法是手动将它们内联(实际上将代码复制粘贴到单个令人讨厌的函数中)。

为什么会变得有点复杂呢?我们将采用的实现来自Android源代码。问题在于,这些源代码在Android版本之间可能略有改变。因此,需要映射这些更改并在代码中制作一组开关,以便完全像原始版本一样运行。

看起来工作量太大了,而且还是一个非常不规范的做法。


这不是一个解决方案,为了做到正确而付出的麻烦并不能证明其自身的价值。 - user
我同意。这太复杂了,而且没有证明自己的价值。虽然想想很有趣 :),而且我相信它完全可以解决堆栈问题(至少可以让你拥有大约两倍的视图)。 - talkol

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接