本文分为两个部分,第一部分是网络优化,第二部分是耗电量优化。
第一部分,网络优化:
一个网络请求可以简单分为连接服务器 -> 获取数据两个部分。
其中连接服务器前还包括 DNS 解析的过程;获取数据后可能会对数据进行缓存。
一、连接服务器优化策略:
1. 不用域名,用 IP 直连:
省去 DNS 解析过程,DNS 全名 Domain Name System,解析意指根据域名得到其对应的 IP 地址。 如 http://www.codekk.com 的域名解析结果就是 104.236.147.76。
首次域名解析一般需要几百毫秒,可通过直接向 IP 而非域名请求,节省掉这部分时间,同时可以预防域名劫持等带来的风险。
当然为了安全和扩展考虑,这个 IP 可能是一个动态更新的 IP 列表,并在 IP 不可用情况下通过域名访问。
2. 服务器合理部署:
服务器多运营商多地部署,一般至少含三大运营商、南中北三地部署。
配合上面说到的动态 IP 列表,支持优先级,每次根据地域、网络类型等选择最优的服务器 IP 进行连接。
对于服务器端还可以调优服务器的 TCP 拥塞窗口大小、重传超时时间(RTO)、最大传输单元(MTU)等。
二、获取数据优化策略:
1. 连接复用:
节省连接建立时间,如开启 keep-alive。
Http 1.1 默认启动了 keep-alive。对于 Android 来说默认情况下 HttpURLConnection 和 HttpClient 都开启了 keep-alive。只是 2.2 之前 HttpURLConnection 存在影响连接池的 Bug,现在已经不存在。
2. 请求合并:
即将多个请求合并为一个进行请求,比较常见的就是网页中的 CSS Image Sprites。 如果某个页面内请求过多,也可以考虑做一定的请求合并。
3. 减小请求数据大小:
(1) 对于 POST 请求,Body 可以做 Gzip 压缩,如日志。
(2) 对请求头进行压缩
这个 Http 1.1 不支持,SPDY 及 Http 2.0 支持。 Http 1.1 可以通过服务端对前一个请求的请求头进行缓存,后面相同请求头用 md5 之类的 id 来表示即可。
4. CDN 缓存静态资源:
缓存常见的图片、JS、CSS 等静态资源。
5. 减小返回数据大小:
(1) 压缩
一般 API 数据使用 Gzip 压缩
(2) 精简数据格式
如 JSON 代替 XML,WebP 代替其他图片格式。
(3) 对于不同的设备不同网络返回不同的内容 如不同分辨率图片大小。
(4) 增量更新
需要数据更新时,可考虑增量更新。如常见的服务端进行 bsdiff,客户端进行 bspatch。
(5) 大文件下载
支持断点续传,并缓存 Http Resonse 的 ETag 标识,下次请求时带上,从而确定是否数据改变过,未改变则直接返回 304。
6. 数据缓存:
缓存获取到的数据,在一定的有效时间内再次请求可以直接从缓存读取数据。
关于 Http 缓存规则 Grumoon 在 Volley 源码解析最后杂谈中有详细介绍。
三、其他优化手段
这类优化方式在性能优化系列总篇中 http://www.trinea.cn/android/performance/ 已经有过完整介绍
1. 预取
包括预连接、预取数据。
2. 分优先级、延迟部分请求
将不重要的请求延迟,这样既可以削峰减少并发、又可以和后面类似的请求做合并。
3. 多连接
对于较大文件,如大图片、文件下载可考虑多连接。 需要控制请求的最大并发量,毕竟移动端网络受限。
第二部分,耗电量优化:
Android手机的电量都主要消耗在:屏幕、网络连接、GPS、传感器上。
那么我们怎么样来改善一下我们的程序呢?
1、 在需要网络连接的程序中,首先检查网络连接是否正常,如果没有网络连接,那么就不需要执行相应的程序。
ConnectivityManager mConnectivity;
TelephonyManager mTelephony;
// 检查网络连接,如果无网络可用,就不需要进行连网操作等
NetworkInfo info = mConnectivity.getActiveNetworkInfo();
if (info == null || !mConnectivity.getBackgroundDataSetting()) {
return false;
}
//判断网络连接类型,只有在3G或wifi里进行一些数据更新。
int netType = info.getType();
int netSubtype = info.getSubtype();
if (netType == ConnectivityManager.TYPE_WIFI) { //WIFI网络
return info.isConnected();
} else if (netType == ConnectivityManager.TYPE_MOBILE && netSubtype == TelephonyManager.NETWORK_TYPE_UMTS && !mTelephony.isNetworkRoaming()) { //3G网络
return info.isConnected();
} else {
return false;
}
2、 使用效率高的数据格式和解析方法JSON。
目前主流的数据格式,使用树形解析(如DOM)和流的方式解析(SAX):
使用流的方式解析效率要高一些,因为DOM解析是在对整个文档读取完后,再根据节点层次等再组织起来。而流的方式是边读取数据边解析,数据读取完后,解析也就完毕了。
在数据格式方面,JSON和Protobuf效率明显比XML好很多,XML和JSON大家都很熟悉,Protobuf是Google提出的,一种语言无关、平台无关、扩展性好的用于通信协议、数据存储的结构化数据串行化方法。结论就是尽量使用SAX等边读取边解析的方式来解析数据,针对移动设备,最好能使用JSON之类的轻量级数据格式为佳。
3、在进行大数据量下载时,尽量使用GZIP方式下载。
HttpUriConnection默认开启了GZIP
4、为了减少电量的消耗,在蜂窝移动网络下,最好做到批量执行网络请求,尽量避免频繁的间隔网络请求。
使用Job Scheduler,应用需要做的事情就是判断哪些任务是不紧急的,可以交给Job Scheduler来处理,Job Scheduler集中处理收到的任务,选择合适的时间,合适的网络,再一起进行执行。
下面是使用Job Scheduler的一段简要示例,需要先有一个JobService:
public class MyJobService extends JobService {
private static final String LOG_TAG = "MyJobService";
@Override
public void onCreate() {
super.onCreate();
Log.i(LOG_TAG, "MyJobService created");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i(LOG_TAG, "MyJobService destroyed");
}
@Override
public boolean onStartJob(JobParameters params) {
// This is where you would implement all of the logic for your job. Note that this runs
// on the main thread, so you will want to use a separate thread for asynchronous work
// (as we demonstrate below to establish a network connection).
// If you use a separate thread, return true to indicate that you need a "reschedule" to
// return to the job at some point in the future to finish processing the work. Otherwise,
// return false when finished.
Log.i(LOG_TAG, "Totally and completely working on job " + params.getJobId());
// First, check the network, and then attempt to connect.
if (isNetworkConnected()) {
new SimpleDownloadTask() .execute(params);
return true;
} else {
Log.i(LOG_TAG, "No connection on job " + params.getJobId() + "; sad face");
}
return false;
}
@Override
public boolean onStopJob(JobParameters params) {
// Called if the job must be stopped before jobFinished() has been called. This may
// happen if the requirements are no longer being met, such as the user no longer
// connecting to WiFi, or the device no longer being idle. Use this callback to resolve
// anything that may cause your application to misbehave from the job being halted.
// Return true if the job should be rescheduled based on the retry criteria specified
// when the job was created or return false to drop the job. Regardless of the value
// returned, your job must stop executing.
Log.i(LOG_TAG, "Whelp, something changed, so I'm calling it on job " + params.getJobId());
return false;
}
/**
* Determines if the device is currently online.
*/
private boolean isNetworkConnected() {
ConnectivityManager connectivityManager =
(ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
return (networkInfo != null && networkInfo.isConnected());
}
/**
* Uses AsyncTask to create a task away from the main UI thread. This task creates a
* HTTPUrlConnection, and then downloads the contents of the webpage as an InputStream.
* The InputStream is then converted to a String, which is logged by the
* onPostExecute() method.
*/
private class SimpleDownloadTask extends AsyncTask {
protected JobParameters mJobParam;
@Override
protected String doInBackground(JobParameters... params) {
// cache system provided job requirements
mJobParam = params[0];
try {
InputStream is = null;
// Only display the first 50 characters of the retrieved web page content.
int len = 50;
URL url = new URL("https://www.google.com");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(10000); //10sec
conn.setConnectTimeout(15000); //15sec
conn.setRequestMethod("GET");
//Starts the query
conn.connect();
int response = conn.getResponseCode();
Log.d(LOG_TAG, "The response is: " + response);
is = conn.getInputStream();
// Convert the input stream to a string
Reader reader = null;
reader = new InputStreamReader(is, "UTF-8");
char[] buffer = new char[len];
reader.read(buffer);
return new String(buffer);
} catch (IOException e) {
return "Unable to retrieve web page.";
}
}
@Override
protected void onPostExecute(String result) {
jobFinished(mJobParam, false);
Log.i(LOG_TAG, result);
}
}
}
然后模拟通过点击Button触发N个任务,交给JobService来处理:
public class FreeTheWakelockActivity extends ActionBarActivity {
public static final String LOG_TAG = "FreeTheWakelockActivity";
TextView mWakeLockMsg;
ComponentName mServiceComponent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_wakelock);
mWakeLockMsg = (TextView) findViewById(R.id.wakelock_txt);
mServiceComponent = new ComponentName(this, MyJobService.class);
Intent startServiceIntent = new Intent(this, MyJobService.class);
startService(startServiceIntent);
Button theButtonThatWakelocks = (Button) findViewById(R.id.wakelock_poll);
theButtonThatWakelocks.setText(R.string.poll_server_button);
theButtonThatWakelocks.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
pollServer();
}
});
}
/**
* This method polls the server via the JobScheduler API. By scheduling the job with this API,
* your app can be confident it will execute, but without the need for a wake lock. Rather, the
* API will take your network jobs and execute them in batch to best take advantage of the
* initial network connection cost.
*
* The JobScheduler API works through a background service. In this sample, we have
* a simple service in MyJobService to get you started. The job is scheduled here in
* the activity, but the job itself is executed in MyJobService in the startJob() method. For
* example, to poll your server, you would create the network connection, send your GET
* request, and then process the response all in MyJobService. This allows the JobScheduler API
* to invoke your logic without needed to restart your activity.
*
* For brevity in the sample, we are scheduling the same job several times in quick succession,
* but again, try to consider similar tasks occurring over time in your application that can
* afford to wait and may benefit from batching.
*/
public void pollServer() {
JobScheduler scheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
for (int i=0; i<10; i++) {
JobInfo jobInfo = new JobInfo.Builder(i, mServiceComponent)
.setMinimumLatency(5000) // 5 seconds
.setOverrideDeadline(60000) // 60 seconds (for brevity in the sample)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) // WiFi or data connections
.build();
mWakeLockMsg.append("Scheduling job " + i + "!\n");
scheduler.schedule(jobInfo);
}
}
}
5、其他
对定位要求不是太高的话尽量不要使用GPS定位,可能使用wifi和移动网络cell定位即可。GPS定位消耗的电量远远高于移动网络定位。
获取屏幕尺寸等信息可以使用缓存技术,不需要进行多次请求。
很多人开发的程序后台都会一个service不停的去服务器上更新数据,在不更新数据的时候就让它sleep,这种方式是非常耗电的,通常情况下,我们可以使用AlarmManager来定时启动服务。如下所示,第30分钟执行一次。
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, MyService.class);
PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
long interval = DateUtils.MINUTE_IN_MILLIS * 30;
long firstWake = System.currentTimeMillis() + interval;
am.setRepeating(AlarmManager.RTC,firstWake, interval, pendingIntent);