《Android优化专题》—— 优化下载效率

《Android优化专题》—— 优化下载效率

一、用有效率的网络访问方式来优化下载

1.无线电状态机

  • Full power:当无线连接被激活的时候,允许设备以最大的传输速率进行操作。
  • Low power:相对Full power来说,算是一种中间状态,差不多50%的传输速率。
  • Standby:最低的状态,没有数据连接需要传输。

典型的3G无线电波状态机图示:

2. apps如何影响无线状态机

创建新连接

每次创建一个网络连接,无线电波就会切换到full power状态(这也是为什么降低连接数可以更省电量以及省流量的原因),结束之后会有一个附加的5s时间切换到low power,再之后会经过12s进入到low energy状态,每次数据传输的会话都会引起无线电波持续消耗大概20s的能量。

传输bundle(序列化)与unbundled(未序列化)数据差别

一个app传递1秒钟的unbundled data会使得无线电波持续活动18秒【18=1秒的传输数据+5秒过渡时间回到low power+12秒过渡时间回到standby】。因此每一分钟,它会消耗18秒high power的电量,42秒的low power的电量。

如果每分钟app会传输bundle的data持续3秒的话,其中会使得无线电波持续在high power状态仅仅8秒钟,在low power状态仅仅12秒钟。 上面第二种传输bundle data的例子,可以看到减少了大量的电量消耗。

3. 预取数据

预取数据是一种减少独立数据传输会话数量的有效方法。在单次操作的时候,通过一次连接,在最大能力下,根据给出的时间下载到所有的数据。

对于预取,取太多,不仅用户可能根本用不到那么多,而且还耗费了电量和流量。取太少,达不到预取的效果。

如何控制预取的大小?

这需要根据正在下载的数据大小与可能被用到的数据量来决定。一个基于上面状态机情况的比较大概的建议是:对于数据来说,大概有50%的机会可能用在当前用户的会话中,那么我们可以预取大约6秒(大约1-2Mb),这大概使得潜在可能要用的数据量与可能已经下载好的数据量相一致。

通常来说,预取1-5Mb会比较好,这种情况下,我们仅仅只需要每隔2-5分钟开始另一段下载。根据这个原理,大数据的下载,比如视频文件,应该每隔2-5秒开始另一段下载,这样能有效的预取到下面几分钟内的数据进行预览。

4. 批量传输与连接

每次初始化一个连接(与需要传输的数据量无关),有可能导致无线电波持续花费20s的电量。

对于数据进行bundle操作,并且创建一个序列可以使得大量数据集中进行发送,这样可以使得无线电波的激活时间尽可能的少,同事减少大部分的电量花费。

5. 减少连接次数

重用之前存在的网络连接比重新创建一个连接是更有效率的。当可以用一个GET请求解决的情况下,不要同时创建多个网络连接。

可以在一个连接要关闭时,不要立即关闭,而是在timeout之前关闭。

使用HttpUrlConnection,而不是HttpClient,前者做了response cache

6. 使用DDMS(Dalvik Debug Monitor Server)网络通信工具来检测网络使用情况

通过监测数据传输的频率和每次传输的数据量,可以看出哪些地方可以进行优化。类似于图中短小钉子形状的地方,可以和附近位置的请求进行merge操作。

Traffic Status API可以使用TrafficStats.setThreadStatsTag()的方法标记数据传输发生在某个Thread里面。可以手动使用tagSocket()进行标记或者untagSocket()来取消标记。

二、调整更新的频率

1.使用C2DM作为轮询方式之一

C2DM是一个用来从server到特定app传输数据的轻量级机制。使用C2DM,server会在某个app有需要获取新数据的时候通知app有这个消息。

但中国大陆的Google框架通常会被移除,使得C2DM没法在中国大陆的App上使用。

可参考各大厂商的推送定制。

2.通过不定时的重复提醒和指数退避来优化轮询操作

如果必须要使用轮询机制,可以考虑以下几个方面的优化:

  1. 如果多个提醒都安排在某个小的时间段内,考虑把这多个操作在一个无线电状态下操作完。
  2. 使用Alarm时,设置的提醒类型为非wake类型(对于非紧急通知消息时,避免在屏幕熄灭状态下,将设备唤醒),减少电量的损耗。
  3. 在app上一次更新操作之后还未被使用的情况下,使用指数退避算法(exponential back-off algorithm)来减少更新频率。
1
2
3
4
5
6
7
8
private void retryIn(long interval) {
boolean success = attemptTransfer();

if (!success) {
retryIn(interval*2 < MAX_RETRY_INTERVAL ?
interval*2 : MAX_RETRY_INTERVAL);
}
}

