Android NDK開發(fā)之旅36--NDK-熱修復(fù)-AndFix的基本使用以及C/C++源碼級分析

前言

熱修復(fù)也叫熱更新,又叫做動(dòng)態(tài)加載、動(dòng)態(tài)修復(fù)、動(dòng)態(tài)更新,是指不通過重新安裝新的APK安裝包的情況下修復(fù)一些線上的BUG。

通過這樣做,可以免去發(fā)版、安裝、重新打開等過程,就可以修復(fù)線上的BUG,防止用戶流失。因此這是幾乎每一個(gè)APP都需要的一個(gè)功能,因此很有學(xué)習(xí)的必要。

需要注意的是:熱修復(fù)只是臨時(shí)的亡羊補(bǔ)牢。在企業(yè)中真正的熱修復(fù)發(fā)版與正式版一樣,需要測試進(jìn)行測試。但是熱修復(fù)也存在一些兼容性問題。因此高質(zhì)量的版本與熱修復(fù)框架才是解決問題的最好的手段。

各大熱修復(fù)框架的對比圖

目前流行的熱修復(fù)技術(shù)有:

  • QQ空間的超級補(bǔ)丁方案
  • 微信的Tinker
  • 阿里巴巴的AndFix、Dexposed
  • 美團(tuán)的Robust
  • 餓了么的MiGo
  • 百度的HotFix

下面給出一些常見的熱修復(fù)框架的對比圖:

熱修復(fù)框架對比.png

可以看出幾乎每一個(gè)框架都有優(yōu)勢和弊端。其中“即時(shí)生效”的意思就是是否能不通過重啟來達(dá)到修復(fù)的效果,就像AndFix,支持即時(shí)生效,但是只能做到方法的替換,而不是替換(新增)類、資源等。選擇什么框架,還是需要根據(jù)APP或者BUG的實(shí)際情況出發(fā)。

關(guān)于更多的熱修復(fù)框架的對比,可以參考一些網(wǎng)上的文章。今天我們主要分析的是阿里巴巴的AndFix。

技術(shù)選型

既然有這么多的技術(shù),那么我們必將面臨技術(shù)選型的問題,因此這里給出一些技術(shù)選型上的參考標(biāo)準(zhǔn):

  • 框架是否符合項(xiàng)目的需求,需求是衡量一切的標(biāo)準(zhǔn)
  • 能夠滿足需求的前提下,選擇學(xué)習(xí)成本最低的(同時(shí)也代表著使用簡單、維護(hù)起來簡單)
  • 學(xué)習(xí)成本差不多的情況下,優(yōu)先選擇大公司的框架

AndFix的基本使用

AndFix的引入

首先我們在gradle腳本中添加AndFix的依賴:

compile 'com.alipay.euler:andfix:0.5.0@aar'

由于熱修復(fù)需要讀寫SD卡,因此需要添加一些權(quán)限,注意6.0的權(quán)限適配問題。如果你的補(bǔ)丁文件是從服務(wù)器下載的,那么就需要聯(lián)網(wǎng)權(quán)限。

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />

初始化

自定義一個(gè)Application,初始化AndFix,這里為了方便演示,在加載Patch文件的時(shí)候,我們省略了校驗(yàn)的步驟:

public class App extends Application {

    private static final String TAG = App.class.getSimpleName();

    //Patch文件的路徑
    private static final String APATCH_PATH = "/out.apatch";

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

        //初始化PatchManager
        PatchManager mPatchManager = new PatchManager(this);
        mPatchManager.init("1.0");

        //加載已加載過的Patch文件
        mPatchManager.loadPatch();
        //添加外部Patch文件
        try {
            // .apatch file path
            String patchFileString = Environment.getExternalStorageDirectory().getAbsolutePath() + APATCH_PATH;
            mPatchManager.addPatch(patchFileString);
            Log.d(TAG, "加載成功:" + patchFileString);
        } catch (Exception e) {
            Log.e(TAG, "", e);
        }
    }
}

AndFix的示例

然后我們寫一個(gè)有BUG的Test類:

public class Test {

    public static int test() {
        return 1 / 0;
    }

}

在Activity中調(diào)用這個(gè)類:

