android进阶篇01、Android类加载机制与Dex文件简介

1,294 阅读10分钟

一、ART与Dalvik虚拟机

1、java虚拟机与Dalvik虚拟机

Dalvik虚拟机是安卓早期版本的虚拟机,每一个应用程序对应一个单独的Dalvik虚拟机,这种设计的好处是当一个进程的虚拟机挂掉不会影响其他进程;java虚拟机中执行的是class文件,而Dalvik虚拟机执行的是dex文件;Java虚拟机是基于栈的虚拟机,而Dalvik虚拟机是基于寄存器的虚拟机;

基于栈的虚拟机:

每个线程运行时都会创建一个线程独享的栈空间,方法调用时会将代表方法的栈帧在此栈中入栈和出栈,并且栈帧中的操作都是在操作数栈中进行的;

基于寄存器的虚拟机:

基于寄存器的虚拟机与基于栈的虚拟机最大的不同之处就是操作不在操作数栈上进行了,而是在一个虚拟寄存器中运行,这些寄存器也存在栈中,本质就是数组,用来暂存指令、数据和地址;基于寄存器的虚拟机指令操作明显变少了,省去了很多移动操作,但其指令也相对复杂了;

2、ART与Dalvik

最明显的区别就是Dalvik支持JIT(just in time)即时编译,这个功能会将一些热点代码进行提前编译或者编译成本机机器码;Dalvik在安装应用的时候将dex提前编译odex文件;而ART支持AOT(ahead of time)提前编译,在安装应用时ART使用设备自带的dex2oat工具来编译应用,dex中的字节码将被编译成本地机器码,这种方式的缺点就是导致apk安装很慢,因为需要多执行一步编译成本地机器码的操作;

从android7开始取消了安装编译成机器码的操作,在执行过程中进行JIT操作,并将信息存入配置文件中,在设备闲置并且充电状态下,会将配置文件中记录的信息进行AOT编译,待下次运行时直接使用;

二、Android类加载器ClassLoader

classloader的作用简单来说就是加载class文件,提供给程序运行时使用,每个class文件内部都有一个ClassLoader字段用来标识自己是由哪个类加载器加载的;

Android中使用的主要类加载器继承结构如下所示:

ClassLoader 抽象类,类加载器的父类
    BootClassLoader ClassLoader的内部类,继承自ClassLoader,主要用来加载AndroidFramework中的类;
    BaseDexClassLoader
        PathClassLoader Android应用程序类加载器,我们项目中用到的除了系统中的类一般用这个加载器进行加载,可以用来加载指定dex,以及jar、zip、apk中的dex文件;
        DexCLassLoader 用来加载指定dex,以及jar、zip、apk中的dex文件,是一个额外提供的动态类加载器;

PathClassLoader和DexCLassLoader共同父类是BaseDexClassLoader,只不过创建DexClassLoader需要传递一个optimizedDirectory参数,这个参数用来确定odex的目录,不过这个参数在api26也已经被废弃了;创建PathClassLoader时这个参数传为null,表示使用默认的路径,为:/data/dalvik-cache;这样其实使用PathClassLoader和DexClassLoader就没什么区别了;

三、双亲委托机制与类加载流程分析

1、当我们加载一个类,会去调用CLassLoader中的loadClass方法:

protected Class<?> loadClass(String name, boolean resolve) {
    // First, check if the class has already been loaded
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        }
        if (c == null) {                   
            c = findClass(name);
        }
    }
    return c;
}

2、在loadClass方法中,首先会去检查是否已经加载过,如果加载过c不为空则直接返回;如果没有加载过先去调用parent的loadclass方法,这样就会递归调用父加载器(父加载器是在创建加载器的时候作为参数传进来的),如果parent为空,就会去调用BootClassLoader;如果在所有父加载器中都没有成功加载,才会调用自己的findclass方法自己加载;

protected Class<?> findClass(String name) throws ClassNotFoundException {
    Class c = pathList.findClass(name, suppressedExceptions);        
    return c;
}

3、ClassLoader类中的findClass是一个空实现,实际调用的是BaseDexClassLoader中的findClass方法,我们在方法中可以看实现非常简单,就是调用了DexPathList的findClass方法:

