[译]FaceBook出品:基于Android的内存优化

原文Memory optimization for feeds on Android——[Udi Cohen](https://www.facebook.com/udinic)

数以百万的人在Android设备上运行FaceBook,浏览着新闻,资讯,大事,页面,朋友圈和他们关心的信息。一个叫Android Feed Platform的团队创建了一个新平台来提供这些信息。因此任何对这个平台的优化,都惠及我们的应用。我们下面着重于滚动的效率,希望能在满足人们求知欲的同时享受如丝顺滑的体验。

为了实现这个目标,我们做了几个自动化工具来测试平台在不同场景和设备上运行的性能,以此衡量出代码在运行时的内存使用率,帧率等。当使用其中一个工具,TraceView,测试发现对Long.valueOf()有频发的调用,使内存中堆积的对象过多,导致崩溃。这篇文章描述了如何解决这个问题,我们权衡的潜在的解决方案的过程,和我们对平台所做的最终优化。

方便的缺点

通过TraceView发现Long.valueOf()的频发调用后,我们进一步地测试发现,在滚动新闻时,意料之外的是这个方法被更频发地调用了。

当我们查看调用堆栈时,发现方法调用者不是Facebook的代码而是编译器的隐式代码插入。调用这个方法用于把long装箱成Long。Java既支持复杂类型,也支持基本类型(像integer,long等关键字),并提供无缝转换。这个特性称为自动装箱,把一个基本类型转化为对应的复杂数据类型。虽然是很便利的特性,同时也创建了对开发者透明的对象。

这些未知对象占用内存总和。

app的heap dump检测出来Long对象占用了很大一部分内存;但是每个对象本身并不大,数量之多令人咂舌。特别使用Dalvik时容易发生问题,不比新一代的Android运行时ART,拥有分代垃圾回收机制,能选择更合适的时机回收数量众多的小对象。当我们将新闻列表上下滚动时,大量对象被创建,垃圾收回策略会让应用暂停,去清理内存。越多的对象堆积,内存回收就更为频繁,导致应用卡顿甚至崩溃,给用户留下劣质的体验。

幸运的是,拥有TraceViewAllocation Tracker这样的好工具,帮我们分析出卡顿的原因,问题处在自动装箱上。我们发现大部分的操作,是把long插入到HashSet<Long>时发生的。(我们使用Hashset<Long>来存储新闻的哈希值,校验新闻是否唯一)。HashSet提供快速的对象获取,因为哈希值计算出来由long表示,然而HashSet<Long>是与复杂对象交互的,当我们调用setStories.put(lStoryHash)时,自动装箱无可避免。

解决方案是,继承Set,使其能与简单类型交互,结果并未如我们想象那么简单。

可行的方案

存在一些与简单类型交互的Set子类,这些库几乎都是十年前的当Java还是J2ME的年代。为了彰显新时代的活力,我们决定在Dalvik/ART上进行测试,确保他们能在更苛刻的条件下表现良好。我们写了个轻量级的测试框架,这些老库将与HashSet一较高下。结果显示这些老库都比HashSet速度快,占用内存少,但是它们内部还是会创建对象,例如TLongHashSet, Trove库的一个类,用1000个例子测试大概分配了2MB内存。

其他测试库,比如PCJColt,结果几乎一致。

已存在的轮子并不符合我们的需求,所以就为Android创建一个合适的Set轮子。看看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里添加对象意味这往HashMap里面添加键和HashSet自己的实例作为值。HashSet通过内部HashMap是否包含相同的键值来校验插入的对象。可以选择使用Android优化过的Map来优化HashSet

介绍LongArraySet

也许你很熟悉LongSparseArray,它是Android提供的一个以long为键的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是这样查询的

整个取值的过程是,用键值的哈希值作为下标,在列表中获取值。时间复杂度是O(1)

LongSparseArray取值是这样的。

LongSparseArray通过二分查找法在有序的键列表中找到键,时间复杂度是O(log N),通过键的下标在值队列中取到对应值。

HashMap需要额外分配空间来避免冲突,导致寻址变慢。LongSparseArray创建两个列表使占用内存小,但为了支持二分查找法,需要一块连续的内存空间。当添加的数量大于当前连续内存数时,需要一整块新的连续内存。当总长度超过1000时,LongSparseArray的表现就不太理想啦,存在巨大的性能问题(参考官方文档或者看Google的短视频

因为LongSparseArray的键是简单类型的,我们可以创造一个新的数据结构,把LongSparseArray作为HashSet的内部类来使用。

就叫LongArraySet吧!

新的数据结构看起来是有希望的,但优化的第一条规则是“测试”。通过先前的测试框架,我们把新的数据结构与HashSet进行比对,通过添加X个item来测试,检查它们内部的结构,随即移除他们。在添加不同数量(X=10,X=100,X=1000….),同时平均下来每次的操作时间,结果是:

我们发现ContainsRemove操作在运行环境下都有性能提升。随着数量的增加,Add操作耗费更多的时间。这个上面LongSparseArray内部结构造成的——当数量超过1000时,表现就比不上HashMap。在我们自己的应用中,只需要处理几百个item,值得替换一下。

我们也看到内存使用的提升,在看HeapAllocate Tracker时,我们发现对象创建的数量减少了,下面是

HashSetLongArraySet在20次循环中添加1000个item的对比结果。

LongArraySet能很好地避免创建Long对象,在这个场景下减少了30%的内存分配。

结论

通过深入了解其他数据结构,我们能够创建出更适合自身的数据结构。垃圾回收运行的次数越少,掉帧的情况发生就越少。使用新的数据结构LongArraySet,和以int为键的IntArraySet,就能优化整个应用的内存使用。

这个例子表明任何可行的方法都需要衡量得出最优解。我们也接受这个方案并不是放之四海而皆准的,对于重量级数据而言,这个提升是有限的,但对于我们来说,是更优解。

你可以在这里找到源码,我们也为接下来面临的挑战而感到兴奋,希望日后还有机会分享更多的干货。

时间: 2024-10-18 02:28:50

[译]FaceBook出品:基于Android的内存优化的相关文章

Android的内存优化

腾讯公司在五月三十一日开展[腾讯Bugly移动开发者沙龙]大会,大会上面叶方正老师讲解了 关于Android的内存优化的问题,不过我感觉叶老师更多的站在了测试的角度上去解释了这一方面,叶老师给我们介绍了很多的工具去测试Android应用在各种情况下的内存占用情况,不过好像对我们开发的帮助并不是特别的大.我在这里总结叶老师所说的重点和自己对内存优化的一些理解,希望能够对大家有所帮助. Android应用优化主要集中在内存和UI流畅度上,从内存占用与泄露.UI流畅度的帧数和响应时间到IO的阻塞式响应

Android代码内存优化建议-OnTrimMemory优化

原文  http://androidperformance.com/2015/07/20/Android代码内存优化建议-OnTrimMemory优化/ OnTrimMemory 回调是 Android 4.0 之后提供的一个API,这个 API 是提供给开发者的,它的主要作用是提示开发者在系统内存不足的时候,通过处理部分资源来释放内存,从而避免被 Android 系统杀死.这样应用在下一次启动的时候,速度就会比较快. 本文通过问答的方式,从各个方面来讲解 OnTrimMemory 回调的使用过

基于Android官方AsyncListUtil优化改进RecyclerView分页加载机制(一)

基于Android官方AsyncListUtil优化改进RecyclerView分页加载机制(一) Android AsyncListUtil是Android官方提供的专为列表这样的数据更新加载提供的异步加载组件.基于AsyncListUtil组件,可以轻易实现常见的RecyclerView分页加载技术.AsyncListUtil技术涉及的细节比较繁复,因此我将分别写若干篇文章,分点.分解AsyncListUtil技术. 先给出一个可运行的例子,MainActivity.java: packag

Android代码内存优化建议-Android官方篇

转自:http://androidperformance.com/ http://developer.android.com/intl/zh-cn/training/displaying-bitmaps/index.html 为了使垃圾回收器可以正常释放程序所占用的内存,在编写代码的时候就一定要注意尽量避免出现内存泄漏的情况(通常都是由于全局成员变量持有对象引用所导致的),并且在适当的时候去释放对象引用.对于大多数的应用程序而言,后面其它的事情就可以都交给垃圾回收器去完成了,如果一个对象的引用不

Android APP内存优化之图片优化

网上有很多大拿分享的关于Android性能优化的文章,主要是通过各种工具分析,使用合理的技巧优化APP的体验,提升APP的流畅度,但关于内存优化的文章很少有看到.在Android设备内存动不动就上G的情况下,的确没有必要去太在意APP对Android系统内存的消耗,但在实际工作中我做的是教育类的小学APP,APP中的按钮.背景.动画变换基本上全是图片,在2K屏上(分辨率2048*1536)一张背景图片就会占用内存12M,来回切换几次内存占用就会增涨到上百兆,为了在不影响APP的视觉效果的前提下,

Android中内存优化

CSDN博客不写,排名会下降,我知道了...... Android内存优化,设计到很多方面,参考别大神的博客,自己也总结一下..... 下面将通过两篇博客,浅析Android 中的内存优化问题.来张图抖索一下精神.... 本片博客将一下内存优化,主要参考工作经验和借鉴大牛的一些博客...... 一.什么是内存? 简单理解,Android内存包括运行内存RAM.和磁盘缓存ROM. 而内存优化,主要值运行内存的优化. RAM(random access memory): 寄存器(Registers)

android开发内存优化之软引用

所有Android的开发者一定都遇到过内存溢出这个头疼的问题,一旦出现这个问题,很难直接确定我们的应用是那里出了问题,要想定位问题的原因,必须通过一些内存分析工具和强大的经验积累才能快速的定位到问题具体出现在那里. 基于移动开发具有的这个特性,本着尽量减少内存消耗的原则,以及我最近遇到的内存堆积(偶尔溢出)问题,总结一下这次解决这个问题的经验. 问题源头:开始App功能没那么多的时候,是没有注意到这个问题的,后来功能越强越多,图片也越来越多的时候,用ADT自带的 Allocation Track

从Android资源角度谈Android代码内存优化

原文链接:http://www.codeceo.com/article/android-resource-android-mem.html 这篇文章主要介绍在实际Android应用程序的开发中,容易导致内存泄露的一些情况.开发人员如果在进行代码编写之前就有内存泄露方面的基础知 识,那么写出来的代码会强壮许多,写这篇文章也是这个初衷.本文从Android开发中的资源使用情况入手,介绍了如何在Bitmap.数据库查询.9- patch.过渡绘制等方面优化内存的使用. Android资源优化 1. B

Android图片内存优化,让app更合理的加载图片

在Android系统中,其实操作的是位图,即:Bitmap.我们知道在windows.macos等一些系统上支持jpg.png.webp等图片格式.为了提高文件的传输以及显示速度可以把图片经过算法把文件压缩成不同的格式,或者使文件变小就可以达到目的.但是在android系统上显示完全就不是这么回事了. 在Android显示一张图片,不管图片是JPEG.PNG.WEBP格式的都需要把这些图片解码成位图格式后Android系统才能正常的显示出来. 我们都知道图片在app中是非常的占用内存的,具体为什