tinker -源码分析

Tinker 思想全量替换新的Dex。它更像是APP的增量更新,
在服务器端通过差异性算法,计算出新旧dex之间的差异包,推送到客户端,进行合成

区别在于不再将patch.dex增加到elements数组中,而是差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并,然后整体替换掉旧的DEX文件,以达到修复的目的。

tinker -源码分析_第1张图片

//data/dat/包名/tinker
tinker -源码分析_第2张图片.png

加载Patch包 (这里的包是合并成功的fix包)

Tinker源码中有isVmArt 这里只是记录笔记

isVmArt 是什么

“虚拟机实现的版本:” + System.getProperty(“java.vm.version”)

isVmart(System.getProperty("java.vm.version"))


 /**
     * vm whether it is art
     *
     * @return
     */
    private static boolean isVmArt(String versionString) {
        boolean isArt = false;
        if (versionString != null) {
            Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
            if (matcher.matches()) {
                try {
                    int major = Integer.parseInt(matcher.group(1));
                    int minor = Integer.parseInt(matcher.group(2));
                    isArt = (major > 2)
                        || ((major == 2)
                        && (minor >= 1));
                } catch (NumberFormatException e) {
                    // let isMultidexCapable be false
                }
            }
        }
        return isArt;
    }


而在Java标准的虚拟机中,类加载可以从class文件中读取,也可以是其他形式的二进制流。因此,我们常常利用这一点,在程序运行时手动加载Class,从而达到代码动态加载执行的目的。

Android平台上虚拟机运行的是Dex字节码,一种对class文件优化的产物,
传统Class文件是一个Java源码文件会生成一个.class文件
而Android是把所有Class文件进行合并,优化,然后生成一个最终的class.dex
目的是把不同class文件重复的东西只需保留一份,如果我们的Android应用不进行分dex处理,最后一个应用的apk只会有一个dex文件

tinker -源码分析_第3张图片
想法 :
通过反射拿到存放Dex数组,替换dex ,来达到热更新的效果 。

BaseDexClassLoader
–>PathDexList
–>findClass方法 遍历DexElement数组
找到对应类就直接返回
DexElement 是存放dex的数组

image

tinker -源码分析_第4张图片

DexClassLoader可以加载jar/apk/dex,可以从SD卡中加载未安装的apk;
PathClassLoader只能加载系统中已经安装过的apk;

package dalvik.system;

import java.io.File;

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        //PathClassLoader的optimizedDirectory只能是null
        //只能加载系统中已经安装过的
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

    public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
        //PathClassLoader的optimizedDirectory只能是null
        //只能加载系统中已经安装过的
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}

-----------------------------------

package dalvik.system;

import java.io.File;

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

----------------------------------------------------------------------

package dalvik.system;

import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

/**
 * Base class for common functionality between various dex-based
 * {@link ClassLoader} implementations.
 */
public class BaseDexClassLoader extends ClassLoader {
    //dex 在DexPathList Element[] dexElements中
    private final DexPathList pathList;

    /**
     * Constructs an instance.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param optimizedDirectory directory where optimized dex files
     * should be written; may be {@code null}
     * @param libraryPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

    @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;
    }

    @Override
    protected URL findResource(String name) {
        return pathList.findResource(name);
    }

    @Override
    protected Enumeration findResources(String name) {
        return pathList.findResources(name);
    }

    @Override
    public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }

    /**
     * Returns package information for the given package.
     * Unfortunately, instances of this class don't really have this
     * information, and as a non-secure {@code ClassLoader}, it isn't
     * even required to, according to the spec. Yet, we want to
     * provide it, in order to make all those hopeful callers of
     * {@code myClass.getPackage().getName()} happy. Thus we construct
     * a {@code Package} object the first time it is being requested
     * and fill most of the fields with dummy values. The {@code
     * Package} object is then put into the {@code ClassLoader}'s
     * package cache, so we see the same one next time. We don't
     * create {@code Package} objects for {@code null} arguments or
     * for the default package.
     *
     * 

There is a limited chance that we end up with multiple * {@code Package} objects representing the same package: It can * happen when when a package is scattered across different JAR * files which were loaded by different {@code ClassLoader} * instances. This is rather unlikely, and given that this whole * thing is more or less a workaround, probably not worth the * effort to address. * * @param name the name of the class * @return the package information for the class, or {@code null} * if there is no package information available for it */ @Override protected synchronized Package getPackage(String name) { if (name != null && !name.isEmpty()) { Package pack = super.getPackage(name); if (pack == null) { pack = definePackage(name, "Unknown", "0.0", "Unknown", "Unknown", "0.0", "Unknown", null); } return pack; } return null; } /** * @hide */ public String getLdLibraryPath() { StringBuilder result = new StringBuilder(); for (File directory : pathList.getNativeLibraryDirectories()) { if (result.length() > 0) { result.append(':'); } result.append(directory); } return result.toString(); } @Override public String toString() { return getClass().getName() + "[" + pathList + "]"; } }

