编写Android app更新模块遇到的问题分析与总结

前不久接到个任务,在我们的app里面添加更新模块,在之前的版本中,我们的更新都是直接通过浏览器下载apk包来安装更新的,我想各位很大一部分应用的更新方法都是这样,因为它简单、方便,但是他也有许多不好的地方,比如需要用户跳转到浏览器页面、下载不可控、网络不好的情况的下失败无法续传,退出浏览器就无法接着下了等。。


     于是我们这个更新模块的需求就来了

1.下载后台进行,退出我们应用下载任务依旧能继续执行操作

2.下载文件支持断点续传

3.下载任务支持没有安装sdcard时也可下载更新

4.notify栏提示操作


   对几个需求稍作分析,解决方法如下:

1.下载更新的线程放到一个service中去,service的好处是不易被系统回收,而且也容易操作。我们需要先在AndroidMainfest.xml文件中去注册这个service

<service android:name="com.dj.app.UpdateService"/>

2.断点续传,请求头中有个重要的参数range,代表的意思的我要取的数据的范围,这个需要服务器支持,默认情况都是支持的。我的思路是下载前先获取文件包大小,然后检查是否已经有下载好的部分了,没有就从头开始,有就接着下。

request.addHeader("Range", "bytes=" + downLength + "-");

为了支持断点续传,我将下载好的文件版本号、文件长度都以prefrence的形似保存下来,下次更新如果还是这个版本就接着下,如果又有更新的了就删掉重下。

private void checkTemFile() {
			existTemFileVersionCode = preferences().getInt(
					UPDATE_FILE_VERSIONCODE, 0);
			if (newestVersionCode == existTemFileVersionCode
					|| newestVersionCode == TEST) {
				File temFile = new File(context.getFilesDir(),
						Integer.valueOf(newestVersionCode) + ".apk");
				if (!temFile.exists()) {
					saveLogFile(newestVersionCode, 0);
				}
			} else {
				deleteApkFile(existTemFileVersionCode);
				saveLogFile(newestVersionCode, 0);
			}
}


3.无sdcard时也能下载,那只能将apk包下载到系统内存中

context.getFilesDir();
这样创建的文件在/data/data/应用包名/files

伪代码:

if (downLength > 0) {//接着上次的下载

	outStream = context.openFileOutput(Integer.valueOf(newestVersionCode) + ".apk",
			Context.MODE_APPEND+ Context.MODE_WORLD_READABLE);

} else {//从头开始下载
      outStream = context.openFileOutput(Integer.valueOf(newestVersionCode) + ".apk",Context.MODE_WORLD_READABLE);
}


4.notify,我设定了一个更新notify的线程专门去观察下载线程的进度,每一分钟更新一次notify中的进度条。

new Thread() {
				public void run() {
					try {
						boolean notFinish = true;
						while (notFinish) {
							Thread.sleep(1000);
							notFinish = false;
							if (downloadThread == null) {
								break;
							}
							if (!downloadThread.downFinish) {
								notFinish = true;
							}
							downloadThread.showNotification();
						}
					} catch (Exception e) {
						e.printStackTrace();
					} finally {
						stopSelf();
					}
				};
			}.start();

downloadThread就是我的下载线程了因为要对不同进度时有不同的控制,这个可以通过notification.contentIntent来进行设定,比如100%的时候,我想要用户点击通知栏,即可进行安装,则应该这样做

notification.tickerText = "下载完成";
					notification.when = System.currentTimeMillis();
					Intent notificationIntent = new Intent();
					notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
					notificationIntent
							.setAction(android.content.Intent.ACTION_VIEW);
					String type = "application/vnd.android.package-archive";
					notificationIntent.setDataAndType(
							Uri.parse("file:///data/data/"
									+ context.getPackageName()
									+ "/files/"
									+ (Integer.valueOf(newestVersionCode)
											.toString()) + ".apk"), type);
					notification.contentView = rv;
					notification.flags = Notification.FLAG_AUTO_CANCEL;
					notification.contentView
							.setProgressBar(
									R.id.update_notification_progressbar, 100,
									p, false);
					notification.contentView.setTextViewText(
							R.id.update_notification_progresstext, p + "%");
					notification.contentView.setTextViewText(
							R.id.update_notification_title, "下载完成,点击安装");
					PendingIntent contentIntent = PendingIntent.getActivity(
							context, 0, notificationIntent, 0);
					notification.contentIntent = contentIntent;


 

 

