Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 熱補丁動態修復框架小結

Android 熱補丁動態修復框架小結

編輯:關於Android編程

 

一、概述

最新github上開源了很多熱補丁動態修復框架,大致有:

上述三個框架呢,根據其描述,原理都來自:安卓App熱補丁動態修復技術介紹,以及Android dex分包方案,所以這倆篇務必要看。這裡就不對三個框架做過多對比了,因為原理都一致,實現的代碼可能差異並不是特別大。

有興趣的直接看這篇原理文章,加上上面框架的源碼基本就可以看懂了。當然了,本篇博文也會做個上述框架源碼的解析,以及在整個實現過程中用到的技術的解析。

二、熱修復原理

對於熱修復的原理,如果你看了上面的兩篇文章,相信你已經大概明白了。重點需要知道的就是,Android的ClassLoader體系,android中加載類一般使用的是PathClassLoaderDexClassLoader,首先看下這兩個類的區別:

對於PathClassLoader,從文檔上的注釋來看:

Provides a simple {@link ClassLoader} implementation that operates
on a list of files and directories in the local file system, but
does not attempt to load classes from the network. Android uses
this class for its system class loader and for its application
class loader(s).

可以看出,Android是使用這個類作為其系統類和應用類的加載器。並且對於這個類呢,只能去加載已經安裝到Android系統中的apk文件。

對於DexClassLoader,依然看下注釋:

A class loader that loads classes from {@code .jar} and
{@code .apk} files containing a {@code classes.dex} entry.
This can be used to execute code not installed as part of an application.

可以看出,該類呢,可以用來從.jar和.apk類型的文件內部加載classes.dex文件。可以用來執行非安裝的程序代碼。

ok,如果大家對於插件化有所了解,肯定對這個類不陌生,插件化一般就是提供一個apk(插件)文件,然後在程序中load該apk,那麼如何加載apk中的類呢?其實就是通過這個DexClassLoader,具體的代碼我們後面有描述。

ok,到這裡,大家只需要明白,Android使用PathClassLoader作為其類加載器,DexClassLoader可以從.jar和.apk類型的文件內部加載classes.dex文件就好了。

上面我們已經說了,Android使用PathClassLoader作為其類加載器,那麼熱修復的原理具體是?

ok,對於加載類,無非是給個classname,然後去findClass,我們看下源碼就明白了。
PathClassLoaderDexClassLoader都繼承自BaseDexClassLoader。在BaseDexClassLoader中有如下源碼:

#BaseDexClassLoader
@Override
protected Class findClass(String name) throws ClassNotFoundException {
    Class clazz = pathList.findClass(name);

    if (clazz == null) {
        throw new ClassNotFoundException(name);
    }

    return clazz;
}

#DexPathList
public Class findClass(String name) {
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;

        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext);
            if (clazz != null) {
                return clazz;
            }
        }
    }

    return null;
}

#DexFile
public Class loadClassBinaryName(String name, ClassLoader loader) {
    return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);

可以看出呢,BaseDexClassLoader中有個pathList對象,pathList中包含一個DexFile的集合dexElements,而對於類加載呢,就是遍歷這個集合,通過DexFile去尋找。

ok,通俗點說:

一個ClassLoader可以包含多個dex文件,每個dex文件是一個Element,多個dex文件排列成一個有序的數組dexElements,當找類的時候,會按順序遍歷dex文件,然後從當前遍歷的dex文件中找類,如果找類則返回,如果找不到從下一個dex文件繼續查找。(來自:安卓App熱補丁動態修復技術介紹)

那麼這樣的話,我們可以在這個dexElements中去做一些事情,比如,在這個數組的第一個元素放置我們的patch.jar,裡面包含修復過的類,這樣的話,當遍歷findClass的時候,我們修復的類就會被查找到,從而替代有bug的類。

說到這,你可能已經露出笑容了,原來熱修復原理這麼簡單。不過,還存在一個CLASS_ISPREVERIFIED的問題,對於這個問題呢,詳見:安卓App熱補丁動態修復技術介紹該文有圖文詳解。

