FaceBook安卓Feed流的内存优化实践

引言

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

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

便利性带来的缺点

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

这里写图片描述

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


这里写图片描述

在对一个示例应用程序的堆栈中发现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

这里写图片描述

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

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

public class HashSet<E> extends AbstractSet<E> implements Set<E>, ... {
    
    transient HashMap<E, HashSet<E>> 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<String> longSparseArray = new LongSparseArray<>();
longSparseArray.put(3L, "Data");
String data = longSparseArray.get(3L); // the value of data is "Data"

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

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

这里写图片描述

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完成每个操作所花费的时间。

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

这里写图片描述

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

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

这里写图片描述

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

结论

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

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

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

https://code.facebook.com/posts/973222319439596

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容