Tinker 思想全量替换新的Dex。它更像是APP的增量更新,
在服务器端通过差异性算法,计算出新旧dex之间的差异包,推送到客户端,进行合成
区别在于不再将patch.dex增加到elements数组中,而是差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并,然后整体替换掉旧的DEX文件,以达到修复的目的。
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文件
想法 :
通过反射拿到存放Dex数组,替换dex ,来达到热更新的效果 。
BaseDexClassLoader
–>PathDexList
–>findClass方法 遍历DexElement数组
找到对应类就直接返回
DexElement 是存放dex的数组
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方法。
//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);
}
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;
}