ok,對於CLASS_ISPREVERIFIED,還是帶大家理一下:

根據上面的文章,在虛擬機啟動的時候,當verify選項被打開的時候,如果static方法、private方法、構造函數等,其中的直接引用(第一層關系)到的類都在同一個dex文件中,那麼該類就會被打上CLASS_ISPREVERIFIED標志。

那麼,我們要做的就是,阻止該類打上CLASS_ISPREVERIFIED的標志。

注意下,是阻止引用者的類,也就是說,假設你的app裡面有個類叫做LoadBugClass,再其內部引用了BugClass。發布過程中發現BugClass有編寫錯誤,那麼想要發布一個新的BugClass類,那麼你就要阻止LoadBugClass這個類打上CLASS_ISPREVERIFIED的標志。

也就是說,你在生成apk之前,就需要阻止相關類打上CLASS_ISPREVERIFIED的標志了。對於如何阻止,上面的文章說的很清楚,讓LoadBugClass在構造方法中,去引用別的dex文件,比如:hack.dex中的某個類即可。

ok,總結下:

其實就是兩件事:1、動態改變BaseDexClassLoader對象間接引用的dexElements;2、在app打包的時候,阻止相關類去打上CLASS_ISPREVERIFIED標志。

如果你沒有看明白,沒事,多看幾遍,下面也會通過代碼來說明。

三、阻止相關類打上CLASS_ISPREVERIFIED標志

ok,接下來的代碼基本上會通過https://github.com/dodola/HotFix所提供的代碼來講解。

那麼,這裡拿具體的類來說:

大致的流程是:在dx工具執行之前,將LoadBugClass.class文件呢,進行修改,再其構造中添加System.out.println(dodola.hackdex.AntilazyLoad.class),然後繼續打包的流程。注意:AntilazyLoad.class這個類是獨立在hack.dex中。

ok,這裡大家可能會有2個疑問:

如何去修改一個類的class文件 如何在dx之前去進行疑問1的操作

(1)如何去修改一個類的class文件

這裡我們使用javassist來操作,很簡單:

ok,首先我們新建幾個類:

package dodola.hackdex;
public class AntilazyLoad
{

}

package dodola.hotfix;
public class BugClass
{
    public String bug()
    {
        return bug class;
    }
}

package dodola.hotfix;
public class LoadBugClass
{
    public String getBugString()
    {
        BugClass bugClass = new BugClass();
        return bugClass.bug();
    }
}

注意下,這裡的package,我們要做的是,上述類正常編譯以後產生class文件。比如:LoadBugClass.class,我們在LoadBugClass.class的構造中去添加一行:

System.out.println(dodola.hackdex.AntilazyLoad.class)

下面看下操作類:

package test;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

public class InjectHack
{
    public static void main(String[] args)
    {
        try
        {
            String path = /Users/zhy/develop_work/eclipse_android/imooc/JavassistTest/;
            ClassPool classes = ClassPool.getDefault();
            classes.appendClassPath(path + bin);//項目的bin目錄即可
            CtClass c = classes.get(dodola.hotfix.LoadBugClass);
            CtConstructor ctConstructor = c.getConstructors()[0];
            ctConstructor
                    .insertAfter(System.out.println(dodola.hackdex.AntilazyLoad.class););
            c.writeFile(path + /output);
        } catch (Exception e)
        {
            e.printStackTrace();
        }

    }
}

ok,點擊run即可了,注意項目中導入javassist-*.jar的包。

首先拿到ClassPool對象,然後添加classpath,如果你有多個classpath可以多次調用。然後從classpath中找到LoadBugClass,拿到其構造方法,在其最後插入一行代碼。ok,代碼很好懂。

ok,我們反編譯看下我們生成的class文件:

ok,關於javassist,如果有興趣的話,大家可以參考幾篇文章學習下:

