看了很多大神写的博客,真是受益匪浅。从菜鸟成长,现在虽然变得不那么菜,但还是有很多东西需要继续的学习。
写博客 第一是希望将自己以前写过的东西(模块化的东西)记录下来,方便以后需要再来查找;第二是希望有人如果需要重复造轮子可以拿去借鉴(如果有帮助的法);第三是有些东西是成长的足迹,能记录下来也是一种生活。
闲话不多扯,进入正题:
版本更新的模块对于当前App来讲,是最基本的功能之一,因为一个App不会只出一个版本,绝大多数的应用都是逐步的完善,将功能和方向在版本迭代中完善和找准。
当然,版本更新也有很多第三方的库,之前使用的是友盟更新,这是一个很不错的平台,但是在年初的时候,友盟通告说年底之后将不再提供版本更新的服务。所以针对版本更新不能再依附于友盟了,其他的三方更新功能也存在,为防止再出现类似的情况,自己去实现这个基本的功能。
一:目的
针对应用的版本更新的迭代,实现持续性的产品需求。
二:要求
1)版本更新首要的是稳定,安全(稳定和安全都应该是软件重要的特性之一)
2)代码的维护成本
三:过程
1)首先要做的是 清楚版本更新的流程(在开发过程中 大家都很清楚,产品定下需求,做开发在开发之前要对需求过一遍)
a)客户端请求服务端,检查服务端的最新版本,然后将服务端的最新版本信息获取到客户端,然后再根据本地版本的信息,选择是否更新。
b)本地版本如果等于服务器的版本,那就无需更新。如果本地版本比服务端版本要小(或者说版本旧),那就需要更新。
c)本地更行应用,一般是在wifi先进行更新(毕竟大多数应用的下载更新还是会耗费几十兆的流量,相对将这些流量使用到其他地方,或许会更划算些),在手机自己的流量下,不更新(当然也可以增加设置,版本更新在任何情况下都可以进行)。
d)版本的更新,在wifi下,可能不会在一次更新下载中 立马就能下载完成,所以就需要记录已经下载的进度。
e)当安装包下载完成之后,就需要在适当的时候进行更新提示及更新操作,然后进行更新。
2)其次是代码实现
a) 几个主要的类
UpdateManager ---用于管理和检查版本更新的manager类
UpdateService ---用于处理版本更新下载新包的服务
NetStatusReceiver --用于监听网络变化的广播
b)版本更新的检查()
代码如下:
//检查更新的网络请求
private void initData(){
mContext = this;
int curVersionCode = CommonHelper.getVersionCode(mContext);
String tempUrl= FinalUtil.HOST_UPDATE + "?app_version=" + curVersionCode + "&app_channel=Huasheng";
//使用OkHttp 框架请求服务 获取版本更新的信息
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(tempUrl).build();
client.newCall(request).enqueue(getCallBackListener());
mHandler.sendEmptyMessageDelayed(TYPE_ENTER,3000);
}
//Okhttp的回调
private Callback getCallBackListener(){
Callback listener = new Callback() {
@Override
public void onFailure(Request request, IOException e) {
Log.i("android","get update info failure");
}
@Override
public void onResponse(Response response) throws IOException {
byte[] data = null;
try{
ResponseBody body = response.body();
if(body != null)
data = body.bytes();
}catch (Exception e){
LogUtil.e("Exception: data is null:" + e.getMessage() +" --> " + (data == null) + " response:" + response.toString());
}
if(data == null) return;
try{
JSONObject jsonObject = new JSONObject(new String(data));
Object tempObject = jsonObject.get("data");
Gson tempGson = new Gson();
UpdateBean bean = tempGson.fromJson(tempObject.toString(), UpdateBean.class);
checkUpdate(bean);
}catch (Exception e){}
}
};
return listener;
}
// 版本更新的检查
private void checkUpdate(UpdateBean bean){
if(bean == null) return;
if(TextUtils.isEmpty(bean.download_link)) return;
strMd5 = bean.apk_md5;
MApplication.mUpdateManager.setServerBean(bean);
if(MApplication.mUpdateManager.checkUpdate(mContext)) {
MApplication.mUpdateManager.doUpdate(mContext,Integer.parseInt(bean.version_code));
}
}
c)进行版本更新
1)代码如下:
//检查是否需要更新
public boolean checkUpdate(Context context){
if(mServerBean != null){
mServerVersionCode = Integer.parseInt(mServerBean.version_code);
mAppVersionCode = DeviceInfoManager.getAppVersionCode(context);
if(mServerVersionCode > mAppVersionCode&& mServerVersionCode > 0 && mAppVersionCode > 0){//需要下载
mDBBean = MApplication.mCommonDao.selectUpdateBean(String.valueOf(mServerVersionCode));
if(mDBBean != null && !TextUtils.isEmpty(mDBBean.version_code)){ //重新下载
int tempDBCode = Integer.parseInt(mDBBean.version_code);
if( tempDBCode != mServerVersionCode){//之前下载的版本与当前服务器上最新版本不一致,删除之前下载的版本
doDeleteLocalFile();
doDeleteDBRecord();
mDBBean = null;
return true;
}else{
if(mDBBean.end == mDBBean.finished)
return false;//表示之前已经下载完了,不需要再次下载
else
return true;
}
}
return true;
}
return false;//当前版本与服务器版本一致时 是不开下载服务的
}else
return false;
}
//进行更新
public void doUpdate(final Context context,final int versionCode){
if(mDBBean == null || TextUtils.isEmpty(mDBBean.version_code)){//初次下载处理,必须得保存下载的数据长度,不然会出现刚进入是自己网,之后切换成无线网进行下载异常情况
ThreadPool.execute(new Runnable() {
@Override
public void run() {
long tempFileLength = doGetDownloadLength(mServerBean.download_link);
mDBBean = new UpdateBean();
mDBBean.end = tempFileLength;
mDBBean.download_link = mServerBean.download_link;
mDBBean.version_code = mServerBean.version_code;
mDBBean.intro = mServerBean.intro;
boolean isInserted = doSaveDBRecord(mDBBean);
if(isInserted && NetUtil.isNetworkConnected(context) && NetUtil.isNetWorkWifi(context)){//有网 有无线网时进行下载
Intent intentStartDownload = new Intent(context,UpdateService.class);
intentStartDownload.putExtra("BEAN",mDBBean);
context.startService(intentStartDownload);
}
}
});
}else if(mDBBean != null && !TextUtils.isEmpty(mDBBean.download_link) && NetUtil.isNetworkConnected(context) && NetUtil.isNetWorkWifi(context)){//表示之前已经下载过,并且没有下载完
if(mDBBean.end > mDBBean.finished){
ThreadPool.execute(new Runnable() {
@Override
public void run() {
Intent intentStartDownload = new Intent(context,UpdateService.class);
intentStartDownload.putExtra("BEAN",mDBBean);
context.startService(intentStartDownload);
}
});
}
}
}
//删除本地文件
private void doDeleteLocalFile(){
File tempFile = new File(DOWNLOAD_FILE_SAVE_PATH + File.separator + DOWNLOAD_FILE_SAVE_NAME);
if(tempFile.exists()){
try {
tempFile.delete();
}catch (Exception e){}
}
}
//删除本地的文件
private void doDeleteDBRecord(){
MApplication.mCommonDao.deleteAllUpdateBean();
}
//获取下载文件的长度
private long doGetDownloadLength(String urlPath){
if(TextUtils.isEmpty(urlPath)) return 0;
long tempFileLength = 0;
HttpURLConnection urlcon = null;
try{
URL url = new URL(urlPath);
urlcon = (HttpURLConnection) url.openConnection();
tempFileLength = urlcon.getContentLength();
}catch (Exception e){}
finally {
if(urlcon != null){
urlcon.disconnect();
}
}
return tempFileLength;
}
//下载的第一次保存要下载的文件(必须要做这个动作,因为存在使用自己流量进入应用,在使用过程中切换成无线wifi)
private boolean doSaveDBRecord(UpdateBean bean){
boolean isInsert = MApplication.mCommonDao.insertUpdateBean(bean);
return isInsert;
}
d)安装包的文件的下载服务处理
1)代码如下:
//请求网络
private void doDownloadFromService(Context context, UpdateBean bean) {
if(bean == null || TextUtils.isEmpty(bean.download_link) || bean.end <= 0){
Intent intentStopService = new Intent(context,UpdateService.class);
context.stopService(intentStopService);//在服务中没有更新的bean,那么不进行更新下载操作.同样也实用于网络的状态的改变
return;//网络状态的改变 开启和关闭更新的服务,但是需要做对空的判断
}
HttpURLConnection conn = null;
RandomAccessFile raf = null;
InputStream in = null;
int respCode = 0;
long finished = 0;
try {
URL url = new URL(bean.download_link);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(3000);
conn.setDoInput(true);
conn.setRequestMethod("GET");
// 设置下载位置
long start = bean.start + bean.finished;
conn.setRequestProperty("Range",
"bytes=" + start + "-" + bean.end);
// 设置文件写入位置
File file = new File(UpdateManager.DOWNLOAD_FILE_SAVE_PATH, UpdateManager.DOWNLOAD_FILE_SAVE_NAME);
raf = new RandomAccessFile(file, "rwd");
raf.seek(start);
finished += bean.finished;
respCode = conn.getResponseCode();
// 开始下载
if (respCode == 206 || respCode == 200) {//206 Partial Content
// 读取数据
in = conn.getInputStream();
int len = -1;
byte[] b = new byte[1024 * 4];
while ((len = in.read(b)) != -1) {
// 写入文件
raf.write(b, 0, len);
finished += len;
LogUtil.e("abcde","finished:" + ((finished * 1.0f) / (bean.end * 1.0f) * 100) + "%");
bean.finished = finished;
if (finished == bean.end) {
bean.status = UpdateManager.DOWNLOAD_STATUS_FINISHED;
} else if (finished > 0 && finished < bean.end) {
bean.status = UpdateManager.DOWNLOAD_STATUS_UNFINISHED;
}
MApplication.mCommonDao.updateUpdateBean(bean);//已经在子线程中 所以这里不在开子线程进行数据的操作写数据库数据
if (isPause) {//因为网络切换 不在联网或者是不再是wifi情况下 就不下载,但是服务会一直存在着的
return;
}
}
//去关闭下载服务
Intent intentStopDownload = new Intent(context, UpdateService.class);
context.stopService(intentStopDownload);
}
} catch (Exception e) {
if (respCode == -1 || respCode == 0) {
if (requestCount < 3) {
//重新联网一次
requestCount++;
connectionClose(raf, conn);
doDownloadFromService(context,bean);
} else {
requestCount = 0;
connectionClose(raf, conn);
}
} else if (respCode == HttpURLConnection.HTTP_INTERNAL_ERROR
|| respCode == HttpURLConnection.HTTP_NOT_FOUND || respCode == HttpURLConnection.HTTP_BAD_GATEWAY
|| respCode == HttpURLConnection.HTTP_UNAUTHORIZED
|| respCode == HttpURLConnection.HTTP_GATEWAY_TIMEOUT) {
connectionClose(raf, conn);
}
e.printStackTrace();
} finally {
connectionClose(raf, conn);
}
}
e)下载完毕之后在应用的适当位置进行提示更新
1)代码实现
//应用的版本更新(最新处理)
private void checkNeedUpdate(){
ThreadPool.execute(new Runnable() {
@Override
public void run() {
bean = MApplication.mCommonDao.selectUpdateBean();
if(bean == null || TextUtils.isEmpty(bean.version_code)) return;//如果数据库只能那个无记录, 或者记录的版本号为空 不更新处理
int dbVerCode = 0;
if(bean != null && !TextUtils.isEmpty(bean.version_code))
dbVerCode = Integer.parseInt(bean.version_code);
int curVerCode = DeviceInfoManager.getAppVersionCode(mContext);
if(bean != null && dbVerCode <= curVerCode && dbVerCode > 0){
deleteUpdateDBandFile();
}
if(dbVerCode > curVerCode && bean != null && bean.end == bean.finished && bean.end > 0 && bean.status == 3){ //在下一版中添加这个状态 1未下载 2下载未完成 3下载完成
doUpdateOperate();
}
}
});
}
private void deleteUpdateDBandFile(){
try{
MApplication.mCommonDao.deleteUpdateBean(bean.version_code);
File tempFile = new File(UpdateManager.DOWNLOAD_FILE_SAVE_PATH + File.separator
+ UpdateManager.DOWNLOAD_FILE_SAVE_NAME);
if(tempFile != null && tempFile.exists()){
tempFile.delete();//如果下载的文件已经和安装的一个版本 就直接删除数据文件和下载的文件
}
return;
}catch (Exception e){}
}
private void doUpdateOperate(){
File file = new File(UpdateManager.DOWNLOAD_FILE_SAVE_PATH + File.separator + UpdateManager.DOWNLOAD_FILE_SAVE_NAME);
String localFileMd5 = "";
if(!file.exists()){
try{
File tempFile = new File(UpdateManager.DOWNLOAD_FILE_SAVE_PATH + File.separator);
if(!tempFile.exists()){
tempFile.mkdirs();
}
}catch (Exception e){}
}else{
localFileMd5 = MD5Helper.getMD5(file).toUpperCase();//转成大写
}
strMd5 = strMd5.toUpperCase(); //转成大写
boolean checkFileMd5 = false;
if(!TextUtils.isEmpty(strMd5) && !TextUtils.isEmpty(localFileMd5) && localFileMd5.equals(strMd5))
checkFileMd5 = true;
if(checkFileMd5 && file != null && file.exists()){
runOnUiThread(new Runnable() {
@Override
public void run() {
commonDialog = new CommonDialog(mContext);
commonDialog.setCanceledOnTouchOutside(false);
commonDialog.setShowTitle(false);
commonDialog.setShowClose(false);
View view = getLayoutInflater().inflate(R.layout.update_dialog, null);
((TextView) view.findViewById(R.id.umeng_update_content)).setText(bean.intro);
commonDialog.setContainer(view);
view.findViewById(R.id.umeng_update_id_ok).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
commonDialog.dismiss();
//设置你的操作事项
//安装应用程序
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
File file = new File(UpdateManager.DOWNLOAD_FILE_SAVE_PATH + File.separator + UpdateManager.DOWNLOAD_FILE_SAVE_NAME);
intent.setDataAndType(Uri.fromFile(file),
"application/vnd.android.package-archive");
startActivity(intent);
}
});
view.findViewById(R.id.umeng_update_id_cancel).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
commonDialog.dismiss();
}
});
view.findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
commonDialog.dismiss();
//设置你的操作事项
//安装应用程序
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
File file = new File(UpdateManager.DOWNLOAD_FILE_SAVE_PATH + File.separator
+ UpdateManager.DOWNLOAD_FILE_SAVE_NAME);
intent.setDataAndType(Uri.fromFile(file),
"application/vnd.android.package-archive");
startActivity(intent);
}
});
commonDialog.showDialog();
}
});
}else{//如果下载的文件MD5值不等,肯定安装不成功.所以这是删除本地数据库记录和本地文件
ThreadPool.execute(new Runnable() {
@Override
public void run() {
deleteUpdateDBandFile();
}
});
}
}
Ps.针对上边代码模块的说明,因为整个博客是对版本更新模块的说明,所以上边的代码只是为说明版本更新流程部分代码,如果有需要查看源码的请在博客最后地址查看源码。
四:总结
1)版本更新是一个相对独立的模块,所以在应用开发中应该放在一个独立的模块中。便于维护。
2) 版本更新是一个简单 但是非常重要的功能,如果在版本更新中出现问题,那时比较严重的。因为出现版本更新更新不了,直接导致的结果是用户无法升级,要么用户只能体验最初的版本,要么用户先删除以前的旧版,再安装新版(这样的用户体验真是糟糕)。
3)版本更新功能只是一个应用为迭代新版本的开始,后续的精彩还是需要各位为之继续努力的。哈哈~~
最后附上源码地址:https://github.com/wzp09tjlg/VersionUpdate.git