来了,Facebook APP Feed流的内存优化实践

点击“开发者技术前线”,选择“星标?”

13:21 在看|星标|留言,  真爱


640?wx_fmt=jpeg

翻译:可可 |英文:https://code.facebook.com/posts/973222319439596

引言

大量的用户每天在Android设备上使用Facebook,滚动新闻Feed流页面,包括个人资料,活动,页面和组,与他们关心的人员和信息进行互动等一系列行为。所有这些不同的Feed类型都由Android Feed Platform小组创建的平台提供支持,因此我们对Feed平台进行的任何优化都可能提高我们的应用程序的性能。我们专注于页面的滚动性能,因为我们希望用户在滚动他们的Feed流页面时有一个平滑的体验。

为了帮助我们实现这一点,我们有几种自动化工具,可以跨不同的场景和不同的设备在Feed平台上运行性能测试,测量代码在运行时内存使用,帧速率等方面的运行情况。其中一个工具Traceview显示了我们的程序对Long.valueOf()函数的调用次数相对较多,这导致对象在内存中累积并导致应用程序卡顿停止等。这篇文章描述这个问题,我们权衡了各种潜在解决方案之后,对改进Feed流平台而进行的一系列优化。

便利性带来的缺点

我们从Traceview的一个方法分析报告中注意到:facebook的app对Long.valueOf()函数的大量调用。之后,我们进行了又进一步的测试,证实了当我们滚动新闻列表时,Long.valueOf()方法的调用会意外升高。

640?wx_fmt=jpeg

当我们查看堆栈时,我们发现这个函数没有被直接的从Facebook的代码调用,而是隐式地由编译器插入的代码。在分配长整型对象的原始长值时调用此函数。Java支持对象和原始的简单类型(例如,整数,字符),并提供了一种在它们之间无缝转换的方式。这种方式称为自动装箱,因为它将基本类型装箱为相应的类型的对象类型。虽然这是一个方便的开发功能,但是它同时也创建了开发人员不知道的新对象。

640?wx_fmt=jpeg

在对一个示例应用程序的堆栈中发现Long对象有大量的存在; 虽然每个对象本身都不大,但是存在的大量的Long对象占据了应用程序在堆中的大部分内存。对于运行Dalvik的设备来说,会有很大的影响。与Android的ART运行时环境不同,Dalvik没有一代间垃圾回收机制,造成很多小对象的垃圾回收效率很低。当我们滚动新闻Feed流,会造成Long对象数量增加,垃圾收集将导致应用程序卡顿来从内存中清除未使用的对象。积累的对象越多,垃圾收集器将越来越频繁地暂停应用程序,导致卡顿使得户体验不佳。

幸运的是,Traceview和Allocation Tracker等工具可以帮助我们找到这些函数调用的位置。在查看了这些自动装箱事件的根源之后,我们发现大多数成因都是:将Long类型的基本类型数据插入HashSet <Long>数据结构中造成。(我们使用这个数据结构存储新闻Feed的哈希值,稍后检查某个哈希是否已经在Set中。)HashSet提供对具体feed的快速访问。由于哈希计算并存储在一个原始的长变量中,然而我们的HashSet仅适用于对象,所以当调用set.put(Hash)时,我们会得到不可避免的自动装箱。

作为一个解决方案,可以使用基本数据类型而不是对象类型的Set实现,但是结果并不像我们预期的那么简单。

目前的解决方案

有几个现有的Java库为原始数据类型提供了Set实现。几乎所有这些类库都是10多年前创建的,当时在移动设备上运行的唯一的Java是J2ME。为了确定可行性,我们需要在Dalvik / ART下进行测试,并确保它们在资源更受限的移动设备上表现良好。我们创建了一个小型测试框架来帮助将这些库与现有的HashSet进行比较。结果表明,这些库中的一些库具有比HashSet更快的运行时间,并且具有较少的Long对象,但是它们仍然在内部分配了很多Long对象。例如,Troow库中的一部分TLongHashSet在测试时分配了大约2 MB的对象,共有1,000个item

640?wx_fmt=jpeg

对其他的类库进行测试,包括PCJ和Colt, 显示了类似的结果。

现有的解决方案不符合我们的需求。我们考虑是否可以创建一个新的Set实现,并针对Android进行优化。在Java的HashSet中,使用单个HashMap来实现一个相对简单的实现。


public class HashSet&lt;E&gt; extends AbstractSet&lt;E&gt; implements Set&lt;E&gt;, ... {

