Android 中插件化學習—教你實現熱補丁動態修復

文章背景

  • 在作互聯網app項目的時候,當咱們發佈迭代了一個新版本,把apk發佈到各個Android應用市場上時,因爲程序猿或是程序媛在編碼上的疏忽,忽然出現了一個緊急Bug時,一般的作法是從新打包,從新發布到各個應用市場。
  • 這不只給公司相關部門增長大量工做量外,比如古時候皇帝下放一道緊急命令時,從州到縣到鎮到村,整條線都提着腦殼忙得不可交,搞的人心惶惶,並且更嚴重的是最終給用戶帶來的是從新下載覆蓋安裝,在必定程度上會流失用戶,嚴重影響了公司的用戶流量。
  • 在這種場景咱們應該採用熱補丁動態修復技術來解決以上這些問題。能夠選擇現成的第三方熱修復SDK,我在這裏不選擇的緣由,主要出於兩點:
  • 一、使用第三方SDK有可能增大咱們的項目包,並且總感受受制於人;
  • 二、追逐技術進階

文章目標

  • android類加載機制介紹
  • javassist動態修改字節碼
  • 實現熱補丁動態修復
  • Android類加載機制html

    1.ClassLoader體系結構

    classloader
    classloader

二、如何加載一個類

咱們先來看一下BaseDexClassLoader源碼中比較重要的code
java

cl11
cl11

  • 根據截圖能夠看到裏面有一個findClass方法
  • 它就是根據類名來查找指定的某一個類
  • 而後在該方法中調用了 DexPathList 實例的pathList.findClass(name, suppressedExceptions)的方法

cl12
cl12

能夠看出最終在此處找到了某一個類android

Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);複製代碼
  • 到這裏咱們能夠直觀的看出該過程是基於android dex分包方案的。
  • 其實最終咱們打包apk時可能有一個或是多個dex文件,默認是一個叫classes.dex的文件。
  • 不論是一個仍是多個,都會一一對應一個Element,按順序排成一個有序的數組dexElements
  • 當找類的時候,會按順序遍歷dex文件,而後從當前遍歷的dex文件中找類,若是找類則返回
  • 若是找不到從下一個dex文件繼續查找
  • 按照這個原理,咱們能夠把有問題的類打包到一個dex(patch.dex)中去,而後把這個dex插入到Elements的最前面,當遍歷findClass的時候,咱們修復的類就會被查找到,從而替代有bug的類便可,那麼下面來進行這一個過程的操做吧。編程

    patch.dex補丁製做

  • 新建一個Hotfix的工程,而後新建一個BugClass類數組

package ydc.hotfix;
public class BugClass {

    public String bug() {
        return "fix bug class";
    }
}複製代碼

在新建一個LoadBugClass類安全

public class LoadBugClass {
    public String getBugString() {
        BugClass bugClass = new BugClass();
        return bugClass.bug();
    }
}複製代碼

注意 LoadBugClass應用了BugClass類。bash

而後在界面層是這樣調用的:微信

13
13

ok,假設咱們把該apk發佈出去了,那麼用戶看到效果應該是「 測試調用方法:fix bug class」。這個時候公司領導認爲這樣的提示對於用戶是致命的。那麼咱們要把BugClass 類中的bug()方法中字符串替換一下,僅僅是修復一句話而已,實在沒有必要走打包發佈下放市場等複雜的流程。app

public String bug() {
        return "fix bug class";
    }複製代碼

ok,把這個有問題的地方修正爲:eclipse

public String bug() {
        return "楊德成正在修復提示語fix bug class";
    }複製代碼

經過dex工具單獨打包成path_dex.jar補丁包

  • 一、配置dex環境變量,最好是對應版本。
    cl14
    cl14
  • 二、驗證dex

cl15
cl15

三、先把BugClass.class文件作成成jar,注意路徑,必定要定位到該位置執行如下命令:

jar cvf path.jar ydc/hotfix/BugClass.class複製代碼

cl16
cl16

  • 四、作成補丁包path_dex.jar
    再把path.jar作成補丁包path_dex.jar,只有經過dex工具打包而成的文件才能被Android虛擬機(dexopt)執行。

