Android版本跟新的实现方式有很多种。
1.渠道更新
正常的版本迭代开发,完成后提交渠道审核,审核后用户可以在渠道市场中下载最新的应用。这种算是比较常见app跟新方式,不过这种方式至少存在几种弊端1.渠道审核周期比较长,审核标准高,无法实现app频度比较高的版本迭代(当然,这种需求比较常见的是用热更新解决方案)。2.无法实现市场产品的统一化,即用户手中的APP版本是可能存在差异化的,这种结果对后期维护,特别是兼容性,简直是一种灾难,所以,现在基本上放弃这种方案。
2.应用内更新
这种方式比较常见。即在应用启动时候,通过接口获得最新的产品版本号,并且跟本地的版本号对比,如果低于最新的版本号,则通过后台线程去下载服务端的最新应用,然后安装替换原来的应用。这种方式不仅可以绕过渠道的审核,而且可是实现频度比较高的跟新。所以基本上,这种方式算是比较正常的。但是,要实现这种方式的跟新,你必须要有一个靠谱的后端,它愿意给你提供这样一个接口,并且,你的服务器具备存放文件的功能(FTP?).如果不能的话,请看第三种。
应用内实现更新的核心在于如何去实现下载后台最新产品。这里提供的方案有两种:
1.系统自带下载器:DownloadManager。
对于应用的需求实现,本人的偏向一贯是如果系统提供实现方案优先采用系统方案,如果系统没有提供方案再自己DIY。因为我一直偏执的认为,Googgle程序员写的代码无论如何总比自己这个渣渣来的强吧。这种偏执可能是来源于自己的不自信,也可能是因为自己菜,菜,菜!当然,还有懒,懒,懒!
闲言少叙,DownloadManager实现下载需求还是比较简单的。DownloadManager这货是系统专门提供给我们下载专用的。 按照习惯,先打开这类,看看这货到底是什么.
/**
* The download manager is a system service that handles long-running HTTP downloads. Clients may
* request that a URI be downloaded to a particular destination file. The download manager will
* conduct the download in the background, taking care of HTTP interactions and retrying downloads
* after failures or across connectivity changes and system reboots.
* <p>
* Apps that request downloads through this API should register a broadcast receiver for
* {@link #ACTION_NOTIFICATION_CLICKED} to appropriately handle when the user clicks on a running
* download in a notification or from the downloads UI.
* <p>
* Note that the application must have the {@link android.Manifest.permission#INTERNET}
* permission to use this class.
*/
原谅我的渣渣翻译:
1.这货是一个用于处理长时间的Http请求下载的系统服务类。客户端通过url下载指定的文件目标。下载器是运行在后台线程,并且可以实现http交互,在下载失败,改变连接方式,或者系统重启的情况下实现重新下载的功能。
2.这货和广播接受者是绝配。应用通过这个Api实现下载功能,需要注册一个广播接收者{#ACTION_NOTIFICATION_CLICKED}以便于处理用户点击正在下载中的通知栏,或者下载的UI展示。
这么官方的解释。我们可以得到什么信息?
1.DownloadManager是一个系统服务类。并且运行在后台线程中。
mDownloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
2.通过广播监测用户操作。
这货是通过广播实现用户交互监测。那我们需要实时的监测下载进度怎么办?感觉这货对我们不太友好,至少提供个回调监听或者什么的吧。不过仔细一想,这跨进程的,实现回调监听也是不太现实。看看源码吧。发现它的两个内部类。
DownloadManager.Request:
下载对象,封装下载需求。如URL,是否需要现实下载UI,下载对象的文件TYPE等。
// 实例化一个下载对象;
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
// 允许被媒体库扫描到;
request.allowScanningByMediaScanner();
// 允许在漫游状态下下载文件;
request.setAllowedOverRoaming(true);
// 默认情况下,mobile和wifi网络情况下都是允许下载的;
// request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
// 下载过程中隐藏系统下载界面;
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);
// 设置存储位置;
request.setDestinationInExternalPublicDir(Environment.getExternalStorageDirectory()
.getAbsolutePath(), fileName);
// 这里设置为apk文件;
request.setMimeType("application/vnd.android.package-archive");
DownloadManager.Query:
系统给我们提供的查询类,可以查询下载状态,下载文件情况等。
既然已经了解这DownloadManager,那就开始干吧。
No1:因为要实现下载进度的查询,先定义个接口吧。
public interface DownloadListener {
// 开始下载
void onDownloadStart();
// 下载暂停
void onDownloadPause();
// 下载进行中(参数为下载的百分比)
void onDownloadRunning(int current);
// 下载成功
void onSuccess();
// 下载失败
void onFailed();
}
No2:自己实现一个下载工具类,方便代码应用;
public class DownloadUtils {
private Context mContext;
private DownloadManager mDownloadManager;
private DownloadListener mListener;
public DownloadUtils(Context context) {
mContext = context;
mDownloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
}
}
为了保证下载后安装的apk永远都是最新的,我直接先实例化了一个线程去删除文件系统中的同名apk。这里通过ContentResolver 去查询文件系统中所有的同名apk是显得有点杀鸡用牛刀,正常情况下,只需要去查看我们在DownloadManager.Request对象中配置的下载路径就可以了,没有必要动用ContentResolver。之所以这样做的目的是因为有些手机(我这里使用的是RedMi Note 4)在返回 文件存储目录和实际文件的存储目录不一致,导致Uri.fromFile(file)返回的uri一直都是找不到文件的。所以,这个是个无奈之举。当然,我们也可以在安装完新版本的apk后马上去删除apk文件,这样保证每次安装的都是最新的apk文件。这里不再多叙。
// 实例化一个线程去删除想用的apk文件。
new Thread(new Runnable() {
@Override
public void run() {
ContentResolver contentResolver = mContext.getContentResolver();
Uri uri = MediaStore.Files.getContentUri("external");
Cursor query = contentResolver.query(uri, null, "mime_type=? and title=?", new
String[]{"application/vnd.android.package-archive", "lottery"}, null);
if (query != null) {
File f= null;
while (query.moveToNext()) {
String filePath = query.getString(query.getColumnIndex(MediaStore.Files.FileColumns
.DATA));
f = new File(filePath);
if(f.exists()){
f.delete();
}
}
if(query!=null){
query.close();
}
}
}
}).start();
下载apk的逻辑
public void downloadApk(String url, String fileName) {
// 实例化一个下载对象;
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
// 允许被媒体库扫描到;
request.allowScanningByMediaScanner();
// 允许在漫游状态下下载文件;
request.setAllowedOverRoaming(true);
// 默认情况下,mobile和wifi网络情况下都是允许下载的;
// request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
// 下载过程中隐藏系统下载界面;
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);
// 设置存储位置;
request.setDestinationInExternalPublicDir(Environment.getExternalStorageDirectory()
.getAbsolutePath(), fileName);
// 这里设置为apk文件;
request.setMimeType("application/vnd.android.package-archive");
// 开始下载;
if (mListener != null) {
mListener.onDownloadStart();
}
// 加入下载队列,下载应该是在后台运行的,这里不需在子线程中去操作;
long downloadId = mDownloadManager.enqueue(request);
// 监听下载进度;
listenDownloadState(mDownloadManager, downloadId);
}
监听下载进度主要是通过DownloadManager.Query对象不断去查询。然后通过接口的方式回调给调用者,这样我们就不必再要通过广播接收者去实现用户交互的监听。
private void listenDownloadState(final DownloadManager manager, final long loadId) {
new Thread(new Runnable() {
@Override
public void run() {
// 实例化一个查询对象;
DownloadManager.Query query = new DownloadManager.Query();
// 通过downloadId 确定查询对象;
query.setFilterById(loadId);
Cursor cursor = null;
boolean listen = true;
while (listen) {
// 查询;
cursor = manager.query(query);
// 确定是否有查询对象;
if (cursor != null && cursor.moveToFirst()) {
switch (cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))) {
case DownloadManager.STATUS_PENDING:
// 在等待下载;
break;
case DownloadManager.STATUS_PAUSED:
// 下载过程中被暂停了
if (mListener != null) {
mListener.onDownloadPause();
}
break;
case DownloadManager.STATUS_RUNNING:
// 下载状态中;
if (mListener != null) {
// 需要下载的比特数;
double totleSize = (double) cursor.getLong(cursor.getColumnIndex
(DownloadManager
.COLUMN_TOTAL_SIZE_BYTES));
// 已经下载的比特数;
double currentSize = (double) cursor.getLong(cursor.getColumnIndex
(DownloadManager
.COLUMN_BYTES_DOWNLOADED_SO_FAR));
// 占的百分比;
int progress = (int) ((currentSize / totleSize) * 100);
// 将百分比数据回调给调用者;
mListener.onDownloadRunning(progress);
}
break;
case DownloadManager.STATUS_SUCCESSFUL:
// 下载成功;
listen = false;
// 安装应用;
installApk(manager.getUriForDownloadedFile(loadId));
if (mListener != null) {
mListener.onSuccess();
}
break;
case DownloadManager.STATUS_FAILED:
listen = false;
// 下载失败;
if (mListener != null) {
mListener.onFailed();
}
break;
}
}
}
if (cursor != null) {
cursor.close();
cursor = null;
}
}
}).start();
}
这里需要说明,当我们通过DownloadManager.Request封装完我们的下载需求后,DownloadManager的enquequ(request) 直接将我们需要的下载对象,加入到下载服务中的下载队列中(并不一定马上下载),并且返回一个下载id,我们后面需要在DownloadManger.Query对象中通过该id去查询下载状态。该id可以定位出下载对象。这是第一点,第二点,在上面的代码中,我们查询下载状态是无节制的,从下载开始,到下载结束(或者失败),这种查询频率是多少,我们无从得知,这种情况会造成两种结果,1.降低性能,没有约束查询频率,内存紧张,毕竟Cursor这东西是比较重量级的。1.当我查询频率过高的时候,系统会直接crash,log显示ManagerDownload.query(query) 没有权限。不知道这是一个系统的bug,还是一种保护方式,防止数据库奔溃。有效的解决方法还没有找到。
最后,调用者只需要通过我们暴露的方法,提供接口实现就好了。这样,应用就可以实现下载进度条的更新了。
// 下载回调;
public void setDownloadListener(DownloadListener listener) {
mListener = listener;
}
No3: 在下载完成后安装apk,这里可以通过广播接收者,也可以在自己的回调监听中完成。
我是直接在下载完成后就安装apk;
// 安装apk
public void installApk(Uri uri) {
if (uri != null) {
Log.d("Permission","Uri: "+uri.toString());
// MediaStore.Files.getContentUri()
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
}
}
通过广播接收者:
public class InstallReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Intent installIntent = new Intent();
installIntent.setAction(Intent.ACTION_VIEW);
installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
installIntent.setDataAndType(Uri.fromFile(new File(Environment.getExternalStorageDirectory().getAbsolutePath() ,
"lottery.apk")), "application/vnd.android.package-archive");
context.startActivity(installIntent);
}
}
manifest中静态注册广播;
<receiver android:name=".InstallReceiver">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
<action android:name="android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED"/>
</intent-filter>
</receiver>
No4:最后不要忘记权限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />// 隐藏系统下载UI需要
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_ALL_DOWNLOADS"/>
基本上以上四个步骤就可以实现,具体效果如下
总结:用DownloadManager下载实现方式比较简单。但是如果需要实时关注下载进度更新的话,可能会存在一些坑。虽然系统给我们提供了query接口,但是过高频率查询引起的权限拒绝导致程序奔溃,还没有有效的解决之道(像定时查询这种解决方案就算了),另一方面,高频度查询导致的CPU与内存紧张问题也是需要考虑的。如果你之道,麻烦告诉一下我,请答应一个小菜鸟真挚的请求。
2.Retrofit+Okhttp自己搭建下载模块。
用Retrofit+OKhttp实现文件下载还是比较有优势的。因为现在应用开发基本上使用的框架都是Retrofit,Okhttp,RxAndroid 。基本上的http交互我们都是使用Retrofit封装,所以,用Retrofit+okhttp搭建自己的下载模块还是有点天然优势的,更为重要的是,它的坑更少(我直接一次就成功了)。而且,感觉速度似乎比使用DownloadManager要快。具体使用步骤如下:
No1:定义一个接口给使用者监测下载过程。
在开始,下载过程中,成功,失败的时候,调用者都可以在接口中实现UI操作。并且该接口中的逻辑全部运行在UI线程中。
public interface OnDownloadListener {
// 开始
void onStart();
// 下载;
void onLoading(int loading);
// 成功;
void onSuccess(Uri uri);
// 失败;
void onFailed(String error);
}
No2:定义Api接口。
需要注意的是在接口中使用了@Streaming 注解,该注解的使用会使得返回的数据以流的方式组织,这样可以避免内存的过大消耗,防止OOM。
@Streaming
@GET
Call<ResponseBody> downloadApk(@Url String url);
No3:工具类DownloaUtils中实现下载业务;
public static void downloadApk(final Activity context, String url, final String fileName, final
OnDownloadListener listener) {
// retrofit 获取call;
Call<ResponseBody> response = ApiHelper.getApi().downloadApk(url);
// 异步;
response.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
// 写入dis的操作已经在子线程中了
Log.d("Permission", "start to write to disk");
writeResponse(context, fileName, response, listener);
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
// 回调也是已经在主线程中的;
listener.onFailed(t.getMessage());
}
});
}
在downloadApk方法中,首先先通过api的Helper类获得api中定义接口所返回的Call<ResponseBody>对象,然后call.enqueue()方法直接进行异步的网络访问。在回调中,将流数据写入到内部存储中。需要注意的一点是,我们这儿定义的下载监听并不是真正意义上的网络下载过程监听,其实仅仅只是数据写入到内部存储中的监听。
数据写入存储器中的逻辑如下:
/**
* 将response写到存储中;
* 涉及到io 操作,直接在线程中;
*/
public static void writeResponse(final Activity context, final String fileName, final
Response<ResponseBody>
response, final OnDownloadListener listener) {
if (FileUtils.externalStorageAvaliable()) {
// 这里先判断存储空间是否足够;
StatFs statFs = new StatFs(Environment.getExternalStorageDirectory().getAbsolutePath());
// 可用的存储空间;
long availableByte = statFs.getAvailableBlocks();
// 需要的存储空间;
long needByte = response.body().contentLength();
// 如果存储空间不够,则,直接下载失败;
if (needByte > availableByte) {
listener.onFailed("存储空间不够");
return;
}
}
// 创建一个新线程去写入存储中;
new Thread(new Runnable() {
@Override
public void run() {
// 显示开始下载;
context.runOnUiThread(new Runnable() {
@Override
public void run() {
Log.d("Permission", "onStart");
listener.onStart();
}
});
long totleBype = response.body().contentLength();
long currentByte = 0;
InputStream inputStream = null;
OutputStream outputStream = null;
File file = null;
try {
// 每次写入的长度;
int length = 0;
// 缓存数组;
byte[] buff = new byte[1024];
// 写入的百分比;
inputStream = response.body().byteStream();
file = FileUtils.createFile(context, fileName);
outputStream = new FileOutputStream(file);
while ((length = inputStream.read(buff)) != -1) {
outputStream.write(buff, 0, length);
currentByte += length;
final int precent = (int) (((double) (currentByte)) / ((double)
(totleBype)) * 100);
Log.d("Permission", "当前进度为:" + precent);
context.runOnUiThread(new Runnable() {
@Override
public void run() {
listener.onLoading(precent);
}
});
}
Log.d("Permission", "onSuccess");
outputStream.flush();
final Uri uri = Uri.fromFile(file);
context.runOnUiThread(new Runnable() {
@Override
public void run() {
listener.onSuccess(uri);
}
});
} catch (FileNotFoundException e) {
e.printStackTrace();
context.runOnUiThread(new Runnable() {
@Override
public void run() {
listener.onFailed("无法找到文件");
}
});
} catch (IOException e) {
e.printStackTrace();
context.runOnUiThread(new Runnable() {
@Override
public void run() {
listener.onFailed("IO读写错误 ");
}
});
} finally {
closeStream(outputStream);
closeStream(inputStream);
}
}
}).start();
}
代码有点长,具体的逻辑为,先通过工具类FileUtils.exteralStorageAvaliable()方法去查看内部存储卡是否已经挂载。
// 内置存储卡是否可用;
public static boolean externalStorageAvaliable() {
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
return true;
}
return false;
}
因为现在手机基本上都已经内置了内部存储卡,这个方法显得有点没有意义。在挂载的情况下,再去内置存储的可用空间是否满足我们下载文件的需求。如果空间不够,直接调用下载失败的回调。
// 这里先判断存储空间是否足够;
StatFs statFs = new StatFs(Environment.getExternalStorageDirectory().getAbsolutePath());
// 可用的存储空间;
long availableByte = statFs.getAvailableBlocks();
// 需要的存储空间;
long needByte = response.body().contentLength();
// 如果存储空间不够,则,直接下载失败;
if (needByte > availableByte) {
listener.onFailed("存储空间不够");
return;
}
当存储检测完成后,直接新建一个线程去实现写入内部存储器的IO操作。过程如上所示,没什么技术含量。唯一需要注意的是,因为希望定义的回调内容是在UI线程中操作的,所以,所有我们调用的接口的代码都放在了Activity.runOnUiThread()中。
N04:一句代码搞定下载;
com.aikding.mj.utils.download.DownloadUtils.downloadApk
(SplashActivity.this, appState.getWapurl(), "lottery" +
".apk", new OnDownloadListener() {
@Override
public void onStart() {
showDownloadAlert();
}
@Override
public void onLoading(int loading) {
if (mProgress != null && mProgress.isShowing()) {
mProgress.setContentText("已下载:" + loading + "%");
}
}
@Override
public void onSuccess(Uri uri) {
if (mProgress != null && mProgress.isShowing()) {
mProgress.setContentText("下载成功");
}
com.aikding.mj.utils.download.DownloadUtils.installApk(SplashActivity.this,uri);
}
@Override
public void onFailed(String error) {
if (mProgress != null && mProgress.isShowing()) {
mProgress.setContentText("下载失败");
}
}
});
调用者直接调用dowanloadApk()方法,实现下载。简单粗暴。具体效果如下
总结:用Retrofit +Okhttp 在使用Retrofit+Okhttp+Rxandroid框架的前提下具备先天优势。说实话,感觉DownloadManager中的坑有点多,而用这种方式,代码虽然多上几行,却没有太多的坑。而且,感觉用这种方式速度比DownloadManager快,至少感性上是这样认为。
3.第三方平台实现版本更新(Buggly).
方案2是存在硬性条件的,如果后端不愿意给接口,或者无法搭建FTP服务器的话,我们只能靠自己。幸运的是,第三方的一些SDK就为我们实现了这些功能。腾讯Buggly SDK就非常便捷的为我们提供了这些功能。目前我在用的就是Buggly,当然市场上还有一些其他的实现该功能的产品,这里只以Buggly为例。Buggly 方便快捷的接入,为我们提供产品迭代,应用统计,热更新的功能,一句话形容,简单,粗暴,易上手。
buggle具体如何介入,可以参考官方文档。这里确实没有照搬照抄的必要。buggly其实只是帮我们将方案2 的逻辑封装成自己的sdk,同时,为我们提供用于下载的FTP服务器。它最大的优势就在于,它可以非常方便的绕过渠道的监管。
Buggly的连接:Buggly