高逼格,超簡單,實現App自動更新,一個方法搞定

jccu7970 8年前發布 | 91K 次閱讀 Android Android開發 移動開發

前言

前段時間寫了一個篇APP自動更新下載的文章自動更新,一個方法搞定,使用系統的DownloadManager 方法超簡潔的實現了apk的下載,不過有好多網友反映有一些機型上面這個方法無法實現下載,經過小編的實驗在部分機型上確實會有這個問題,所以其中下載的部分只能通過其它方法搞定了。正好看到網上好多關于使用Retrofit實現下載并且監聽進度的文章,并且我一直在看Retrofit的東西但是一直沒有機會用到,所以我正好拿這個練練手,最終我使用Retrofit + OkHttp + RxBus + Notification + Service實現了這個自動更新下載apk的功能。Demo已經上傳的github了大家可以下載下來自己看看,你不僅可以解決App更新的問題,并且可以通過實踐了解到一些比較不錯的技術。github地址:https://github.com/shanyao0/DownLoadManager,大家多多star和fork,謝謝。。。

原理

基本原理和使用方法跟自動更新,一個方法搞定一樣大家不懂得可以參考這里,同樣還是一個方法搞定超級簡單,不過只是這次的逼格更高,機型兼容性更好。這次手動實現了下載和系統通知進度功能。

  • Retrofit2和okhttp實現了apk的下載
  • 自定義類實現Retrofit2的Callback類在里面通過IO流寫入文件并且使用RxBus訂閱下載進度
  • 自定義類實現okhttp3的ResponseBody類并且在里面使用RxBus發布下載進度信息
  • 在Service中使用Retrofit在后臺下載文件
  • 發送Notifaction到通知欄前臺界面展示進度情況

所以我希望大家可以跟著我下面的實現步驟一步一步的實現這個功能,這樣你不僅可以在你的項目中使用高逼格的技術,還可以對這些技術有一個比較初步的認識。

實現步驟

1. 創建UpdateManger管理類

這個類主要寫了兩個管理更新和彈框的方法,比較簡單,跟自動更新,一個方法搞定的差不多除了下載部分

  / - 檢測軟件更新 */
  public void checkUpdate(final boolean isToast) {
      /  在這里請求后臺接口,獲取更新的內容和最新的版本號 /
      // 版本的更新信息
      String version_info = "更新內容\n" + " 1. 車位分享異常處理\n" + " 2. 發布車位折扣格式統一\n" + " ";
      int mVersion_code = DeviceUtils.getVersionCode(mContext);// 當前的版本號
      int nVersion_code = 2;
      if (mVersion_code < nVersion_code) {
          // 顯示提示對話
          showNoticeDialog(version_info);
      } else {
          if (isToast) {
              Toast.makeText(mContext, "已經是最新版本", Toast.LENGTH_SHORT).show();
          }
      }
  }

/** * 顯示更新對話框 * * @param version_info */
private void showNoticeDialog(String version_info) {
    // 構造對話框
    AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
    builder.setTitle("更新提示");
    builder.setMessage(version_info);
    // 更新
    builder.setPositiveButton("立即更新", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            dialog.dismiss();
            // 啟動后臺服務下載apk
            mContext.startService(new Intent(mContext, DownLoadService.class));
        }
    });
    // 稍后更新
    builder.setNegativeButton("以后更新", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            dialog.dismiss();
        }
    });
    Dialog noticeDialog = builder.create();
    noticeDialog.show();
}</code></pre> 

2. 初始化RxBus進行簡單封裝

RxBus的使用可以參考這里用RxJava實現事件總線

import rx.Observable;
import rx.subjects.PublishSubject;
import rx.subjects.SerializedSubject;
import rx.subjects.Subject;

public class RxBus {

    private static volatile RxBus mInstance;
    private final Subject<Object, Object> bus;

    private RxBus() {
        bus = new SerializedSubject<>(PublishSubject.create());
    }

    /** * 單例RxBus * * @return */
    public static RxBus getDefault() {
        RxBus rxBus = mInstance;
        if (mInstance == null) {
            synchronized (RxBus.class) {
                rxBus = mInstance;
                if (mInstance == null) {
                    rxBus = new RxBus();
                    mInstance = rxBus;
                }
            }
        }
        return rxBus;
    }

    /** * 發送一個新事件 * * @param o */
    public void post(Object o) {
        bus.onNext(o);
    }

    /** * 返回特定類型的被觀察者 * * @param eventType * @param <T> * @return */
    public <T> Observable<T> toObservable(Class<T> eventType) {
        return bus.ofType(eventType);
    }

}

3. 自己定制ResponseBody類FileResponseBody

public class FileResponseBody extends ResponseBody {

    Response originalResponse;

    public FileResponseBody(Response originalResponse) {
        this.originalResponse = originalResponse;
    }

    @Override
    public MediaType contentType() {
        return originalResponse.body().contentType();
    }

    @Override
    public long contentLength() {// 返回文件的總長度,也就是進度條的max
        return originalResponse.body().contentLength();
    }