findViewById(R.id.btn_test).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        String res = Test.test() + "";
        Toast.makeText(MainActivity.this, res, Toast.LENGTH_SHORT).show();
    }
});

把當(dāng)前的代碼簽名打包成一個(gè)APK,我們修改名字問old.apk。

然后把Test的BUG修改,再次簽名打包成一個(gè)APK,我們修改名字問new.apk。

public class Test {

    public static int test() {
        return 1 / 1;
    }

}

下載生成Patch文件的工具、生成Patch文件

然后我們?nèi)ndFix的官網(wǎng)下載生成Patch文件的工具:

//Windows電腦用
apkpatch.bat
//Linux、蘋果電腦用
apkpatch.sh

然后我們把剛剛生產(chǎn)的兩個(gè)APK文件、簽名文件放到同一個(gè)目錄。由于筆者使用的是Ubuntu系統(tǒng),因此需要給apkpatch.sh添加執(zhí)行的權(quán)限,Ubuntu下,簽名文件的格式是jks。

然后執(zhí)行下面的命令,生產(chǎn)Patch文件:

./apkpatch.sh -f new.apk -t old.apk -o out -k nan.jks -p 123456 -a nan -e 123456

在命令里面我們執(zhí)行了新舊兩個(gè)APK文件,輸出路徑,簽名文件,簽名密碼,簽名文件的別名以及密碼。

最終我們輸出一個(gè)文件:

new-e726f4396cbed42d1cf50fb2d781c9d9.apatch

我們修改名字為out.apatch,放到手機(jī)的SD卡根目錄下面。

效果

一開始如果沒有放入out.apatch的時(shí)候,APP運(yùn)行的時(shí)候是直接因?yàn)锽UG而崩潰的,但是放入了out.apatch之后,APP的BUG被修復(fù)了。

AndFix源碼分析

準(zhǔn)備步驟

為了更方便地查看、分析AndFix的源碼,我們將源碼導(dǎo)入Android Studio中,由于AndFix源碼是使用ndk-build進(jìn)行構(gòu)建的,為了更加方便導(dǎo)入,筆者根據(jù)Android.mk編寫了一個(gè)CMake腳本,供讀者參考:

#配置CMake的最低版本
cmake_minimum_required(VERSION 3.4.1)

#配置工程路徑
#set(path_project /home/wuhuannan/AndroidPrj/AndFix)
set(path_project D:/AndroidStudio/AndFix)
#JNI文件夾的路徑
set(path_jni ${path_project}/jni)

#配置頭文件的包含路徑
include_directories(${path_project}/jni/art)
include_directories(${path_project}/jni/dalvik)
include_directories(${path_project}/jni)

#添加自己寫的庫
add_library(andfix
            SHARED
            ${path_jni}/andfix.cpp ${path_jni}/art/art_method_replace.cpp ${path_jni}/art/art_method_replace_4_4.cpp ${path_jni}/art/art_method_replace_5_0.cpp ${path_jni}/art/art_method_replace_5_1.cpp ${path_jni}/art/art_method_replace_6_0.cpp ${path_jni}/art/art_method_replace_7_0.cpp ${path_jni}/dalvik/dalvik_method_replace.cpp
            )

#找到Android的log庫(這個(gè)庫已經(jīng)在Android平臺中了)
find_library(
            log-lib
            log
            )

#找到Android的的庫(這個(gè)庫已經(jīng)在Android平臺中了),這個(gè)庫貌似用不上,姑且先加上吧
find_library(
            android-lib
            android
            )

#把需要的庫鏈接到自己的庫中
target_link_libraries(
            andfix
            ${log-lib}
            ${android-lib}
            )

一、Patch文件分析

官網(wǎng)給出的AndFix核心原理如下圖所示:

AndFix核心原理.png

主要是通過Native層的Hook技術(shù),實(shí)現(xiàn)方法的動(dòng)態(tài)替換,而替換哪個(gè)方法,就需要根據(jù)Patch文件中的@MethodReplace注解而決定。

從上面的例子中我們可以直觀地看到,我們的BUG是通過加載一個(gè)Patch文件來修復(fù),那么我們就從這個(gè)Patch文件作為我們源碼分析的入口吧。

Patch文件實(shí)際上一個(gè)zip壓縮的文件,因此我們不妨把它的后綴名改為zip,然后用解壓縮工具打開。可以看到,

