Android版本跟新实现方案。

            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 





    

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值