http://www.ibm.com/developerworks/cn/java/j-dyn0916/ http://zhxing.iteye.com/blog/1703305

(2)如何在dx之前去進行(1)的操作

ok,這個就結合https://github.com/dodola/HotFix的源碼來說了。

將其源碼導入之後,打開app/build.gradle

apply plugin: 'com.android.application'

task('processWithJavassist') << {
    String classPath = file('build/intermediates/classes/debug')//項目編譯class所在目錄
    dodola.patch.PatchClass.process(classPath, project(':hackdex').buildDir
            .absolutePath + '/intermediates/classes/debug')//第二個參數是hackdex的class所在目錄

}
android {
    applicationVariants.all { variant ->
        variant.dex.dependsOn << processWithJavassist //在執行dx命令之前將代碼打入到class中
    }
}

你會發現,在執行dx之前,會先執行processWithJavassist這個任務。這個任務的作用呢,就和我們上面的代碼一致了。而且源碼也給出了,大家自己看下。

ok,到這呢,你就可以點擊run了。ok,有興趣的話,你可以反編譯去看看dodola.hotfix.LoadBugClass這個類的構造方法中是否已經添加了改行代碼。

關於反編譯的用法,工具等,參考:http://blog.csdn.net/lmj623565791/article/details/23564065

ok,到此我們已經能夠正常的安裝apk並且運行了。但是目前還未涉及到打補丁的相關代碼。

四、動態改變BaseDexClassLoader對象間接引用的dexElements

ok,這裡就比較簡單了,動態改變一個對象的某個引用我們反射就可以完成了。

不過這裡需要注意的是,還記得我們之前說的,尋找class是遍歷dexElements;然後我們的AntilazyLoad.class實際上並不包含在apk的classes.dex中,並且根據上面描述的需要,我們需要將AntilazyLoad.class這個類打成獨立的hack_dex.jar,注意不是普通的jar,必須經過dx工具進行轉化。

具體做法:

jar cvf hack.jar dodola/hackdex/*
dx  --dex --output hack_dex.jar hack.jar 

如果,你沒有辦法把那一個class文件搞成jar,去百度一下…

ok,現在有了hack_dex.jar,這個是干嘛的呢?

應該還記得,我們的app中部門類引用了AntilazyLoad.class,那麼我們必須在應用啟動的時候,降這個hack_dex.jar插入到dexElements,否則肯定會出事故的。

那麼,Application的onCreate方法裡面就很適合做這件事情,我們把hack_dex.jar放到assets目錄。

下面看hotfix的源碼:

/*
 * Copyright (C) 2015 Baidu, Inc. All Rights Reserved.
 */
package dodola.hotfix;

import android.app.Application;
import android.content.Context;

import java.io.File;

import dodola.hotfixlib.HotFix;

/**
 * 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,在app的私有目錄創建一個文件,然後調用Utils.prepareDex將assets中的hackdex_dex.jar寫入該文件。
接下來HotFix.patch就是去反射去修改dexElements了。我們深入看下源碼:


/*
 * Copyright (C) 2015 Baidu, Inc. All Rights Reserved.
 */
package dodola.hotfix;

/**
 * Created by sunpengfei on 15/11/4.
 */
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;
        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;

}

ok,其實就是文件的一個讀寫,將assets目錄的文件,寫到app的私有目錄中的文件。

下面主要看patch方法

/*
 * Copyright (C) 2015 Baidu, Inc. All Rights Reserved.
 */
package dodola.hotfixlib;

import android.annotation.TargetApi;
import android.content.Context;

import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

/* compiled from: ProGuard */
public final class HotFix
{
    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的類型做了下判斷,原理都是反射,我們看其中一個分支hasDexClassLoader();

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


 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);
}

首先查找類dalvik.system.BaseDexClassLoader,如果找到則進入if體。

在injectAboveEqualApiLevel14中,根據context拿到PathClassLoader,然後通過getPathList(pathClassLoader),拿到PathClassLoader中的pathList對象,在調用getDexElements通過pathList取到dexElements對象。

