Google 致力于为黑人社区推动种族平等。查看具体举措

优化下载以实现高效网络访问

使用无线装置传输数据可能是导致应用出现过度耗电问题的最重要的根源之一。为了最大限度地减轻与网络活动相关的过度耗电问题,您务必要了解连接模式会如何影响底层无线装置硬件。

这节课介绍了无线装置状态机,并说明了应用的连接模式如何与此状态机进行交互。这节课还进一步推荐了一些方法,帮助您最大限度地减少数据连接、使用预先抓取功能并整合传输活动,以便最大限度地减轻与数据传输相关的过度耗电问题。

无线装置状态机

处于全面活动状态的无线装置会消耗大量电量,因此该装置会在不同的能量状态之间转换,以便在不使用时节省电量,同时在需要时尝试最大限度地缩短与为无线装置“接通电源”相关的延迟。

典型 3G 网络无线装置的状态机包含 3 种能量状态:

  1. 满功率:在连接处于活动状态时使用,让设备能够以尽可能高的速度传输数据。
  2. 低功率:一种中间状态,所使用的电池电量约为满功率状态下的 50%。
  3. 待机:最低能量状态;处于此状态时,没有网络连接处于活动状态,或不需要网络连接。

虽然低功率和待机状态所消耗的电池电量明显减少,但它们也会导致网络请求严重延迟。从低功率状态返回满功率状态需要大约 1.5 秒,而从待机状态切换到满功率状态需要大约 2 秒。

为了最大限度地缩短延迟,状态机利用延迟来推迟向低能量状态的转换。图 1 使用的是 AT&T 的典型 3G 无线装置的时间设置。

图 1. 典型的 3G 无线装置状态机。

每个设备上的无线装置状态机(特别是相关的转换延迟(“拖尾时间”)和启动延迟)因所采用的无线电技术(2G、3G、LTE 等)而有所不同,并由设备所采用的运营商网络进行定义和配置。

这节课根据 AT&T 提供的数据介绍了典型的 3G 无线装置的代表性状态机。不过,一般原则和对应的最佳做法适用于所有无线装置实现。

这种方法对典型的网络浏览活动尤为有效,因为它可以在用户浏览网页时阻止出现意外的延迟。相对较短的拖尾时间还可以确保在浏览会话结束后,无线装置可以立即切换到低能量状态。

遗憾的是,这种方法可能会导致在 Android 等现代智能手机操作系统上运行的应用效率低下,因为应用既在前台运行(此时降低延迟很重要),又在后台运行(此时应优先考虑电池续航时间)。

应用会如何影响无线装置状态机

每次您创建新的网络连接时,无线装置都会转换为满功率状态。如果是上述典型 3G 无线装置状态机,它会在传输期间以及额外 5 秒的拖尾时间内始终保持满功率状态,并在随后的 12 秒内保持低能量状态。因此,对于典型的 3G 设备,每个数据传输会话都会导致无线装置消耗能量近 20 秒。

在实践中,如果某个应用每 18 秒传输一次非整合数据并持续 1 秒,就会导致无线装置在即将进入待机模式时重新切换回高功率状态,进而永久保持活动状态。因此,它每分钟有 18 秒以高功率状态消耗电池电量,并在剩余的 42 秒内以低功率状态消耗电池电量。

相比之下,如果同一款应用每分钟进行 3 秒的整合传输,无线装置保持高功率状态的时间就只有 8 秒,而保持低功率状态的时间只有额外的 12 秒。

第 2 个示例使无线装置每分钟有额外的 40 秒时间处于待机状态,从而大大减少电池耗电量。

图 2. 整合传输和非整合传输的无线装置耗电量对比情况。

预先抓取数据

预先抓取数据是减少单独的数据传输会话数量的有效方法。借助预先抓取,您可以在单个爆发期中通过单个连接满负荷地下载给定时间段内可能需要的所有数据。

通过提前加载传输,您可以减少下载数据所需的无线装置激活次数。因此,您不仅可以延长电池续航时间,还可以缩短延迟、降低所需带宽并降低下载次数。

预先抓取还可以最大限度地缩短因在执行某项操作或查看数据之前等待下载完成而导致的应用内延迟,从而改进用户体验。

