搞懂android如何加载程序


android使用的是Dalvik(4.4之前包括4.4)和ART虚拟机(4.4之后包括4.4),虚拟机运行dex格式的应用程序,dex是class优化后的产物,区别于java虚拟机直接运行class格式的应用程序,由于一个dex文件可以包含若干个类,因此它就可以将各个类中重复的字符串和其它常数只保存一次,从而节省了空间,这样就适合在内存和处理器速度有限的手机系统中使用,当虚拟机要运行程序时,首先要将对应的程序文件加载到内存中,那虚拟机是如何加载程序文件的?使用类加载器!如图:

android类加载器

android使用PathClassLoader.java或DexClassLoader.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 splitDexPath(String path) {    return splitPaths(path, null, false);}
private static ArrayList splitPaths(String path1, String path2,        boolean wantDirectories) {    ArrayList result = new ArrayList();    splitAndAdd(path1, wantDirectories, result);    splitAndAdd(path2, wantDirectories, result);    return result;}
private static void splitAndAdd(String path, boolean wantDirectories,        ArrayList 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格式的文件列表,这个列表存储的就是要加载的dex文件。介绍完参数,我们看下函数makeDexElements:

private static Element[] makeDexElements(ArrayList files,        File optimizedDirectory) {    ArrayList elements = new ArrayList();    /*     * 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 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 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 suppressedExceptions = new ArrayList();        ShareReflectUtil.expandFieldArray(dexPathList, "dexElements",             makePathElements(dexPathList,            new ArrayList(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 files,             File optimizedDirectory,            ArrayList 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 混合编译与对热补丁影响解析

更多相关文章

  1. 一款常用的 Squid 日志分析工具
  2. GitHub 标星 8K+!一款开源替代 ls 的工具你值得拥有!
  3. RHEL 6 下 DHCP+TFTP+FTP+PXE+Kickstart 实现无人值守安装
  4. Linux 环境下实战 Rsync 备份工具及配置 rsync+inotify 实时同步
  5. Jack 服务编译问题 Android(安卓)7.0
  6. Android开发实践:JNI函数签名生成器
  7. Android(安卓)2.1 中 JNI 层 camera 的应用
  8. Android(安卓)Zxing集成
  9. Android(安卓)实现布局动态加载

随机推荐

  1. android工具之TraceView学习笔记
  2. Android(安卓)应用语言设置的实现
  3. Android之glide加载图片圆角效果
  4. Android多点触控(图片的缩放Demo)
  5. Android(安卓)VelocityTracker的使用
  6. Android(安卓)发展史
  7. Android通过共享用户ID来实现多Activity
  8. Android(安卓)深入理解 View 的绘制流程
  9. Android学习笔记1-Activity的Lifecycle和L
  10. android 获取路径目录方法以及判断目录是