依然在該路徑下執行如下命令:

dx --dex --output=path_dex.jar path.jar

cl17
cl17

  • 五、拷貝path_dex

    咱們把path_dex文件拷貝到assets目錄下

    cl18
    cl18

開始來打補丁

  • 一、將咱們的補丁包path_dex插入到上面提到的裝有dex的有序數組dexElements的最前面

首先咱們看一下hotfix的源碼:

cl19
cl19

根據截圖所示,作了兩個動做複製代碼
  • a、建立一個私有目錄,並把補丁包文件寫入到該目錄下

    • a一、 建立私有目錄
      File dexPath = new File(getDir(「dex」, Context.MODE_PRIVATE), 「path_dex.jar」);複製代碼
    • a二、文件讀寫方式把補丁包文件寫入到剛建立的私有目錄下
    public class Utils {
    private static final int BUF_SIZE = 2048;
    
    public static boolean prepareDex(Context context, File dexInternalStoragePath, String dex_file) {
       BufferedInputStream bis = null;
       OutputStream dexWriter = null;
    
       try {
           bis = new BufferedInputStream(context.getAssets().open(dex_file));
           dexWriter = new BufferedOutputStream(new FileOutputStream(dexInternalStoragePath));
           byte[] buf = new byte[BUF_SIZE];
           int len;
           while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
               dexWriter.write(buf, 0, len);
           }
           dexWriter.close();
           bis.close();
           return true;
       } catch (IOException e) {
           if (dexWriter != null) {
               try {
                   dexWriter.close();
               } catch (IOException ioe) {
                   ioe.printStackTrace();
               }
           }
           if (bis != null) {
               try {
                   bis.close();
               } catch (IOException ioe) {
                   ioe.printStackTrace();
               }
           }
           return false;
       }
    }
    }複製代碼
  • b、path_dex插入到上面提到的裝有dex的有序數組dexElements的最前面patch方法中的代碼以下:
public static void patch(Context context, String patchDexFile, String patchClassName) {
        if (patchDexFile != null && new File(patchDexFile).exists()) {
            try {
                if (hasLexClassLoader()) {
                    injectInAliyunOs(context, patchDexFile, patchClassName);
                } else if (hasDexClassLoader()) {
                    injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);
                } else {

                    injectBelowApiLevel14(context, patchDexFile, patchClassName);

                }
            } catch (Throwable th) {
            }
        }
    }複製代碼
  • 根據代碼所示,這根據傳入的文件類型類類加載器ClassLoader的類型作了下判斷,
  • 根據上文提到過的ClassLoader 體系原理,咱們的補丁包應該走的是hasDexClassLoader()分支,該方法代碼以下:

    private static boolean hasDexClassLoader() {
          try {
              Class.forName("dalvik.system.BaseDexClassLoader");
              return true;
          } catch (ClassNotFoundException e) {
              return false;
          }
      }複製代碼

    系統中確定會存在」dalvik.system.BaseDexClassLoader」類,那麼接下來應該進入injectAboveEqualApiLevel14(context, patchDexFile, patchClassName)方法,代碼以下:

private static void injectAboveEqualApiLevel14(Context context, String str, String str2)
        throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
        Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
            getDexElements(getPathList(
                new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));
        Object a2 = getPathList(pathClassLoader);
        setField(a2, a2.getClass(), "dexElements", a);
        pathClassLoader.loadClass(str2);
    }複製代碼

根據Android系統源碼解讀源以上代碼

PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();複製代碼

獲得沒有打補丁以前的dexElements有序數組對象

  • a、getPathList(pathClassLoader)方法解讀:
    private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
          IllegalAccessException {
          return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
      }複製代碼

根據以上代碼片斷,能夠看出這裏根據引用類名稱」BaseDexClassLoader」查找有個叫」pathList」屬性名的被引用類型。

private static Object getField(Object obj, Class cls, String str)
        throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cls.getDeclaredField(str);
        declaredField.setAccessible(true);
        return declaredField.get(obj);
    }複製代碼