不过,如果使用过于激进,预先抓取可能会因下载未使用的数据而产生过度耗电问题加剧和带宽使用量增加的风险。此外,还要确保预先抓取不会在应用等待预先抓取完成时导致应用启动出现延迟。实际上,这可能意味着要逐步处理数据,或按优先级启动连续传输,以便优先下载和处理应用启动所需的数据。

预先抓取的积极程度取决于要下载的数据大小以及使用这些数据的可能性。一般指南是:根据上述状态机,对于在当前用户会话中使用的可能性为 50% 的数据,您通常可以预先抓取大约 6 秒钟(大约 1-2 Mb)。之后,下载未使用数据的潜在成本便会与一开始不下载数据可能产生的节省量持平。

一般来说,建议按照以下形式预先抓取数据:每 2 到 5 分钟启动一次下载、每次下载 1 到 5 兆字节的数据。

按照这一原则,大型下载内容(例如视频文件)应该分段定期(每 2 到 5 分钟)下载,从而仅预先抓取用户在接下来的几分钟内可能会观看的视频数据。

请注意,您应该对后续下载进行整合(如下一部分批量传输和连接中所述),并且这些近似值因连接类型和速度而异(如根据连接类型修改下载模式中所述)。

我们来看一些实际的例子:

音乐播放器

您可以选择预先抓取整个专辑,但是如果用户在第一首歌曲结束后就停止聆听,则会浪费大量的带宽和电池续航时间。

更好的方法是除了正在播放的歌曲之外,再缓冲一首歌曲。对于流式传输音乐,可以考虑使用 HTTP Live Streaming 爆发式地传输音频流,以模仿上述预先抓取方法,而不是维持连续的数据流并导致无线装置始终保持活动状态。

新闻阅读器

许多新闻应用都仅在用户选择某个类别后才下载标题、仅当用户想要阅读时才下载完整报道,并在用户滚动到视图中后立即下载缩略图,试图通过这种方式降低带宽。

如果使用这种方法,无线装置会在用户的新闻阅读会话的大部分时间内保持活动状态,因为用户会滚动浏览标题、更改类别并阅读报道。不仅如此,在各能量状态之间持续切换还会导致在切换类别或阅读报道时出现严重的延迟。

更好的方法是在启动时预先抓取数量合理的数据,首先预先抓取第一组新闻标题和缩略图(确保低延迟的启动时间),然后继续预先抓取剩余的标题和缩略图,以及每篇报道的报道正文(至少从主要标题列表起)。

另一种替代方案是预先抓取每个标题、缩略图、报道正文,甚至可能是完整的报道图片(通常是按照预定的时间表在后台进行)。这种方法的风险是花费大量带宽和电池续航时间下载从未使用的内容,因此实现时应该谨慎。

一种解决方案是仅在连接到 WNAL 时(并且可能仅在设备充电时)才安排完整下载。根据连接类型修改下载模式对此进行了更详细的阐述。

批量传输和连接

每次您启动连接(无论相关数据传输的大小如何),都可能在使用典型的 3G 无线装置时导致无线装置消耗电量近 20 秒。

如果某款应用每 20 秒 ping 一次服务器,只是为了确认此应用正在运行并对用户可见,则会让无线装置无限期地保持开启状态,从而导致产生高昂的电池成本,却几乎没有进行实际的数据传输。

考虑到这一点,请务必整合数据传输并创建待处理的传输队列。如果操作正确,您可以有效地相移应在相似的时间窗口中发生的传输,以使其全部同时发生,确保无线装置消耗电量的时长尽可能短暂。

这种方法的底层理念是在每次传输会话中尽可能多地传输数据,以限制您需要的会话数量。

这意味着您应该通过将容忍延迟的传输加入队列并优先处理排定的更新和预先抓取来批量处理传输,以便在需要具有时效性的传输时执行所有这些传输。同样,排定更新和定期预先抓取应该启动待处理传输队列的执行。

举一个实际的例子,让我们回到之前在预先抓取数据中提到的示例。