Patch文件解壓.png

我們可以看到解壓出來的是一個(gè)MeTA-INF文件夾以及dex文件。其中MeTA-INF文件夾里面的PATCH.MF文件保存的是這個(gè)Patch的一些關(guān)鍵信息。等下我們分析源碼的時(shí)候需要用到。

再者就是下面的這個(gè)dex文件,我們不妨利用dex2jar-2.0工具對其進(jìn)行反編譯,Linux中的命令如下:

./d2j-dex2jar.sh -f -o out.jar classes.dex

反編譯出來以后我們我們再次解壓jar包,直接拖動(dòng)AS中打開:

package com.nan.andfixdemo;

import com.alipay.euler.andfix.annotation.MethodReplace;

public class Test_CF {
    public Test_CF() {
    }

    @MethodReplace(
        clazz = "com.nan.andfixdemo.Test",
        method = "test"
    )
    public static int test() {
        return 1;
    }
}

可以看到,其實(shí)這個(gè)dex文件就把我們需要替換的方法添加了一個(gè)@MethodReplace注解。

二、AndFix中Java層核心類分析

AndFix里面有幾個(gè)核心的類,其中包括:

代表補(bǔ)丁文件的類:Patch
補(bǔ)丁文件的管理:PatchManager
修復(fù)的類:AndFixManager
與C/C++層交互的類:AndFix

一個(gè)Patch文件實(shí)質(zhì)上是對應(yīng)一個(gè)Patch對象的:

public class Patch implements Comparable<Patch> {

//省略一些代碼
private final File mFile;
private String mName;
private Date mTime;
private Map<String, List<String>> mClassesMap;

public Patch(File file) throws IOException {
    mFile = file;
    init();
}

@SuppressWarnings("deprecation")
private void init() throws IOException {
    //省略一些代碼
}

public String getName() {
    return mName;
}

public File getFile() {
    return mFile;
}

public Set<String> getPatchNames() {
    return mClassesMap.keySet();
}

public List<String> getClasses(String patchName) {
    return mClassesMap.get(patchName);
}

public Date getTime() {
    return mTime;
}

@Override
public int compareTo(Patch another) {
    return mTime.compareTo(another.getTime());
}

}

從上面的構(gòu)造方法中可以看出,AndFix在創(chuàng)建Patch對象的時(shí)候,調(diào)用了inti方法:

private void init() throws IOException {
    JarFile jarFile = null;
    InputStream inputStream = null;
    try {
        jarFile = new JarFile(mFile);
        JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);
        inputStream = jarFile.getInputStream(entry);
        Manifest manifest = new Manifest(inputStream);
        Attributes main = manifest.getMainAttributes();
        mName = main.getValue(PATCH_NAME);
        mTime = new Date(main.getValue(CREATED_TIME));

        mClassesMap = new HashMap<String, List<String>>();
        Attributes.Name attrName;
        String name;
        List<String> strings;
        for (Iterator<?> it = main.keySet().iterator(); it.hasNext();) {
            attrName = (Attributes.Name) it.next();
            name = attrName.toString();
            if (name.endsWith(CLASSES)) {
                strings = Arrays.asList(main.getValue(attrName).split(","));
                if (name.equalsIgnoreCase(PATCH_CLASSES)) {
                    mClassesMap.put(mName, strings);
                } else {
                    mClassesMap.put(
                            name.trim().substring(0, name.length() - 8),// remove
                                                                        // "-Classes"
                            strings);
                }
            }
        }
    } finally {
        if (jarFile != null) {
            jarFile.close();
        }
        if (inputStream != null) {
            inputStream.close();
        }
    }

}

可以看出,在Patch構(gòu)造的時(shí)候,調(diào)用了Java提供的一些讀取jar文件的API去讀取Patch文件。主要就是PATCH.MF文件這個(gè)文件:

Manifest-Version: 1.0
Patch-Name: new
Created-Time: 20 Apr 2017 02:45:20 GMT
From-File: new.apk
To-File: old.apk
Patch-Classes: com.nan.andfixdemo.Test_CF
Created-By: 1.0 (ApkPatch)

例如讀取了Patch名、創(chuàng)建日期等,其中最核心的就是讀取Patch-Classes,這就是需要修復(fù)的類的全名:

if (name.equalsIgnoreCase(PATCH_CLASSES)) {
    mClassesMap.put(mName, strings);
} else {
    mClassesMap.put(
            name.trim().substring(0, name.length() - 8),// remove
                                                        // "-Classes"
            strings);
}

因?yàn)檫@個(gè)Patch-Classes有可能會叫Classes,因此這里需要分兩種情況處理。

上面就分析了Patch對象的構(gòu)建,最終通過getClasses()方法就可以得到需要修復(fù)的所有的類。

三、AndFix中Java層基本修復(fù)流程分析

我們回到自定義的Application中:

//初始化PatchManager
PatchManager mPatchManager = new PatchManager(this);
mPatchManager.init("1.0");

//加載已加載過的Patch文件
mPatchManager.loadPatch();
//添加外部Patch文件
try {
    // .apatch file path
    String patchFileString = Environment.getExternalStorageDirectory().getAbsolutePath() + APATCH_PATH;
    mPatchManager.addPatch(patchFileString);
    Log.d(TAG, "加載成功:" + patchFileString);
} catch (Exception e) {
    Log.e(TAG, "", e);
}

一開始我們初始化了AndFix,然后調(diào)用loadPatch方法進(jìn)行補(bǔ)丁的加載:

public void loadPatch() {
    mLoaders.put("*", mContext.getClassLoader());// wildcard
    Set<String> patchNames;
    List<String> classes;
    for (Patch patch : mPatchs) {
        patchNames = patch.getPatchNames();
        for (String patchName : patchNames) {
            classes = patch.getClasses(patchName);
            mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
                    classes);
        }
    }
}

在loadPatch方法方法中,先拿到Map中的ClassLoader,最后通過AndFixManager的fix方法進(jìn)行修復(fù)。

由于AndFix在加載Patch之后,會將當(dāng)前的Patch保存起來,下次將不再加載。那么我們的Patch一開始其實(shí)是從下面這段代碼加載的:

try {
    // .apatch file path
    String patchFileString = Environment.getExternalStorageDirectory().getAbsolutePath() + APATCH_PATH;
    mPatchManager.addPatch(patchFileString);
    Log.d(TAG, "加載成功:" + patchFileString);
} catch (Exception e) {
    Log.e(TAG, "", e);
}

核心邏輯就是通過PatchManager的addPatch方法進(jìn)行加載:

public void addPatch(String path) throws IOException {
    File src = new File(path);
    File dest = new File(mPatchDir, src.getName());
    if(!src.exists()){
        throw new FileNotFoundException(path);
    }
    if (dest.exists()) {
        Log.d(TAG, "patch [" + path + "] has be loaded.");
        return;
    }
    FileUtil.copyFile(src, dest);// copy to patch's directory
    Patch patch = addPatch(dest);
    if (patch != null) {
        loadPatch(patch);
    }
}

我們可以看到,AndFix是把我們添加進(jìn)來的Patch文件進(jìn)行了copy,放到一個(gè)特定的目錄中去的,下次就不會再加載了。因此我們在使用AndFix的時(shí)候,如果再次修復(fù)BUG的時(shí)候,就需要修改Patch文件的名字了,否則將不會再次加載,這是一個(gè)隱藏的大坑?。〔贿^這樣做的好處就是省去了每次重復(fù)加載的工作,提高了APP的性能。

最后也是調(diào)用loadPatch方法進(jìn)行加載以及fix。

四、AndFix中Java層修復(fù)流程分析

從上面的分析我們知道,Java層最終會調(diào)用AndFixManager的fix方法進(jìn)行修復(fù)(方法替換)。那么我們不妨先進(jìn)來看一看究竟:

public synchronized void fix(File file, ClassLoader classLoader,
        List<String> classes) {

    //判斷AndFix是否可用,主要是判斷是否正確初始化了
    if (!mSupport) {
        return;
    }

    //驗(yàn)證Patch文件(MD5驗(yàn)證,相關(guān)代碼請自行分析)
    if (!mSecurityChecker.verifyApk(file)) {// security check fail
        return;
    }

    try {
        File optfile = new File(mOptDir, file.getName());
        boolean saveFingerprint = true;
        if (optfile.exists()) {
            if (mSecurityChecker.verifyOpt(optfile)) {
                saveFingerprint = false;
            } else if (!optfile.delete()) {
                return;
            }
        }

        //加載Patch文件中的dex文件
        final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
                optfile.getAbsolutePath(), Context.MODE_PRIVATE);

        if (saveFingerprint) {
            mSecurityChecker.saveOptSig(optfile);
        }

        //自定義一個(gè)ClassLoader去加載需要修復(fù)的類
        ClassLoader patchClassLoader = new ClassLoader(classLoader) {
            @Override
            protected Class<?> findClass(String className)
                    throws ClassNotFoundException {
                Class<?> clazz = dexFile.loadClass(className, this);
                if (clazz == null
                        && className.startsWith("com.alipay.euler.andfix")) {
                    return Class.forName(className);// annotation’s class
                                                    // not found
                }
                if (clazz == null) {
                    throw new ClassNotFoundException(className);
                }
                return clazz;
            }
        };

        //查找相應(yīng)的修復(fù)注解,如果找到,就進(jìn)行修復(fù)
        Enumeration<String> entrys = dexFile.entries();
        Class<?> clazz = null;
        while (entrys.hasMoreElements()) {
            String entry = entrys.nextElement();
            if (classes != null && !classes.contains(entry)) {
                continue;// skip, not need fix
            }
            clazz = dexFile.loadClass(entry, patchClassLoader);
            if (clazz != null) {
                fixClass(clazz, classLoader);
            }
        }
    } catch (IOException e) {
        Log.e(TAG, "pacth", e);
    }
}

在AndFixManager的fix方法中,一開始進(jìn)行了一些是否初始化的驗(yàn)證、Patch文件的驗(yàn)證,然后就加載Patch包中的dex文件,生成一個(gè)DexFile對象。

然后通過自定義的類加載器加載DexFile對象中的類。這里由于是加載我們第三方的class,因此需要自定義一個(gè)類加載器。加載成功以后,就通過反射的方式循環(huán)去讀方法上面的注解,如果找到了注解,就進(jìn)行修復(fù)。

下面繼續(xù)看fixClass方法,這里就是通過循環(huán)找到MethodReplace注解,然后調(diào)用replaceMethod進(jìn)行方法替換。(AndFix熱修復(fù)實(shí)質(zhì)就是方法的替換)

private void fixClass(Class<?> clazz, ClassLoader classLoader) {
    Method[] methods = clazz.getDeclaredMethods();
    MethodReplace methodReplace;
    String clz;
    String meth;
    for (Method method : methods) {
        methodReplace = method.getAnnotation(MethodReplace.class);
        if (methodReplace == null)
            continue;
        clz = methodReplace.clazz();
        meth = methodReplace.method();
        if (!isEmpty(clz) && !isEmpty(meth)) {
            //方法替換,參數(shù)分別是:類加載器、需要修復(fù)的類、修復(fù)好的方法、被修復(fù)的方法
            replaceMethod(classLoader, clz, meth, method);
        }
    }
}

MethodReplace注解的定義如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodReplace {
    String clazz();

    String method();
}

可以看到,這是一個(gè)運(yùn)行時(shí)的注解,只能夠使用在方法上面。注解中指定了類名以及方法名。

通過分析方法的調(diào)用鏈,replaceMethod方法最終會調(diào)用AndFix的靜態(tài)方法replaceMethod:

private static native void replaceMethod(Method dest, Method src);

可以看到這是一個(gè)native方法。

五、AndFix中C/C++層修復(fù)流程分析

我們找到andfix.cpp,找到了replaceMethod的實(shí)現(xiàn):

static void replaceMethod(JNIEnv* env, jclass clazz, jobject src, jobject dest) {
    if (isArt) {
        art_replaceMethod(env, src, dest);
    } else {
        dalvik_replaceMethod(env, src, dest);
    }
}

這里需要判斷當(dāng)前的虛擬機(jī)類型是dalvik還是art。在JNI初始化的時(shí)候,需要注冊虛擬機(jī)(方法替換的實(shí)質(zhì)就是通過Hook虛擬機(jī)層的一些流程實(shí)現(xiàn)的,下面將會介紹到):

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        return -1;
    }
    assert(env != NULL);

    if (!registerNatives(env)) { //注冊
        return -1;
    }
    
    //需要返回JNI 1.4以上的版本
    result = JNI_VERSION_1_4;

    return result;
}

其中注冊虛擬機(jī)的時(shí)候,調(diào)用了registerNatives,最終調(diào)用registerNativeMethods方法,進(jìn)行native方法的鏈接(注冊與一些與類關(guān)聯(lián)的native方法,向虛擬機(jī)進(jìn)行登記,這是JNI實(shí)現(xiàn)的另外一種方式,具體可以參考JNI相關(guān)文檔,這里不再深入):

static int registerNatives(JNIEnv* env) {
    if (!registerNativeMethods(env, JNIREG_CLASS, gMethods,
            sizeof(gMethods) / sizeof(gMethods[0])))
        return JNI_FALSE;

    return JNI_TRUE;
}

這里傳入的gMethods數(shù)組如下:

static JNINativeMethod gMethods[] = {
/* name, signature, funcPtr */
{ "setup", "(ZI)Z", (void*) setup }, { "replaceMethod",
        "(Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V",
        (void*) replaceMethod }, { "setFieldFlag",
        "(Ljava/lang/reflect/Field;)V", (void*) setFieldFlag }, };

gMethods實(shí)質(zhì)就是Andfix中的一些方法:setup、replaceMethod、setFieldFlag,會在JNI加載的時(shí)候調(diào)用,進(jìn)行初始化,下面主要看setup方法中的Java實(shí)現(xiàn):

public static boolean setup() {
    try {
        final String vmVersion = System.getProperty("java.vm.version");
        boolean isArt = vmVersion != null && vmVersion.startsWith("2");
        int apilevel = Build.VERSION.SDK_INT;
        return setup(isArt, apilevel);
    } catch (Exception e) {
        Log.e(TAG, "setup", e);
        return false;
    }
}

在Java代碼中,判斷了當(dāng)前虛擬機(jī)類型是dalvik還是art,獲取了API的等級。最終又調(diào)用了native層的setup方法,下面來看native層setup方法的實(shí)現(xiàn):

static jboolean setup(JNIEnv* env, jclass clazz, jboolean isart,
        jint apilevel) {
    isArt = isart;
    LOGD("vm is: %s , apilevel is: %i", (isArt ? "art" : "dalvik"),
            (int )apilevel);
    if (isArt) {
        return art_setup(env, (int) apilevel);
    } else {
        return dalvik_setup(env, (int) apilevel);
    }
}

具體的虛擬機(jī)注冊比較復(fù)雜,為了簡單起見,我們只分析一下dalvik虛擬機(jī)的初始化:

extern jboolean __attribute__ ((visibility ("hidden"))) dalvik_setup(
        JNIEnv* env, int apilevel) {
    //通過dlopen(該方法在系統(tǒng)頭文件dlfcn.h中)加載libdvm.so
    void* dvm_hand = dlopen("libdvm.so", RTLD_NOW);
    //進(jìn)行Hook
    if (dvm_hand) {
        dvmDecodeIndirectRef_fnPtr = dvm_dlsym(dvm_hand,
                apilevel > 10 ?
                        "_Z20dvmDecodeIndirectRefP6ThreadP8_jobject" :
                        "dvmDecodeIndirectRef");
        if (!dvmDecodeIndirectRef_fnPtr) {
            return JNI_FALSE;
        }
        dvmThreadSelf_fnPtr = dvm_dlsym(dvm_hand,
                apilevel > 10 ? "_Z13dvmThreadSelfv" : "dvmThreadSelf");
        if (!dvmThreadSelf_fnPtr) {
            return JNI_FALSE;
        }
        jclass clazz = env->FindClass("java/lang/reflect/Method");
        jClassMethod = env->GetMethodID(clazz, "getDeclaringClass",
                        "()Ljava/lang/Class;");

        return JNI_TRUE;
    } else {
        return JNI_FALSE;
    }
}

先來補(bǔ)充一些基本知識:

Android虛擬機(jī)初始化.png
  1. 如上圖所示,Android虛擬機(jī)是有別于Java原生的虛擬機(jī)的,它執(zhí)行的是dex文件而不是class文件。Android虛擬機(jī)分為dalvik虛擬機(jī)和art虛擬機(jī)。
  2. 虛擬機(jī)(進(jìn)程)啟動(dòng)的時(shí)候會加載一個(gè)很重要的動(dòng)態(tài)庫文件(libdalvik.so或者libart.so)。
  3. Java在虛擬機(jī)環(huán)境中執(zhí)行,每個(gè)Java方法都會對應(yīng)一個(gè)底層的函數(shù)指針,當(dāng)Java方法被調(diào)用的時(shí)候,實(shí)質(zhì)虛擬機(jī)會找到這個(gè)函數(shù)指針然后去執(zhí)行底層的方法,從而Java方法被執(zhí)行。

我們回到虛擬機(jī)初始化的分析中來,dalvik_setup方法主要做了兩個(gè)步驟:

  1. 通過調(diào)用dlopen(該方法在系統(tǒng)頭文件dlfcn.h中)加載libdvm.so(這個(gè)so在APP進(jìn)程初始化的時(shí)候會加載),這個(gè)加載是為了下一步的Hook做準(zhǔn)備。
  2. 加載完libdvm.so之后,就可以進(jìn)行Hook了。在API10以上、以下,Java方法調(diào)用的時(shí)候會執(zhí)行不同的底層的系統(tǒng)函數(shù),因此必須Hook不同的系統(tǒng)函數(shù)才會有效。Hook成功以后,在這些系統(tǒng)函數(shù)調(diào)用的時(shí)候,就會調(diào)用我們自己的代碼,進(jìn)行替換。

我們在loadPatch的時(shí)候,最終會調(diào)用AndFixManager的fix方法,根據(jù)一系列的調(diào)用鏈,最終會調(diào)用dalvik_replaceMethod或者art_replaceMethod。下面繼續(xù)以dalvik虛擬機(jī)為例,繼續(xù)來看dalvik_replaceMethod方法的實(shí)現(xiàn):

extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
        JNIEnv* env, jobject src, jobject dest) {
    jobject clazz = env->CallObjectMethod(dest, jClassMethod);

    //我們可以看到剛剛我們Hook成功的兩個(gè)函數(shù)
    ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(dvmThreadSelf_fnPtr(), clazz);
    clz->status = CLASS_INITIALIZED;

    //進(jìn)行方法替換
    Method* meth = (Method*) env->FromReflectedMethod(src);
    Method* target = (Method*) env->FromReflectedMethod(dest);
    LOGD("dalvikMethod: %s", meth->name);

//  meth->clazz = target->clazz;
    meth->accessFlags |= ACC_PUBLIC;
    meth->methodIndex = target->methodIndex;
    meth->jniArgInfo = target->jniArgInfo;
    meth->registersSize = target->registersSize;
    meth->outsSize = target->outsSize;
    meth->insSize = target->insSize;

    meth->prototype = target->prototype;
    meth->insns = target->insns;
    meth->nativeFunc = target->nativeFunc;
}

這就是AndFix的核心代碼了,當(dāng)BUG方法被底層系統(tǒng)函數(shù)調(diào)用的時(shí)候,我們的Hook的鉤子函數(shù)就會調(diào)用,然后就是進(jìn)行有BUG與無BUG的Java方法的所有成員的變量替換,達(dá)到一個(gè)貍貓換太子的目的。

總結(jié)一句話就是,通過Hook系統(tǒng)的底層函數(shù),在我們有BUG的Java方法被調(diào)用的時(shí)候,通過一句“刀下留人”,然后貍貓換太子一樣,調(diào)用我們已經(jīng)修復(fù)好的方法。

結(jié)束語

關(guān)于AndFix熱修復(fù)的使用與分析就到這里了,有一些東西可能解析得不是特別清楚,畢竟這些玩意還是非常深入的,對于我們一般的開發(fā)者來說,會使用一些常見的熱修復(fù)框架即可,無需太過深入。深入分析源碼通常來說只是為了我們更好去使用而已。

如果覺得我的文字對你有所幫助的話,歡迎關(guān)注我的公眾號:

公眾號:Android開發(fā)進(jìn)階

我的群歡迎大家進(jìn)來探討各種技術(shù)與非技術(shù)的話題,有興趣的朋友們加我私人微信huannan88,我拉你進(jìn)群交(♂)流(♀)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容