    transient HashMap&lt;E, HashSet&lt;E&gt;&gt; backingMap;    

    ...

    @Override public boolean add(E object) {
        return backingMap.put(object, this) == null;    
    }

    @Override public boolean contains(Object object) {
        return backingMap.containsKey(object);    
    }

    ...        
}	

向HashSet添加新item意味着将其添加到内部HashMap,其中对象是关键字,而HashSet的实例是该值。要检查对象成员身份,HashSet将检查其内部HashMap是否包含对象作为键。可以使用Android优化的map和相同的原则来实现HashSet的替代方案。

引进LongArraySet

你可能已经熟悉了LongSparseArray,它是Android支持类库中的一个类,用作使用long类型作为key的map。使用示例

LongSparseArray&lt;String&gt; longSparseArray = new LongSparseArray&lt;&gt;();
longSparseArray.put(3L, "Data");String data = longSparseArray.get(3L); // the value of data is "Data"

LongSparseArray的工作方式与HashMap不同。当调用mapHashmap.get(KEY5)时,下图说明了如何在HashMap中找到该值:

640?wx_fmt=jpeg

当使用HashMap上的键检索值时,它使用密钥的哈希值作为索引访问数组中的值,即O(1)时间复杂度的的直接访问。对LongSparseArray进行相同的调用如下所示:

640?wx_fmt=jpeg

LongSparseArray使用二分搜索,运行时间为O(log N)的时间复杂度操作搜索排序密钥数组的密钥值。数组中的键的索引值用于查找values数组中的值。

HashMap分配一个大数组,以避免hash冲突,但是这样导致搜索速度较慢。LongSparseArray分配两个小数组,使其内存占用更小。但是为了支持其搜索算法,LongSparseArray需要在连续的内存块中分配其内部数组。添加更多的item将需要在当前空间不足的情况下分配新的数组。LongSparseArray的工作原理使得它在保存超过1,000个项目时效率下降,这些差异对性能有更重要的影响。(您可以在官方文档中了解有关LongSparseArray的更多信息,并通过观看Google的简短视频。)

由于LongSparseArray的键是原始long类型,所以我们可以使用与HashSet相同的方法创建一个数据结构,使用LongSparseArray作为内部映射而不是HashMap。

建立LongArraySet

新的数据结构更加合理。通过使用与之前相同的测试框架,我们将新的数据结构与HashSet进行了比较。每个数据结构都通过添加X个item进行测试,检查每个item的存在,然后删除所有item。我们使用不同数量的item(X = 10,X = 100,X = 1,000 ...)运行测试,并平均每个item完成每个操作所花费的时间。

运行时结果(时间显示为纳秒):

640?wx_fmt=jpeg

我们看到使用新数据结构的contains和delete方法的运行时效率改进。另外,随着数组中item数的增加,添加新item花费更多时间。这与我们已经知道的LongSparseArray是一致的 ,当item数量超过1,000时,它与HashMap的表现不一样。在我们的用例中,我们只处理了数百个item,所以这是一个我们愿意做的权衡。

我们也看到了内存使用有很大的改善。在查看堆转储和分配跟踪报告时,我们注意到对象分配的减少。下面是当添加1,000个item进行20次迭代时,HashSet和LongArraySet实现的并行分配报告:

640?wx_fmt=png

除了避免所有Long对象分配之外,LongSparseArray更具有内存效率,在这种情况下的分配减少了约30%。

结论

通过了解其他数据结构如何工作,我们能够为我们的需求创建一个更优化的数据结构。垃圾收集器必须工作的越少,这样丢帧的可能性就越低。使用新的LongArraySet类和类似的IntArraySet作为原始int数据类型,我们能够在整个应用程序中减少大量的对象内存分配。

这个案例研究表明了我们选择数据结构的重要性。虽然这种解决方案对于所有用例来说并不完美,因为这种实现对于非常大的数据集来说较慢,但是还可以继续对我们的代码进行优化。

你可以在下面网址找到两个数据结构的源代码。我们很高兴继续努力应对挑战,优化我们的Feed平台,并与社区分享我们的解决方案。

---END---

选择”开发者技术前线 “星标?,内容一触即达。点击原文更多惊喜!

开发者技术前线 汇集技术前线快讯和关注行业趋势,大厂干货,是开发者经历和成长的优秀指南。

历史推荐

支付宝 App 启动性能优化

为什么我不推荐你用 Flutter 开发?

今日头条 App 屏幕适配方案落地研究

640?

点个在看,解锁更多惊喜!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值