//Tinker 的类图
加载的代码实际上在生成的Application中调用的,其父类为TinkerApplication,在其attachBaseContext中辗转会调用到loadTinker()方法,在该方法内部,反射调用了TinkerLoader的tryLoad方法。

tinker -源码分析_第5张图片

//package com.tencent.tinker.loader.SystemClassLoaderAdder类;
installDexes方法中

V19.install(classLoader, files, dexOptDir);

根据不同的系统版本,去反射处理dexElements。

/**
     * Installer for platform versions 19.
     */
    private static final class V19 {

        //loader为 PathClassLoader
        private static void install(ClassLoader loader, List additionalClassPathEntries,
                                    File optimizedDirectory)
            throws IllegalArgumentException, IllegalAccessException,
            NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
            /* 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.
             *找到pathList属性
             */
            Field pathListField = ShareReflectUtil.findField(loader, "pathList");
            //拿到pathList实例,实际就是DexPathList类 
            Object dexPathList = pathListField.get(loader);
            ArrayList suppressedExceptions = new ArrayList();
            ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
                new ArrayList(additionalClassPathEntries), optimizedDirectory,
                suppressedExceptions));
            if (suppressedExceptions.size() > 0) {
                for (IOException e : suppressedExceptions) {
                    Log.w(TAG, "Exception in makeDexElement", e);
                    throw e;
                }
            }
        }

        /**
         * A wrapper around
         * {@code private static final dalvik.system.DexPathList#makeDexElements}.
         * files 是一个ArrayList列表,它对应的就是apk/dex/jar文件,因为我们可以指定多个文件。
         * optimizedDirectory 是前面传入dex的输出路径
         * suppressedExceptions 为一个异常列表 
         * makeDexElements()方法
         * 把本地的dex文件直接替换到Element[]数组中去,达到修复的目的。
         */
        private static Object[] makeDexElements(
            Object dexPathList, ArrayList files, File optimizedDirectory,
            ArrayList suppressedExceptions)
            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {

            Method makeDexElements = null;
            try {
                makeDexElements = ShareReflectUtil.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
                    ArrayList.class);
            } catch (NoSuchMethodException e) {
                Log.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure");
                try {
                    makeDexElements = ShareReflectUtil.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);
        }
    }

---------
//ShareReflectUtil类  
 public static Field findField(Object instance, String name) throws NoSuchFieldException {
        //例如 找"pathList" 
        //没有通过super父类找
        for (Class clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
            try {
                Field field = clazz.getDeclaredField(name);

                if (!field.isAccessible()) {
                    //打开权限
                    field.setAccessible(true);
                }

                return field;
            } catch (NoSuchFieldException e) {
                // ignore and search next
            }
        }

        throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
    }

//ShareReflectUtil类  中
 public static void expandFieldArray(Object instance, String fieldName, Object[] extraElements)
    throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
    //比如:DexPathList类中获取final Element[] dexElements
    //Element[] dexElem ents 存放的就是dex 
    //我们如果替换它是不是就是实现类更新!!!!

    Field jlrField = findField(instance, fieldName);

    //获取运行app中dex数组
    Object[] original = (Object[]) jlrField.get(instance);
    //复制到一个新的数组 数组长度为运行中dex的长度,和需要插入dex的数组长度
    Object[] combined = (Object[]) Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length);

    // NOTE: changed to copy extraElements first, for patch load first

    //先把要插入(需要更新)的dex数组 copy新数组中 
    System.arraycopy(extraElements, 0, combined, 0, extraElements.length);
    //再把运行中的dex数组 copy新数组中 
    System.arraycopy(original, 0, combined, extraElements.length, original.length);

    jlrField.set(instance, combined);
}

tinker -源码分析_第6张图片

TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
                Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed.apk");

 -----------------               

//TinkerInstaller.java
    /**
     * new patch file to install, try install them with :patch process
     *
     * @param context
     * @param patchLocation
     */
    public static void onReceiveUpgradePatch(Context context, String patchLocation) {
        Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);
    }

-----------------               

//DefaultPatchListener.java
    /**
     * when we receive a patch, what would we do?
     * you can overwrite it
     *
     * @param path path文件路径
     * @return
     */
     @Override
    public int onPatchReceived(String path) {
        File patchFile = new File(path);

        //对patch包验证,是不是加载过,版本号等
        int returnCode = patchCheck(path, SharePatchFileUtil.getMD5(patchFile));

        if (returnCode == ShareConstants.ERROR_PATCH_OK) {
            //通过Intent 发送给TinkerPatchService 处理
            TinkerPatchService.runPatchService(context, path);
        } else {
            Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
        }
        return returnCode;
    }

你可能感兴趣的:(热更新)