public Class<?> findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        Class<?> clazz = element.findClass(name, definingContext, suppressed);
        if (clazz != null) {
            return clazz;
        }
    }
    return null;
}

4、在这个方法中,会去循环遍历dexElements,然后调用Element的findClass方法;那么这个dexElements是啥呢?在DexPathList构造方法中对其进行了赋值:

public DexPathList(ClassLoader definingContext, String dexPath,
        String librarySearchPath, File optimizedDirectory) {
    // save dexPath for BaseDexClassLoader
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                       suppressedExceptions, definingContext);
}

5、splitDexPath --> splitPaths ,会返回一个List;

private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
    List<File> result = new ArrayList<>();
    if (searchPath != null) {
        for (String path : searchPath.split(File.pathSeparator)) {
            if (directoriesOnly) {
                try {
                    StructStat sb = Libcore.os.stat(path);
                    if (!S_ISDIR(sb.st_mode)) {
                        continue;
                    }
                } catch (ErrnoException ignored) {
                    continue;
                }
            }
            result.add(new File(path));
        }
    }
    return result;
}

6、makeDexElements方法会返回一个Element数组,可以看到在此方法中通过File构建DexFile,然后通过DexFile构建Element,最后将Elements数组返回:

private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
        List<IOException> suppressedExceptions, ClassLoader loader) {
  Element[] elements = new Element[files.size()];
  int elementsPos = 0;
  /*
   * Open all files and load the (direct or contained) dex files up front.
   */
  for (File file : files) {
      if (file.isDirectory()) {
          // We support directories for looking up resources. Looking up resources in
          // directories is useful for running libcore tests.
          elements[elementsPos++] = new Element(file);
      } else if (file.isFile()) {
          String name = file.getName();

          if (name.endsWith(DEX_SUFFIX)) {
              // Raw dex file (not inside a zip/jar).
              try {
                  DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
                  if (dex != null) {
                      elements[elementsPos++] = new Element(dex, null);
                  }
              } catch (IOException suppressed) {
                  System.logE("Unable to load dex file: " + file, suppressed);
                  suppressedExceptions.add(suppressed);
              }
          } else {
              DexFile dex = null;
              try {
                  dex = loadDexFile(file, optimizedDirectory, loader, elements);
              } catch (IOException suppressed) {
                  /*
                   * 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).
                   * Let dex == null and hang on to the exception to add to the tea-leaves for
                   * when findClass returns null.
                   */
                  suppressedExceptions.add(suppressed);
              }

              if (dex == null) {
                  elements[elementsPos++] = new Element(file);
              } else {
                  elements[elementsPos++] = new Element(dex, file);
              }
          }
      } else {
          System.logW("ClassLoader referenced unknown path: " + file);
      }
  }
  if (elementsPos != elements.length) {
      elements = Arrays.copyOf(elements, elementsPos);
  }
  return elements;
}

7、现在我们知道了dexElements是啥了,我们回到步骤3,调用了Element的findClass

public Class<?> findClass(String name, ClassLoader definingContext,
            List<Throwable> suppressed) {
    return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
            : null;
}

8、然后调用DexFile的loadClassBinaryName方法:

public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
    return defineClass(name, loader, mCookie, this, suppressed);
}

9、接着会调用defineClass:

private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                 DexFile dexFile, List<Throwable> suppressed) {
    Class result = null;
    try {
        result = defineClassNative(name, loader, cookie, dexFile);
    } catch (NoClassDefFoundError e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    } catch (ClassNotFoundException e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    }
    return result;
}

10、最后调用到Native方法defineClassNative:

private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
DexFile dexFile) throws ClassNotFoundException, NoClassDefFoundError;

四、热修复思路策略

通过上面类加载机制的分析可知,Elements数组中存储的Element元素,Element元素又是通过DexFile构建的,DexFile只是对dex文件的一个封装而已;因此我们可以总结一种热修复的思路:

