Android热修复
热修复对比
本文参考https://www.jianshu.com/p/5f390be47ce8
阿里开源地址AndFix:https://github.com/alibaba/AndFix
腾讯开源地址tinker:https://github.com/Tencent/tinker
阿里的AndFix、美团的Robust以及QZone的超级补丁方案
Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新
阿里AndFix
AndFix作为native解决方案,首先面临的是稳定性与兼容性问题,更重要的是它无法实现类替换,它是需要大量额外的开发成本的
缺点:
只能修改方法
- 不能增加方法
- 不能增加类
- 不能增加资源
使用方法就不介绍了,直接去GitHub地址上按照上面的方式接入,很简单几个步骤
apatch生成方式:
- 下载这个工具 apkpatch-1.0.3
- apkpatch -f <new> -t <old> -o <output> -k <keystore> -p <> -a <alias> -e <>
-a,--alias <alias> keystore entry alias.
-e,--epassword <***> keystore entry password.
-f,--from <loc> new Apk file path.
-k,--keystore <loc> keystore path.
-n,--name <name> patch name.
-o,--out <dir> output dir.
-p,--kpassword <***> keystore password.
-t,--to <loc> old Apk file path.
比如 我的demo的命令:
apkpatch.bat -f fix.apk -t old.apk -o out -k myjoke.jks -p 123456 -a key0 -e 123456
原理:新老apk生成apatch 插分包 ,启动加载apatch包,替换有BUG的方法
底层机制:
- 在方法上面加注解
@MethodReplace(clazz="com.qingguoguo.connotationjoke.MainActivity", method="onClick")
public void onClick(View paramView)
{
Log.i(MainActivity.TAG, "修复后");
ToastUtils.showShort("修复后:" + 1);
}
- 修复方法
static void replaceMethod(JNIEnv* env, jclass clazz, jobject src, jobject dest) {
if (isArt) {
art_replaceMethod(env, src, dest); }
else {
dalvik_replaceMethod(env, src, dest); }
}
- 4.4版本以后 isArt art架构 4.4版本前期dalvik架构
extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
jobject clazz = env->CallObjectMethod(dest, jClassMethod);
ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
dvmThreadSelf_fnPtr(), clazz);
clz->status = CLASS_INITIALIZED;
Method* meth = (Method*) env->FromReflectedMethod(src);
Method* target = (Method*) env->FromReflectedMethod(dest);
LOGD("dalvikMethod: %s", meth->name);
// meth->clazz = target->clazz;
meth->accessFlags |= ACC_PUBLIC;
meth->methodIndex = target->methodIndex;
meth->jniArgInfo = target->jniArgInfo;
meth->registersSize = target->registersSize;
meth->outsSize = target->outsSize;
meth->insSize = target->insSize;
meth->prototype = target->prototype;
meth->insns = target->insns;
// 错误的方法指向正确的方法
meth->nativeFunc = target->nativeFunc;
}
AndFix使用过程中注意事项
- 每次生成差分包后一定要测试;
- 尽量的不要分包,不要分多个dex
- 涉及到NDK AndFix.java 不能混淆
- 在加固,加壳之前生成差分包。
- 只能修复方法,不能增加成员变量,不能增加方法
仿tinker机制的热修复
底层原理是,通过替换dex文件的dexElements
通过Activity类的加载流程浅析,来引入
public final class ActivityThread {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
}
public Activity newActivity(ClassLoader cl, String className, Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
// 通过classLoader找到activity的calss,利用反射实例化对象 TestActivity
return (Activity)cl.loadClass(className).newInstance();
}
那看看这个loadClass()方法怎么来的
点进去后发现来到ClassLoader 类,而ClassLoader是一个抽象类
ClassLoader.png
那就看看它的上面newActivity传进来的是什么ClassLoader
public abstract class ClassLoader {
...
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
...
}
//上面传进newActivity的是
java.lang.ClassLoader cl = appContext.getClassLoader();
是来自ContextImpl 类的方法返回的是 ClassLoader.getSystemClassLoader()
继续看源码
class ContextImpl extends Context {
...
@Override
public ClassLoader getClassLoader() {
return mClassLoader != null ? mClassLoader :
(mPackageInfo != null ? mPackageInfo.getClassLoader() :
ClassLoader.getSystemClassLoader());
}
...}
public abstract class ClassLoader {
static private class SystemClassLoader {
public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}
...
@CallerSensitive
public static ClassLoader getSystemClassLoader() {
return SystemClassLoader.loader;
}
...
/**
* Encapsulates the set of parallel capable loader types.
*/
private static ClassLoader createSystemClassLoader() {
String classPath = System.getProperty("java.class.path", ".");
String librarySearchPath = System.getProperty("java.library.path", "");
// String[] paths = classPath.split(":");
// URL[] urls = new URL[paths.length];
// for (int i = 0; i < paths.length; i++) {
// try {
// urls[i] = new URL("file://" + paths[i]);
// }
// catch (Exception ex) {
// ex.printStackTrace();
// }
// }
//
// return new java.net.URLClassLoader(urls, null);
// TODO Make this a java.net.URLClassLoader once we have those?
return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}
...
}
找到了最后还是ClassLoader类创建了PathClassLoader
说明最后传进newActivity()方法的是PathClassLoader的对象
类的关系是:
PathClassLoader---->BaseDexClassLoader --->ClassLoader
loadClass()方法在ClassLoader类中
ClassLoader类中loadClass()方法,最后调用了BaseDexClassLoader()中的findClass()方法
实际是通过BaseDexClassLoader类的属性pathList来加载类
protected Class<?> loadClass(String name, boolean resolve)
359 throws ClassNotFoundException
360 {
361 // First, check if the class has already been loaded
362 Class c = findLoadedClass(name);
363 if (c == null) {
364 long t0 = System.nanoTime();
365 try {
366 if (parent != null) {
367 c = parent.loadClass(name, false);
368 } else {
369 c = findBootstrapClassOrNull(name);
370 }
371 } catch (ClassNotFoundException e) {
372 // ClassNotFoundException thrown if class not found
373 // from the non-null parent class loader
374 }
375
376 if (c == null) {
377 // If still not found, then invoke findClass in order
378 // to find the class.
379 long t1 = System.nanoTime();
380 c = findClass(name);
381
382 // this is the defining class loader; record the stats
383 }
384 }
385 return c;
386 }
public class BaseDexClassLoader extends ClassLoader {
30 private final DexPathList pathList;
31
32 /**
33 * Constructs an instance.
34 *
35 * @param dexPath the list of jar/apk files containing classes and
36 * resources, delimited by {@code File.pathSeparator}, which
37 * defaults to {@code ":"} on Android
38 * @param optimizedDirectory directory where optimized dex files
39 * should be written; may be {@code null}
40 * @param librarySearchPath the list of directories containing native
41 * libraries, delimited by {@code File.pathSeparator}; may be
42 * {@code null}
43 * @param parent the parent class loader
44 */
45 public BaseDexClassLoader(String dexPath, File optimizedDirectory,
46 String librarySearchPath, ClassLoader parent) {
47 super(parent);
48 this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
49 }
50
51 @Override
52 protected Class<?> findClass(String name) throws ClassNotFoundException {
53 List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
54 Class c = pathList.findClass(name, suppressedExceptions);
55 if (c == null) {
56 ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
57 for (Throwable t : suppressedExceptions) {
58 cnfe.addSuppressed(t);
59 }
60 throw cnfe;
61 }
62 return c;
63 }
64
Class c = pathList.findClass(name, suppressedExceptions);
DexPathList类中的属性dexElements是用来保存dex的数组
每个dex文件其实就是DexFile对象
遍历dexElements,然后通过DexFile去加载class文件
加载成功就返回,否则返回null,
*/
50/*package*/ final class DexPathList {
51 private static final String DEX_SUFFIX = ".dex";
52 private static final String zipSeparator = "!/";
55 private final ClassLoader definingContext;
62 private Element[] dexElements;
65 private final Element[] nativeLibraryPathElements;
68 private final List<File> nativeLibraryDirectories;
69
71 private final List<File> systemNativeLibraryDirectories;
76 private IOException[] dexElementsSuppressedExceptions;
public Class findClass(String name, List<Throwable> suppressed) {
414 for (Element element : dexElements) {
415 DexFile dex = element.dexFile;
416
417 if (dex != null) {
418 Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
419 if (clazz != null) {
420 return clazz;
421 }
422 }
423 }
424 if (dexElementsSuppressedExceptions != null) {
425 suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
426 }
427 return null;
428 }
429
看到了吗?
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
类的加载最后是同过dex.loadClassBinaryName
而dex是来自于dexElements
顺着来说,也就是类的加载,是先从类加载器的PathList的dexElements属性中,获取dex文件
然后遍历出要找的类,找到就返回,找不到就返回null
那我们就可以在这里做手脚了,理一下思路,怎么操作
从服务器获取修复bug后的fix.dex文件
加载dexElements并且把它插入的正在运行的类加载器的dexElements最前面
这样findClass遍历的时候就会先找到没有bug的class !!!
//说到这里就在多扯几句话吧,类加载器不仅可用系统的,还可以自定义
//loadClass中的判断 if (c == null) 就用 c = findClass(name);
//类加载双亲委托模式 父加载器为null就走 c = findClass(name),而我们就可以重写findClass方法来实现
//自定义类加载器
//自定义类加载器的作用:
(1)加密:Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,
可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,
这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载
(2)从非标准的来源加载代码:如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,
从指定的来源加载类。
双亲委派模型的好处:
(1)主要是为了安全性,避免用户自己编写的类动态替换 Java的一些核心类,比如 String。
(2)同时也避免了类的重复加载,因为 JVM中区分不同类,不仅仅是根据类名,
相同的 class文件被不同的 ClassLoader加载就是不同的两个类。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);
}
}
return c;
}
原理大概就是这样,代码,文章最前面的链接里面有
也贴一个我自己写的GitHub地址吧
https://github.com/qingguoguo/ConnotationJoke