正文  软件开发 > 编程综合 >

Android APK 更新之路

一、前言 提到 APK 更新,大家可能会想到友盟(umeng)更新,市场上已有数万款应用在使用友盟自动更新的服务。但友盟于 2016 年 10 月 15 日起停止了更新服务。那么我...

一、前言

提到 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);
            }

        }
    }

本篇示例的打印结果如下:

man

注册广播监听通知栏点击事件和下载完成事件

当用户点击通知栏中的下载列表时,系统会发出 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);
    }

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 包:

安装补丁,接着运行:

来自:http://www.jianshu.com/p/61336c6f750a