Android热修复技术 --- 类加载机制

Android热修复,在最近几年里已经不是什么新颖的技术了,很多公司都开始搞起了自己的热修复框架,最开始的像腾讯的Tinker,阿里的AndFix、Sophix,美团的Robust,想要自己实现一套热修复的框架,就需要了解其中的原理

类加载机制

  • 1 热修复概念
  • 2 热修复使用到的技术
  • 3 几种常见的热修复框架对比
    • 3.1 AndFix
    • 3.2 Robust
    • 3.3 Tinker
  • 4 类加载机制
    • 4.1 Android类加载器
    • 4.2 双亲委派
    • 4.3 dex加载
    • 4.4 从源码看PathClassLoader和DexClassLoader的异同

1 热修复概念

什么是热修复,就是对线上版本的静默更新。当发布线上之后,如果出现了严重的bug,通常需要重新发版来修复,但是频繁地发版显然不是最佳解决方案,正常的发版流程是2个月发布一个新的版本,那么如果在用户无感知的情况下,修复线上bug,避免频繁地发版,热修复就随之出现了。
Android热修复技术 --- 类加载机制_第1张图片
在开发端,通过Gradle插件生成补丁包,并上传到云端,客户端通过判断是否需要下载新的补丁包,并执行热修复

2 热修复使用到的技术

1 ClassLoader类加载机制
2 Dex动态加载技术 – hook反射
3 差分打包技术 – bsdiff
4 字节码插桩 – ASM Javassist
5 Gradle插件 – 发布差分包
6 so库的编译

3 几种常见的热修复框架对比

目前主流的热修复框架通常采用以下3种修复方式
1 native层hook Java层代码 bug fix
2 编译打包时字节码插桩
3 动态加载Dex文件,类加载技术

3.1 AndFix

Android热修复技术 --- 类加载机制_第2张图片
AndFix是通过native层hook java层的代码,通常在native层实现热修复是不需要重启修复,这是即时生效的;
例如方法B中有bug,那么需要通过热修复替代这个方法;我们知道,所有方法的调用,都会在JVM中入栈,执行完成之后 出栈,方法在JVM中是一个ArtMethod结构体,那么在JVM运行这个方法之前,在Native层完成这个方法的替换,那么就完成了热修复的工作,而且是即时生效的

3.2 Robust

Robust采用的技术是编译时字节码插桩技术,这个过程在gradle-plugin中发生,在编译打包阶段,对每个函数注入一段逻辑代码,通过判断是否执行插入的这段代码,这个过程也是即时生效的;

3.3 Tinker

Tinker采用的是Dex动态加载技术,通过反射的方式,将待修复的类放在dexElements数组的前面,在类加载的时候,首先加载这个待修复的类,因为类加载机制不会重复加载类,达到修复的目的,但这个方式是需要重启生效的(出现bug的类在ClassLoader中是不能替换的,存在缓存中,只能重启重新进行类加载)

以上3种方式是目前热修复常见的3种方式,其实各有利弊,像native层处理需要大量的开发成本,跟Robust一样,只能达到修复bug的目的,不能新增类和轻量级的功能;而Tinker则是需要重启才能生效

4 类加载机制

Android应用和Java类的加载机制基本一致,Java类将代码编译成class文件,JVM加载class文件;而Android多出的一步就是将class文件转换为dex文件,通过dalvik或者Art虚拟机加载,Android也有自己的类加载器

4.1 Android类加载器

Android热修复技术 --- 类加载机制_第3张图片
Android中有3个父类加载器,BootClassLoader、BaseDexClassLoader、URLClassLoader,其中BaseDexClassLoader有两个子类,PathClassLoader和DexClassLoader

PathClassLoader主要用于加载我们自己写的Java/Kotlin代码,只能加载已经安装的apk文件(data/app目录);而DexClassLoader则是能够加载指定目录的文件,除了apk,还有jar包,比PathClassLoader更灵活

在Android当中,使用最多的就是两个加载器,PathClassLoader(默认的加载器),BootClassLoader(PathClassLoader的父类加载器)
Android热修复技术 --- 类加载机制_第4张图片

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)


        findViewById<TextView>(R.id.tv_classloader).text = "${classLoader}"
        findViewById<TextView>(R.id.tv_super_classloader).text = "${classLoader.parent}"

    }
}

4.2 双亲委派

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
        // 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);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                c = findClass(name);
            }
        }
        return c;
}