    @Override
    public BufferedSource source() {
        return Okio.buffer(new ForwardingSource(originalResponse.body().source()) {
            long bytesReaded = 0;

            @Override
            public long read(Buffer sink, long byteCount) throws IOException {
                long bytesRead = super.read(sink, byteCount);
                bytesReaded += bytesRead == -1 ? 0 : bytesRead;
                // 通過RxBus發布進度信息
                RxBus.getDefault().post(new FileLoadingBean(contentLength(), bytesReaded));
                return bytesRead;
            }
        });
    }
}

這個別問我問啥這樣寫,是從ophttp3的官方demo里面搞過來的東西,如果大家有精力可以研究下,到時別忘了告訴我一聲,謝謝

4. 自己定制Callback類FileCallback

一個抽象類實現了Callback的onResponse方法并且在里面利用IO流把文件保存到了本地
定義了兩個抽象方法讓子類實現,onSuccess()當讀寫完成之后將文件回調給實現類以便安裝apk,onLoading()在文件讀寫的過程中通過訂閱下載的進度把進度信息progress和total回調給實現類以便在通知中實時顯示進度信息

public abstract class FileCallback implements Callback<ResponseBody>{
    /** * 訂閱下載進度 */
    private CompositeSubscription rxSubscriptions = new CompositeSubscription();
    /** * 目標文件存儲的文件夾路徑 */
    private String destFileDir;
    /** * 目標文件存儲的文件名 */
    private String destFileName;

    public FileCallback(String destFileDir, String destFileName) {
        this.destFileDir = destFileDir;
        this.destFileName = destFileName;
        subscribeLoadProgress();// 訂閱下載進度
    }
    /** * 成功后回調 */
    public abstract void onSuccess(File file);

    /** * 下載過程回調 */
    public abstract void onLoading(long progress, long total);

    /** * 請求成功后保存文件 */
    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
        try {
            saveFile(response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /** * 通過IO流寫入文件 */
    public File saveFile(Response<ResponseBody> response) throws Exception {
        InputStream in = null;
        FileOutputStream out = null;
        byte[] buf = new byte[2048];
        int len;
        try {
            File dir = new File(destFileDir);
            if (!dir.exists()) {// 如果文件不存在新建一個
                dir.mkdirs();
            }
            in = response.body().byteStream();
            File file = new File(dir,destFileName);
            out = new FileOutputStream(file);
            while ((len = in.read(buf)) != -1){
                out.write(buf,0,len);
            }
            // 回調成功的接口
            onSuccess(file);
            unSubscribe();// 取消訂閱
            return file;
        }finally {
            in.close();
            out.close();
        }
    }
    /** * 訂閱文件下載進度 */
    private void subscribeLoadProgress() {
        rxSubscriptions.add(RxBus.getDefault()
                .toObservable(FileLoadingBean.class)// FileLoadingBean保存了progress和total的實體類
                .onBackpressureBuffer()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<FileLoadingBean>() {
                    @Override
                    public void call(FileLoadingBean fileLoadEvent) {
                        onLoading(fileLoadEvent.getProgress(), fileLoadEvent.getTotal());
                    }
                }));
    }
    /** * 取消訂閱,防止內存泄漏 */
    private void unSubscribe() {
        if (!rxSubscriptions.isUnsubscribed()) {
            rxSubscriptions.unsubscribe();
        }
    }
}

保存了progress和total的實體類

public class FileLoadingBean {
    /** * 文件大小 */
    long total;
    /** * 已下載大小 */
    long progress;

    public long getProgress() {
        return progress;
    }

    public long getTotal() {
        return total;
    }

    public FileLoadingBean(long total, long progress) {
        this.total = total;
        this.progress = progress;
    }
}

5. 在后臺Service中利用Retrofit2和okhttp下載并安裝apk,同時發送通知在前臺展示下載進度

Retrofit和okhttp的使用請參考鴻洋大神的Retrofit2 完全解析 探索與okhttp之間的關系
通知Notification大家直接參考官網Notification就行

public class DownLoadService extends Service {

    /** * 目標文件存儲的文件夾路徑 */
    private String  destFileDir = Environment.getExternalStorageDirectory().getAbsolutePath() + File
            .separator + "M_DEFAULT_DIR";
    /** * 目標文件存儲的文件名 */
    private String destFileName = "shan_yao.apk";

    private Context mContext;
    private int preProgress = 0;
    private int NOTIFY_ID = 1000;
    private NotificationCompat.Builder builder;
    private NotificationManager notificationManager;
    private Retrofit.Builder retrofit;
    /** * 為什么在這個方法調用下載的邏輯?而不是onCreate?我在下面有解釋 */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        mContext = this;
        loadFile();
        return super.onStartCommand(intent, flags, startId);
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    /** * 下載文件 */
    private void loadFile() {
        initNotification();
        if (retrofit == null) {
            retrofit = new Retrofit.Builder();
        }
        // 使用Retrofit進行文件的下載
        retrofit.baseUrl("http://112.124.9.133:8080/parking-app-admin-1.0/android/manager/adminVersion/")
                .client(initOkHttpClient())
                .build()
                .create(IFileLoad.class)
                .loadFile()
                .enqueue(new FileCallback(destFileDir, destFileName) {

                    @Override
                    public void onSuccess(File file) {
                        Log.e("zs", "請求成功");
                        // 安裝軟件
                        cancelNotification();
                        installApk(file);
                    }

                    @Override
                    public void onLoading(long progress, long total) {
                        Log.e("zs", progress + "----" + total);
                        updateNotification(progress * 100 / total);// 更新前臺通知
                    }

                    @Override
                    public void onFailure(Call<ResponseBody> call, Throwable t) {
                        Log.e("zs", "請求失敗");
                        cancelNotification();// 取消通知
                    }
                });
    }

