实现热修复以及其原理

简要说一下热修复的背景
当我们发现有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<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.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<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;
}
...
看到没有其是在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<File> 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<File> result 里面
然后我们makeDexElements进入这个方法里看是怎么生成dexElements
...
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)) {
// 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<String> libPaths = new ArrayList<>(10);
然后会走一个方法这个方法主要会把系统的环境路径以及apk的安装目录下data/app/package/lib/armbeaiv7a存放到outLibPaths下
...
public static void makePaths(ActivityThread activityThread,
boolean isBundledApp,
ApplicationInfo aInfo,
List<String> outZipPaths,
List<String> 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这是本人代码地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容