转载来源:http://blog.csdn.net/l2show/article/details/53307523

之前有说到Tinker的修复原理是跟Qzone类似,这里就详细分析一下为什么这样做可以修复补丁.虽然其他Android版本的源码实现可能不一样,但是都是基于相同的原理.所以这里就以Android 6.0的源码为例介绍原理.具体每个系统版本的不同实现下面会详细说明.

首先从加载dex文件的入口开始看, /libcore/dalvik/src/main/Java/dalvik/system/DexClassLoader.java这个类很简单,只是继承了BaseDexClassLoader在构造方法中调用了父类的构造方法.

public class DexClassLoader extends BaseDexClassLoader {public DexClassLoader(String dexPath, String optimizedDirectory,String libraryPath, ClassLoader parent) {super(dexPath, new File(optimizedDirectory), libraryPath, parent);}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

继续进入/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java类中,BaseDexClassLoader在构造方法中创建出了一个很重要的对象pathList,至于为什么说他重要可以看下面的findclass方法.findclass方法是根据类名在运行时从dex文件中找出并将其返回回来,而真正的findclass是通过pathList对象的方法来操作的.

public class BaseDexClassLoader extends ClassLoader {private final DexPathList pathList;public BaseDexClassLoader(String dexPath, File optimizedDirectory,String libraryPath, ClassLoader parent) {super(parent);this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);}@Overrideprotected 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;}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

最终在/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java类中找到findclass方法,该方法是按顺序遍历dexElements,只要dexElement中的dex文件中包含有该class就加载出class然后直接return.所以利用findclass这种特性把补丁包dex插入dexElements的首位,系统在findClass的时候就优先拿到补丁包中的class,从而达到修复bug的目的.

final class DexPathList {private final Element[] dexElements;public Class findClass(String name, List<Throwable> suppressed) {for (Element element : dexElements) {DexFile dex = element.dexFile;if (dex != null) {Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);if (clazz != null) {return clazz;}}}if (dexElementsSuppressedExceptions != null) {suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));}return null;}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

校验Dex文件

讲过dex修复的原理,回到Tinker的dex补丁加载流程.在loadTinkerJars之后,先确保之前checkComplete时是否筛选出物理有效的dex文件以供加载,再拿到PathClassLoader供后面使用.

        if (dexList.isEmpty()) {Log.w(TAG, "there is no dex to load");return true;}PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();if (classLoader != null) {Log.i(TAG, "classloader: " + classLoader.toString());} else {Log.e(TAG, "classloader is null");ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_CLASSLOADER_NULL);return false;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