上面這個片斷經過反射找到對應的被引用類」DexPathList」,上個」BaseDexClassLoader」系統源碼:

cl20
cl20

  • b、getDexElements(getPathList(pathClassLoader))方法解讀:

    private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
          return getField(obj, obj.getClass(), "dexElements");
      }複製代碼

    上面的這個代碼片斷根據a步驟獲得的DexPathList對象獲取到了沒有打補丁以前的dexElements有序數組對象

    private static Object getField(Object obj, Class cls, String str)
          throws NoSuchFieldException, IllegalAccessException {
          Field declaredField = cls.getDeclaredField(str);
          declaredField.setAccessible(true);
          return declaredField.get(obj);
      }複製代碼

    根據代碼可知依然使用反射原理獲取DexPathList對象中的有序數組dexElements。

    DexPathList類系統源碼以下:

    cl21
    cl21

    補丁包path_dex.jar轉化爲dexElements對象

  • 第一步、
    • 根據咱們在上面所建立的私有目錄及私有文件,建立一個DexClassLoader,還記得這個來是用來幹嗎的嗎,上面已經提到到,再次提醒一下,用來加載從.jar文件內部加載classes.dex文件,沒錯咱們要用它來加載咱們的補丁包文件。

new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))

根據該類的系統源碼看出其實該類的構造函數並無作具體的事情

cl22
cl22

真正作之情的是它的直接父類BaseDexClassLoader的構造函數,如圖所示

cl23
cl23

this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);複製代碼

看到沒,根據傳入參數初始化了咱們補丁包對應的 DexPathList對象,注意這一步僅僅是初始化哦

  • b、getPathList(new DexClassLoader(str, context.getDir(「dex」, 0).getAbsolutePath(), str, context.getClassLoader()))方法解讀:
private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
        IllegalAccessException {
        return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }複製代碼
private static Object getField(Object obj, Class cls, String str)
        throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cls.getDeclaredField(str);
        declaredField.setAccessible(true);
        return declaredField.get(obj);
    }複製代碼

上面這兩段代碼根據引用名」dalvik.system.BaseDexClassLoader」和被引用類屬性名」pathList」獲得DexPathList對象

  • c、而後調用getDexElements方法
private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
        return getField(obj, obj.getClass(), "dexElements");
    }複製代碼
private static Object getField(Object obj, Class cls, String str)
        throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cls.getDeclaredField(str);
        declaredField.setAccessible(true);
        return declaredField.get(obj);
    }複製代碼

上面的這兩端片斷根據 DexPathList類及屬性名dexElements獲取到咱們補丁包對應的有序數組dexElements上面已經獲得了兩個有序數組dexElements,一個存放的的是沒有打補丁以前的dex有序數組dexElements,另一個是咱們的補丁包對應的dex有序數組dexElements,那麼是否是到了該合併兩個數組的時候了呢,沒錯

Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
            getDexElements(getPathList(
                new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));複製代碼

到這裏終於知道這整句代碼到底幹了什麼事情了,Object a 就是咱們合併後的有序dex數組dexElements合併過程以下:

private static Object combineArray(Object obj, Object obj2) {
        Class componentType = obj2.getClass().getComponentType();
        int length = Array.getLength(obj2);
        int length2 = Array.getLength(obj) + length;
        Object newInstance = Array.newInstance(componentType, length2);
        for (int i = 0; i < length2; i++) {
            if (i < length) {
                Array.set(newInstance, i, Array.get(obj2, i));
            } else {
                Array.set(newInstance, i, Array.get(obj, i - length));
            }
        }
        return newInstance;
    }複製代碼

其實就是把補丁包對應的dex插入到原來有序數組dexElements的最前面了。

  • d、獲得最新的」PathList」對象
    Object a2 = getPathList(pathClassLoader);複製代碼
  • e、從新設置DexPathList 的有序數組對象dexElements值
    setField(a2, a2.getClass(), 「dexElements」, a);
    private static void setField(Object obj, Class cls, String str, Object obj2)
          throws NoSuchFieldException, IllegalAccessException {
          Field declaredField = cls.getDeclaredField(str);
          declaredField.setAccessible(true);
          declaredField.set(obj, obj2);
      }複製代碼

