一、前言
提到 APK 更新,大家可能会想到友盟(umeng)更新,市场上已有数万款应用在使用友盟自动更新的服务。但友盟于 2016 年 10 月 15 日起停止了更新服务。那么我们需要自己处理 APK 更新的业务。
本篇主要讲解以下知识点:
使用 DownloadManager 更新
基于 RxJava 和 retrofit 扩展的 Android 线程安全 http 请求库下载 APK 更新
热更新(AndFix)
我们来啾啾第一个知识点。
DownloadManager 更新
Android 2.3(API level 9)开始 Android 用系统服务(Service)的方式提供了DownloadManager 来优化处理长时间的下载操作。DownloadManager 对后台下载,下载状态回调,断点续传,下载环境设置,下载文件的操作等都有很好的支持。
本篇基于 Android 4.0 ~7.0 (SDK 14~24) 开发,众所周知 Android 6.0 的 Runtime Permissions (运行时权限)。
下面具体来看看 DownloadManager 更新的具体流程。
AndroidManifest 清单文件配置权限
下载文件需要使用到网络权限,文件读写权限:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
获取当前的版本号
getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
后台需要提供查询最新版本号的接口,获取接口数据与当前版本号对比,判定是否需要更新。
获取 DownloadManager 实例
DownloadManager manager = (DownloadManager)
appContext.getSystemService(Context.DOWNLOAD_SERVICE);
下面来看看 DownloadManager 提供哪些接口:
public long enqueue(Request request) 执行下载,返回 downloadId,downloadId 可用于后面查询下载信息。若网络不满足条件、Sdcard 挂载中、超过最大并发数等异常则会等待下载,正常则直接下载。
int remove(long… ids) 删除下载,若下载中取消下载。会同时删除下载文件和记录。参数 ids 为 enqueue 返回的 downloadId 集合。
Cursor query(Query query) 查询下载信息。
getMaxBytesOverMobile(Context context) 返回移动网络下载的最大值
rename(Context context, long id, String displayName) 重命名已下载项的名字
getRecommendedMaxBytesOverMobile(Context context) 获取建议的移动网络下载的大小
其它:通过查看代码我们可以发现还有个 CursorTranslator 私有静态内部类。这个类主要对 Query 做了一层代理。将 DownloadProvider 和 DownloadManager之间做了个映射。
接着来看看 DownloadManager.Request 的请求参数。
组装 DownloadManager.Request 请求参数
//获取Request的实例对象
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(appUrl));
显示信息:
//设置一些基本显示信息
request.setTitle(name); //通知栏标题
request.setDescription(description);//通知栏内容
request.setMimeType("application/vnd.android.package-archive");//文件的类型
网络类型:
//NETWORK_MOBILE移动网络
//NETWORK_WIFI wifi网络
//NETWORK_BLUETOOTH 蓝牙
req.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
通知栏显示类型:
request.setNotificationVisibility(DownloadManager.Request
.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
- VISIBILITY_HIDDEN 下载UI不会显示,也不会显示在通知中,如果设置该值,
需要声明android.permission.DOWNLOAD_WITHOUT_NOTIFICATION - VISIBILITY_VISIBLE 当处于下载中状态时,可以在通知栏中显示;当下载完成后,通知栏中不显示
- VISIBILITY_VISIBLE_NOTIFY_COMPLETED 当处于下载中状态和下载完成时状态,均在通知栏中显示
- VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION 只在下载完成时显示在通知栏中。
文件的保存位置:
- 保存到外部环境的私有目录:file:///storage/emulated/0/Android/data/your-package/files/Download/app.apk
request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, "app.apk");
- 保存到外部环境的共有目录: file:///storage/emulated/0/Download/app.apk
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "app.apk");
- 自定义文件路径
setDestinationUri(Uri uri)
添加请求下载的网络链接的http头,比如User-Agent,gzip压缩等:
request.addRequestHeader(String header, String value)
漫游:
//true 允许
//false 不允许
request.setAllowedOverRoaming(false);
其他:
setAllowedOverMetered(boolean allow) //是否允许计量
setRequiresCharging(boolean requiresCharging)//是否在充电环境下
setVisibleInDownloadsUi(boolean isVisible)//是否显示下载界面
...
下面是本文创建Request的示例代码:
request.setTitle(name);
request.setDescription(description);
//在通知栏显示下载进度
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
request.allowScanningByMediaScanner();
request.setNotificationVisibility(DownloadManager.Request
.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
}
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
request.setDestinationInExternalPublicDir(SAVE_APP_LOCATION, SAVE_APP_NAME);
加入下载队列
DownloadManager manager = (DownloadManager) appContext.getSystemService(Context.DOWNLOAD_SERVICE);
manager.enqueue(request);
下载信息查询
DownloadManager 下载工具并没有提供相应的回调接口用于返回实时的下载进度状态。可以通过 DownloadManager.query 方法进行查询,该方法返回一个 Cursor 对象,具体看以下代码:
private void queryDownloadManager(long id) {
DownloadManager mDownloadManager = (DownloadManager)
this.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Query query = new DownloadManager.Query().setFilterById(id);
//可以对query设置一些过滤条件
//setFilterById(long… ids)根据下载id进行过滤
//setFilterByStatus(int flags)根据下载状态进行过滤
Cursor cursor = mDownloadManager.query(query);
if (cursor != null) {
while (cursor.moveToNext()) {
String bytesDownload = cursor.getString(cursor.getColumnIndex(DownloadManager
.COLUMN_BYTES_DOWNLOADED_SO_FAR));
String description = cursor.getString(cursor.getColumnIndex(DownloadManager
.COLUMN_DESCRIPTION));
String cid = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_ID));
String localUri = cursor.getString(cursor.getColumnIndex(DownloadManager
.COLUMN_LOCAL_URI));
String mimeType = cursor.getString(cursor.getColumnIndex(DownloadManager
.COLUMN_MEDIA_TYPE));
String title = cursor.getString(cursor.getColumnIndex(DownloadManager
.COLUMN_TITLE));
String status = cursor.getString(cursor.getColumnIndex(DownloadManager
.COLUMN_STATUS));
String totalSize = cursor.getString(cursor.getColumnIndex(DownloadManager
.COLUMN_TOTAL_SIZE_BYTES));
Log.i("MainActivity", "bytesDownload:" + bytesDownload);
Log.i("MainActivity", "description:" + description);
Log.i("MainActivity", "cid:" + cid);
Log.i("MainActivity", "localUri:" + localUri);
Log.i("MainActivity", "mimeType:" + mimeType);
Log.i("MainActivity", "title:" + title);
Log.i("MainActivity", "status:" + status);
Log.i("MainActivity", "totalSize:" + totalSize);
}
}
}
本篇示例的打印结果如下:
注册广播监听通知栏点击事件和下载完成事件
当用户点击通知栏中的下载列表时,系统会发出 ACTION_NOTIFICATION_CLICKED 事件广播;下载完成时会发出 ACTION_DOWNLOAD_COMPLETE 事件广播,那么我们就可以实现一个广播接收器处理点击和完成时的状态。请看下面代码:
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) {
installApk(context);
} else if (intent.getAction().equals(DownloadManager.ACTION_NOTIFICATION_CLICKED)) {
//Toast.makeText(context, "Clicked", Toast.LENGTH_SHORT).show();
}
}
如文本下载 apk 文件,下载完成时就自动安装,使用意图进行 apk 安装:
// 安装Apk
private void installApk(Context context) {
try {
Intent i = new Intent(Intent.ACTION_VIEW);
String filePath = DownloadManagerUtils.APP_FILE_NAME;
i.setDataAndType(Uri.parse("file://" + filePath), "application/vnd.android" +
".package-archive");
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);
} catch (Exception e) {
Log.e(TAG, "安装失败");
e.printStackTrace();
}
}
DownloadManager 更新就讲到这里了,源码在文章的后面会附上。
基于 RxJava 和 retrofit 扩展的 Android 线程安全 http 请求库下载 APK 更新
针对 DownloadManager 更新,我们还可以通过 http 请求库下载 apk 文件进行更新。
提到 http 请求库,就不得不提到 Novate 库,功能非常强大,使用便利,看看它有哪些功能:
- 加入基础API,减少Api冗余
- 支持离线缓存
- 支持多种方式访问网络(get,put,post ,delete)
- 支持Json字符串,表单提交
- 支持文件下载和上传
- 支持请求头统一加入
- 支持对返回结果的统一处理
- 支持自定义的扩展API
- 支持统一请求访问网络的流程控制
我下载了源码,并修改了进度条的接口。下载文件相信大家都比较熟悉了,我这里就不再细讲了。如果有什么疑问请链接上面地址查看。
新建通知
以下给出本篇用到的消息代码:
private NotificationCompat.Builder buildNotification() {
final Resources res = mContext.getResources();
// This image is used as the notification's large icon (thumbnail).
// TODO: Remove this if your notification has no relevant thumbnail.
final Bitmap picture = BitmapFactory.decodeResource(res, R.mipmap.ic_launcher);
return new NotificationCompat.Builder(mContext).
setContentTitle("更新包下载中...")
.setTicker("准备下载...")
.setProgress(100, 0, false)
.setContentText(String.format(mContext.getResources()
.getString(R.string.apk_progress), 0) + "%")
.setLargeIcon(picture)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher)
.setAutoCancel(false);
}
//更新消息进度
public void showProgressNotification(int progress) {
if (mBuilder == null) {
mBuilder = buildNotification();
}
Notification notification = mBuilder.setProgress(100, progress, false)
.setContentText(String.format(mContext.getResources().getString(R.string
.apk_progress), progress) + "%")
.build();
notify(mContext, notification);
}
如果你还想了解更多 Notification 实现显示下载进度,请连接 Android中使用Notification实现应用更新显示下载进度
apk下载
private void downloadApk() {
RetrofitClient.getInstance(this).createBaseApi()
.download(DOWN_URL, new CallBack() {
@Override
public void onError(Throwable e) {
Log.e("HttpActivity", "onError--------2222" + e.getMessage());
mHttpNotification.removeProgressNotification();
}
@Override
public void onStart() {
super.onStart();
mHttpNotification.showProgressNotification(0);
}
@Override
public void onSucess(String path, String name, long fileSize) {
mHttpNotification.removeProgressNotification();
installApk(HttpActivity.this);
}
@Override
public void onProgress(int progress) {
super.onProgress(progress);
mCircleProgressView.setProgress(progress);
mHttpNotification.showProgressNotification(progress);
}
});
}
如果你还有疑问,在文章结尾处下载源码进行查看。
更新全过程效果图:
热更新(AndFix)
热更新技术近段时间非常火爆,各个大公司都相继开发自己的热更新框架。由于公司主要项目基于电商商城,所以我选择了阿里巴巴的 AndFix 热更新的实现,使用起来也比较简单。至少在我的测试下修改一些小的 BUG 是没有问题的。
我的开发工具是 Android Studio ,第一步导包:
app 的 dependencies 的节点下:
compile 'com.alipay.euler:andfix:0.3.1@aar'
第二步配置 MyApplication 类:
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public void onCreate() {
super.onCreate();
String version = "";
try {
version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
mPatchManager = new PatchManager(getApplicationContext());
mPatchManager.init(version);
mPatchManager.loadPatch();
try {
String patchFileString = "/sdcard" + APATCH_PATH;
mPatchManager.addPatch(patchFileString);
} catch (IOException e) {
e.printStackTrace();
}
}
首先获取到版本号,系统会判断版本号,只有相同的版本号的时候会执行热更新。其中 String patchFileString = "/sdcard" + APATCH_PATH;
是我测试的补丁存放路径。你需要替换成你自己的存放路径。
注意:文件的权限。
然后在 MainActivity 中写一个打印吐司的方法:
private void showToast() {
Toast.makeText(this, "你好啊", Toast.LENGTH_LONG).show();
}
然后打包,重命名为 old.apk
接着修改吐司的内容:
private void showToast() {
Toast.makeText(this, "你好啊,世界", Toast.LENGTH_LONG).show();
}
重新打包,命名为 new.apk
下载apkpatch工具
下面是我的目录结构:
用红线框框住的是签名文件,补丁包,旧包。
打开 cmd ->cd 到 apkpatch 的目录,如我 F:\AndroidTools\apkpatch 目录下,下图我已用红框圈住:
然后输入:
apkpatch.bat -f new.apk -t old.apk -o output -k demo.jks -p 123456 -a boby -e 123456
其中:
-f 是新apk的名字
-t 是旧apk的名字
-o 是输出补丁的文件夹位置
-k 是 keystore(jks)文件的名称
-p 是keystore文件的密码
-a 是项目的别名
-e 别名的密码
回车,不出现错误,补丁打包成功。
打开 output 目录,则可以看到 out.apatch 文件。
补丁文件上传到后台,然后通过接口下载到 /sdcard/out.apatch 目录下。
注意 /sdcard/out.apatch 路径,跟 MyApplication 中的一致。
看看效果:
安装 old.apk 包:
安装补丁,接着运行:
若你有什么疑问请留言,如果对你有所帮助,请关注一下。