Tinker 是一个开源项目(Github链接),它是微信官方的 Android 热补丁解决方案,它支持动态下发代码、So 库以及资源,让应用能够在不需要重新安装的情况下实现更新。
当前市面的热补丁方案有很多,其中比较出名的有阿里的 AndFix、美团的 Robust 以及 QZone 的超级补丁方案。
1、AndFix作为native解决方案,首先面临的是稳定性与兼容性问题,更重要的是它无法实现类替换,它是需要大量额外的开发成本的;
2、Robust兼容性与成功率较高,但是它与AndFix一样,无法新增变量与类只能用做的bugFix方案;
3、Qzone方案可以做到发布产品功能,但是它主要问题是插桩带来Dalvik的性能问题,以及为了解决Art下内存地址问题而导致补丁包急速增大的。
特别是在Android N之后,由于混合编译的inline策略修改,对于市面上的各种方案都不太容易解决而Tinker热补丁方案不仅支持类、So以及资源的替换,它还是2.X-8.X(1.9.0以上支持8.X)的全平台支持。利用Tinker我们不仅可以用做bugfix,甚至可以替代功能的发布。Tinker已运行在微信的数亿Android设备上,那么为什么你不使用Tinker呢?
由于原理与系统限制,Tinker有以下已知问题:
1、Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件(1.9.0支持新增非export的Activity)
2、由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
在Android N上,补丁对应用启动时间有轻微的影响;
3、不支持部分三星android-21机型,加载补丁时会主动出"TinkerRuntimeException:checkDexInstall failed";
4、对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。
要了解tinker热更新原理就要先了解Android的类加载流程Android中虚拟机类加载流程图如下:
DexClassLoader 和 PathClassLoader
在Android中,ClassLoader是一个抽象类,实际开发过程中,一般是使用其具体的子类DexClassLoader、PathClassLoader这些类加载器来加载类的,不同之处是:
1、PathClassLoader:支持加载DEX或者已经安装的APK(因为存在缓存的DEX)
2、DexClassLoader:支持加载APK、DEX和JAR,也可以从SD卡进行加载。
这2个类都继承于BaseDexClassLoader, BaseDexClassLoader继承于ClassLoader。
DexClassLoader的构造方法:
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
PathClassLoader的构造方法:
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
BaseDexClassLoader的构造方法:
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
DexPathList的loadDexFile方法
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
private static String optimizedPathFor(File path,File optimizedDirectory) {
String fileName = path.getName();
if (!fileName.endsWith(DEX_SUFFIX)) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot < 0) {
fileName += DEX_SUFFIX;
} else {
StringBuilder sb = new StringBuilder(lastDot + 4);
sb.append(fileName, 0, lastDot);
sb.append(DEX_SUFFIX);
fileName = sb.toString();
}
}
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}
由上可知:
1、optimizedDirectory是用来缓存需要加载的dex文件的,并创建一个DexFile对象,如果它为null,那么会直接使用dex文件原有的路径来创建DexFile对象。
2、optimizedDirectory必须是一个内部存储路径,无论哪种动态加载,加载的可执行文件一定要存放在内部存储。DexClassLoader可以指定自己的optimizedDirectory,所以它可以加载外部的dex,因为这个dex会被复制到内部路径的optimizedDirectory。
3、PathClassLoader没有optimizedDirectory,所以它只能加载内部的dex,这些大都是存在系统中已经安装过的apk里面的。
首先给出tinker官方的热更新原理图:
由上图可以看出tinker的主要流程是:
1、通过生成的fix.dex,也就是修复包的dex文件与base.dex也就是已经发布出去的需要修复的dex文件进行一个对比,生成patch.dex文件。
2、然后通过patch.dex文件与classes.dex文件合并生成新的fix_classes.dex文件代替掉原来的classes.dex文件。
3、将合成后的全量dex 插入到dex elements前面,完成修复
public Intent tryLoad(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag) {
Intent resultIntent = new Intent();
long begin = SystemClock.elapsedRealtime();
tryLoadPatchFilesInternal(app, tinkerFlag, tinkerLoadVerifyFlag, resultIntent); //在tryLoadPatchFilesInternal中首先要进行环境校验,完成校验流程后再加载补丁,校验的详细内容不展开讨论
long cost = SystemClock.elapsedRealtime() - begin;
ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
return resultIntent;
}
tinker中的类加载器TinkerDexLoader
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public static boolean loadTinkerJars(Application application, boolean tinkerLoadVerifyFlag, String directory, Intent intentResult, boolean isSystemOTA) {
PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();
String dexPath = directory + "/" + DEX_PATH + "/";
File optimizeDir = new File(directory + "/" + DEX_OPTIMIZE_PATH);
ArrayList<File> legalFiles = new ArrayList<>();
final boolean isArtPlatForm = ShareTinkerInternals.isVmArt();
// 获取合法的文件列表
for (ShareDexDiffPatchInfo info : dexList) {
//for dalvik, ignore art support dex
if (isJustArtSupportDex(info)) {
continue;
}
String path = dexPath + info.realName;
File file = new File(path);
}
legalFiles.add(file);
}
//isSystemOTA判断,如果用户是ART环境并且做了OTA升级,加载dex补丁的时候首先将最近一次的补丁全部DexFile.loadDex一遍.之所以这样做是因为有些场景做了OTA后,OTA的规则可能发生变化,在这种情况下去加载上个系统版本oat过的dex就会出现问题.
if (isSystemOTA) {
parallelOTAResult = true;
parallelOTAThrowable = null;
Log.w(TAG, "systemOTA, try parallel oat dexes!!!!!");
TinkerParallelDexOptimizer.optimizeAll(
legalFiles, optimizeDir,
new TinkerParallelDexOptimizer.ResultCallback() {
long start;
@Override
public void onSuccess(File dexFile, File optimizedDir, File optimizedFile) {
// Do nothing.
Log.i(TAG, "success to optimize dex " + dexFile.getPath() + "use time " + (System.currentTimeMillis() - start));
}
@Override
public void onFailed(File dexFile, File optimizedDir, Throwable thr) {
parallelOTAResult = false;
parallelOTAThrowable = thr;
Log.i(TAG, "fail to optimize dex " + dexFile.getPath() + "use time " + (System.currentTimeMillis() - start));
}
}
);
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, parallelOTAThrowable);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_PARALLEL_DEX_OPT_EXCEPTION);
return false;
}
}
try {
//接下来就是调用SystemClassLoaderAdder的installDexes方法
SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);
} catch (Throwable e) {
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION);
return false;
}
return true;
}
加载patch.dex过程,在tinker中针对不同的版本有不同的加载代码下面是版本23的加载代码:
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
Field pathListField = ShareReflectUtil.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader); //通过反射拿到classloader的patchlist变量
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// 通过反射获取pathList的dexElements参数,把经过合并后的DexElements设置为pathList的dexElements。
ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,suppressedExceptions));
}
private static Object[] makePathElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Method makePathElements;
try {
//反射pathList的makeDexElements方法,传入插件补丁dexList路径与优化过的opt目录,通过这个方法生成一个新的DexElements,这个DexElements为插件的DexElements。
makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class,
List.class);
} catch (NoSuchMethodException e) {
Log.e(TAG, "NoSuchMethodException: makePathElements(List,File,List) failure");
try {
makePathElements = ShareReflectUtil.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);
}
}
上面代码主要是:
通过反射获取到patchlist的dexElements字段,然后将合成补丁后的dex文件插入到dexelements数组的前面,这样打了补丁后的dex就可以先记载到从而完成修复工作。
1、http://www.tinkerpatch.com/Docs/intro
2、https://github.com/Tencent/tinker
3、https://www.jianshu.com/p/2216554d3291