自己動手做android熱更新框架

搞懂android如何加載程序


android使用的是Dalvik(4.4之前包括4.4)和ART虛擬機(4.4之后包括4.4),虛擬機運行dex格式的應用程序,dex是class優化后的產物,區別于java虛擬機直接運行class格式的應用程序,由于一個dex文件可以包含若干個類,因此它就可以將各個類中重復的字符串和其它常數只保存一次,從而節省了空間,這樣就適合在內存和處理器速度有限的手機系統中使用,當虛擬機要運行程序時,首先要將對應的程序文件加載到內存中,那虛擬機是如何加載程序文件的?使用類加載器!如圖:

android類加載器

android使用PathClassLoader.javaDexClassLoader.java這兩個類加載器,下面讓我們詳細了解一下它們。

DexClassLoader.java


public class DexClassLoader extends BaseDexClassLoader {

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

}

拿到代碼,我們看到這個類其實很簡單,它繼承于BaseDexClassLoader.java,有一個構造函數。所以我們先通過注釋來了解一下這個類的作用。

看下整個類的注釋,分為三段:

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文件(內部為dex文件)中加載類文件,可以用來執行那些沒有被安裝在應用中的代碼。

This class loader requires an application-private, writable directory to cache optimized classes. Use {@code Context.getDir(String, int)} to create such a directory: {@code File dexOutputDir = context.getDir("dex", 0);}

  • 這個類加載器需要一個應用私有的,可寫的目錄去緩存那些優化過的類文件,可以使用Context.getDir(String, int)去創建這樣的一個目錄。

Do not cache optimized classes on external storage. External storage does not provide access controls necessary to protect your application from code injection attacks.

  • 不要在外部存儲緩存那些優化過的類文件,外部存儲不提供必要的訪問控制,不能保護你的應用來自注入代碼的攻擊。

讀到這里,我們知道了這是一個類加載器,可用來加載那些還沒有被安裝到應用中的代碼,并且告訴我們使用這個類加載器時,要用到一個私有的可寫目錄,并警告這個目錄不能是外部存儲。

再來看下構造方法的注釋,分為三段:

Creates a {@code DexClassLoader} that finds interpreted and native code. Interpreted classes are found in a set of DEX files contained in Jar or APK files.

  • 創建DexClassLoader類加載器,加載器可以從jar文件和apk文件中得到一組dex文件。

The path lists are separated using the character specified by the {@code path.separator} system property, which defaults to {@code :}.

  • 文件路徑列表是由冒號“:”這個系統屬性字符隔開構成的,就是說第一個參數dexPath是一組dex的文件路徑,通過冒號分隔開。

@param dexPath the list of jar/apk files containing classes and resources, delimited by {@code File.pathSeparator}, which defaults to {@code ":"} on Android

@param optimizedDirectory directory where optimized dex files should be written; must not be {@code null}

@param libraryPath the list of directories containing native libraries, delimited by {@code File.pathSeparator}; may be {@code null}

@param parent the parent class loader

  • 參數dexPath,是一組jar/apk文件(內部包含類文件和資源,可以認為就是dex文件),由冒號隔開,這個正式上面那一條所說的,支持一次加載多個dex文件。

  • 參數optimizedDirectory,存入經過優化的dex文件的目錄,這個目錄不能為null。

  • 參數libraryPath,存放本地庫的目錄列表,由分隔符隔開,可以為null。

  • 參數parent,父類加載器。

PathClassLoader.java


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

}

這個類也是比較簡單,繼承于BaseDexClassLoader.java,有兩個構造函數。

同樣我們看下這個類的注釋:

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).

  • 提供了一個簡單的ClassLoader實現,運行本地的文件系統中的文件或者目錄列表程序,不支持網絡加載類文件,Android在系統中使用這個類加載程序。

構造函數我們就不看了,相關參數解釋在DexClassLoader.java的構造函數中已有說明。

選擇


看過了兩個不同類型的類加載器,通過其注釋我們可以明確,熱更新就是用DexClassLoader.java來實現,因為DexClassLoader.java針對沒有安裝的程序,而PathClassLoader.java針對已經安裝的程序。熱更新正是要運行那些,還沒有被安裝的程序文件。

BaseDexClassLoader.java


DexClassLoader.java是我們需要的類加載器,所以繼續深入了解下它的代碼,直接看它的父類BaseDexClassLoader.java。這里我們只需要搞懂三個地方:

  • 成員變量pathList
private final DexPathList pathList;
  • 構造方法BaseDexClassLoader

構造函數BaseDexClassLoader主要干了一件事,初始化成員變量pathList:

this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
  • 函數findClass

函數findClass是類加載器的核心方法,前面我們提到虛擬機使用類加載器加載程序文件,findClass正是這個加載過程的實現,類加載器通過findClass可以找到所有的類文件。我們繼續看下findClass函數內部,其中主要功能代碼就這句:

Class clazz = pathList.findClass(name)。

到此我們可以知道,所有的線索都指向了成員變量pathList,所以,要搞懂類加載器具體怎么加載類,就需要看DexPathList.java

DexPathList.java


首先我們看DexPathList.java的構造函數,篩掉里面的一些判斷邏輯和異常處理邏輯后,其主要代碼只有兩句:

this.definingContext = definingContext;
this.dexElements = makeDexElements(splitDexPath(dexPath),optimizedDirectory);

第一句是類加載器本身的賦值,為了得到類加載本身。

第二句我們先看下makeDexElements函數的各個參數:optimizedDirectory很顯然是類加載器初始化時候的優化目錄,這個參數不做過多解釋,重點看下splitDexPath(dexPath)參數,我們找到splitDexPath函數:

private static ArrayList<File> splitDexPath(String path) {
    return splitPaths(path, null, false);
}
private static ArrayList<File> splitPaths(String path1, String path2,
        boolean wantDirectories) {
    ArrayList<File> result = new ArrayList<File>();
    splitAndAdd(path1, wantDirectories, result);
    splitAndAdd(path2, wantDirectories, result);
    return result;
}
private static void splitAndAdd(String path, boolean wantDirectories,
        ArrayList<File> resultList) {
    if (path == null) {
        return;
    }

    String[] strings = path.split(Pattern.quote(File.pathSeparator));

    for (String s : strings) {
        File file = new File(s);

        if (! (file.exists() && file.canRead())) {
            continue;
        }

        /*
        * Note: There are other entities in filesystems than
        * regular files and directories.
        */
        if (wantDirectories) {
            if (!file.isDirectory()) {
                continue;
            }
        } else {
            if (!file.isFile()) {
                continue;
            }
        }

        resultList.add(file);
    }
}

通過跟蹤幾個層級的調用,我們可以知道,這個函數最終是為了得到拆分(以冒號拆分)dexPath后存儲成ArrayList<File>格式的文件列表,這個列表存儲的就是要加載的dex文件。介紹完參數,我們看下函數makeDexElements:

private static Element[] makeDexElements(ArrayList<File> files,
        File optimizedDirectory) {
    ArrayList<Element> elements = new ArrayList<Element>();

    /*
     * Open all files and load the (direct or contained) dex files
     * up front.
     */
    for (File file : files) {
        ZipFile zip = null;
        DexFile dex = null;
        String name = file.getName();

        if (name.endsWith(DEX_SUFFIX)) {
            // Raw dex file (not inside a zip/jar).
            try {
                dex = loadDexFile(file, optimizedDirectory);
            } catch (IOException ex) {
                System.logE("Unable to load dex file: " + file, ex);
            }
        } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                || name.endsWith(ZIP_SUFFIX)) {
            try {
                zip = new ZipFile(file);
            } catch (IOException ex) {
                /*
                 * Note: ZipException (a subclass of IOException)
                 * might get thrown by the ZipFile constructor
                 * (e.g. if the file isn't actually a zip/jar
                 * file).
                 */
                System.logE("Unable to open zip file: " + file, ex);
            }

            try {
                dex = loadDexFile(file, optimizedDirectory);
            } catch (IOException ignored) {
                /*
                 * IOException might get thrown "legitimately" by
                 * the DexFile constructor if the zip file turns
                 * out to be resource-only (that is, no
                 * classes.dex file in it). Safe to just ignore
                 * the exception here, and let dex == null.
                 */
            }
        } else {
            System.logW("Unknown file type for: " + file);
        }

        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, zip, dex));
        }
    }

    return elements.toArray(new Element[elements.size()]);
}

因為類加載器的設計不僅支持dex文件格式,還支持jar,apk,zip這些壓縮文件格式,所以makeDexElements這里針對這個設計做了相應實現,通過區分文件列表中各自的文件格式,做了相應不同的處理,這里我們只看dex格式的實現(其他壓縮文件無非是多了一層解壓操作,虛擬機只認dex文件,所以這些壓縮文件里放的也是dex文件,具體讀者可自行繼續了解):

if (name.endsWith(DEX_SUFFIX)) {
    // Raw dex file (not inside a zip/jar).
    try {
        dex = loadDexFile(file, optimizedDirectory);
    } catch (IOException ex) {
        System.logE("Unable to load dex file: " + file, ex);
    }
}

我們可以看到,通過loadDexFile函數得到DexFile格式文件,查看loadDexFile函數:

private static DexFile loadDexFile(File file, File optimizedDirectory)
        throws IOException {
    if (optimizedDirectory == null) {
        return new DexFile(file);
    } else {
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
}

得知用的是DexFile.loadDex方法,關于DexFile.java具體函數實現,我們一會再看,我們繼續看函數makeDexElements,獲取完dex后,new了Element對象,并將Element對象添加到列表中,經過對傳入dex文件列表的循環,最終我們得到了一個Element數組。

其實這個函數的整個過程可以概括為,將dex文件列表轉換成類加載器可操作的Element數組。

回看一下BaseDexClassLoader.java的findClass函數:

protected Class<?> findClass(String name) throws ClassNotFoundException {
    Class clazz = pathList.findClass(name);
    if (clazz == null) {
        throw new ClassNotFoundException(name);
    }
    return clazz;
}

追蹤到DexPathList.java中:

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

其實就是遍歷Element數組,依賴DexFile.java的loadClassBinaryName函數,查找需要的類。

我們發現,當遍歷Element數組,一旦找到需要的類后,就停止遍歷,不再對Element數組后面的文件進行查找,這正是我們做熱更新方案的關鍵點!利用這個點,如圖:

熱更新方案

我們先將修改后的class文件,打成dex補丁包,利用類反射的方式,修改Element數組,把修改過的dex補丁包放在數組最前面,這樣一旦找到修改后的class,就不再會去找后面那個有問題的class,從而實現了bug修改!

DexFile.java


上面提到了DexFile.java的兩個方法,這里稍作介紹:

  • loadDex

這個函數其實最終調用的是openDexFile函數,此方法為native方法。

  • loadClassBinaryName

這個函數其實最終調用的是defineClass函數,此方法為native方法。

這塊我們暫且追蹤到這里,有興趣的可以繼續深入看下。

熱更新框架ShotFix的實現


通過上面的分析,我們明確了熱更新方案的原理,下面具體實現一下(附上demo地址https://github.com/sarlmoclen/ShotFixDemo)。

demo中,把加載補丁放在了Application的onCreate中:

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        ShotFix.hotFix(MyApplication.this);
    }

}

打開ShotFix.java,我們來看hotFix這個函數:

/**
 * 熱更新
 */
public static void hotFix(Context context) {
    if (context == null) {
        return;
    }
    File fileDexDirectory = new File(getDexDirectory(context));
    if(!fileDexDirectory.exists()){
        fileDexDirectory.mkdirs();
        return;
    }
    File[] listDexFiles = fileDexDirectory.listFiles();
    String dexPath = null;
    for (File file : listDexFiles) {
        if (file.getName().endsWith(DEX_SUFFIX)) {
            dexPath = file.getAbsolutePath() + ":";
        }
    }
    if(TextUtils.isEmpty(dexPath)){
        return;
    }
    if (Build.VERSION.SDK_INT >= 14) {
        loadDex(context, dexPath);
    } 
}

首先指定了外部存儲Android/data/包名/files/dex_directory(選此目錄不需要用戶授權)這個文件夾為我們存放補丁dex文件的目錄,然后從這個目錄中篩選出后綴名為.dex的文件,獲取這些文件的路徑,用冒號:隔離拼接起來得到dexPath。之后做了一個版本控制,目前只支持到4.0.3以上,小于此版本的系統可以忽略不計了,當大于等于4.0.3版本時,調用loadDex函數:

/**
 * 加載dex
 */
private static void loadDex(Context context, String dexPath) {
    File fileOptimizedDirectory = new File(getOptimizedDirectory(context));
    if (!fileOptimizedDirectory.exists()) {
        fileOptimizedDirectory.mkdirs();
    }
    try {
        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
        DexClassLoader dexClassLoader = new DexClassLoader(
                dexPath,
                fileOptimizedDirectory.getAbsolutePath(),
                null,
                pathClassLoader
        );
        combineDex(pathClassLoader,dexClassLoader);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

loadDex函數,首先獲取了app本身的類加載器pathClassLoader和加載補丁的類加載器dexClassLoader。這里分別說明一下:

  • pathClassLoader為app本身的類加載器,通過這個類加載器,使用類反射方式可獲取到app原本的dexElements數組。

  • dexClassLoader為我們新建的類加載器,前面通過注釋了解到這個類加載器可用來加載還沒有安裝的程序文件,所以此處使用它來加載我們的補丁,加載后便可通過類反射方式,獲取補丁的dexElements數組。

得到兩個類加載器后,剩下的就是合并工作,調用combineDex函數:

/**
 * 合并dex
 */
private static void combineDex(PathClassLoader pathClassLoader, 
        DexClassLoader dexClassLoader)
        throws IllegalAccessException, NoSuchFieldException {
    Object[] pathDexElements = getDexElements(getPathList(pathClassLoader));
    Object[] dexDexElements = getDexElements(getPathList(dexClassLoader));
    Object[] combined = combineElements(dexDexElements, pathDexElements);
    setDexElements(getPathList(pathClassLoader),combined);
}

首先利用類反射方式,獲取這兩個類加載器各自的dexElements,調用combineElements函數,按照上面講的熱修復原理,合并生成新的dexElements:

/**
 * 數組合并
 */
private static Object[] combineElements(Object[] dexDexElements, 
        Object[] pathDexElements) {
    Object[] combined = (Object[]) Array.newInstance(
            dexDexElements.getClass().getComponentType()
            , dexDexElements.length + pathDexElements.length);
    System.arraycopy(dexDexElements, 0, combined, 0, dexDexElements.length);
    System.arraycopy(pathDexElements, 0, combined, dexDexElements.length, 
        pathDexElements.length);
    return combined;
}

最后把app本身的dexElements數組修改為合并后的dexElements:

setDexElements(getPathList(pathClassLoader),combined);

到此修復過程完成。

測試


寫個測試demo,如下:

public class MainActivity extends AppCompatActivity {

    private TextView name;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        name = findViewById(R.id.name);
        name.setText("find bug");
    }

}

編譯安裝程序:

修改前的程序

這里認為顯示find bug是有問題的,需要修改為fix bug。首先我們修改錯誤的代碼:

public class MainActivity extends AppCompatActivity {

    private TextView name;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        name = findViewById(R.id.name);
        //delete wrong code
        //name.setText("find bug");
        //add right code
        name.setText("fix bug");
    }

}

然后重新編譯程序,點擊Bulid->Rebuild Project,我們可以得到編譯好的class文件:

class文件

如圖中,我們取從包名開始的目錄,內部只拿有修改的class文件,將這些放在新建的dex文件夾里:

dex存放目錄

使用android提供的dx工具,將class文件編譯為dex文件,命令如下:

  • dx --dex --output=D:\dex\classes_fix.dex D:\dex

此命令中D:\dex\classes_fix.dex為設置的生成文件(包括路徑和名稱),D:\dex為class文件的目錄。執行命令成功后:

生成dex文件

我們看到dex目錄下,出現了class_fix.dex文件,這就是我們要的補丁文件。把補丁文件拷貝到手機指定的存放目錄下:

dex文件

殺死程序重啟,看下效果:

修改后的程序

測試修復成功!

對比Tinker


閱讀Tinker的代碼,我們找見其加載dex邏輯在tinker-android-loader這個moudle中。
依次找見TinkerLoader.java中的:

 boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, 
        patchVersionDirectory, oatDex, resultIntent, isSystemOTA);

TinkerDexLoader.java中的:

 SystemClassLoaderAdder.installDexes(application,
         classLoader, optimizeDir, legalFiles);

最終找到加載dex邏輯為SystemClassLoaderAdder.java中的installDexes函數:

public static void installDexes(Application application, 
        PathClassLoader loader, File dexOptDir, List<File> files)
    throws Throwable {
    Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", 
        dex size:" + files.size());

    if (!files.isEmpty()) {
        files = createSortedAdditionalPathEntries(files);
        ClassLoader classLoader = loader;
        if (Build.VERSION.SDK_INT >= 24 && !checkIsProtectedApp(files)) {
            classLoader = AndroidNClassLoader.inject(loader, application);
        }
        //because in dalvik, if inner class is not the same 
        //classloader with it wrapper class.
        //it won't fail at dex2opt
        if (Build.VERSION.SDK_INT >= 23) {
            V23.install(classLoader, files, dexOptDir);
        } else if (Build.VERSION.SDK_INT >= 19) {
            V19.install(classLoader, files, dexOptDir);
        } else if (Build.VERSION.SDK_INT >= 14) {
            V14.install(classLoader, files, dexOptDir);
        } else {
            V4.install(classLoader, files, dexOptDir);
        }
        //install done
        sPatchDexCount = files.size();
        Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" 
            + sPatchDexCount);

        if (!checkDexInstall(classLoader)) {
            //reset patch dex
            SystemClassLoaderAdder.uninstallPatchDex(classLoader);
            throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
        }
    }
}

很明顯,Tinker支持了所有android版本,版本支持分了四個區間段去不同處理:V23(23到28),V19(19到22),V14(14到18),V4(1到13),先看下V23的處理:

private static final class V23 {

    private static void install(ClassLoader loader, 
            List<File> additionalClassPathEntries,
                                File optimizedDirectory)
            throws IllegalArgumentException, IllegalAccessException,
            NoSuchFieldException, InvocationTargetException, 
            NoSuchMethodException, IOException {
        /* The patched class loader is expected to be a descendant of
         * dalvik.system.BaseDexClassLoader. We modify its
         * dalvik.system.DexPathList pathList field to append additional DEX
         * file entries.
         */
        Field pathListField = ShareReflectUtil.findField(loader, "pathList");
        Object dexPathList = pathListField.get(loader);
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", 
            makePathElements(dexPathList,
            new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
            suppressedExceptions));
        if (suppressedExceptions.size() > 0) {
            for (IOException e : suppressedExceptions) {
                Log.w(TAG, "Exception in makePathElement", e);
                throw e;
            }

        }
    }

    /**
     * A wrapper around
     * {@code private static final dalvik.system.DexPathList#makePathElements}.
     */
    private static Object[] makePathElements(
            Object dexPathList, ArrayList<File> files, 
            File optimizedDirectory,
            ArrayList<IOException> suppressedExceptions)
            throws IllegalAccessException, InvocationTargetException, 
            NoSuchMethodException {

        Method makePathElements;
        try {
            makePathElements = ShareReflectUtil.findMethod(dexPathList, 
                "makePathElements", List.class, File.class,
                List.class);
        } catch (NoSuchMethodException e) {
            Log.e(TAG, "NoSuchMethodException: 
                makePathElements(List,File,List) failure");
            try {
                makePathElements = ShareReflectUtil.findMethod(dexPathList,
                 "makePathElements", ArrayList.class, File.class, ArrayList.class);
            } catch (NoSuchMethodException e1) {
                Log.e(TAG, "NoSuchMethodException: 
                    makeDexElements(ArrayList,File,ArrayList) failure");
                try {
                    Log.e(TAG, "NoSuchMethodException: try use v19 instead");
                    return V19.makeDexElements(dexPathList, 
                        files, optimizedDirectory, suppressedExceptions);
                } catch (NoSuchMethodException e2) {
                    Log.e(TAG, "NoSuchMethodException: 
                        makeDexElements(List,File,List) failure");
                    throw e2;
                }
            }
        }

        return (Object[]) makePathElements.invoke(dexPathList,
             files, optimizedDirectory, suppressedExceptions);
    }
}

由代碼可知,在函數install中,首先根據:

Field pathListField = ShareReflectUtil.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);

得到了pathClassLoader中的pathList對象,然后利用makePathElements函數,通過傳入的pathList對象參數,補丁dex文件列表參數,優化目錄參數,異常捕獲列表參數,得到了補丁的dexElements數組,其原理是依靠 DexPathList.java內部函數makePathElements功能,生成補丁的dexElements數組。最終調用ShareReflectUtil.java的expandFieldArray函數:

public static void expandFieldArray(Object instance, String fieldName, 
        Object[] extraElements)
    throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
    Field jlrField = findField(instance, fieldName);

    Object[] original = (Object[]) jlrField.get(instance);
    Object[] combined = (Object[]) Array.newInstance(original.getClass()
        .getComponentType(), original.length + extraElements.length);

    // NOTE: changed to copy extraElements first, for patch load first

    System.arraycopy(extraElements, 0, combined, 0, extraElements.length);
    System.arraycopy(original, 0, combined, extraElements.length, original.length);

    jlrField.set(instance, combined);
}

合成app本身的dexElements數組和補丁的dexElements數組。

其他版本的處理類似,有興趣的可以自己閱讀下,我們發現Tinker的基本思路跟本文所講的一樣,唯獨區別是獲取補丁dexElements數組的方式,目前還沒有搞懂Tinker為什么沒有選擇本文所講的獲取補丁dexElements數組的方式,讀者可以思考一下。

這里作者閱讀過androidV14到V28的源碼,類加載的代碼框架沒有很大的變化,pathList和dexElements每個版本都存在,所以應該能支持V14到V28的系統,因為手機數量有限,只測試了V23的手機,沒有問題,如果大家有測試到問題,可反饋到評論中。

Dalvik和ART下的熱更新問題


android4.4之前使用Dalvik虛擬機,4.4之后ART虛擬機,4.4可切換Dalvik和ART。簡單講述下這倆個虛擬機區別:

  • Dalvik:

使用Just in Time技術(JIT即時編譯),安裝app時,opt工具會把dex文件優化成odex文件,每次運行app時,會解釋odex生成本機機器碼再執行。

  • ART:

android N 之前使用Ahead of Time技術 (AOT預編譯),安裝app時,會把dex解釋成oat本地機器碼,app運行時直接執行機器碼。

android N開始包括之后使用混合編譯

很明顯運行app時,ART較Dalvik更快,但這樣導致安裝時間加長,且安裝占用內存更多,即便如此,app運行更加流暢也是值得的!那關于熱更新,這兩個虛擬機又有哪些地方需要我們注意?如下:

  • Dalvik的CLASS_ISPREVERIFIED問題

我們知道android有65536問題,每個dex文件的方法數不能超過65536,當一個app的代碼越來越多,方法數超過65536,就需要使用分包技術,最終編譯出來的app就會包含多個dex文件。

那CLASS_ISPREVERIFIED是什么呢?其字面意思:類是否預先驗證,說白了這是一個是否驗證的標志。app在安裝時opt工具會把dex文件優化成odex文件,即此時app會被執行dexopt操作,其中就有這樣的邏輯:當同一個dex文件內類A的方法調用類B的方法,就會給類A打上CLASS_ISPREVERIFIED的標志,被打上這個標記的類不能引用其他dex中的類,否則就會報錯,這就是CLASS_ISPREVERIFIED問題。很顯然,我們的補丁如果要修改類B中的問題,因為補丁是一個單獨dex文件,所以就會觸發CLASS_ISPREVERIFIED問題。

我們實踐一下,這里我找了一個android4.3系統的手機,繼續修改demo的代碼,新建類ClassIsPreverifiedTest.java(作為類B):

public class ClassIsPreverifiedTest {

    private static final String TAG = "ClassIsPreverifiedTest";

    public void log(){
        Log.i(TAG,"find bug");
    }

}

我們用類MainActivity.java(作為類A)調用ClassIsPreverifiedTest.java的函數:

public class MainActivity extends AppCompatActivity {

    private TextView name;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        name = findViewById(R.id.name);
        //delete wrong code
        name.setText("find bug");
        //add right code
        //name.setText("fix bug");
        ClassIsPreverifiedTest classIsPreverifiedTest = new ClassIsPreverifiedTest();
        classIsPreverifiedTest.log();
    }

}

運行程序:

com.sarlmoclen.demo I/ClassIsPreverifiedTest: find bug

我們約定“find bug”為有問題,“fix bug”為解決問題,這里把程序修改成“fix bug”:

public class ClassIsPreverifiedTest {

    private static final String TAG = "ClassIsPreverifiedTest";

    public void log(){
        //delete wrong code
        //Log.i(TAG,"find bug");
        //add right code
        Log.i(TAG,"fix bug");
    }

}

按照上面講的方式生成補丁,并把補丁放到手機指定文件夾中,重新打開程序:

com.sarlmoclen.demo E/AndroidRuntime: FATAL EXCEPTION: main
    java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
        at com.sarlmoclen.demo.MainActivity.onCreate(MainActivity.java:20)
        at android.app.Activity.performCreate(Activity.java:5372)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1104)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2270)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2362)
        at android.app.ActivityThread.access$700(ActivityThread.java:168)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1329)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:137)
        at android.app.ActivityThread.main(ActivityThread.java:5493)
        at java.lang.reflect.Method.invokeNative(Native Method)
        at java.lang.reflect.Method.invoke(Method.java:525)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1209)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1025)
        at dalvik.system.NativeStart.main(Native Method)

程序掛掉了,報錯Class ref in pre-verified class resolved to unexpected implementation,這就是CLASS_ISPREVERIFIED問題。

  • ART的內存地址錯亂問題

ART虛擬機下,dex文件最終會編譯成本地機器碼,在dex2oat時已經將各個類的地址寫死,若補丁包中的類出現字段或者方法的修改,會出現內存地址錯亂。

未完待續...

參考文章


Android N 混合使用 AOT 編譯,解釋和 JIT 三種運行時
Android N 混合編譯與對熱補丁影響解析

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容