践行

践行

网络较差的情况下,怎样提供流畅的ios应用体验

作者/Gaurav Vaish

就职于雅虎公司的移动和新兴产品团队,为每月有数亿人使用的移动应用创建优雅的可重用方案。他曾是IIT全球指导计划的成员,还在印度班加罗尔创立了InColeg Learning及Edujini Labs有限公司。

在移动端应用中使用网络是必不可少的,但减少网络延迟的方法却是有限的,因此,你应该着手对网络条件进行最大程度的优化,并预先对不同的场景进行规划。

这篇文章将重点关注影响整体延迟的因素,并讨论如何充分利用现有的信息来最大程度地提高性能。

在网络中完成的大多数工作是你无法控制的,因此确定衡量的标准非常重要。我们将在这部分讨论一些较为重要的性能相关指标。请注意,此处并非要列举出所有的指标,只是挑出在性能优化相关的测量中更为重要的一些指标。

图1 展示了典型的网络请求全景图。

{%}

图 1 网络——设备至服务器

后续讨论的大致结构是:先进行相关指标的说明,然后是一个或多个示例,最后是最佳实践。

DNS查找时间

发起连接的第一步是 DNS 查找。如果你的应用严重依赖网络操作,DNS 的查找时间会使应用变慢。在一个关于两个位置的统计样本中,从加利福尼亚州的森尼韦尔市访问 www.google.com 时,DNS 查找时间是 2846 毫秒,而从印度的新德里访问时只花费了 34 毫秒(见图2)。

查找时间与主 DNS 服务器的性能成函数关系。最终的连接时间与追踪到目的 IP 地址的路由成函数关系。

使用内容分发网络(content delivery network,CDN)将延迟最小化是一种常见的做法。在图2 中,你应该注意到 www.google.com 在两个地点解析的 IP 地址是不同的。在森尼韦尔市,它被解析成了美国的一台服务器,而在新德里,它被解析成了印度的服务器。但因为 DNS 会为每一个独有的子域名进行查找,所以,拥有多个 CDN 主机名会导致应用的速度变慢。

{%}

图 2 www.google.com 的 DNS 查询时间——加州的森尼韦尔市(上)和印度的新德里(下)

为了最大限度地减少 DNS 查询时间所产生的延迟,你应该遵循以下的最佳实践。

  • 最小化应用使用的专有域名的数量。按照路由的一般工作方式,多个域名是不可避免的。最好是能做到以下几点:

    (1) 身份管理(登录、注销、配置文件)

    (2) 数据服务(API 端点)

    (3) CDN(图片和其他静态人工产品)

    有可能需要其他域名(例如,用于提供视频、上传检测数据、具体的子数据服务、广告投放,甚至是国家特定的全球本地化)。如果子域名数量上升至两位数,那么势必会引发担忧。

  • 在应用启动时不需要连接所有的域名,可能只需要身份管理和初始画面所需的数据。对于后续的子域名,尝试更早地进行 DNS 解析,也被称为 DNS 预先下载。为实现此操作,你可以参考以下两点。

    如果子域名和主机在控制范围内,你可以配置一个预设的 URL,不返回任何数据,只返回 HTTP 204 的状态码,然后提前对该 URL 发起连接。

    第二个方法是使用 gethostbyname 执行一个明确的 DNS 查找。然而,针对不同的协议,主机可能会解析至不同的 IP,例如,HTTP 请求可能会解析至一个地址,而 HTTPS 会解析至另一个地址。虽然不是很常见,但第 7 层的路由可以根据实际的请求解析 IP 地址,例如,图像是一个地址,视频是另外一个地址。鉴于这些因素,在连接之前解析 DNS 经常是无用的,对主机进行伪连接会更有效。

SSL握手时间

为了安全起见,可以假设应用中所有的连接均是通过 TLS/SSL 的(使用 HTTPS)。HTTPS 在连接开始时,先进行 SSL 握手,SSL 握手主要是验证服务器证书,同时共享用于通信的随机密钥。这一操作听起来简单,但是却有很多步骤,还会耗费较多时间(见图3)。

