1. 程式人生 > >Android熱修復框架——AndFix

Android熱修復框架——AndFix

一直關注App的熱修復的技術發展,之前做的應用也沒用使用到什麼熱修復開源框架。在App的熱修復框架沒有流行之前,做的應用上線後發現一個小小的Bug,就要馬上發一個新的版本。今天看了熱修復技術,感覺挺好玩的,就實現了使用的全過程。下面記錄使用開源框架阿里巴巴的AndFix過程。

這裡說的不是熱修復怎麼實現修bug的原理,這裡說的是怎麼使用AndFix。如果你想了解更多的andFix實現原理,你可以參考下面的文章:

1.使用極光推送訊息到該應用的版本需要下載補丁,如果應用收到了訊息後,應用判斷當前的版本是否需要下載補丁。如果應用沒有收到訊息的通知,則下次啟動App的時候,獲取友盟線上引數來判斷是否需要下載補丁。

2.應用啟動的後,有則通過 Android 檔案下載file-downloader框架來下載到SD下並且通過使用AndFix來載入到應用中。

步驟
1.在gradle檔案中增加相應的依賴。這裡我使用thindownlaodmanager來下載補丁,使用極光推送來推送自定義訊息下載補丁通知,使用友盟線上引數來獲取補丁包的資訊。也許你會問為了修復一個補丁而增加這麼多的依賴,值得嗎?我認為還可以吧,因為我的專案一般會使用到這些

AndFix的引入是: compile 'com.alipay.euler:andfix:[email protected]'

file-downloader框架的引入是:compile 'org.wlf:FileDownloader:0.3.1'


  • 極光推送自定義訊息(自定義訊息有長度限制,所以補丁的下載url寫成拼接形式:站點+下載資源名稱)


定義相對應的Bean


4.在啟動的自定義Application類進行初始化工作(AndFix、極光的初始化)
public class MyApplication extends Application {

    private static final String TAG = "AndFix";
    public static String  VERSION_NAME="";
    public static PatchManager mPatchManager;

    @Override
    public void onCreate() {
        super.onCreate();

        try {
            PackageInfo mPackageInfo = this.getPackageManager().getPackageInfo(this.getPackageName(), 0);
            VERSION_NAME=mPackageInfo.versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        JPushInterface.init(this);
        initAndFix();
    }

    private void initAndFix() {
        mPatchManager=new PatchManager(this);
        mPatchManager.init(VERSION_NAME);
        Log.d(TAG, "initAndFix: inited.");
        mPatchManager.loadPatch();
    }
}

5.再加上推送推送的自定義內容的處理:(當推送訊息過來的時候應用處於執行狀態的時候,程式會處理訊息進行下載補丁包)
    private PatchBean bean=new PatchBean();
    private WeakHandler mHandler = new WeakHandler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            Toast.makeText(MainActivity.this,(String)msg.obj,Toast.LENGTH_LONG).show();
            if (msg.what == MSG_WHAT_DOWNLOAD){
                String message = (String) msg.obj;
                if (TextUtils.isEmpty(message)) return false;
                try {
                    bean = GsonUtils.getInstance().parse(PatchBean.class, message);
                    RepairBugUtil.getInstance().comparePath(MainActivity.this, bean);

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return false;
        }
    });



    //for receive customer msg from jpush server
    private MessageReceiver mMessageReceiver;
    public static final String MESSAGE_RECEIVED_ACTION = "MESSAGE_RECEIVED_ACTION";
    public static final String KEY_MESSAGE = "message";

    public void registerMessageReceiver() {
        mMessageReceiver = new MessageReceiver();
        IntentFilter filter = new IntentFilter();
        filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
        filter.addAction(MESSAGE_RECEIVED_ACTION);
        registerReceiver(mMessageReceiver, filter);
    }

    public class MessageReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            if (MESSAGE_RECEIVED_ACTION.equals(intent.getAction())) {
                String message = intent.getStringExtra(KEY_MESSAGE);
                Message msg = new Message();
                msg.what = MSG_WHAT_DOWNLOAD;
                msg.obj = message;
                mHandler.sendMessage(msg);
            }
        }
    }

6.補丁包的生成

  • 下載AndFix的補丁生成工具:here
  • 生成補丁的檔案需要的檔案有:原apk檔案,修復Bug後生成的新apk,簽名檔案。

在解壓apkpatch工具的目錄下,開啟命令列輸入以下命令生成補丁包。apkpatch -m <apatch_path...> -o <output> -k <keystore> -p <***> -a <alias> -e <***>


  • 將生成的補丁放到指定伺服器上。

7.自己測試一下成不成啦~

程式碼

package com.vson.hotfix.utils;

import android.content.Context;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;

import com.vson.hotfix.MyApplication;
import com.vson.hotfix.PatchBean;
import com.vson.hotfix.SPConst;

import org.wlf.filedownloader.DownloadFileInfo;
import org.wlf.filedownloader.FileDownloadConfiguration;
import org.wlf.filedownloader.FileDownloader;
import org.wlf.filedownloader.listener.OnFileDownloadStatusListener;

import java.io.File;
import java.io.IOException;