以一个使用上述预先抓取例程的新闻应用为例。新闻阅读器会收集分析信息,以了解用户的阅读模式并对最热门的报道进行排名。为了随时获取最新新闻,它每小时检查一次更新。为了节省带宽,它只预先抓取缩略图,然后在缩略图被选中时才下载完整的照片,而不是下载每篇报道的完整照片。

在此示例中,在应用内收集的所有分析信息都应该整合在一起并加入下载队列,而不是一边收集一边传输。所产生的整合内容应在下载完整尺寸的照片或执行每小时更新时传输。

所有时间敏感传输或按需传输(例如下载完整尺寸的图片)都应该优先于排定的定期更新。计划更新应与按需传输同时执行,并让排定的下一次更新在设置的间隔过后发生。这种方法通过下载必要的时间敏感照片,降低了执行定期更新的成本。

减少连接

重复使用现有网络连接通常比启动新的网络连接更高效。通过重复使用连接,网络还可以更智能地对拥塞和相关的网络数据问题做出响应。

您不应该创建多个同步连接来下载数据,或将多个连续的 GET 请求连接起来,而应该尽量将这些请求整合成单个 GET 请求。

例如,针对要在单个请求/响应中返回的各个新闻报道发出单个请求,比针对多个新闻类别进行多个查询更高效。无线装置需要变为活动状态才能传输与服务器和客户端超时相关的终止/终止确认数据包,因此最好在不使用时关闭连接,而不是等待超时。

尽管如此,过早关闭连接可能会阻止对其进行重复使用,而这就需要建立新连接并产生额外的开销。一个实用的折中方案是不要立即关闭连接,但仍然要在固有超时过期之前将其关闭。

使用 Network Profiler 识别问题

使用 Network Profiler 跟踪应用发出网络请求的时间。您可以监控应用传输数据的方式和时间,并适当优化底层代码。

图 3 显示的是每隔大约 15 秒少量传输一次数据的模式,表明通过预先抓取每个请求或整合上传可以显著提高效率。

图 3. 跟踪网络使用情况。

通过监控数据传输频率以及每次连接期间传输的数据量,您可以确定应用在哪些方面可以实现更高的电池能效。通常,您应该寻找可以延迟或会导致以后的传输被抢占的短暂峰值。

为了更好地识别形成传输峰值的原因,借助 Traffic Stats API,您可以使用 TrafficStats.setThreadStatsTag() 方法对在线程中发生的数据传输进行标记,然后分别使用 TrafficStats.tagSocket()TrafficStats.untagSocket() 手动为单个套接字进行标记/取消标记。例如:

Kotlin

    TrafficStats.setThreadStatsTag(0xF00D)
    TrafficStats.tagSocket(outputSocket)
    // Transfer data using socket
    TrafficStats.untagSocket(outputSocket)
    

Java

    TrafficStats.setThreadStatsTag(0xF00D);
    TrafficStats.tagSocket(outputSocket);
    // Transfer data using socket
    TrafficStats.untagSocket(outputSocket);
    

HttpURLConnection 库根据当前的 TrafficStats.getThreadStatsTag() 值自动对套接字进行标记。在通过 keep-alive 池进行再循环时,此库还会对套接字进行标记和取消标记。

Kotlin

    private class IdentifyTransferSpikeTask : AsyncTask<String, Nothing, String>() {

        override fun onPreExecute() = TrafficStats.setThreadStatsTag(0xF00D)

        override fun doInBackground(vararg urls: String): String {
            try {
                // Make network request using HttpURLConnection.connect()
            }
        }

        override fun onPostExecute(result: String) = TrafficStats.clearThreadStatsTag()
    }
    

Java

    private class IdentifyTransferSpikeTask extends AsyncTask<String, Void, String> {
        @Override
        protected void onPreExecute() {
          TrafficStats.setThreadStatsTag(0xF00D);
        }

        @Override
        protected String doInBackground(String... urls) {
            try {
                // Make network request using HttpURLConnection.connect()
            }
        }

        @Override
        protected void onPostExecute(String result) {
            TrafficStats.clearThreadStatsTag();
       }
    }
    

Android 4.0 支持套接字标记,但实时统计信息仅在搭载 Android 4.0.3 或更高版本的设备上显示。