{%}

图 3 SSL 握手

你应该遵循以下的最佳实践。

  • 最大程度地减少应用发起的连接数。因此,也需要减少应用连接的独有域名的数量。
  • 请求结束后不要关闭 HTTP/S 连接。

为所有的 HTTPS 请求添加头 Connection: keep-alive。这确保了同样的连接在下一次请求时可以复用。

  • 使用域分片。如此一来,虽然连接的是不同的主机名,你也可以使用同一个 socket,只要它们解析为相同的 IP,可以使用相同的证书(例如,在通配符域)就行了。

    域分片在 SPDY 及其后续版本——HTTP/2(https://http2.github.io)——中是可用的。你需要一个支持上述任意一种格式的网络库。

网络类型

由于用户逐步丢弃了桌面设备,这也就放弃了总是处于连接状态的高速带宽网络,转而使用有同等质量的 WiFi 网络或使用间歇连接的带宽可变的移动网络。更有挑战的场景是,用户是移动的。当设备在移动信号塔之间切换时,网络和质量也会发生变化。一个设备可能在任何时刻从 LTE 网络切换到 GPRS 或进入无信号区域。对于这种情况,人们往往束手无策。

主机的可到达性

你可以使用苹果公司的可到达性库(https://developer.apple.com/library/ios/samplecode/Reachability/Introduction/Intro.html)或使用 Tony Million 对该库的简易替换(https://github.com/tonymillion/Reachability),主机的可到达性发生变化时,替换的库支持回调的调用。

如果设备闲置超过几秒(具体的值是不确定的,这里的几秒也可能变成几分钟),网络无线电可能已经关闭,这将导致无线资源控制器发生额外的几百或几千毫秒的延迟。

先确定主机的可到达性,从而确保应用具备处理此种场景的能力。

一般情况下,iPhone 和 iPad 可以使用下列任何网络连接到互联网。

  • WiFi

    如果 WiFi 网络是私有网络(如家庭或办公室连接),那么你可以期望连接至互联网的网络是持续且质量较好的。

    但是,处于 WiFi 网络中并不能保证一定连接上了互联网。例如,当设备连接到某一公共热点(例如,在旅馆或购物商场)时,如果用户没有成功地提供适当的凭证,那么将无法访问互联网。

    即使设备已经连接至互联网,也可能有一些限制,比如可连接的域名或端口限制。举个例子,www.google.comwww.yahoo.com 域可能是允许的,但 mail.google.com 或 mail.yahoo.com 却可能无法使用。

  • 4G:LTE、HSPA+(高速的数据网络)

    这些是最新一代的数据网络。在第一个真正的业务数据发送前,一般会有 100~600 毫秒的延迟。这些网络以亚毫秒的间隔动态地创建无线相关的资源,并且爆发性地发送数据。理论上来说,速度会从 100Mbps 浮动至 1Gbps。对于高速率移动的通信,如在汽车或火车上,速度可能会在 100Mbps;对于低速率移动的通信,如行人或静止的用户,速度可能会达到 1Gbps。

  • 3G:HSDPA、HSUPA、UMTS、DMA2000(中等速度的数据网络)

    这些是上一代的数据网络,但使用频率可能比 LTE 更频繁。

    3G 的速度可能会从 200Kbps 变化至超过 50Mbps。双向的传输速度可能并不对称。HSDPA 具有较高的下行速度,HSUPA 具有较高的上行速度。

  • 2G:EDGE、GPRS(低速的数据网络)

    20 世纪 90 年代的网络仍然没有消亡。这些都是初始数字网络(第 1 代网络使用模拟信号),提供了较低的带宽。EDGE 理论上具有 500Kbps 的极限速度,而 GPRS 最高只能达到 50Kbps。

    可到达性库可以给出访问主机的详细网络信息。使用此信息来确定传输内容的类型(例如,文本、图像或是视频),以及传输的各种项目的大小,等等。

0.2%的数据传输;46%的功耗!

2011 年,密歇根大学和 AT & T 发布了“移动应用性能资源使用情况”(http://mobilityfirst.winlab.rutgers.edu/documents/mobisys11.pdf),这篇研究性论文分析了移动应用的网络使用和功耗效率。

论文探讨了潘多拉公司,将其在移动网络的间歇性网络传输的低效率问题作为经典案例研究。虽然问题已经修复了,但案例分析还是值得阅读的。

当播放一首歌曲时,应用会将这首歌全部下载,这是正确的行为:下载尽可能多的数据,让无线电关闭的时间尽可能长。

但是,在传送之后,应用会每 60 秒定期地发送检测事件。这些事件仅占传输总字节的 0.2%,但却占了应用总功耗的 46%。

事件的数据通常是比较小的,但因为无线电在较长的时间都会保持激活状态,所以它将应用的电量消耗增加了一倍。

通过将这些数据分发至一些请求之中,或在无线电已经处于激活状态时再发送数据,就可以消除不必要的能量拖尾,实现更高的电源效率。

为确保你的应用不会成为类似案件研究的一部分,在开发以网络为中心的应用时,你可以遵循以下的最佳实践。

  • 设计时考虑不同的网络可用性。在移动网络中,唯一不变的是,网络可用性是多变的。对于流媒体,最好选择 HTTP 实时流或任何可用的自适应比特率流媒体技术,这些技术可以在某一时刻针对可用带宽进行动态切换,切换至当前带宽的最佳流质量,从而提供流畅的视频播放。

    对于非流媒体内容,你需要实现一些策略,确定在单次拉取时应该下载多少数据,并且数据量必须是自适应的。例如,你可能不希望在最新一次更新时,一次拉取所有的 200 封新邮件。你可以先下载前 50 封邮件,再逐步下载更多邮件。

    同样,在低速网络时,不要打开视频自动播放功能,这可能会花费用户很多钱。

    对于自定义的非流媒体数据拉取,要保持对服务器的关注。让客户端发送网络特征数,服务器决定返回的记录条数。这样一来,你可以在不发布应用新版本的情况下进行适应性改变。

  • 出现失败时,在随机的、以指数增长的延迟后进行重试

    例如,第一次失败后,应用可能会在 1 秒后重试。第二次失败时,应用在 2 秒后重试,接着是 4 秒的延迟。不要忘记对每个会话设置最多的自动重试次数。

  • 设立强制刷新之间的最短时间。当用户明确要求刷新时,不要立即发出请求。相反,检查是否已经存在一个请求,或当前请求与上次请求的时间间隔是否小于阈值。如果满足上述条件,则不要发送此次请求。

  • 使用可到达性库发现网络状态的变化。如图4 所示,使用指示条向用户展示不可用的状态,毕竟设备没有网络连接并非你的错。通过让用户了解潜在的连接问题,可以避免你的应用受到指责。

    {%}

    图 4 Spotify、Facebook 和 TOI 针对离线网络状态的指示条

  • 不要缓存网络状态。不论是通过触发请求时的回调来获取状态,还是在发送请求之前显式地检查状态,要始终使用网络敏感度高的任务的最新值。

  • 基于网络类型下载内容。如果想要展示一个图像,不用总是下载原始的、高质量的图像。应该始终下载和设备适配的图像——iPhone 4S 所需的图像尺寸和第三代 iPad 所需的差别很大。

    如果应用有视频内容,最好有一个与之关联的预览图像。如果应用支持自动播放功能,在非 WiFi 网络中只展示预览图像,因为自动播放会花费用户很多钱。

    此外,针对图像、音频和视频等,提供一个关闭自动下载或关闭自动播放功能的选项。图5 展示了 WhatsApp 应用中相关的设置。

    {%}

    图 5 WhatsApp 中对图像、音频和视频内容下载的可选设置

  • 乐观地预先下载。在 WiFi 网络中预先下载用户在后续时刻需要的内容。随后就可以使用缓存内容了。最好分次下载内容,在使用之后关掉网络连接,这有助于节省电量。

    预先下载永远都是争论的焦点。在下载最少数据和获取最近可能需要使用的所有内容之间,人们总会存在激烈争执。

    没有黄金法则可循。这在很大程度上取决于平均数据的大小、完成的下载数、预期的使用模式和网络条件。如果网络不断变化,而且你需要执行最小的数据传输,看看是否可以分批处理这些请求。

  • 如果适用,当网络可用时,支持同步的离线存储。通常情况下,网络缓存就足够了。但如果需要更多的结构化数据,使用本地文件或 Core Data 会是一个较好的选择。

    对游戏来说,缓存最近一级的详细信息。对邮件应用来说,存储一些带有附件的最新电子邮件是一个不错的选择。

    根据不同的应用,你可能会允许用户创建新的离线内容,离线内容会在网络连接可用时和服务器进行同步。例如,在邮件应用中编写新邮件或回复某封邮件时;在社交应用中更新资料图片时;拍摄将要上传的照片或视频时。

    总是将网络和通信与 UI 解耦。如果应用可以进行离线操作,那么就通知用户离线操作是可行的,否则就通知用户不能进行离线操作。不要让用户已经开始和应用进行交互之后,获取不到返回值。这会是一个糟糕的用户体验。

    图6 展示了离线模式下的 Facebook 和 E*Trade 应用。

    Facebook 应用通知用户网络不可用,但允许发布评论或状态更新。当网络可用时,上述内容会在后续进行同步。在 E*Trade 应用中,用户也可以与应用进行交互,但当查找某一股票的报价时,应用会进入死胡同,这会导致糟糕的用户体验。

    {%}

    图 6

    需要注意的是,网络条件总是会超出应用的控制,但在这些限制范围内提供的用户体验却是受应用控制的。要将可用选项做到最好,这些选项包括离线存储、网络可到达性、网络类型、执行(或不执行)网络操作,通知(或不通知)用户相关的信息。

延迟

延迟是指从服务器请求资源时,在网络传输上花费的额外时间。设置用于测量网络延迟的系统是很重要的。

网络延迟可以通过使用请求过程中花费的总时间减去服务器上花费的时间(计算和服务响应)来测量:

Round-Trip Time = (Timestamp of Response - Timestamp of Request)
Network Latency = Round-Trip Time - Time Spent on Server

花费在服务器上的时间可以由服务器来计算。对客户端而言,往返的时间是准确可用的。服务器可以将花费的时间放在响应的自定义头部,然后客户端就可以用来计算延迟了。

下面,我们看下计算延迟的示例代码。该代码假设响应包含了自定义头部 X-Server-Time,这个时间以毫秒为单位,包含在服务器上花费的时间。

//server - NodeJS
app.post("/some/path", function(req, res) {
    var startTime = new Date().getTime();
    //处理
    var body = processRequest(req);
    var endTime = new Date().getTime();
    var serverTime = endTime - startTime;
    res.header("X-Server-Time", String(serverTime));
    res.send(body);
});

//client - iOS app
-(void)fireRequestWithLatency:(NSURLRequest *)request {

    NSDate *startTime = nil;
    AFHTTPRequestOperation *op =
        [[AFHTTPRequestOperation alloc] initWithRequest:request];
    [op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *op, id res) {

        NSDate *endTime = [NSDate date];
        NSTimeInterval roundTrip = [endTime timeIntervalSinceDate:startTime];
        long roundTripMillis = (long)(roundTrip * 1000);

        NSHTTPURLResponse *res = op.response;
        NSString *serverTime = [res.allHeaderFields objectForKey:@"X-Server-Time"];
        long serverTimeMillis = [serverTime longLongValue];

        long latencyMillis = roundTripMillis - serverTimeMillis;

    } failure:^(AFHTTPRequestOperation *op, NSError *error) {
        //处理错误。如果需要,向用户展示错误
    }];

    startTime = [NSDate date];
    [op start];
}

上面的代码关于网络延迟的表达基本准确,但它包括了服务端线路上刷新数据花费的时间,以及在客户端解析响应花费的时间。如果可以分离出来,它将提供真实的网络延迟时间,包括任何设备开销。

如果你有数据来分析任何模式下的延迟,还需跟踪下列数据。

  • 连接超时

    跟踪连接超时的次数是非常重要的。根据网络质量(较薄弱的基础设施或较低的容量),该指标会提供详细的地理区域分类,网络质量将反过来帮助规划同步时间的传输。例如,同步会在短时间间隔传输,比如几分钟,而不用在某一个特定时间跨时区同步。

  • 响应超时

    捕捉连接成功但响应超时的数量。这有助于根据地理位置和日期、年份的时间来规划数据中心的容量。

  • 载荷大小

    请求以及响应的大小完全可以在服务器端进行测量。使用此数据可以识别任何可能降低网络操作速度的峰值,并确定一些可用选项:通过选择合适的序列化格式(JSON、CSV、Protobuf 等)减少数据占位,或者分割数据并使用增量同步(例如,通过使用小的批量大小或在多个块中发送部分数据)。

较差网络容量的最大化

我曾经开发过一个神奇的体育应用,当时的工程师团队渐渐注意到应用的延迟变得更长,超时(连接和响应超时)变得更多了。我们还发现,服务器通常会发送超过 200KB 的压缩 JSON 数据作为初始开销,因此我们必须在比赛开始前约 20 分钟内做这件事情。

在比赛当天晚上,现场有超过 10 000 个用户连接至一个基站,总共有 50 000~80 000 个用户,造成了移动数据网络的阻塞。

虽然不能做任何事情改善连接,但我们使用了一些技巧来改善体验。开始时,我们向设备发送了推送通知。在开始前几个小时发送出去的第一个推送通知用于询问用户是否将要去比赛场。并非所有用户都回应了,但相当多的用户回应了(我们采用了游戏化来激励)。这不仅提供了估算流量的数据,更重要的是明确了哪些用户需要通知。

第二个推送通知只发送给了表示将要前往比赛场的用户。这个推送通知是在比赛的前 20 分钟分批发送出去的。如果球场有 1000 个用户,其中的 100 个用户会在初始的两分钟收到通知,下 100 个用户会在接下来的 2 分钟收到通知,并依次类推。

通知将唤醒应用,通过使用地理位置来决定是否获取数据。现在不再是 1000 个人同时连接,连接将在以 100 人为一组的用户组中同时进行。

显而易见,你不能指望每个用户立即打开应用,但一个推送通知将有助于唤醒应用,并让它同步数据。与应用进一步交互时会变得更加顺畅。

 

{%}

《高性能iOS应用开发》共5个部分,主要从性能的衡量标准、对应用至关重要的核心优化点、iOS应用开发特有的性能优化技术以及性能的非代码方面,讲解了应用性能的优化问题。

 

 

追踪内存泄露,提升Android应用体验

作者/Doug Sillars

是 AT&T 开发者计划中的性能推广领导者。他帮助了成千上万的移动开发人员将性能的最佳实践应用到 App 上。他开发的工具和总结的最佳实践,帮助开发人员使 App 运行得更快,同时使用了更少的数据和电量。他和妻子生活在华盛顿州的一个小岛上,并在家教育三个孩子。

为了诊断 App 在哪里出现了内存泄露,需要分析 App 内存中的所有文件。如果能找出需要释放的文件或者内存中重复的文件,那么就可以在代码中解决这些问题。这样就确保了对象能被正确地释放,或者说确保了内存中文件的复用(而不是内存中存储多个重复的实例)。

为了解析 App 内存中的文件,需要在电脑中保存一个内存堆转储。在监控器(装着一半绿色 Android 液体的圆柱体)中紧邻着 Heap Dump 的图标与之类似,但有一个向下的红色箭头。该图标可以为电脑保存堆转储信息以便进行进一步的解析。

已保存的堆转储是 Android 特有的格式文件。要用其他工具打开文件,必须要先进行文件转换。转换工具 hprof-conv 存储于 Android SDK 工具目录下的:

hprof-conv <existing_filename> <converted_filename>

如果你是从 Android Studio 的 DDMS 中收集的堆转储,那么就不需要专门转换了,因为在 Android Studio 的 DDMS 下它是自动转换运行的。

当创建堆转储的时候,试着复现严重的内存问题。如果可以控制 App 的大小,或者模仿记录的任何行为,内存数据将会以 hprof 文件格式转存。找出泄露可能会非常棘手,而且需要一直盯着工具,所以说泄露越大,反而越容易被找到。

为了解析堆转储,可以利用 Eclipse 的内存分析工具(MAT)。在2015年年初,Square 公司发布了 LeakCanary——一个使得 MAT 的解析更加自动化的开源库。当调试时,该工具就会记录 App 的内存泄露问题。首先,我们要了解如何运用 MAT 发现内存泄露,之后弄清 LeakCanary 是如何简化进程的。

MAT

Eclipse 的内存分析工具(MAT)就像它的名字一样: 对内存堆进行详细分析的工具。MAT 是 Eclipse IDE 的一部分,但是如果 Android 的开发迁移到了 Android Studio 上,你可以从 Eclipse.org(https://eclipse.org/mat/)上面下载一个独立的 MAT App 使用。

在 MAT 中打开 hprof 文件时,它的确对文件做了一些处理,并询问你是否需要一个自定义的报告。如果正在查询内存泄露,一般我会选择 Leak Suspects 的报告。它会显示使用内存最多的对象。一旦这些运行起来,打开的工具上方就会出现很多标签页。

MAT 工具在不同的窗口上提供了大量的数据。下图展示的是主视图上的 Overview 的标签页。它显示的是内存主要消耗的饼图。饼图的每个区域代表一块被分配的内存,点击每个区域将会看到这块区域的详细信息。最大的内存块是灰色的,代表的是空闲内存。第二大的内存块是 Iceberg 类(包含 2 个字节数组的 ArrayList),大概占用 4MB 的内存。

{%}

图 MAT 概况

正如在饼图中 Iceberg 类呈高亮状态,Inspector 窗口也提供了更多关于 Iceberg 类当前引用对象的信息。就像在代码中看到的一样,窗口同样展示了 iceSheet ArrayList

{%}

图 MAT 的 Inspector 窗口

将主视图从 Overview 切换到 Leak Suspect 报告,产生了另一个列出内存泄露猜想(基于使用的内存)的饼图。下面的图,显示了两个不同堆转储的饼图。左边的饼图是屏幕旋转两次之后的成果,有两个泄露预测,最大的部分大概占用了 27MB(字节数组),Java 类大概占用 6.1MB(Java 类)。右边的图表是屏幕旋转多次之后的结果,字节数组占用的内存大概是 27MB,但是 Java 类的内存分配激增到了 36MB。如果还不知道内存泄露的位置,这看起来是一个很好的发现机会。

{%}

图 两个内存堆转储的 MAT 泄露猜想图

饼图的下方是一个描述了所有可疑信息的黄框。在这个图中,我们将会根据第二次追踪(多次屏幕旋转)继续分析。

{%}

图 MAT 泄露猜想 1

猜想 1 是 Iceberg 类,在一个 Java 对象中使用了 37MB(全部内存的 47%)。点击详细信息的链接可以更进一步地研究猜想的内容。

{%}

图 MAT 的泄露视图

在这种情况下,泄露猜测报告确定了问题所在。在 The Shortest Path to Accumulation Point(在内存中对象引用的路径都保持如此)的视图直指 ArrayList iceSheet 。当然,在这个例子中,路径并不复杂,但它确实有作用。

当然也有一些巧妙的内存信息:iceSheet 有一个 8B 的浅堆,同时有一个 37MB 的保留堆。浅堆是对象所用的内存,而保留堆是对象和所有对象引用对象的内存(在上面的例子中,是 18 个 2.09MB 字节的数组)。就像是树根在土壤中紧紧地抓住了树干部分,依旧在内存中的对象抓住了它们在内存中引用的所有其他对象。这很明显就是内存泄露。

很少有这么简单的例子。如果泄露不是非常明显,则需要更进一步的分析。让我们一起看看 MAT 中可以帮助隔离内存泄露的其他选项。

按下那个长得像条形图表(也就是,下图中的深色方框标记的地方)的图标,一个内存分布图就会被创建。

{%}

图 MAT 分布图

这篇报告根据类分析了内存使用(再次以浅堆和保留堆进行区分)。在图 “MAT 分布图”中,有几个线索可以用于分析内存泄露。

  • byte[](第一个方框部分)包括了所有的图片(以及在 iceSheet 数组列表中的所有项)。66MB 比预期的要多,这是因为在图“两个内存堆转储的 MAT 泄露猜想”中,只有 27MB 字节数组作为图片。

  • java.lang.Object[](第二个方框部分)有一个很小的浅堆,但是有大于 38MB 的保留堆。

  • java.lang.Class(第三个方框部分)有一个类似的小浅堆,但是却有一个更大的保留堆。

这些线索都表示了小文件,但这些小文件却又对其他对象有庞大的引用。所以我们应该更进一步研究这些类。

为了更仔细地检查 byte[] 的对象列表,右击这一行,并且选择 List Objects,然后选择“With Incoming references”。这样将会产生一个根据保留堆排序的新表格。

{%}

图 MAT 列出的 byte[] 的对象

在列表的顶部,可以看到由屏幕旋转得到的 18 个 2MB 的数组。如果想在垃圾回收的时候找到阻塞的根对象,对一个对象右击,并且选择“Path to GC Roots”→“excluding weak references”(在垃圾回收的过程中,弱引用不会阻塞对象)。这将会打开一个新的窗口,如下图所示。

{%}

图 垃圾回收根部 2 字节数组

垃圾回收根部路径再次确认了 iceSheet 就是内存泄露的罪魁祸首。上图选择研究第一个字节数组,报告的第二行显示它在含有 18 个元素的数组列表的第 0 个位置。最后一行又出现了 iceSheet 的名字。我们再一次找到了内存泄露的原因。hprof 文件保存在本书的 GitHub 仓库里(https://github.com/dougsillars/HighPerformanceAndroidApps)。我将追踪 iceSheet 内存泄露的 java.lang.Objectjava.lang.classes 留作一个练习,大家可以根据与上面相同的步骤找到同样的答案。

要想学习 Android 是如何处理内存分配,并找到方法优化 App 的内存分配,使用 Eclipse MAT 是一个不错的方法。但是在版本迭代快速的时代,你可能没有时间学习和调研一种新的工具来诊断内存泄露。好在 Square 团队开源了 LeakCanary,一款使 MAT 做的大部分事情能自动化输出的测试工具。

LeakCanary

Square 公司开发的 LeakCanary 被用来减少 App 遇到的内存泄露错误。他们在不同的设备上发现了相同的崩溃,然后在 MAT 上做了一些实质性的实验来发掘是什么引发了泄露。这种方法比较慢,而他们想要在发布给最终的用户之前找到这个内存泄露,LeakCanary 就诞生了。对于内存泄露,它就像是“煤矿中的金丝雀”:在内存溢出、崩溃之前,就可以嗅出内存泄露。自从使用了 LeakCanary,Square 公司报告(https://corner.squareup.com/2015/05/leak-canary.html)显示,OOM 崩溃下降了 94%。让我们一起看一下这个工具是如何运作的。

根据 Square 的说明(https://github.com/square/leakcanary),启动和运行 LeakCanary 非常简单。我在“Is this a goat?”App 上运用了它,并且放到了 GitHub 上面。

在 build.gradle 文件上添加两个依赖:

debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1'

在“Is it a goat?”App 的应用类中,添加如下:

//LeakCanary引用监视器
public static RefWatcher getRefWatcher(Context context) {
    AmiAGoat application = (AmiAGoat) context.getApplicationContext();
    return application.refWatcher;
}
private RefWatcher refWatcher;


@Override public void onCreate() {
    super.onCreate();
    //在App创建上 - 打开LeakCanary

    refWatcher = LeakCanary.install(this);
}

然后给 CancelTheWatch 类和 Iceberg 类添加了特定的引用 watcher:

//LeakCanary监测变量
RefWatcher wishTheyHadAWatch = AmiAGoat.getRefWatcher(this);
wishTheyHadAWatch.watch(NoNeed);

RefWatcher icebergWatch = AmiAGoat.getRefWatcher(this);
icebergWatch.watch(theBigOne);

现在,当我运行“Is it a goat?”App,打开内存泄露,旋转屏幕时,事情发生了。在短暂的延迟之后,LeakCanary 输出堆转储和相应的分析。写在日志上的报告如下:

05-25 15:43:28.283  17998-17998/<app>I/iceberg:
                   Captain, I think we might have hit something.
05-25 15:43:51.356  17998-18750/<app> D/LeakCanary: In <app>:1.0:1.
    * <app>.Iceberg has leaked:
    * GC ROOT static <app>.CancelTheWatch.iceberg
    * leaks <app>.Iceberg instance
    * Reference Key: 52614375-1531-47b1-96d7-4ec986861794
    * Device: motorola google Nexus 6 shamu
    * Android Version: 5.1 API: 22 LeakCanary: 1.3.1
    * Durations: watch=5443ms, gc=154ms, heap dump=2864ms, analysis=14302ms
    * Details:
    * Class <app>.CancelTheWatch
    |   static $staticOverhead = byte[] [id=0x12c9f9a1;length=8;size=24]
    |   static iceberg = <app>.Iceberg [id=0x1317e860]
    * Instance of <app>.Iceberg
    |   static $staticOverhead = byte[] [id=0x12c88e21;length=8;size=24]
    |   static iceSheet = java.util.ArrayList [id=0x12c267a0]

这个跟踪显示了关于设备的一切信息。据此追踪可知,Iceberg 类有泄露,整个过程花费的时长(垃圾回收用了 154 毫秒,收集堆转储用了 2 秒,分析用了 14 秒),以及哪些对象在类中引起了泄露。GitHub 文档一步步地引导你向服务器机群上报这些泄露和堆转储。(注意,对于明显的延迟原因,内存泄露应该在调试版本就进行修复,这对内部测试具有重大的意义!)最终,这个报告将会在设备的通知栏进行提示,并以一个叫“Leaks”的新 App 在 App 列表中出现。

{%}

图 LeakCanary 截图:摘要(顶部)和详情(底部)

LeakCanary 将存储设备最开始的 7 个泄露,并且会有一个菜单和其他人分享这个泄露和堆转储的信息。在内部测试中使用 LeakCanary 将帮助你发现 MAT 发现不了的内存泄露问题,快速定位 App 中的内存泄露,减少崩溃量,并提高 App 的性能。

小结

对于识别正在泄露内存的对象和类这个功能来说,MAT 依旧是一个优秀的工具。理解 MAT 暴露的内存链接是非常重要的。然而,LeakCanary 可以代替不断使用的 MAT 来测试内存问题。

 

{%}

《高性能Android应用开发》是Android性能方面的关键性指南。主要从电池、内存、CPU和网络方面讲解了电池管理、工作效率和速度这几个方面的性能优化问题,并介绍了一些有助于确定和定位性能问题所属类型的工具。