ok,那麼我們的hack_dex.jar如何轉化為dexElements對象呢?

通過源碼可以看出,首先初始化了一個DexClassLoader對象,前面我們說過DexClassLoader的父類也是BaseDexClassLoader,那麼我們可以通過和PathClassLoader同樣的方式取得dexElements。

ok,到這裡,我們取得了,系統中PathClassLoader對象的間接引用dexElements,以及我們的hack_dex.jar中的dexElements,接下來就是合並這兩個數組了。

可以看到上面的代碼使用的是combineArray方法。

合並完成後,將新的數組通過反射的方式設置給pathList.

接下來看一下反射的細節:

private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
            IllegalAccessException
{
    return getField(obj, Class.forName(dalvik.system.BaseDexClassLoader), pathList);
}

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);
}

其實都是取成員變量的過程,應該很容易懂~~

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;
}

ok,這裡的兩個數組合並,只需要注意一件事,將hack_dex.jar裡面的dexElements放到新數組前面即可。

到此,我們就完成了在應用啟動的時候,動態的將hack_dex.jar中包含的DexFile注入到ClassLoader的dexElements中。這樣就不會查找不到AntilazyLoad這個類了。

ok,那麼到此呢,還是沒有看到我們如何打補丁,哈,其實呢,已經說過了,打補丁的過程和我們注入hack_dex.jar是一致的。

你現在運行HotFix的app項目,點擊menu裡面的測試:

會彈出:調用測試方法:bug class

接下來就看如何完成熱修復。

五、完成熱修復

ok,那麼我們假設BugClass這個類有錯誤,需要修復:

package dodola.hotfix;

public class BugClass
{
    public String bug()
    {
        return fixed class;
    }
}

可以看到字符串變化了:bug class -> fixed class .

然後,編譯,將這個類的class->jar->dex。步驟和上面是一致的。

 jar cvf path.jar dodola/hotfix/BugClass.class 
 dx  --dex --output path_dex.jar path.jar 

拿到path_dex.jar文件。

正常情況下,這個玩意應該是下載得到的,當然我們介紹原理,你可以直接將其放置到sdcard上。

然後在Application的onCreate中進行讀取,我們這裡為了方便也放置到assets目錄,然後在Application的onCreate中添加代碼:

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, hack_dex.jar);
        HotFix.patch(this, dexPath.getAbsolutePath(), dodola.hackdex.AntilazyLoad);
        try
        {
            this.getClassLoader().loadClass(dodola.hackdex.AntilazyLoad);
        } catch (ClassNotFoundException e)
        {
            e.printStackTrace();
        }

        dexPath = new File(getDir(dex, Context.MODE_PRIVATE), path_dex.jar);
        Utils.prepareDex(this.getApplicationContext(), dexPath, path_dex.jar);
        HotFix.patch(this, dexPath.getAbsolutePath(), dodola.hotfix.BugClass);

    }
}

其實就是添加了後面的3行,這裡需要說明一下,第一行依舊是復制到私有目錄,如果你是sdcard上,那麼操作基本是一致的,這裡就別問:如果在sdcard或者網絡上怎麼處理~

ok,那麼再次運行我們的app。

ok,最後說一下,說項目中有一個打補丁的按鈕,在menu下,那麼你也可以不在Application裡面添加我們最後的3行。

你運行app後,先點擊打補丁,然後點擊測試也可以發現成功修復了。

如果先點擊測試,再點擊打補丁,再測試是不會變化的,因為類一旦加載以後,不會重新再去重新加載了。

ok,到此,我們的熱修復的原理,已經解決方案,我相信已經很詳細的介紹完成了,如果你有足夠的耐心一定可以實現。中間制作補丁等操作,我們的操作比較麻煩,自動化的話,可以參考https://github.com/jasonross/Nuwa。

最後就是對於QQ空間團隊,以及開源作者的感謝了~~


 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved