本文分为两个部分,第一部分是网络优化,第二部分是耗电量优化。
第一部分,网络优化:
一个网络请求可以简单分为连接服务器 -> 获取数据两个部分。
其中连接服务器前还包括 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<JobParameters, Void, String> { 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);