阿里百川HotFix2.0熱修復初體驗


博客原地址:http://blog.csdn.net/allan_bst/article/details/72904721html


1、什麼是熱修復java

熱修復說白了就是」打補丁」,好比大家公司上線一個app,用戶反應有重大bug,須要緊急修復。若是按照通
常作法,那就是程序猿加班搞定bug,而後測試,從新打包併發布。這樣帶來的問題就是成本高,效率低。因而,熱
修復就應運而生.通常經過事先設定的接口從網上下載無Bug的代碼來替換有Bug的代碼。這樣就省事多了,用
戶體驗也好。(以下圖所示:Android 插件化技術的三個技術點以及它們的應用場景)
android


2、熱修復原理shell


ClassLoader

Java 中,要加載一個類須要用到 ClassLoader 。數組

Android 中有三個 ClassLoader, 分別爲 URLClassLoader 、 PathClassLoader 、 DexClassLoader 。其中安全

  • URLClassLoader 只能用於加載jar文件,可是因爲 dalvik 不能直接識別jar,因此在 Android 中沒法使用這個加載器。
  • PathClassLoader 它只能加載已經安裝的apk。由於 PathClassLoader 只會去讀取 /data/dalvik-cache 目錄下的 dex 文件。例如咱們安裝一個包名爲 com.allan.xxx 的 apk,那麼當 apk 安裝過程當中,就會在 /data/dalvik-cache 目錄下生產一個名爲 data@app@com.allan.xxx-1.apk@classes.dex 的 ODEX 文件。在使用 PathClassLoader 加載 apk 時,它就會去這個文件夾中找相應的 ODEX 文件,若是 apk 沒有安裝,天然會報 ClassNotFoundException 。
  • DexClassLoader 是最理想的加載器。它的構造函數包含四個參數:

  1. 1.dexPath,指目標類所在的APK或jar文件的路徑.類裝載器將從該路徑中尋找指定的目標類,  
  2.   該類必須是APK或jar的全路徑.若是要包含多個路徑,路徑之間必須使用特定的分割符分隔,  
  3.   特定的分割符可使用System.getProperty(「path.separtor」)得到.  
  4. 2.dexOutputDir,因爲dex文件被包含在APK或者Jar文件中,所以在裝載目標類以前須要先從APK或Jar文件  
  5.   中解壓出dex文件,該參數就是制定解壓出的dex 文件存放的路徑.在Android系統中,  
  6.   一個應用程序通常對應一個Linux用戶id,應用程序僅對屬於本身的數據目錄路徑有寫的權限,  
  7.   所以,該參數可使用該程序的數據路徑.  
  8. 3.libPath,指目標類中所使用的C/C++庫存放的路徑  
  9. 4.classload,是指該裝載器的父裝載器,通常爲當前執行類的裝載器  
1.dexPath,指目標類所在的APK或jar文件的路徑.類裝載器將從該路徑中尋找指定的目標類,
      該類必須是APK或jar的全路徑.若是要包含多個路徑,路徑之間必須使用特定的分割符分隔,
      特定的分割符可使用System.getProperty(「path.separtor」)得到.
    2.dexOutputDir,因爲dex文件被包含在APK或者Jar文件中,所以在裝載目標類以前須要先從APK或Jar文件
      中解壓出dex文件,該參數就是制定解壓出的dex 文件存放的路徑.在Android系統中,
      一個應用程序通常對應一個Linux用戶id,應用程序僅對屬於本身的數據目錄路徑有寫的權限,
      所以,該參數可使用該程序的數據路徑.
    3.libPath,指目標類中所使用的C/C++庫存放的路徑
    4.classload,是指該裝載器的父裝載器,通常爲當前執行類的裝載器


framework源碼 中的 dalvik.system 包下,找到 DexClassLoader 源碼,實際內容是在它的父類 BaseDexClassLoader 中,順帶一提,這個類最低在API14開始有用。包含了兩個變量:服務器

  1. private final String originalPath;  
  2. private final DexPathList pathList;  
  3. //pathList就是多dex的結構列表,查看 其源碼:  
  4. /** class definition context */  
  5. private final ClassLoader definingContext;  
  6.   
  7. /** list of dex/resource (class path) elements */  
  8. private final Element[] dexElements;  
  9.   
  10. /** list of native library directory elements */  
  11. private final File[] nativeLibraryDirectories;  
