Android热修复

2018-03-28  本文已影响0人  青果果

热修复对比

本文参考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解决方案,首先面临的是稳定性与兼容性问题,更重要的是它无法实现类替换,它是需要大量额外的开发成本的

缺点:

只能修改方法

  1. 不能增加方法
  2. 不能增加类
  3. 不能增加资源
    使用方法就不介绍了,直接去GitHub地址上按照上面的方式接入,很简单几个步骤

apatch生成方式:

  1. 下载这个工具 apkpatch-1.0.3
  2. 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的方法
底层机制:

  1. 在方法上面加注解
@MethodReplace(clazz="com.qingguoguo.connotationjoke.MainActivity", method="onClick")
  public void onClick(View paramView)
  {
    Log.i(MainActivity.TAG, "修复后");
    ToastUtils.showShort("修复后:" + 1);
  }
  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);   }
}
  1. 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使用过程中注意事项

  1. 每次生成差分包后一定要测试;
  2. 尽量的不要分包,不要分多个dex
  3. 涉及到NDK AndFix.java 不能混淆
  4. 在加固,加壳之前生成差分包。
  5. 只能修复方法,不能增加成员变量,不能增加方法

仿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

上一篇下一篇

猜你喜欢

热点阅读