Android类加载同样遵循双亲委派机制,当一个子类加载器加载这个类时,如果加载过了那么就直接返回字节码文件;如果没有加载过,首先会向父类请求是否加载过,如果加载过了那么就直接返回父类加载过的字节码文件;如果没有加载过,那么调用父类的loadClass,递归判断,
如果整个链路上都没有加载过,那么由当前类加载器调用findClass,从dex文件中找出并加载这个类

什么时候会触发类加载?
· new XX 创建一个类
· 当静态方法或者静态变量被调用时 Class.property
· 反射
· loadClass

误区:
这里有一个误区

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}

虽然PathClassLoader是继承自BaseDexClassLoader,但是PathClassLoader的父类加载器并不是BaseDexClassLoader,从前面的例子中也可以看到,它的父类是parent,这里不要认为父类就是父类加载器,这是两个概念

4.3 dex加载

PathClassLoader的构造方法都是调用父类的构造方法,去BaseDexClassLoader看下源码

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    Class c = pathList.findClass(name, suppressedExceptions);
    if (c == null) {
        ClassNotFoundException cnfe = new ClassNotFoundException(
                "Didn't find class \"" + name + "\" on path: " + pathList);
        for (Throwable t : suppressedExceptions) {
            cnfe.addSuppressed(t);
        }
        throw cnfe;
    }
    return c;
}

这里有一个类需要关注一下,pathList(DexPathList),是在BaseDexClassLoader的构造方法中完成了初始化

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
        String librarySearchPath, ClassLoader parent, boolean isTrusted) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);

    if (reporter != null) {
        reportClassLoaderChain();
    }
}

其中
dexPath:目标类所在的apk、dex或者jar文件的路径(SD卡也可以),这个路径可以是多个路径,使用分隔符:分开
librarySearchPath:加载程序文件时需要用到的so库的路径
parent:当前类加载器的父加载器

# DexPathList.java

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

在DexPathList的构造方法中,初始化了一个Element数组

# DexPathList.java / makeDexElements

private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
    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();
            DexFile dex = null;
            if (name.endsWith(DEX_SUFFIX)) { //如果是以 .dex为结尾
                // Raw dex file (not inside a zip/jar).
              try {
                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 {
              try {
                  dex = loadDexFile(file, optimizedDirectory, loader, elements);
              } catch (IOException suppressed) {

                  suppressedExceptions.add(suppressed);
              }

              if (dex == null) {
                  elements[elementsPos++] = new Element(file);
              } else {
                  elements[elementsPos++] = new Element(dex, file);
              }
          }
          if (dex != null && isTrusted) {
            dex.setTrusted();
          }
      } else {
          System.logW("ClassLoader referenced unknown path: " + file);
      }
  }
  //把剩余的文件拷贝到elements数组中
  if (elementsPos != elements.length) {
      elements = Arrays.copyOf(elements, elementsPos);
  }
  return elements;
}

在这里,会将apk中的dex文件存放到dexElements数组当中,调用DexPathList的findClass方法,遍历dexElements数组,从数组中找到这个类然后加载

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

    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

这个也是Tinker实现的原理,如果某个类出现bug,那么将这个类打成patch包,放在dexElements数组第一位,在加载这个类后,当执行到bug类时将不会重新加载,而是使用bug fix类

4.4 从源码看PathClassLoader和DexClassLoader的异同

先看一下两者构造方法的异同

public class PathClassLoader extends BaseDexClassLoader {

	public PathClassLoader(String dexPath, ClassLoader parent) {
	    super(dexPath, null, null, parent);
	}
	
	public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
	    super(dexPath, null, librarySearchPath, parent);
	}
}
public class DexClassLoader extends BaseDexClassLoader {

    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

可以看出,DexClassLoader多了一个构造参数optimizedDirectory,用于存放优化后的dex文件,路径可以为空

在DexPathList的makeDexElements方法中,对于dex文件,需要调用loadDexFile方法来生成一个DexFile

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

这里会判断optimizedDirectory是否为空,在PathClassLoader中传入的参数为空,那么在DexClassLoader中传入了这个路径,会调用DexFile的loadDex方法

因为apk其实也是一个压缩文件zip包,像第一次启动时,PathClassLoader会将apk解压存在/data/dalvik-cache目录下,而使用DexClassLoader则是会将apk中可运行的文件提取出来,存放在optimizedDirectory路径下,那么应用程序启动时将会加载optimizedDirectory下的文件,启动速度更快,这就是odex优化

你可能感兴趣的:(gradle,android,android,studio,热修复,类加载机制)