比如我们有个类存在bug,我们可以单独修改此类,修改完此类之后将此类打包成dex文件,然后通过此dex文件创建一个Element元素,再将新建的Element插入到Elements数组头部,我们可以通过反射修改以上操作,这样在加载elements数组时从前往后加载,就会将我们修改的类加载上去,从而不会加载后面有bug的类了,从而完成修复。

五、Dex文件简介

1、apk文件的反编译

apk的反编译指的是apk压缩文件解压,然后将里面的dex文件转换为jar包,然后通过可视化工具来查看jar包中的class文件;

2、apk文件加固的手段:

反模拟器、代码虚拟化、还有就是最常用的加密手段;

3、加密框架流程:

首先将我们apk文件中的源dex文件取出来,然后进行加密处理的新的源dex文件;然后自己创建壳dex文件,将壳dex文件和加密后的源dex文件以及apk包中的其他文件重新打包成新的apk文件,然后进行签名处理变成可执行的apk文件;

4、dex文件结构

dex文件是android系统的可执行文件,包含应用程序的全部操作指令和运行时数据,将原来每个class文件都有的信息合为一体,相比多个class文件来说减少了class的冗余;

Dex文件格式:

文件头
    header ,dex文件头部,记录整个dex文件的相关属性;例如魔数、签名、file_size、header_size等;
索引区
    string_ids 字符串数据索引,记录了每个字符串在数据区的偏移量
    type_ids 类型数据索引,记录了每个类型的字符串索引
    proto_ids 原型数据索引,记录了方法声明的字符串、返回类型的字符串和参数列表的字符串
    field_ids 字段数据索引,记录了所属类,类型以及名称
    method_ids 类方法索引,记录了方法所属类,方法声明以及方法名等信息
数据区
    class_defs 类定义数据索引,记录指定类各类信息,包含接口、超类、类数据偏移量;
    data 数据区,保存各个类的真实数据
    link_data 连接数据区

5、apk打包流程

1)、首先通过aapt工具将资源文件编译到R.java文件中,然后通过aidl工具将aidl文件转换为java的接口文件,最后将R文件、项目源代码和aidl接口文件通过java编译器编译成class文件;

2)、通过dex文件将编译后的class文件和第三方库文件一起编译成dex文件;

3)、将编译后的dex文件和资源文件以及其他文件比如sdk文件一起打包成apk文件;

4)、最后通过apksigner工具将打包后的apk文件进行签名;

6、双亲委托机制:

类加载器在加载类时,首先查找是否已经加载过此类,如果没有找到,就将加载任务交给父加载器,依次递归;如果父加载器可以完成加载任务,就返回;如果父加载器无法完成加载任务或者没有父加载器时才自己去加载;双亲委托主要有两个好处:一个是避免类的重复加载;二是防止核心api被篡改;

7、对称加密和非对称加密:

对称加密就是指加密和解密用同一个密钥;典型的加密算法有DES、RC4、AES等;

非对称加密就是指加密和解密使用不同的密钥,分别成为公钥和私钥;公钥机密的文件只有对应的私钥才能解密;同样的;私钥加密的文件只有对应的公钥才能解密;

六、增量更新

增量更新的关键在于增量一词,我们传统更新app的方式一般就是下载新的apk,然后进行安装;其实我们每次更新只是在上一个版本的基础上改了一小部分内容,因此我们可以下载一个代表新版本与老版本差异的差分包,然后利用此差分包与老版本apk合成新版本的apk,这样相比下载整个apk每次只需要下载一个差分包,而差分包一般都比整个apk小得多,这就是增量更新的过程;

增量更新两个比较重要的工具是bsdiff和bspatch:

bsdiff用于比较新旧apk生成差分包;

bspatch用于将旧apk和差分包合成新apk;

我们可以通过bsdiff.c和bspatch.c两个原文件生成bsdiff.exe和bspatch.exe两个可执行文件,这两个可执行文件可以直接在window的cmd窗口中使用,而我们在代码程序中可以使用bapatch.c原文件;

DexDiff

DexDiff是Tinker结合Dex文件格式设计的一个专门针对Dex的差分算法。根据Dex的文件格式,对两个Dex中每一项数据进行差分记录。