/**
 * Created by vson on 2016/3/11.
 */
public class RepairBugUtil {

    private static final String TAG = "AndFix";
    private static final int THREAD_COUNT = 3;  //下載的執行緒數
    private LocalPreferencesHelper mLocalPreferencesHelper;

    private static class SingletonHolder {
        public static final RepairBugUtil INSTANCE = new RepairBugUtil();
    }

    public static RepairBugUtil getInstance() {
        return SingletonHolder.INSTANCE;
    }

    public void downloadAndLoad(final Context context,final PatchBean bean) {

        // 1、建立Builder
        FileDownloadConfiguration.Builder builder = new FileDownloadConfiguration.Builder(context);
         // 2.配置Builder
        // 配置下載檔案儲存的資料夾
        builder.configFileDownloadDir(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator +
                "FileDownloader");
        // 配置同時下載任務數量,如果不配置預設為2
        builder.configDownloadTaskSize(3);
        // 配置失敗時嘗試重試的次數,如果不配置預設為0不嘗試
        builder.configRetryDownloadTimes(5);
        // 開啟除錯模式,方便檢視日誌等除錯相關,如果不配置預設不開啟
        builder.configDebugMode(true);
        // 配置連線網路超時時間,如果不配置預設為15秒
        builder.configConnectTimeout(25000);// 25秒
        // 3、使用配置檔案初始化FileDownloader
        FileDownloadConfiguration configuration = builder.build();
        FileDownloader.init(configuration);
        FileDownloader.start(SPConst.URL_PREFIX+bean.url);
        FileDownloader.registerDownloadStatusListener(new OnFileDownloadStatusListener() {
            @Override
            public void onFileDownloadStatusWaiting(DownloadFileInfo downloadFileInfo) {

            }

            @Override
            public void onFileDownloadStatusPreparing(DownloadFileInfo downloadFileInfo) {

            }

            @Override
            public void onFileDownloadStatusPrepared(DownloadFileInfo downloadFileInfo) {

            }

            @Override
            public void onFileDownloadStatusDownloading(DownloadFileInfo downloadFileInfo, float downloadSpeed, long remainingTime) {

            }

            @Override
            public void onFileDownloadStatusPaused(DownloadFileInfo downloadFileInfo) {

            }

            @Override
            public void onFileDownloadStatusCompleted(DownloadFileInfo downloadFileInfo) {
                Toast.makeText(context, "下載成功", Toast.LENGTH_LONG).show();
                // add patch at runtime
                try {
                    // .apatch file path
                    String patchFileString = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator +
                            "FileDownloader"+ File.separator+bean.url;

                    MyApplication.mPatchManager.addPatch(patchFileString);
                    Log.d(TAG, "apatch:" + patchFileString + " added.");

                    //複製且載入補丁成功後,刪除下載的補丁
                    File f = new File(patchFileString);
                    if (f.exists()) {
                        boolean result = new File(patchFileString).delete();
                        if (!result)
                            Log.e(TAG, patchFileString + " delete fail");
                    }
//                            mLocalPreferencesHelper.saveOrUpdate(SPConst.IsHavePathDownLoad, false);
                } catch (IOException e) {
                    Log.e(TAG, "", e);
                } catch (Throwable throwable) {

                }
            }

            @Override
            public void onFileDownloadStatusFailed(String url, DownloadFileInfo downloadFileInfo, FileDownloadStatusFailReason failReason) {
                Toast.makeText(context, "下載失敗", Toast.LENGTH_LONG).show();

            }
        });

    }


    public void comparePath(Context context, PatchBean RemoteBean) throws Exception {
        if (mLocalPreferencesHelper == null) {
            mLocalPreferencesHelper = new LocalPreferencesHelper(context, SPConst.SP_NAME);
        }
        String pathInfo = mLocalPreferencesHelper.getString(SPConst.PATH_INFO);
        final PatchBean localBean = null; //GsonUtils.getInstance().parseIfNull(PatchBean.class, pathInfo);
        //遠端的應用版本跟當前應用的版本比較
        if (MyApplication.VERSION_NAME.equals(RemoteBean.app_v)) {
            //遠端的應用版本跟本地儲存的應用版本一樣,但補丁不一樣,則需要下載重新
            /**
             *第一種情況:當本地記錄的Bean為空的時候(剛安裝的時候可能為空)並且遠端的Bean的path_v不為空的時候需要下載補丁。
             * 第二種情況:當本地記錄的path_v和遠端Bean的path_v不一樣的時候需要下載補丁。
             */
            if (localBean == null && !TextUtils.isEmpty(RemoteBean.path_v)
                    || localBean.app_v.equals(RemoteBean.app_v) &&
                    !localBean.path_v.equals(RemoteBean.path_v)) {
                downloadAndLoad(context, RemoteBean);//
                String json = GsonUtils.getInstance().parse(RemoteBean);
                mLocalPreferencesHelper.saveOrUpdate(SPConst.PATH_INFO, json);
            } /*else {
                mLocalPreferencesHelper.saveOrUpdate(SPConst.IsHavePathDownLoad, false);
            }*/
        }
    }

}