二进制退避算法

  1. 确定基本退避时间,一般为端到端的往返时间为2t,2t也成为冲突窗口或争用期。
  2. 定义参数k,k与冲突次数有关,规定k不能超过10,k=Min[冲突次数,10]。在冲突次数大于10,小于16时,k不再增大,一直取值为10。
  3. 从离散的整数集合[0,1,2,……,(2k-1)]中随机的取出一个数r,等待的时延为r倍的基本退避时间,等于r x 2t。r的取值范围与冲突次数k有关,r可选的随机取值为2k个、这也是称为二进制退避算法的起因。
  4. 当冲突次数大于10以后,都是从0—210-1个2t中随机选择一个作为等待时间。
  5. 当冲突次数超过16次后,发送失败,丢弃传输的帧,发送错误报告。

三、使用缓存来避免重复下载

减少下载的最基本方法是仅仅下载你想要的数据,通过类似上次更新时间来制定查询数据的条件。在下载图片时,server尽量减少图片的大小,比如对图片进行剪裁等处理。

1. 缓存到本地

1
2
3
4
5
6
7
8
9
10
11
12
13
14
long currentTime = System.currentTimeMillis());

HttpURLConnection conn = (HttpURLConnection) url.openConnection();

long expires = conn.getHeaderFieldDate("Expires", currentTime);
long lastModified = conn.getHeaderFieldDate("Last-Modified", currentTime);

setDataExpirationDate(expires);

if (lastModified < lastUpdateTime) {
// Skip update
} else {
// Parse update
}

可以使用下面的方法获取External缓存目录:目录是Android/data/data/com.xxx.xxx/cache

1
Context.getExternalCacheDir();

获取内部缓存的方法

1
Context.getCache()

这里注意不要随便在sdcard下创建目录存放缓存,因为这个文件夹不会随着程序的卸载而删除。既影响用户体验,又会把一些不想让用户知道的数据泄露出去。

2. 使用HttpUrlConnect Response缓存

我们可以通过反射机制开启HTTP response cache

1
2
3
4
5
6
7
8
9
10
11
private void enableHttpResponseCache() {
try {
long httpCacheSize = 10 * 1024 * 1024; // 10 MiB
File httpCacheDir = new File(getCacheDir(), "http");
Class.forName("android.net.http.HttpResponseCache")
.getMethod("install", File.class, long.class)
.invoke(null, httpCacheDir, httpCacheSize);
} catch (Exception httpResponseCacheNotAvailable) {
Log.d(TAG, "HTTP response cache is unavailable.");
}
}

也可以在onCreate中加入以下代码开启

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected void onCreate(Bundle savedInstanceState) {
...

try {
File httpCacheDir = new File(context.getCacheDir(), "http");
long httpCacheSize = 10 * 1024 * 1024; // 10 MiB
HttpResponseCache.install(httpCacheDir, httpCacheSize);
} catch (IOException e) {
Log.i(TAG, "HTTP response cache installation failed:" + e);
}
}

protected void onStop() {
...

HttpResponseCache cache = HttpResponseCache.getInstalled();
if (cache != null) {
cache.flush();
}
}

以上代码会在Android4.0以上开启response code,所有cache中的HTTP请求都可以直接在本地存储中响应,就不需要开启一个新的网络连接。被cache起来的response可以被server确保没有过期,这样减少了带宽。

四、根据网络类型来切换下载模式

WIFI要比无线电波消耗的电量要少很多,而且对于无线电波而言(3G,2G,LTE等)也存在不同电量的区别。

1. 尽量WIFI

我们尽量要在连接WIFI的时候进行下载,更新数据等操作。

2. 尽量使用更大的带宽下载更多的数据,而不是经常去下载

相对更宽的带宽会有更长的状态切换时间(从full power过渡到standby有更长一段时间的延迟),过渡时间的电量通常是固定的,每次传输会话过程中尽量一次性把事情做完,而不是断断续续请求就更有效率了。

如果LTE无线电的带宽与电量消耗都是3G无线电的2倍,我们应该在每次会话的时候都下载4倍于3G的数据量,或者是差不多10Mb

我们可以根据connectivity manager来判断当前激活的无线电波,根据结果来做prefetch。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
ConnectivityManager cm =
(ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);

TelephonyManager tm =
(TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);

NetworkInfo activeNetwork = cm.getActiveNetworkInfo();

int PrefetchCacheSize = DEFAULT_PREFETCH_CACHE;

switch (activeNetwork.getType()) {
case (ConnectivityManager.TYPE_WIFI):
PrefetchCacheSize = MAX_PREFETCH_CACHE; break;
case (ConnectivityManager.TYPE_MOBILE): {
switch (tm.getNetworkType()) {
case (TelephonyManager.NETWORK_TYPE_LTE |
TelephonyManager.NETWORK_TYPE_HSPAP):
PrefetchCacheSize *= 4;
break;
case (TelephonyManager.NETWORK_TYPE_EDGE |
TelephonyManager.NETWORK_TYPE_GPRS):
PrefetchCacheSize /= 2;
break;
default: break;
}
break;
}
default: break;
}