private final String originalPath;
private final DexPathList pathList;
//pathList就是多dex的結構列表,查看 其源碼:
/** class definition context */
private final ClassLoader definingContext;

/** list of dex/resource (class path) elements */
private final Element[] dexElements;

/** list of native library directory elements */
private final File[] nativeLibraryDirectories;
dexElements 就是一個dex列表,那麼咱們就能夠把每一個 Element 當成是一個 dex。

看下PathClassLoader代碼網絡

public class PathClassLoader extends BaseDexClassLoader {

    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}


DexClassLoader代碼併發

public class DexClassLoader extends BaseDexClassLoader {

    public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

兩個ClassLoader就兩三行代碼,只是調用了父類的構造函數.app

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

在BaseDexClassLoader 構造函數中建立一個DexPathList類的實例,這個DexPathList的構造函數會建立一個dexElements 數組

public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
        ... 
        this.definingContext = definingContext;
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        //建立一個數組
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
        ... 
    }

而後BaseDexClassLoader 重寫了findClass方法,調用了pathList.findClass,跳到DexPathList類中.

/* package */final class DexPathList {
    ...
    public Class findClass(String name, List<Throwable> suppressed) {
            //遍歷該數組
        for (Element element : dexElements) {
            //初始化DexFile
            DexFile dex = element.dexFile;

            if (dex != null) {
                //調用DexFile類的loadClassBinaryName方法返回Class實例
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }       
        return null;
    }
    ...
}
會遍歷這個數組,而後初始化DexFile,若是DexFile不爲空那麼調用DexFile類的loadClassBinaryName方法返回Class實例.

此時咱們整理一下思路,DexClassLoader 包含有一個dex數組 Element[] dexElements ,其中每一個dex文件是一個Element,當須要加載類的時候會遍歷 dexElements,若是找到類則加載,若是找不到從下一個 dex 文件繼續查找。

那麼咱們的實現就是把這個插件 dex 插入到 Elements 的最前面,這麼作的好處是不只能夠動態的加載一個類,而且因爲 DexClassLoader 會優先加載靠前的類,因此咱們同時實現了宿主 apk 的熱修復功能。


3、熱修復之HotFix2.0體驗

先來看一下他的通俗易懂的原理圖




接下來講一下如何實現hotfix,首先打開阿里百川網址http://baichuan.taobao.com,下載最新官方的SDK,其中包括打包工具,調試工具

1.首先在阿里百川的個人應用中添加一個我的應用,記下來應用的APP IDAPPKey,和RSA密鑰


2.建立好應用後,打開AndroidStudio新建一個Project,而後集成hotfix

gradle遠程倉庫依賴, 打開項目找到app的build.gradle文件,添加以下配置:

添加maven倉庫地址:

  1. repositories {  
  2.    maven {  
  3.        url "http://repo.baichuan-android.taobao.com/content/groups/BaichuanRepositories"  
  4.    }  
  5. }  
repositories {
   maven {
       url "http://repo.baichuan-android.taobao.com/content/groups/BaichuanRepositories"
   }
}

  1. 添加gradle座標版本依賴:  
  2.   
  3. dependencies {  
  4.     compile 'com.taobao.android:alisdk-hotfix:2.0.9'  
  5. }  
添加gradle座標版本依賴:

dependencies {
    compile 'com.taobao.android:alisdk-hotfix:2.0.9'
}

若是編譯期間報utdid重複, 因此此時進行以下處理便可, 關閉傳遞性依賴:

  1. compile ('com.taobao.android:alisdk-hotfix:2.0.9') {  
  2.      exclude(module:'utdid4all')  
  3. }  
compile ('com.taobao.android:alisdk-hotfix:2.0.9') {
     exclude(module:'utdid4all')
}

添加權限

  1. <! -- 網絡權限 -->  
  2. <uses-permission android:name="android.permission.INTERNET" />  
  3. <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />  
  4. <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />  
  5. <! -- 外部存儲讀權限 -->  
  6. <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>  
<! -- 網絡權限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<! -- 外部存儲讀權限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

若是API 23以上別忘了添加運行時權限,由於讀寫SD卡爲危險權限


配置AndroidManifest文件,這時用到咱們前面所說的那三個id,key,密鑰了

AndroidManifest.xml中間的application節點下添加以下配置,吧value的數值改爲那三個便可:

  1. <meta-data  
  2. android:name="com.taobao.android.hotfix.IDSECRET"  
  3. android:value="App ID" />  
  4. <meta-data  
  5. android:name="com.taobao.android.hotfix.APPSECRET"  
  6. android:value="App Secret" />  
  7. <meta-data  
  8. android:name="com.taobao.android.hotfix.RSASECRET"  
  9. android:value="RSA密鑰" />  
<meta-data
android:name="com.taobao.android.hotfix.IDSECRET"
android:value="App ID" />
<meta-data
android:name="com.taobao.android.hotfix.APPSECRET"
android:value="App Secret" />
<meta-data
android:name="com.taobao.android.hotfix.RSASECRET"
android:value="RSA密鑰" />
3.接下來進行初始化工作,官方文檔建議在App全局的Application的onCreate()方法進行初始化

  1. SophixManager.getInstance().setContext(this)  
  2.                 .setAppVersion(appVersion)  
  3.                 .setAesKey(null)  
  4.                 .setEnableDebug(true)  
  5.                 .setPatchLoadStatusStub(new PatchLoadStatusListener() {  
  6.                     @Override  
  7.                     public void onLoad(final int mode, final int code, final String info, final int handlePatchVersion) {  
  8.                         // 補丁加載回調通知  
  9.                         if (code == PatchStatus.CODE_LOAD_SUCCESS) {  
  10.                             // 代表補丁加載成功  
  11.                         } else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {  
  12.                             // 代表新補丁生效須要重啓. 開發者可提示用戶或者強制重啓;  
  13.                             // 建議: 用戶能夠監聽進入後臺事件, 而後應用自殺  
  14.                         } else if (code == PatchStatus.CODE_LOAD_FAIL) {  
  15.                             // 內部引擎異常, 推薦此時清空本地補丁, 防止失敗補丁重複加載  
  16.                             // SophixManager.getInstance().cleanPatches();  
  17.                         } else {  
  18.                             // 其它錯誤信息, 查看PatchStatus類說明  
  19.                         }  
  20.                     }  
  21.                 }).initialize();  
  22. SophixManager.getInstance().queryAndLoadNewPatch();  
SophixManager.getInstance().setContext(this)
                .setAppVersion(appVersion)
                .setAesKey(null)
                .setEnableDebug(true)
                .setPatchLoadStatusStub(new PatchLoadStatusListener() {
                    @Override
                    public void onLoad(final int mode, final int code, final String info, final int handlePatchVersion) {
                        // 補丁加載回調通知
                        if (code == PatchStatus.CODE_LOAD_SUCCESS) {
                            // 代表補丁加載成功
                        } else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {
                            // 代表新補丁生效須要重啓. 開發者可提示用戶或者強制重啓;
                            // 建議: 用戶能夠監聽進入後臺事件, 而後應用自殺
                        } else if (code == PatchStatus.CODE_LOAD_FAIL) {
                            // 內部引擎異常, 推薦此時清空本地補丁, 防止失敗補丁重複加載
                            // SophixManager.getInstance().cleanPatches();
                        } else {
                            // 其它錯誤信息, 查看PatchStatus類說明
                        }
                    }
                }).initialize();
SophixManager.getInstance().queryAndLoadNewPatch();

》》》》》》》》》》》》下面內容摘取自官方文檔《《《《《《《《《《《《《
  1. 1.3.2 接口說明  
  2. 1.3.2.1 initialize方法  
  3.   
  4.     initialize(): <必選>  
  5.     該方法主要作些必要的初始化工做以及若是本地有補丁的話會加載補丁, 但不會自動請求補丁。所以須要自行調用queryAndLoadNewPatch方法拉取補丁。這個方法調用須要儘量的早, 推薦在Application的onCreate方法中調用, initialize()方法調用以前你須要先調用以下幾個方法, 方法調用說明以下:  
  6.     setContext(this): <必選> Application上下文context  
  7.     setAppVersion(appVersion): <必選> 應用的版本號  
  8.     setAesKey(aesKey): <可選> 用戶自定義aes祕鑰, 會對補丁包採用對稱加密。這個參數值必須是16位數字或字母的組合,是和補丁工具設置裏面AES Key保持徹底一致, 補丁才能正確被解密進而加載。此時平臺無感知這個祕鑰, 因此不用擔憂百川平臺會利用大家的補丁作一些非法的事情。  
  9.     setEnableDebug(true/false): <可選> 默認爲false, 是否調試模式, 調試模式下會輸出日誌以及不進行補丁簽名校驗. 線下調試此參數能夠設置爲true, 查看日誌過濾TAG:Sophix, 同時強制不對補丁進行簽名校驗, 全部就算補丁未簽名或者簽名失敗也發現能夠加載成功. 可是正式發佈該參數必須爲false, false會對補丁作簽名校驗, 不然就可能存在安全漏洞風險  
  10.     setPatchLoadStatusStub(new PatchLoadStatusListener()): <可選> 設置patch加載狀態監聽器, 該方法參數須要實現PatchLoadStatusListener接口, 接口說明見1.3.2.2說明  
  11.     setUnsupportedModel(modelName, sdkVersionInt):<可選> 把不支持的設備加入黑名單,加入後不會進行熱修復。modelName爲該機型上Build.MODEL的值,這個值也能夠經過adb shell getprop | grep ro.product.model取得。sdkVersionInt就是該機型的Android版本,也就是Build.VERSION.SDK_INT,若設爲0,則對應該機型全部安卓版本。  
  12.   
  13. 1.3.2.2 queryAndLoadNewPatch方法  
  14.   
  15. 該方法主要用於查詢服務器是否有新的可用補丁. SDK內部限制連續兩次queryAndLoadNewPatch()方法調用不能短於3s, 不然的話就會報code:19的錯誤碼. 若是查詢到可用的話, 首先下載補丁到本地, 而後  
  16.   
  17.     應用本來沒有補丁, 那麼若是當前應用的補丁是熱補丁, 那麼會馬上加載(不論是冷補丁仍是熱補丁). 若是當前應用的補丁是冷補丁, 那麼須要重啓生效.  
  18.     應用已經存在一個補丁, 首先會把以前的補丁文件刪除, 而後不馬上加載, 而是等待下次應用重啓再加載該補丁  
  19.     補丁在後臺發佈以後, 並不會主動下行推送到客戶端, 須要手動調用queryAndLoadNewPatch方法查詢後臺補丁是否可用.  
  20.     只會下載補丁版本號比當前應用存在的補丁版本號高的補丁, 好比當前應用已經下載了補丁版本號爲5的補丁, 那麼只有後臺發佈的補丁版本號>5纔會從新下載.  
  21.   
  22. 同時1.4.0以上版本服務後臺上線了「一鍵清除」補丁的功能, 因此若是後臺點擊了「一鍵清除」那麼這個方法將會返回code:18的狀態碼. 此時本地補丁將會被強制清除, 同時不清除本地補丁版本號  
  23. 1.3.2.3 cleanPatches()方法  
  24.   
  25. 清空本地補丁  
  26. 1.3.2.4 PatchLoadStatusListener接口  
  27.   
  28. 該接口須要自行實現並傳入initialize方法中, 補丁加載狀態會回調給該接口, 參數說明以下:  
  29.   
  30.     mode: 補丁模式, 0:正常請求模式 1:掃碼模式 2:本地補丁模式  
  31.     code: 補丁加載狀態碼, 詳情查看PatchStatusCode類說明  
  32.     info: 補丁加載詳細說明, 詳情查看PatchStatusCode類說明  
  33.     handlePatchVersion: 當前處理的補丁版本號, 0:無 -1:本地補丁 其它:後臺補丁  
  34.   
  35. 這裏列舉幾個常見的code碼說明, 詳情查看SDK中PatchStatus類的代碼,其中有具體說明  
  36.   
  37.     code: 1 補丁加載成功  
  38.     code: 6 服務端沒有最新可用的補丁  
  39.     code: 11 RSASECRET錯誤,官網中的密鑰是否正確請檢查  
  40.     code: 12 當前應用已經存在一箇舊補丁, 應用重啓嘗試加載新補丁  
  41.     code: 13 補丁加載失敗, 致使的緣由不少種, 好比UnsatisfiedLinkError等異常, 此時應該嚴格檢查logcat異常日誌  
  42.     code: 16 APPSECRET錯誤,官網中的密鑰是否正確請檢查  
  43.     code: 18 一鍵清除補丁  
  44.     code: 19 連續兩次queryAndLoadNewPatch()方法調用不能短於3s  
1.3.2 接口說明
1.3.2.1 initialize方法

    initialize(): <必選>
    該方法主要作些必要的初始化工做以及若是本地有補丁的話會加載補丁, 但不會自動請求補丁。所以須要自行調用queryAndLoadNewPatch方法拉取補丁。這個方法調用須要儘量的早, 推薦在Application的onCreate方法中調用, initialize()方法調用以前你須要先調用以下幾個方法, 方法調用說明以下:
    setContext(this): <必選> Application上下文context
    setAppVersion(appVersion): <必選> 應用的版本號
    setAesKey(aesKey): <可選> 用戶自定義aes祕鑰, 會對補丁包採用對稱加密。這個參數值必須是16位數字或字母的組合,是和補丁工具設置裏面AES Key保持徹底一致, 補丁才能正確被解密進而加載。此時平臺無感知這個祕鑰, 因此不用擔憂百川平臺會利用大家的補丁作一些非法的事情。
    setEnableDebug(true/false): <可選> 默認爲false, 是否調試模式, 調試模式下會輸出日誌以及不進行補丁簽名校驗. 線下調試此參數能夠設置爲true, 查看日誌過濾TAG:Sophix, 同時強制不對補丁進行簽名校驗, 全部就算補丁未簽名或者簽名失敗也發現能夠加載成功. 可是正式發佈該參數必須爲false, false會對補丁作簽名校驗, 不然就可能存在安全漏洞風險
    setPatchLoadStatusStub(new PatchLoadStatusListener()): <可選> 設置patch加載狀態監聽器, 該方法參數須要實現PatchLoadStatusListener接口, 接口說明見1.3.2.2說明
    setUnsupportedModel(modelName, sdkVersionInt):<可選> 把不支持的設備加入黑名單,加入後不會進行熱修復。modelName爲該機型上Build.MODEL的值,這個值也能夠經過adb shell getprop | grep ro.product.model取得。sdkVersionInt就是該機型的Android版本,也就是Build.VERSION.SDK_INT,若設爲0,則對應該機型全部安卓版本。

1.3.2.2 queryAndLoadNewPatch方法

該方法主要用於查詢服務器是否有新的可用補丁. SDK內部限制連續兩次queryAndLoadNewPatch()方法調用不能短於3s, 不然的話就會報code:19的錯誤碼. 若是查詢到可用的話, 首先下載補丁到本地, 而後

    應用本來沒有補丁, 那麼若是當前應用的補丁是熱補丁, 那麼會馬上加載(不論是冷補丁仍是熱補丁). 若是當前應用的補丁是冷補丁, 那麼須要重啓生效.
    應用已經存在一個補丁, 首先會把以前的補丁文件刪除, 而後不馬上加載, 而是等待下次應用重啓再加載該補丁
    補丁在後臺發佈以後, 並不會主動下行推送到客戶端, 須要手動調用queryAndLoadNewPatch方法查詢後臺補丁是否可用.
    只會下載補丁版本號比當前應用存在的補丁版本號高的補丁, 好比當前應用已經下載了補丁版本號爲5的補丁, 那麼只有後臺發佈的補丁版本號>5纔會從新下載.

同時1.4.0以上版本服務後臺上線了「一鍵清除」補丁的功能, 因此若是後臺點擊了「一鍵清除」那麼這個方法將會返回code:18的狀態碼. 此時本地補丁將會被強制清除, 同時不清除本地補丁版本號
1.3.2.3 cleanPatches()方法

清空本地補丁
1.3.2.4 PatchLoadStatusListener接口

該接口須要自行實現並傳入initialize方法中, 補丁加載狀態會回調給該接口, 參數說明以下:

    mode: 補丁模式, 0:正常請求模式 1:掃碼模式 2:本地補丁模式
    code: 補丁加載狀態碼, 詳情查看PatchStatusCode類說明
    info: 補丁加載詳細說明, 詳情查看PatchStatusCode類說明
    handlePatchVersion: 當前處理的補丁版本號, 0:無 -1:本地補丁 其它:後臺補丁

這裏列舉幾個常見的code碼說明, 詳情查看SDK中PatchStatus類的代碼,其中有具體說明

    code: 1 補丁加載成功
    code: 6 服務端沒有最新可用的補丁
    code: 11 RSASECRET錯誤,官網中的密鑰是否正確請檢查
    code: 12 當前應用已經存在一箇舊補丁, 應用重啓嘗試加載新補丁
    code: 13 補丁加載失敗, 致使的緣由不少種, 好比UnsatisfiedLinkError等異常, 此時應該嚴格檢查logcat異常日誌
    code: 16 APPSECRET錯誤,官網中的密鑰是否正確請檢查
    code: 18 一鍵清除補丁
    code: 19 連續兩次queryAndLoadNewPatch()方法調用不能短於3s



4. 生成補丁
patch補丁包生成須要使用到打補丁工具SophixPatchTool, 如還未下載打包工具,請前往文檔SDK下載&版本更新記錄下載Android打包工具。
打開這個工具

  • 舊包:<必填> 選擇基線包路徑(有問題的APK)
  • 新包:<必填> 選擇新包路徑(修復過該問題APK)
  • 日誌:打開日誌輸出窗口。
  • 高級:展開高級選項,見2.2.1。
  • 設置:配置其餘信息。
  • GO!:開始生成補丁!
按照說明先選擇舊版本(有Bug)的apk包,和新版本的apk包,接下來我來演示一個Demo
【下面這兩個Demo的app我就把有bug的叫作bapp 】
1>首先看bapp的源碼【篇幅有限,只看有bug的部分】

  1.     /** 
  2.      * 監聽返回鍵 
  3.      */  
  4. //    @Override  
  5. //    public void onBackPressed() {  
  6. //        if(drawerLayout.isDrawerOpen(nv)){  
  7. //            drawerLayout.closeDrawers();  
  8. //            return;  
  9. //        }  
  10. //        if (System.currentTimeMillis() - newTime > 2000) {  
  11. //            newTime = System.currentTimeMillis();  
  12. //            Snackbar snackbar = Snackbar.make(pager, "再按一次返回鍵退出程序", Snackbar.LENGTH_SHORT);  
  13. //            snackbar.getView().setBackgroundColor(getResources().getColor(R.color.colorPrimary));  
  14. //            snackbar.show();  
  15. //        } else {  
  16. //            finish();  
  17. //        }  
  18. //    }  
/**
     * 監聽返回鍵
     */
//    @Override
//    public void onBackPressed() {
//        if(drawerLayout.isDrawerOpen(nv)){
//            drawerLayout.closeDrawers();
//            return;
//        }
//        if (System.currentTimeMillis() - newTime > 2000) {
//            newTime = System.currentTimeMillis();
//            Snackbar snackbar = Snackbar.make(pager, "再按一次返回鍵退出程序", Snackbar.LENGTH_SHORT);
//            snackbar.getView().setBackgroundColor(getResources().getColor(R.color.colorPrimary));
//            snackbar.show();
//        } else {
//            finish();
//        }
//    }

很簡單,在MainActivity中,將返回鍵退出程序的監聽給註釋掉,也就是,bapp點擊返回鍵會沒有任何效果
2>而後app的源碼,就不贅述了,很簡單,就是取消bapp的註釋,讓程序從新能夠監聽返回鍵退出
下面咱們來編譯baap和app
【咱們的目的是經過SophixPatchTool將這兩個app對比,生成一個補丁,在不推送升級app的狀況下修復bapp的不能退出程序的bug】

使用SophixPatchTool,按照提示選擇bapp和app,點擊Go,生成一個baichuan-hotfix-patch.jar,而後將這個jar文件
移動到手機的根目錄,而後打開咱們上面下載的Sophix調試工具(一個安卓端的apk),接下來請看高端操做
上gif:::
【講解下這個圖片,首先這個Demo是bapp,不具有點擊返回鍵退出功能,在Sophix調試工具上輸入bapp包名,輸入jar補丁絕對路徑,點擊應用補丁,重啓bapp後,bapp就修復了不能點擊返回鍵退出的bug,理論上不用重啓】
差很少也就這些了,有什麼問題能夠留言交流
相關文章
相關標籤/搜索