依然是使用反射機制設置新值。

  • f、加載咱們有bug的類
    pathClassLoader.loadClass(str2);複製代碼
    str參數是經過如下代碼傳入,即(ydc.hotfix.BugClass)
    HotFix.patch(this, dexPath.getAbsolutePath(), 「ydc.hotfix.BugClass」);複製代碼

這時候loadClass到的就是咱們補丁包中的BugClass類了,這是由於咱們把補丁包對應的dex文件插入到dexElements最前面。因此找到就BugClass直接返回了,代碼以下:

public Class findClass(String name, List<Throwable> suppressed) {
       for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
              Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
              if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }複製代碼

按照咱們以前的推論,到這裏應該就完成了補丁動態修復了,那麼真的是這樣的嗎,咱們不防運行下項目看看。

很不幸,運行時報錯:

cl24
cl24

-這是因爲LoadBugClass引用了BugClass,可是發現這這兩個類所在的dex不在一塊兒,其中:

  • LoadBugClass在classes.dex中
  • BugClass在path_dex.jar中結果發生了錯誤。

    究其緣由是 pathClassLoader.loadClass(str2)的時候,會去校驗LoadBugClass所在的dex和BugClass所在的dex是不是同一個,不是則會報錯。那麼校驗的前提是有一個叫CLASS_ISPREVERIFIED的類標誌,若是引用者被打上這個標識,就會去校驗,就會致使報錯,那麼咱們能夠想象若是引用者LoadBugClass 沒被打上這個標識,是否就會運行經過了呢,沒錯,就是這個原理。

阻止LoadBugClass打上CLASS_ISPREVERIFIED標誌

  • 咱們應該知道LoadBugClass引用了BugClass,類加載器是先加載引用者
  • 因此我在LoadBugClass的構造方法中來作這件事情,其實咱們要作的就是動態的在構造方法中,引用一個別的類
  • 而後把這個被引用類打包成一個單獨的dex文件。這樣就能夠防止了LoadBugClass類被打上CLASS_ISPREVERIFIED的標誌了,那咱們如今來開始作這件事情。

  • 一、動態被注入類的製做

    • a、新建一個hackdex的Module,我這裏來自HotFix的源碼,你也能夠本身新建

cl25
cl25

  • b、在該Module之下,新建一個AntilazyLoad空類。

    ```
    package dodola.hackdex;

/**

  • Created by sunpengfei on 15/11/3.
    */
    public class AntilazyLoad {
    }
    ```
    c、打包成單獨的dex文件,打包步驟徹底等同於補丁包的製做,因此我這裏就不在走這個過程了,而後把它放置在assets下

cl26
cl26

d、依然要把這個dex文件插入到dexElements有序數組的中,插入原理和補丁包插入原理徹底一致,並且這個dex文件須要在程序的入口進行插入,保證它是在有序數組的最前面,由於咱們要把該dex文件中的AntilazyLoad要動態注入到其它包裏面的某一個類的構造方法中。切記,dexElements裏面能夠塞入無數個dex文件。

/**
 * Created by sunpengfei on 15/11/4.
 */
public class HotfixApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");
        Utils.prepareDex(this.getApplicationContext(), dexPath, "hackdex_dex.jar");
        HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
        try {
            this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}複製代碼

ok,下面就是如何注入的問題了,這個時候應該到了咱們的AOP三劍客之一」javassist」閃亮登場了。

javassist實現動態代碼注入

javassist這貨是個好東西啊,它能夠以無侵入的方式重構你的原代碼。我以前編寫過另一個三劍客之一的文章,原理基本同樣。參考地址:blog.csdn.net/xinanheisha…