下面我将下载线程完整的代码贴出来

	class DownloadThread extends Thread {
		private static final String UPDATE_FILE_VERSIONCODE = "updateTemFileVersionCode";
		private static final String UPDATE_FILE_LENGTH = "updateTemFileLength";
		private static final String TEST_UPDATE_FILE_LENGTH = "testupdatefilelength";
		private static final int BUFFER_SIZE = 1024;
		private static final int NETWORK_CONNECTION_TIMEOUT = 15000;
		private static final int NETWORK_SO_TIMEOUT = 15000;
		private int newestVersionCode, existTemFileVersionCode;
		private String downUrl;
		private int fileLength = Integer.MAX_VALUE;
		private int downLength;
		private boolean downFinish;

		private static final int CHECK_FAILED = -1;
		private static final int CHECK_SUCCESS = 0;
		private static final int CHECK_RUNNING = 1;
		private int checkStatus;
		private boolean isChecking = false;

		private Context context;
		private boolean isPercentZeroRunning = false;
		private boolean stop;
		private Object block = new Object();
		private boolean receiverRegistered = false;

		private NotificationManager mNM;
		private RemoteViews rv;

		public DownloadThread(Context context, String downUrl, int versionCode) {
			super("DownloadThread");
			this.downUrl = downUrl;
			this.newestVersionCode = versionCode;
			this.context = context;
			this.mNM = (NotificationManager) context
					.getSystemService(Context.NOTIFICATION_SERVICE);
			this.rv = new RemoteViews(context.getPackageName(), R.layout.notify);
			this.downFinish = false;
		}

		private void checkTemFile() {
			existTemFileVersionCode = preferences().getInt(
					UPDATE_FILE_VERSIONCODE, 0);
			if (newestVersionCode == existTemFileVersionCode
					|| newestVersionCode == TEST) {
				File temFile = new File(context.getFilesDir(),
						Integer.valueOf(newestVersionCode) + ".apk");
				if (!temFile.exists()) {
					saveLogFile(newestVersionCode, 0);
				}
			} else {
				deleteApkFile(existTemFileVersionCode);
				saveLogFile(newestVersionCode, 0);
			}
		}

		private void deleteApkFile(int existVersionCode) {
			File temFile = new File(context.getFilesDir(),
					Integer.valueOf(existVersionCode) + ".apk");
			if (temFile.exists()) {
				temFile.delete();
			}
		}

		private SharedPreferences preferences() {
			return context.getSharedPreferences(context.getPackageName(),
					Context.MODE_WORLD_READABLE | Context.MODE_WORLD_WRITEABLE);
		}

		private void saveLogFile(int versionCode, int downloadLength) {
			SharedPreferences.Editor edit = preferences().edit();
			if (versionCode == TEST) {
				edit.putInt(TEST_UPDATE_FILE_LENGTH, downloadLength);
			} else {
				edit.putInt(UPDATE_FILE_VERSIONCODE, versionCode);
				edit.putInt(UPDATE_FILE_LENGTH, downloadLength);
			}
			edit.commit();
		}

		@Override
		public void run() {
			checkTemFile();
			this.stop = false;
			while (!downFinish) {
				Log.i(TAG, "download thread start : while()");
				if (newestVersionCode == TEST) {
					downLength = preferences().getInt(TEST_UPDATE_FILE_LENGTH,
							0);
				} else {
					downLength = preferences().getInt(UPDATE_FILE_LENGTH, 0);
				}

				InputStream is = null;
				FileOutputStream outStream = null;
				try {
					// check the network
					ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
					NetworkInfo ni = cm.getActiveNetworkInfo();
					boolean con = ni == null ? false : ni
							.isConnectedOrConnecting();
					synchronized (block) {
						if (!con) {
							context.registerReceiver(
									receiver,
									new IntentFilter(
											ConnectivityManager.CONNECTIVITY_ACTION));
							receiverRegistered = true;
							try {
								Log.i(TAG, "network is not ok : block.wait()");
								block.wait();
							} catch (InterruptedException e1) {
							}
						}
					}

					if (fileLength == Integer.MAX_VALUE) {
						URL url = new URL(downUrl);
						HttpURLConnection conn = (HttpURLConnection) url
								.openConnection();
						if (conn.getResponseCode() / 100 == 2
								&& conn.getContentLength() != -1) {
							fileLength = conn.getContentLength();
							Log.i(TAG, "getContentLength == " + fileLength);
						} else {
							Thread.sleep(5000);
							Log.i(TAG, "getContentLength failed : retry in 5 second later");
							continue;
						}
					}

					HttpClient httpClient = new DefaultHttpClient();
					HttpGet request = new HttpGet(downUrl);
					request.addHeader("Range", "bytes=" + downLength + "-");

					if (downLength < fileLength) {
						HttpHost proxy = HttpBase.globalProxy();
						HttpParams httpParams = request.getParams();
						HttpConnectionParams.setConnectionTimeout(httpParams,
								NETWORK_CONNECTION_TIMEOUT);
						HttpConnectionParams.setSoTimeout(httpParams,
								NETWORK_SO_TIMEOUT);
						ConnRouteParams.setDefaultProxy(request.getParams(),
								proxy);

						HttpResponse response = httpClient.execute(request);
						Log.i(TAG, "getContent's response status == "
								+ response.getStatusLine().getStatusCode());
						if (response.getStatusLine().getStatusCode() / 100 != 2) {
							continue;
						}
						HttpEntity entity = response.getEntity();
						is = entity.getContent();
						byte[] buffer = new byte[BUFFER_SIZE];
						int offset = 0;

						if (downLength > 0) {
							outStream = context
									.openFileOutput(
											Integer.valueOf(newestVersionCode)
													+ ".apk",
											Context.MODE_APPEND
													+ Context.MODE_WORLD_READABLE);

						} else {
							outStream = context
									.openFileOutput(
											Integer.valueOf(newestVersionCode)
													+ ".apk",
											Context.MODE_WORLD_READABLE);
						}

						while ((offset = is.read(buffer, 0, BUFFER_SIZE)) != -1
								&& !stop) {
							outStream.write(buffer, 0, offset);
							downLength += offset;
						}
					}
					if (downLength == fileLength) {
						File apkFile = new File(context.getFilesDir(),
								Integer.valueOf(newestVersionCode) + ".apk");
						if (isApkFileOK(apkFile)) {
							checkStatus = CHECK_SUCCESS;
						} else {
							deleteApkFile(newestVersionCode);
							saveLogFile(existTemFileVersionCode, 0);
							checkStatus = CHECK_FAILED;
						}
						this.downFinish = true;
					}

				} catch (Exception e) {
				} finally {
					saveLogFile(newestVersionCode, downLength);
					if (receiverRegistered) {
						context.unregisterReceiver(receiver);
						receiverRegistered = false;
					}

					if (stop || downFinish) {
						break;
					}
					try {
						if (outStream != null) {
							outStream.close();
						}
						if (is != null) {
							is.close();
						}
					} catch (IOException e) {
					}
				}
			}

		}

		@Override
		public void interrupt() {
			synchronized (block) {
				Log.i(TAG, "block.notify()");
				block.notify();
			}
		}

		private void cancel() {
			stop = true;
			mNM.cancel(MOOD_NOTIFICATIONS);
		}

		private boolean isApkFileOK(File file) {
			checkStatus = CHECK_RUNNING;
			// first check the file header
			/*if (file.isDirectory() || !file.canRead() || file.length() < 4) {
				return false;
			}
			DataInputStream in = null;
			try {
				in = new DataInputStream(new BufferedInputStream(
						new FileInputStream(file)));
				int test = in.readInt();
				if (test != 0x504b0304)
					return false;
			} catch (IOException e) {
				return false;
			} finally {
				try {
					in.close();
				} catch (IOException e) {
				}
			}*/

			// second unZip file to check(without saving)
			boolean result = unzip(file);
			isChecking = false;
			return result;
		}

		private boolean unzip(File unZipFile) {
			boolean succeed = true;
			ZipInputStream zin = null;
			ZipEntry entry = null;
			try {
				zin = new ZipInputStream(new FileInputStream(unZipFile));
				boolean first = true;
				while (true) {
					if ((entry = zin.getNextEntry()) == null) {
						if (first)
							succeed = false;
						break;
					}
					first = false;
					if (entry.isDirectory()) {
						zin.closeEntry();
						continue;
					}
					if (!entry.isDirectory()) {
						byte[] b = new byte[1024];
						@SuppressWarnings("unused")
						int len = 0;
						while ((len = zin.read(b)) != -1) {
						}
						zin.closeEntry();
					}
				}
			} catch (IOException e) {
				succeed = false;
			} finally {
				if (null != zin) {
					try {
						zin.close();
					} catch (IOException e) {
					}
				}
			}
			return succeed;
		}

		public void showNotification() {
			float result = (float) downLength / (float) fileLength;
			int p = (int) (result * 100);
			if (p == 0 && isPercentZeroRunning || p == 100 && isChecking) {
				return;
			} else if (p != 0) {
				isPercentZeroRunning = false;
			}
			Notification notification = new Notification(R.drawable.icon, null,
					0);

			if (p == 100) {
				if (checkStatus == CHECK_RUNNING) {
					notification.tickerText = "开始检查下载文件";
					notification.when = System.currentTimeMillis();
					notification.flags = notification.flags
							| Notification.FLAG_ONGOING_EVENT
							| Notification.FLAG_NO_CLEAR;
					notification.contentView = rv;
					notification.contentView.setProgressBar(
							R.id.update_notification_progressbar, 100, p, true);
					notification.contentView.setTextViewText(
							R.id.update_notification_title, "正在检查下载文件...");
					PendingIntent contentIntent = PendingIntent.getActivity(
							context, 0, null, 0);
					notification.contentIntent = contentIntent;
					isChecking = true;
				} else if (checkStatus == CHECK_FAILED) {
					notification.tickerText = "文件验证失败!";
					notification.when = System.currentTimeMillis();
					notification.flags = notification.flags
							| Notification.FLAG_AUTO_CANCEL;
					notification.contentView = rv;
					notification.contentView.setProgressBar(
							R.id.update_notification_progressbar, 100, p, false);
					notification.contentView.setTextViewText(
							R.id.update_notification_title, "文件验证失败,请重新下载");
					Intent notificationIntent = new Intent(context,
							UpdateService.class);
					notificationIntent.setAction(ACTION_STOP);
					PendingIntent contentIntent = PendingIntent.getService(
							context, 0, notificationIntent, 0);
					notification.contentIntent = contentIntent;
				} else {
					notification.tickerText = "下载完成";
					notification.when = System.currentTimeMillis();
					Intent notificationIntent = new Intent();
					notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
					notificationIntent
							.setAction(android.content.Intent.ACTION_VIEW);
					String type = "application/vnd.android.package-archive";
					notificationIntent.setDataAndType(
							Uri.parse("file:///data/data/"
									+ context.getPackageName()
									+ "/files/"
									+ (Integer.valueOf(newestVersionCode)
											.toString()) + ".apk"), type);
					notification.contentView = rv;
					notification.flags = Notification.FLAG_AUTO_CANCEL;
					notification.contentView
							.setProgressBar(
									R.id.update_notification_progressbar, 100,
									p, false);
					notification.contentView.setTextViewText(
							R.id.update_notification_progresstext, p + "%");
					notification.contentView.setTextViewText(
							R.id.update_notification_title, "下载完成,点击安装");
					PendingIntent contentIntent = PendingIntent.getActivity(
							context, 0, notificationIntent, 0);
					notification.contentIntent = contentIntent;
				}

			} else if (p == 0) {
				notification.tickerText = "准备下载";
				notification.when = System.currentTimeMillis();
				Intent notificationIntent = new Intent(context,
						UpdateService.class);
				notificationIntent.setAction(ACTION_STOP);
				notification.flags = notification.flags
						| Notification.FLAG_ONGOING_EVENT;
				notification.contentView = rv;
				notification.contentView.setProgressBar(
						R.id.update_notification_progressbar, 100, p, true);
				notification.contentView.setTextViewText(
						R.id.update_notification_title, "正在准备下载(点击取消)");
				PendingIntent contentIntent = PendingIntent.getService(context,
						0, notificationIntent, 0);
				notification.contentIntent = contentIntent;
				isPercentZeroRunning = true;
			} else {
				notification.tickerText = "开始下载";
				notification.when = System.currentTimeMillis();
				Intent notificationIntent = new Intent(context,
						UpdateService.class);
				notificationIntent.setAction(ACTION_STOP);
				notification.contentView = rv;
				notification.flags = notification.flags
						| Notification.FLAG_ONGOING_EVENT;
				notification.contentView.setProgressBar(
						R.id.update_notification_progressbar, 100, p, false);
				notification.contentView.setTextViewText(
						R.id.update_notification_progresstext, p + "%");
				notification.contentView.setTextViewText(
						R.id.update_notification_title, "正在下载(点击取消)");
				PendingIntent contentIntent = PendingIntent.getService(context,
						0, notificationIntent, 0);
				notification.contentIntent = contentIntent;
			}

			mNM.notify(MOOD_NOTIFICATIONS, notification);
		}
	}