    public interface IFileLoad {
        @GET("download")
        Call<ResponseBody> loadFile();
    }

    /** * 安裝軟件 * * @param file */
    private void installApk(File file) {
        Uri uri = Uri.fromFile(file);
        Intent install = new Intent(Intent.ACTION_VIEW);
        install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        install.setDataAndType(uri, "application/vnd.android.package-archive");
        // 執行意圖進行安裝
        mContext.startActivity(install);
    }

    /** * 初始化OkHttpClient * * @return */
    private OkHttpClient initOkHttpClient() {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.connectTimeout(100000, TimeUnit.SECONDS);
        builder.networkInterceptors().add(new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Response originalResponse = chain.proceed(chain.request());
                return originalResponse
                        .newBuilder()
                        .body(new FileResponseBody(originalResponse))//將自定義的ResposeBody設置給它
                        .build();
            }
        });
        return builder.build();
    }

    /** * 初始化Notification通知 */
    public void initNotification() {
        builder = new NotificationCompat.Builder(mContext)
                .setSmallIcon(R.mipmap.ic_launcher)// 設置通知的圖標
                .setContentText("0%")// 進度Text
                .setContentTitle("QQ更新")// 標題
                .setProgress(100, 0, false);// 設置進度條
        notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);// 獲取系統通知管理器
        notificationManager.notify(NOTIFY_ID, builder.build());// 發送通知 
    }

    /** * 更新通知 */
    public void updateNotification(long progress) {
        int currProgress = (int) progress;
        if (preProgress < currProgress) {
            builder.setContentText(progress + "%");
            builder.setProgress(100, (int) progress, false);
            notificationManager.notify(NOTIFY_ID, builder.build());
        }
        preProgress = (int) progress;
    }

    /** * 取消通知 */
    public void cancelNotification() {
        notificationManager.cancel(NOTIFY_ID);
    }
}

需要注意的幾個地方

  • 為什么在onStartCommand里面下載apk而不是onCreate里面

    這個跟service的生命周期有關系,onStartCommand()在每次調用startService時候都會調用一次,而onCreate()方法只有在服務第一次啟動的時候才會掉用。當一個服務已經開啟了那么再次調用startService不會在調用onCreate()方法,而onStartCommand()會被再次調用。

  • 發送通知設置進度用的是setProgress,這個方法只有在4.0以上才能用

    如果不用這個方法想要兼容低版本,就只能使用自定義的Notification,然后自己創建一個含有ProgressBar的layout布局,但是自定義的Notification在不同的系統中的適配處理太麻煩了,因為不同的系統的通知欄的背景顏色不一致,我們需要對不同的背景做不同的適配才能保證上面的文字能夠正常顯示,比方你寫死了一個白色的文字,但是系統通知的背景也是白色,這樣一來文字就不能正常顯示了,當然如果使用系統的通知樣式無法滿足你的需求,只能使用自定義樣式,可以參考這篇文章Android自定義通知樣式適配。如果僅僅是為了兼容低版本我個人感覺完全沒有必要,大家可以看看友盟指數,所以沒有必要去做兼容。這個看不同的需求而定。
    指數

  • 在更新通知時我做了判斷

    在更新通知時我做了判斷看下代碼,這里我設置只有當進度等于1時并且發生改變時做更新,因為progress的值可能為1.1,1.2,1.3,但是顯示的都會是1%,如果我不做判斷限制那么每次onLoading的回調都會更新通知內容,也就是當1.1時會更新,1.2時也會執行更新的方法,但是前臺展示的都是1%,所以做了好多無用功,并且當你不做限制時整個app會非常的卡,有時會卡死。這個你可以不做限制自己運行下Demo試試,絕壁卡死。

    /** * 更新通知 */
    public void updateNotification(long progress) {
        int currProgress = (int) progress;
        if (preProgress < currProgress) {
            builder.setContentText(progress + "%");
            builder.setProgress(100, (int) progress, false);
            notificationManager.notify(NOTIFY_ID, builder.build());
        }
        preProgress = (int) progress;
    }

總結

最后我還是希望大家如果有時間可以自己跟著我的步驟敲敲代碼,看看那些我推薦的文章,我相信大家肯定能收獲好多,大家有什么好的建議或者我哪里寫的有問題的希望能夠及時給我反饋。如果這篇文章對你有幫助的話歡迎大家多多的關注我,支持我,別忘了到github上star和fork我哈,源碼DownLoadManager

 

閱讀原文

 

 本文由用戶 jccu7970 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!