TinkerLoader.tryLoad时只是校验了dex_meta.txt文件的签名信息,并没有校验所有的dex文件的合法性.如果在ApplicationLike处配置了tinkerLoadVerifyFlag为true, 则每次加载dex补丁之前都对文件做MD5,并对比dex_meta.txt中对应的MD5信息.

        for (DexDiffPatchInfo info : dexList) {//for dalvik, ignore art support dexif (isJustArtSupportDex(info)) {continue;}String path = dexPath + info.realName;File file = new File(path);if (tinkerLoadVerifyFlag) {long start = System.currentTimeMillis();String checkMd5 = isArtPlatForm ? info.destMd5InArt : info.destMd5InDvm;if (!PatchFileUtil.verifyDexFileMd5(file, checkMd5)) {//it is good to delete the mismatch fileIntentUtil.setIntentReturnCode(intentResult, Constants.ERROR_LOAD_PATCH_VERSION_DEX_MD5_MISMATCH);intentResult.putExtra(IntentUtil.INTENT_PATCH_MISMATCH_DEX_PATH,file.getAbsolutePath());return false;}Log.i(TAG, "verify dex file:" + file.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));}legalFiles.add(file);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

OTA在特定情况下重新load Dex

在v1.7.5的版本开始有了isSystemOTA判断,只要用户是ART环境并且做了OTA升级则在加载dex补丁的时候就会先把最近一次的补丁全部DexFile.loadDex一遍.这么做的原因是有些场景做了OTA后,oat的规则可能发生变化,在这种情况下去加载上个系统版本oat过的dex就会出现问题.

        if (isSystemOTA) {parallelOTAResult = true;parallelOTAThrowable = null;Log.w(TAG, "systemOTA, try parallel oat dexes!!!!!");ParallelDexOptimizer.optimizeAll(legalFiles, optimizeDir,new ParallelDexOptimizer.ResultCallback() {@Overridepublic void onSuccess(File dexFile, File optimizedDir) {// Do nothing.}@Overridepublic void onFailed(File dexFile, File optimizedDir, Throwable thr) {parallelOTAResult = false;parallelOTAThrowable = thr;}});if (!parallelOTAResult) {Log.e(TAG, "parallel oat dexes failed");intentResult.putExtra(IntentUtil.INTENT_PATCH_EXCEPTION, parallelOTAThrowable);IntentUtil.setIntentReturnCode(intentResult, Constants.ERROR_LOAD_PATCH_VERSION_PARALLEL_DEX_OPT_EXCEPTION);return false;}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

dex补丁的重置是在线程池中执行,并且利用CountDownLatch挂起主线程,直到线程池中的task都执行完毕再恢复主线程.在很极端的情况下可能会造成ANR.

    private static boolean optimizeAllLocked(Collection<File> dexFiles, File optimizedDir, AtomicInteger successCount, ResultCallback cb) {final CountDownLatch lauch = new CountDownLatch(dexFiles.size());final ExecutorService threadPool = Executors.newCachedThreadPool();long startTick = System.nanoTime();for (File dexFile : dexFiles) {OptimizeWorker worker = new OptimizeWorker(dexFile, optimizedDir, successCount, lauch, cb);threadPool.submit(worker);}try {lauch.await();long timeCost = (System.nanoTime() - startTick) / 1000000;if (successCount.get() == dexFiles.size()) {Log.i(TAG, "All dexes are optimized successfully, cost: " + timeCost + " ms.");return true;} else {Log.e(TAG, "Dexes optimizing failed, some dexes are not optimized.");return false;}} catch (InterruptedException e) {Log.w(TAG, "Dex optimizing was interrupted.", e);return false;} finally {threadPool.shutdown();}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

加载Dex

经过一系列的校验,终于到真正加载dex补丁的步骤了.Tinker加载dex补丁按照系统版本不同分成了四条分支.同样加载失败之后记录失败信息到intentResult中.

  1. V4 Android SDK版本小于14
  2. V14 Android SDK版本小于19
  3. V19 Android SDK版本小于 23
  4. V23 Android SDK版本大于等于23 
    • Android N 改造ClassLoader
        try {SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);} catch (Throwable e) {Log.e(TAG, "install dexes failed");intentResult.putExtra(IntentUtil.INTENT_PATCH_EXCEPTION, e);IntentUtil.setIntentReturnCode(intentResult, Constants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION);return false;}Log.i(TAG, "after loaded classloader: " + application.getClassLoader().toString());
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • V4 Android SDK版本小于14

    在Android SDK4到14之间PathClassLoader.java的实现是直接继承自ClassLoader,findClass时是根据mFiles数组来遍历mDexs数组(类似于dexElements).从mDexs数组中的dex根据类名来加载Class,规则也是按照遍历的顺序加载,只要有加载出来的Class就直接return掉.

    Android 2.3.6版本源码

    public class PathClassLoader extends ClassLoader {private final String path;private final String[] mPaths;
    private final File[] mFiles;
    private final ZipFile[] mZips;
    private final DexFile[] mDexs;@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException
    {//System.out.println("PathClassLoader " + this + ": findClass '" + name + "'");byte[] data = null;int length = mPaths.length;for (int i = 0; i < length; i++) {//System.out.println("My path is: " + mPaths[i]);if (mDexs[i] != null) {Class clazz = mDexs[i].loadClassBinaryName(name, this);if (clazz != null)return clazz;} else if (mZips[i] != null) {String fileName = name.replace('.', '/') + ".class";data = loadFromArchive(mZips[i], fileName);} else {File pathFile = mFiles[i];if (pathFile.isDirectory()) {String fileName =mPaths[i] + "/" + name.replace('.', '/') + ".class";data = loadFromDirectory(fileName);} else {//System.out.println("PathClassLoader: can't find '"//    + mPaths[i] + "'");}}}throw new ClassNotFoundException(name + " in loader " + this);
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    DexClassLoader.java的构造方法中可以看到path,mPaths,mFilesmZipsmDexs五个关键属性之间是互相联系的,所以在做热修复时要同时对这五个属性同步操作,来确保数据的一致性.

        this.path = path;this.libPath = libPath;mPaths = path.split(":");int length = mPaths.length;//System.out.println("PathClassLoader: " + mPaths);mFiles = new File[length];mZips = new ZipFile[length];mDexs = new DexFile[length];.../* open all Zip and DEX files up front */for (int i = 0; i < length; i++) {//System.out.println("My path is: " + mPaths[i]);File pathFile = new File(mPaths[i]);mFiles[i] = pathFile;if (pathFile.isFile()) {try {mZips[i] = new ZipFile(pathFile);}catch (IOException ioex) {}if (wantDex) {/* we need both DEX and Zip, because dex has no resources */try {mDexs[i] = new DexFile(pathFile);}catch (IOException ioex) {}}}}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    所以在Tinker中,要加载这类系统的补丁包最核心的地方就是path,mPaths,mFilesmZipsmDexs五个属性的的操作.根据补丁文件的个数建立四个关键属性对应的数组,再通过遍历补丁文件,对四个数组和一个字符串进行填充.再利用反射将新的数组插入到原数组头部,完成补丁加载的过程.

        private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory)throws IllegalArgumentException, IllegalAccessException,NoSuchFieldException, IOException {int extraSize = additionalClassPathEntries.size();Field pathField = ReflectUtil.findField(loader, "path");StringBuilder path = new StringBuilder((String) pathField.get(loader));String[] extraPaths = new String[extraSize];File[] extraFiles = new File[extraSize];ZipFile[] extraZips = new ZipFile[extraSize];DexFile[] extraDexs = new DexFile[extraSize];for (ListIterator<File> iterator = additionalClassPathEntries.listIterator();iterator.hasNext();) {File additionalEntry = iterator.next();String entryPath = additionalEntry.getAbsolutePath();path.append(':').append(entryPath);int index = iterator.previousIndex();extraPaths[index] = entryPath;extraFiles[index] = additionalEntry;extraZips[index] = new ZipFile(additionalEntry);//edit by zhangshaowenString outputPathName = PatchFileUtil.optimizedPathFor(additionalEntry, optimizedDirectory);//for below 4.0, we must input jar or zipextraDexs[index] = DexFile.loadDex(entryPath, outputPathName, 0);}pathField.set(loader, path.toString());ReflectUtil.expandFieldArray(loader, "mPaths", extraPaths);ReflectUtil.expandFieldArray(loader, "mFiles", extraFiles);ReflectUtil.expandFieldArray(loader, "mZips", extraZips);try {ReflectUtil.expandFieldArray(loader, "mDexs", extraDexs);} catch (Exception e) {}}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    对原数组的操作是利用反射,先拿到原数组的对象original, 再根据original的类型长度以及补丁数组的长度重新创建出一个新数组combined.接下来使用arraycopy将补丁数组和原数组copy到combined中,最后将该数组赋值给filedName对应的属性.

        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 firstSystem.arraycopy(extraElements, 0, combined, 0, extraElements.length);System.arraycopy(original, 0, combined, extraElements.length, original.length);jlrField.set(instance, combined);
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
  • V14 Android SDK版本小于19

    在这个Android版本的区间内不再像老版本的那样要维护四个数组,源码从中抽离出了一个类DexPathList.java,加载dex的关键数组也变成了dexElements,并且dexElements是根据makeDexElements方法生成的.对比过源码其实就可以发现dexElements其实就是老版本中mFilesmZipsmDexs的封装,makeDexElements方法就是老版本DexClassLoader.java构造方法中对数组初始化的动作.

    Android 4.2.2版本源码

    final class DexPathList {/** list of dex/resource (class path) elements */private final Element[] dexElements;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;}/*** Makes an array of dex/resource path elements, one per element of* the given array.*/private static Element[] makeDexElements(ArrayList<File> files,File optimizedDirectory) {ArrayList<Element> elements = new ArrayList<Element>();/** Open all files and load the (direct or contained) dex files* up front.*/for (File file : files) {File 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)) {zip = file;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()]);}
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    系统既然自己做了封装,那么我们反射调用起来也会更方便.首先反射拿到反射得到PathClassLoader中的pathList对象,再将补丁文件通过反射调用makeDexElements得到补丁文件的Element[],再将补丁包的Element数组插入到dexElements中,方法如V4.完成补丁加载.

        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,File optimizedDirectory)throws IllegalArgumentException, IllegalAccessException,NoSuchFieldException, InvocationTargetException, NoSuchMethodException {/* 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 = ReflectUtil.findField(loader, "pathList");Object dexPathList = pathListField.get(loader);//通过反射调用makeDexElements方法生成补丁包的dex数组,再将其插入到dexElements的头部ReflectUtil.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));}/*** A wrapper around* {@code private static final dalvik.system.DexPathList#makeDexElements}.*/private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory)throws IllegalAccessException, InvocationTargetException,NoSuchMethodException {Method makeDexElements =ReflectUtil.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);//反射调用makeDexElements方法根据files得到新dexElements数组return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
  • V19 Android SDK版本小于 23

    在该版本系统区间中,加载补丁涉及到的修改只是增加了一个exElementsSuppressedExceptions异常数组的维护.所以在加载补丁的时候就跟V14差不多了.既然只是多了一个异常的管理,为什么Tinker源码在利用反射找makeDexElements(ArrayList,File,ArrayList),如果找不到就接着找makeDexElements(List,File,List)?为了在Android源码中找到答案我去查找了4.4, 5.0,5.1版本的DexPathList源码,发现方法的参数都是ArrayList,根本没有List.百思不得姐之后就问了一下Tinker的作者,他们说在线上发现有机子的rom中这个方法的参数就是List.

        private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory,ArrayList<IOException> suppressedExceptions)throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {Method makeDexElements = null;try {makeDexElements = ReflectUtil.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,ArrayList.class);} catch (NoSuchMethodException e) {Log.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure");try {makeDexElements = ReflectUtil.findMethod(dexPathList, "makeDexElements", List.class, File.class, List.class);} catch (NoSuchMethodException e1) {Log.e(TAG, "NoSuchMethodException: makeDexElements(List,File,List) failure");throw e1;}}return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions);}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
  • V23 Android SDK版本大于等于23

    因为V23中包含有Android 7.0的系统版本,由于Android N混合编译与对热补丁影响解析,这会造成要修复的class被缓存在App image中,App image中的class会插入PathClassLoader中,而PathClassLoader 加载补丁的时候不会替换缓存的class,最终会导致在全量更新的情况下有可能部分类是从base.apk中加载,部分类是从patch.dex中加载,抛出IllegalAccessError.Tinker的解决方案是在运行时改写PathClassLoader来加载类,让App image中的缓存失效.

    所以要解决N里面混编的问题,核心着手点就是要替换PathClassLoader使他在加载dex的时候不加载做过优化的dex文件,重新加载原始的dex文件.这个点要从哪里切入呢? 在Android 7.0的源码中定位到了在makePathElements方法中调用的loadDexFile方法.从代码上来看是要在调用的时候有传递有效的optimizedDirectory参数,就会去opt过的路径下加载dex文件.所以我们在调用的时候不传optimizedDirectory参数就可以达到重新加载原始dex文件从而去除混编优化的目的.

    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);}
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    知道了解决方案和切入点,接下来分析一下Tinker做法.在加载补丁之前利用反射替换原PathClassLoader以及与它相关的所有引用.首先根据原PathClassLoader的parent 构建出AndroidNClassLoader;再反射拿到original的pathList;接着反射拿到pathList对象的definingContext属性,因为该属性是original的引用,需要拿到之后替换成新loader的引用;继续反射拿到androidNClassLoader的pathList对象,并且替换成original的;再反射拿到original的pathList的dexElements,并且遍历出dexElements中真实的dex文件名之后存储起来;接下来反射拿到original的pathList的makePathElements方法并调用注意方法第二个参数optDir要设置为null,重新生成dexElements数组,并替换原来的数组.最终完成AndroidNClassLoader的创建,以及子类引用的替换.

    private static AndroidNClassLoader createAndroidNClassLoader(PathClassLoader original) throws Exception {//let all element ""AndroidNClassLoader androidNClassLoader = new AndroidNClassLoader("",  original);Field originPathList = ShareReflectUtil.findField(original, "pathList");Object originPathListObject = originPathList.get(original);//should reflect definingContext alsoField originClassloader = ShareReflectUtil.findField(originPathListObject, "definingContext");originClassloader.set(originPathListObject, androidNClassLoader);//copy pathListField pathListField = ShareReflectUtil.findField(androidNClassLoader, "pathList");//just use PathClassloader's pathListpathListField.set(androidNClassLoader, originPathListObject);//we must recreate dexFile due to dexCacheList<File> additionalClassPathEntries = new ArrayList<>();Field dexElement = ShareReflectUtil.findField(originPathListObject, "dexElements");Object[] originDexElements = (Object[]) dexElement.get(originPathListObject);for (Object element : originDexElements) {DexFile dexFile = (DexFile) ShareReflectUtil.findField(element, "dexFile").get(element);additionalClassPathEntries.add(new File(dexFile.getName()));//protect for java.lang.AssertionError: Failed to close dex file in finalizer.oldDexFiles.add(dexFile);}Method makePathElements = ShareReflectUtil.findMethod(originPathListObject, "makePathElements", List.class, File.class,List.class);ArrayList<IOException> suppressedExceptions = new ArrayList<>();Object[] newDexElements = (Object[]) makePathElements.invoke(originPathListObject, additionalClassPathEntries, null, suppressedExceptions);dexElement.set(originPathListObject, newDexElements);return androidNClassLoader;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    做完新AndroidNClassLoader的创建之后就是替换真正的ClassLoader的引用了.在全局Context中持有的LoadedApk的对象mPackageInfo的属性中,有一个ClassLoader类的对象mClassLoader.层层反射将mClassLoader的引用替换为上面创建出来的AndroidNClassLoader对象.同时将Thread中持有的ClassLoader也同步替换为AndroidNClassLoader.至此PathClassLoader的修改和替换都已经完成了,接下来就可以正常得加载补丁dex了.

        String defBase = "mBase";String defPackageInfo = "mPackageInfo";String defClassLoader = "mClassLoader";Context baseContext = (Context) ReflectUtil.findField(application, defBase).get(application);Object basePackageInfo = ReflectUtil.findField(baseContext, defPackageInfo).get(baseContext);Field classLoaderField = ReflectUtil.findField(basePackageInfo, defClassLoader);Thread.currentThread().setContextClassLoader(reflectClassLoader);classLoaderField.set(basePackageInfo, reflectClassLoader);
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在Android系统在该版本区间之内时,DexPathList类中的findclass方法跟V19相比是没有变化的.但是生成dexElements数组用的方法名发生了变化.所以在这个版本中反射生成补丁包的Element[]就需要兼容这些变化.

    Android 6.0.0版本源码, 相比老版本makeDexElements(ArrayList,File,ArrayList)方法变成了makePathElements(List,File,List).

     /*** Makes an array of dex/resource path elements, one per element of* the given array.*/
    private static Element[] makePathElements(List<File> files, File optimizedDirectory,List<IOException> suppressedExceptions) {···
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Android 7.0.0版本源码,该方法名又发生了变化.根据职能做了一些区分和重载.

     /*** Makes an array of dex/resource path elements, one per element of* the given array.*/
    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,List<IOException> suppressedExceptions,ClassLoader loader) {return makeElements(files, optimizedDirectory, suppressedExceptions, false, loader);
    }/*** Makes an array of directory/zip path elements, one per element of the given array.*/
    private static Element[] makePathElements(List<File> files,List<IOException> suppressedExceptions,ClassLoader loader) {return makeElements(files, null, suppressedExceptions, true, loader);
    }private static Element[] makePathElements(List<File> files, File optimizedDirectory,List<IOException> suppressedExceptions) {return makeElements(files, optimizedDirectory, suppressedExceptions, false, null);
    }private static Element[] makeElements(List<File> files, File optimizedDirectory,List<IOException> suppressedExceptions,boolean ignoreDexFiles,ClassLoader loader) {···
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    结合上面两个系统版本的分析,在反射findMethod时做多种情况的处理,同时也避免V19中描述的rom问题.

        private static Object[] makePathElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory,ArrayList<IOException> suppressedExceptions)throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {Method makePathElements;try {makePathElements = ReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class,List.class);} catch (NoSuchMethodException e) {Log.e(TAG, "NoSuchMethodException: makePathElements(List,File,List) failure");try {makePathElements = ReflectUtil.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);}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

卸载 Dex补丁

校验和加载Dex补丁的流程都已经分析完了.接下来就是验证补丁是否加载成功,这里是通过反射拿到TinkerTestDexLoad.isPatch静态属性来判断.在没有补丁加载的情况下都是返回false的,在补丁中修改isPatch属性为true.所以只要反射拿到isPatch的属性为true就说明补丁已经成功加载进来了.如果返回false则卸载Dex补丁恢复到加载之前的数据状态.

    private static boolean checkDexInstall(ClassLoader classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {Class<?> clazz = Class.forName(CHECK_DEX_CLASS, true, classLoader);Field filed = ShareReflectUtil.findField(clazz, CHECK_DEX_FIELD);boolean isPatch = (boolean) filed.get(null);Log.w(TAG, "checkDexInstall result:" + isPatch);return isPatch;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

补丁的卸载就不需要跟加载一样要区分那么多版本,因为Android 4.0开始就都是在操作DexPathList类中的dexElements数组,Android 4.0之前是在V4补丁加载中提到过的那四个数组.卸载就根据补丁的数量把数组头部的数据都移除掉.其中reduceFieldArray的作用跟加载补丁V4中提到的expandFieldArray完全相反.

    public static void uninstallPatchDex(ClassLoader classLoader) throws Throwable {if (sPatchDexCount <= 0) {return;}if (Build.VERSION.SDK_INT >= 14) {Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");Object dexPathList = pathListField.get(classLoader);ShareReflectUtil.reduceFieldArray(dexPathList, "dexElements", sPatchDexCount);} else {ShareReflectUtil.reduceFieldArray(classLoader, "mPaths", sPatchDexCount);ShareReflectUtil.reduceFieldArray(classLoader, "mFiles", sPatchDexCount);ShareReflectUtil.reduceFieldArray(classLoader, "mZips", sPatchDexCount);try {ShareReflectUtil.reduceFieldArray(classLoader, "mDexs", sPatchDexCount);} catch (Exception e) {}}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

到这里Dex补丁的加载流程就完全分析完了,下面会继续分析SO和资源的操作流程.

Android 热修复方案Tinker(三) Dex补丁加载相关推荐

  1. Android 热修复方案Tinker(五) SO补丁加载

    基于Tinker V1.7.5 Android 热修复方案Tinker(一) Application改造 Android 热修复方案Tinker(二) 补丁加载流程 Android 热修复方案Tink ...

  2. android热修复方案

    热补丁方案有很多,其中比较出名的有腾讯Tinker.阿里的AndFix.美团的Robust以及QZone的超级补丁方案.他们的优劣如下: 一.Tinker 热修复 Tinker通过 Dexdiff 算 ...

  3. 干货满满,Android热修复方案介绍

    摘要:在云栖社区技术直播中,阿里云客户端工程师李亚洲(毕言)从技术原理层面解析和比较了业界几大热修复方案,揭开了Qxxx方案.Instant Run以及阿里Sophix等热修复方案的神秘面纱,帮助大家 ...

  4. Android 热修复方案分析

    绝大部分的APP项目其实都需要一个动态化方案,来应对线上紧急bug修复发新版本的高成本.之前有利用加壳,分拆两个dex结合DexClassLoader实现了一套全量更新的热更方案.实现原理在Andro ...

  5. android热修复技术tinker,Android热修复方案第一弹——Tinker篇

    背景 一款App的正常开发流程应该是这样的:新版本上线-->用户安装-->发现Bug-->紧急修复-->重新发布新版本-->提示用户安装更新,从表面上看这样的开发流程顺理 ...

  6. Android热修复之Tinker集成最新详解

    前言 该文章属于初级集成详解,侧重Tinker的使用,如若想深入了解其原理请自行查阅相关文档Tinker相关文档 当前市面的热补丁方案有很多,其中比较出名的有阿里的 AndFix.美团的 Robust ...

  7. 【Android 热修复】Tinker 简介

    文章目录 一.Tinker 简介 二. 源码资源 一.Tinker 简介 https://github.com/Tencent/tinker/tree/dev/tinker-android 页面的 t ...

  8. Android 热修复 Tinker Gradle Plugin解析

    本文已在我的公众号hongyangAndroid原创首发. 转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/72667669 本文 ...

  9. android revre view,Android热修复之微信Tinker使用初探

    前几天,万众期待的微信团队的Android热修复框架tinker终于在GitHub上开源了.java 今天拿下来集成使用了一下,发现md上对集成使用的过程介绍的比较精简(后来发现wiki上面却是很详细 ...

最新文章

  1. html中的li排成一行怎么写,html怎么实现li元素有点并分列
  2. MicroPython支持图形化编辑了:Python Editor带你轻松玩转MicroPython
  3. C语言 之 如何清除输入缓冲区所有内容
  4. 如何在单台计算机上配置 Windows XP SP2 网络保护技术
  5. git 版本控制器 初学习,工作中的问题及其解决方法
  6. 独立线性度 最佳直线
  7. java学习(86):Interage方法compareto,parseint,intvalue
  8. OpenVINO主要工作流程
  9. 二维数组的最大联通子数组和
  10. 用Python统计瓦尔登湖的词频
  11. Linux bind DNS配置
  12. 【Java】Hello world
  13. 曼昆《经济学原理微观》读书笔记
  14. BC26 OpenCPU RTC/PSM_EINT API接口
  15. 淘宝褚霸谈做技术的心态
  16. IP签名档PHP源码,IPCard 一款天气图标签名档源码
  17. NYOJ小明的存钱计划
  18. vue2路由手动创建二级路由路由传参路由守卫打包上线
  19. DAO组织决定风险投资,Tiger DAO VC善用群体的智慧
  20. 财务审计工作内容有哪些

热门文章

  1. java hibernate 查询_Hibernate查询方式
  2. 智能硬件「卖多少」才不算贵?
  3. ​2022年第四届大数据与计算国际研讨会(WBDC 2022)​
  4. github好用的插件
  5. 2022-1-20 Leetcode 387.字符串中的第一个唯一字符
  6. 空气动力学 惯性导航 四轴飞机
  7. 微型计算机存容量基本单位,微型计算机内存容量的基本单位是()。
  8. 静电放电发生器操作规程
  9. 【Blender】摄像机-01常用操作
  10. 推荐一款好用的外语复读工具——aboboo