可以看到,我的下载是包裹在一个while循环中的,假如没有下载完成,我会一直重复这个循环,可以注意到,我在取数据的时候有个标志位stop

while ((offset = is.read(buffer, 0, BUFFER_SIZE)) != -1
								&& !stop) {
							outStream.write(buffer, 0, offset);
							downLength += offset;
						}
 有了这个就可以在外面控制强制停止下载。 

在下载开始阶段我最先做的时就是检查网络情况,如果没有网络,我就使用一个block让这个线程阻塞掉,有人会问那什么时候恢复呢?我在service里面加了个广播

private final BroadcastReceiver receiver = new BroadcastReceiver() {

		@Override
		public void onReceive(Context context, Intent intent) {
			if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent
					.getAction())) {
				NetworkInfo info = (NetworkInfo) intent
						.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
				boolean hasConnectivity = (info != null && info.isConnected()) ? true
						: false;
				if (hasConnectivity && downloadThread != null) {
					downloadThread.interrupt();
				}
			}
		}

};

可以监听系统网络情况,如果连上了,就调用interrupt()来唤醒线程。这样做的好处时,在没有网络时,这个线程不会无限循环的去获取数据。


最后在这个版本发布后因为代码一些bug导致,如果网络数据获取有问题,则用户下载下来的安装包就会解析错误,而且这是个死胡同,除非去手动下载个好的安装包来装,否则我们软件会一直提示,一直完成,但是一直装不上。。。哎,我们用户可是百万级啊,这个bug很致命,还好当时做了备用方案,可以由服务器控制客户端更新方法,于是改为以前的更新。