步驟
  • a、建立buildSrc模塊,這個項目是使用Groovy開發的,聽說這貨具有Java, JavaScript, Phython, Ruby等等語言的優勢,並且Groovy依賴於Java的,和Java無縫掛接的

    • 你能夠到這裏下載SDK:groovy-lang.org/download.ht…
    • 而後,配置path環境變量,Groovy的安裝挺簡單的,基本上和JDK的安裝差很少
    • 固然,這是Groovy自帶的最基本的開發工具,你能夠查看它如何支持as的
    • 若是是eclipse的話選擇菜單項「Help->Install New Software」以後重啓eclipse工具便可利用eclipse開發Groovy應用程序了
    • 可是工程名必定要叫」buildSrc」,這裏我就直接使用了HotFix,你也能夠本身構建,若你以爲閒麻煩,也能夠下載個人demo裏面獲取。

    cl27
    cl27

  • b、導入javassist
    ```
    apply plugin: 'groovy'

repositories {
mavenCentral()
}

dependencies {
compile gradleApi()
compile 'org.codehaus.groovy:groovy-all:2.3.6'
compile 'org.javassist:javassist:3.20.0-GA'
}

- c、PatchClass 代碼截圖以下

![cl28](http://upload-images.jianshu.io/upload_images/4614633-fa0bae9c8d2cd99e?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

其實很簡單的,這幾句的意思就是經過反射相關類,而後在相關類的構造方法中插入一句輸出語句。複製代碼

CtClass c = classes.getCtClass("ydc.hotfix.BugClass")
if (c.isFrozen()) {
c.defrost()
}
println("====添加構造方法====")
def constructor = c.getConstructors()[0];
constructor.insertBefore("System.out.println(dodola.hackdex.AntilazyLoad.class);")
//constructor.insertBefore("System.out.println(888);")
c.writeFile(buildDir)

執行完這段代碼以後,也無形中應用了AntilazyLoad這個類。

- d、這個工程不須要引用到主app(Module)中,只須要在 app->build.gradle中配置一個任務:

![cl29](http://upload-images.jianshu.io/upload_images/4614633-480c93281a78a0ca?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

在配置一下,侵入時期

![cl30](http://upload-images.jianshu.io/upload_images/4614633-788d25a94c10e7ad?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

ok,總算把整個過程寫完了,準備開始運行了,無論你激不激動,反正本人是挺激動的了。在運行以前,先看一下咱們的引用者類

![cl31](http://upload-images.jianshu.io/upload_images/4614633-0e9fd52e4d9d09b4?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
沒錯,能夠確認這是咱們的源代碼,化成灰我也能夠認出它來。在看一下運行以後的引用者類

![cl32](http://upload-images.jianshu.io/upload_images/4614633-5fe92167e9cd0644?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

沒錯,就是這個效果,咱們的源碼被javassist 赤裸裸的侵犯了,是否是瞬間覺的本身的「東西」很不安全,這就是AOP編程的強大之處啊。

項目講解到這裏,我想估計沒有幾我的能有耐心的看到這裏來了,由於以爲文章實在太長,須要有多大耐心才能扛到這裏,連我本身也懷疑本身如何寫出來的,不過我認爲,這麼強大並且實用的技術點,不是可以三五兩語就能說清的,咱們要有足夠的耐心來探索咱們所不知的,有耐心,咱們就有但願,有但願就不會失望!

ok,咱們見證一下奇蹟。

![cl33](http://upload-images.jianshu.io/upload_images/4614633-dbffea3064afe73e?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

看到這效果,我手已累,鍵盤已壞。。。。
>Demo下載地址:
>
> http://download.csdn.net/download/xinanheishao/9902530
演示環境:demo導入不能正常運行,建議先調整環境,跑起來,再進階。複製代碼

classpath 'com.android.tools.build:gradle:1.3.0'

#Thu Jul 13 16:40:06 CST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip
```
若是默認的jdk環境找不到,手動指向一下

cl34
cl34

若是你已經準備好足夠信心的話,能夠按照文章,本身嘗試一方

最後感謝騰訊空間給出的解決方法思路和HotFix開源做者。

原文地址

blog.csdn.net/xinanheisha…

項目相關:

相關demo下載地址:

download.csdn.net/download/xi…

若是你以爲此文對您有所幫助,歡迎入羣 QQ交流羣 :232203809
微信公衆號:終端研發部

技術+職場
技術+職場
相關文章
相關標籤/搜索