实现热修复以及其原理

简要说一下热修复的背景
当我们发现有bug,然后需要去解决这些bug,这个时候又要进行发包提醒用户下载或者强制用户更新这就很容易失去用户,所以我们可以采用热修复 插件化等技术在用户毫无感觉的情况下更新。
我们首先说一下类加载他是怎么工作的了
...
getClassLoader().loadClass(全类名径)
/libcore/ojluni/src/main/java/java/lang/ClassLoader.java
public Class loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
// during the entire class loading process.
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;
}
...
首先会检查类是不是已经被加载了,第一次进来肯定没有加载的这个时候就会走
/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
里面的findClass(name);
...
@Override
protected Class findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
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.findClass(name, suppressedExceptions);
这个时候就让外面来查看一下pathList究竟是什么
...
private final DexPathList pathList;
public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) // TODO We should support giving this a library search path maybe.
super(parent);
this.pathList = new DexPathList(this, dexFiles);
}
...
看到是在构造函数中进行实例化的接下来外面看一下
/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
...
public Class findClass(String name, List 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;
}
...
看到没有其是在Element 里面查找的
接下来我们就来看看dexElements数组是什么时候赋值的
...
new PathClassLoader()
/libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
到其父类/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
...
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
this(dexPath, optimizedDirectory, librarySearchPath, parent, false);
}
...
接下来就解释一下者四个参数
dexPath,指的是在Androdi包含类和资源的jar/apk类型的文件集合,指的是包含dex文件。如果有多个文件者用“:”分隔开,用代码就是File.pathSeparator。
optimizedDirectory,指的是odex优化文件存放的路径,可以为null,那么就采用默认的系统路径即/data/dalvik-cache。
libraryPath,包含 C/C++ 库的路径集合,多个路径用文件分隔符分隔分割,可以为null。
parent,parent类加载器
接下来我们继续看源码
...
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();
}
}
...
接下来
/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
...
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
...
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
....
}
...
看到这里我们知道了之前dexElements是怎么的来的
接下来我们看一下
来看看splitDexPath到底做什么了。
...
private static List splitPaths(String searchPath, boolean directoriesOnly) {

    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;

}
...
看到searchPath.split(File.pathSeparator)可以看出根据:来截取字符串,就是多个dexpath之间用:分割,然后变成file,
被加进去 List result 里面
然后我们makeDexElements进入这个方法里看是怎么生成dexElements
...
private static Element[] makeDexElements(List files, File optimizedDirectory,
List 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)) {
// 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) {
/

* 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);
}
}
if (dex != null && isTrusted) {
dex.setTrusted();
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
...
可以看到他首先会建一个数组以我们有多少文件来设置大小
private static final String DEX_SUFFIX = ".dex";
接者就开始循环遍历我们可以看到他有两种一种是以 if (name.endsWith(DEX_SUFFIX))以dex文件
其实不管是以dex为结尾还是不以dex为结尾他们都会走
dex = loadDexFile(file, optimizedDirectory, loader, elements);
...
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(这个参数的作用我在上面有做解释)是否为null,
如果为null我们就直接new DexFile(file, loader, elements);否则DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
/libcore/dalvik/src/main/java/dalvik/system/DexFile.java
...
static DexFile loadDex(String sourcePathName, String outputPathName,
int flags, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
return new DexFile(sourcePathName, outputPathName, flags, loader, elements);
}
...
我们点进去发现他也是实例化new DexFile唯一的区别就是参数个数不一样了
接下来我们看一下
看这两个他们的调用时机是不一样的当我们有传optimizedDirectory这个路径时。
mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
如果没有就 mCookie = openDexFile(fileName, null, 0, loader, elements);可见其为null
...
private static Object openDexFile(String sourceName, String outputName, int flags,
ClassLoader loader, DexPathList.Element[] elements) throws IOException {
// Use absolute paths to enable the use of relative paths when testing on host.
return openDexFileNative(new File(sourceName).getAbsolutePath(),
(outputName == null)
? null
: new File(outputName).getAbsolutePath(),
flags,
loader,
elements);
}
...
可见Native层里面会把我们传进去的文件进行解压然后就会将那些解析的类返回出来外面就保存到Element 数组里面去了。然后外面就可以找到了。
然后说一下PathClassLoader,DexClassLoader 的区别
PathClassLoader只能加载已安装的apk下dex文件
DexClassLoader 可以加载dex/jar/apk/zip也可以从SD目录下的加载)
为什么其实就是因为DexClassLoader 可以传optimizedDirectory(指的是odex优化文件存放的路径)路径
带那么看一下源码
/libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
...
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
...
他的构造函数只有两个使用可以清楚看到不能传optimizedDirectory
现在我先来说说art和Dalvik虚拟机
Dalvik中的应用每次运行时字节码都需要通过jit编译器编译成机器码(可以看成是及时编译这样会大大拉低效率)】
而art在系统安装应用程序的时候就进行dex2oat预编译,把多个dex合并为一个oat文件,供android设备使用
这样子的就是将字节码预先编译成机器码直接存放在本地,等需要用的时候就来取,这样子就不会像dav那样每次都要进行编译从而提高了效率。
接下来我来说说PathClassLoader是在什么时候被实例化了
说到这个我就简单说一我们的应用进程都是由ActivityManagerService来请求Zygote来创建出来的,接着我们activity启动通过ams最后回到我们ActivyThread类里面的
handleLaunchActivity那么我们就从这个方法开始说起。
然后回跳到performLaunchActivity
然后 Application app = r.packageInfo.makeApplication(false, mInstrumentation);
接着 getClassLoader();
...
public ClassLoader getClassLoader() {
synchronized (this) {
if (mClassLoader == null) {
createOrUpdateClassLoaderLocked(null /
addedPaths/);
}
return mClassLoader;
}
}
...
接着会走着里面的
...
createOrUpdateClassLoaderLocked(null /
addedPaths/);{
if (!mIncludeCode) {
if (mClassLoader == null) {
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
mClassLoader = ApplicationLoaders.getDefault().getClassLoader(
"" /
codePath /, mApplicationInfo.targetSdkVersion, isBundledApp,
librarySearchPath, libraryPermittedPath, mBaseClassLoader,
null /
classLoaderName */);
StrictMode.setThreadPolicy(oldPolicy);
mAppComponentFactory = AppComponentFactory.DEFAULT;
}
}
getClassLoader(){
ClassLoaderFactory.createClassLoader(
zip, null, parent, classLoaderName);接着会走这里面的方法
}
com.android.internal.os.ClassLoaderFactory这个类中
*/
public static ClassLoader createClassLoader(String dexPath,
String librarySearchPath, ClassLoader parent, String classloaderName) {
if (isPathClassLoaderName(classloaderName)) {
return new PathClassLoader(dexPath, librarySearchPath, parent);
} else if (isDelegateLastClassLoaderName(classloaderName)) {
return new DelegateLastClassLoader(dexPath, librarySearchPath, parent);
}
throw new AssertionError("Invalid classLoaderName: " + classloaderName);
}
...
看到这就会就行实例化完成了
对了还有一点我们的librarySearchPath, 这个存放so路径是怎么来
原来在之前我们走了createOrUpdateClassLoaderLocked这个方法的时候会有
final List libPaths = new ArrayList<>(10);
然后会走一个方法这个方法主要会把系统的环境路径以及apk的安装目录下data/app/package/lib/armbeaiv7a存放到outLibPaths下
...
public static void makePaths(ActivityThread activityThread,
boolean isBundledApp,
ApplicationInfo aInfo,
List outZipPaths,
List outLibPaths) {
if (aInfo.primaryCpuAbi != null) {
// Add fake libs into the library search path if we target prior to N.
if (aInfo.targetSdkVersion < Build.VERSION_CODES.N) {
outLibPaths.add("/system/fake-libs" +
(VMRuntime.is64BitAbi(aInfo.primaryCpuAbi) ? "64" : ""));
}
for (String apk : outZipPaths) {
outLibPaths.add(apk + "!/lib/" + aInfo.primaryCpuAbi);
}
}
}
...
// final String librarySearchPath = TextUtils.join(File.pathSeparator, libPaths);通过这个就直接把我们刚刚保存到outLibPaths变成字符串以;分割开来。
接下来我将一下so的加载流程
...
System.loadLibrary("");
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
...
java.lang.Runtime里面的loadLibrary0下
String filename = loader.findLibrary(libraryName);
会去我们的PathClassLoader可是里面没有这个方法那么我们就去父类
/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
...
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
...
又会走到
/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
...
//libraryName “native-lib”
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);//如果之前加载过了 绝对路径返回给你
for (NativeLibraryElement element : nativeLibraryPathElements) {
String path = element.findNativeLibrary(fileName);
if (path != null) {
return path;
}
}
return null;
}
public String findNativeLibrary(String name) {
maybeInit();
if (zipDir == null) {
String entryPath = new File(path, name).getPath();
if (IoUtils.canOpenReadOnly(entryPath)) {
return entryPath;
}
} else if (urlHandler != null) {
// Having a urlHandler means the element has a zip file.
// In this case Android supports loading the library iff
// it is stored in the zip uncompressed.
String entryName = zipDir + '/' + name;
if (urlHandler.isEntryStored(entryName)) {
return path.getPath() + zipSeparator + entryName;
}
}
return null;
}
...
主要是为了返回so的绝对路径,其中path实例化NativeLibraryElement最后其实是我们之前实例化PathClassLoader的时候路径
这样走完会放回我们
synchronized void loadLibrary0(ClassLoader loader, String libname) {
nativeLoad(filename, loader);走完这个我们java层的so文件就跟踪完成了
}
所以我们要热修复so只要比错误的so快加载就可以了我们可以传我们so所放的地方的绝对路径来
System.load();
而我们要修复代码bug其实就很简单
只要把我们从服务端获下载下来正确的classes2.dex
然后通过DexClassLoader加载 然后我们把他和原来的dexElements进行合并,其实就是插在原来的前面
因为我们加载类有一个机制(双亲委托模式)。只要找到该类就不会往下走了
代码在https://github.com/yang1992yff/fixBug这是本人代码地址

你可能感兴趣的:(实现热修复以及其原理)