这也是我在上面的代码中下载完成时加入了最后一步,校验安装包的过程。我们都知道apk就是一个zip文件,通常对一个zip文件的校验,最简单的是校验文件头是不是0x504b0304,但是这只是文件格式的判断,加入是文件内容字节损坏还是查不出来,只能通过去unzip这个文件来捕获异常。在上面unzip(File unZipFile)方法中,我尝试去解压软件,对ZipInputStream流我没做任何处理,仅仅是看这个解压过程是否正常,以此判断这个zip文件是否正常,暂时也没想到更好的办法。

		private boolean unzip(File unZipFile) {
			boolean succeed = true;
			ZipInputStream zin = null;
			ZipEntry entry = null;
			try {
				zin = new ZipInputStream(new FileInputStream(unZipFile));
				boolean first = true;
				while (true) {
					if ((entry = zin.getNextEntry()) == null) {
						if (first)
							succeed = false;
						break;
					}
					first = false;
					if (entry.isDirectory()) {
						zin.closeEntry();
						continue;
					}
					if (!entry.isDirectory()) {
						byte[] b = new byte[1024];
						@SuppressWarnings("unused")
						int len = 0;
						while ((len = zin.read(b)) != -1) {
						}
						zin.closeEntry();
					}
				}
			} catch (IOException e) {
				succeed = false;
			} finally {
				if (null != zin) {
					try {
						zin.close();
					} catch (IOException e) {
					}
				}
			}
			return succeed;
		}



 
 
 
 
 
 
 
 
 
 
 

                
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值