从java层实现Tinker热修复

Tinker热修复
代码中的注释别忘记看!!!!!



一、Android中的 类加载器

首先我们需要了解类加载器,我们要明白我们所有类的加载都是通过getClassLoader().loadClass();,这是我们开始热修复的重要前提。先提出一个问题,大家知道ClassLoader classLoader = getClassLoader();这个classLoader是哪个对象嘛?肯定不是ClassLoader,不然也没有必要说,其实是PathClassLoader,其实在Android中类加载器有很多个,如下

类名作用
BootClassLoaderAndroid系统启动的时候会使用这个来预加载常用类
PathClassLoader其实这个就是加载已经安装好的APK,也可以加载lib
DexClassLoader用来加载当前应用以外的(第三方的dex或者lib)

多讲一句:PathClassLoader和DexClassLoader都继承BaseDexClassLoader(继承ClassLoader)
下面我们通过阅读源码的方式来换个角度看 在线阅读源码链接
既然getClassLoader返回的是PathClassLoader,那我们就来看看到底是个啥东西。

  • PathClassLoader.class
1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package dalvik.system;
18
19/**
20 * Provides a simple {@link ClassLoader} implementation that operates on a list
21 * of files and directories in the local file system, but does not attempt to
22 * load classes from the network. Android uses this class for its system class
23 * loader and for its application class loader(s).
24 */
25 public class PathClassLoader extends BaseDexClassLoader {
26    /**
27     * Creates a {@code PathClassLoader} that operates on a given list of files
28     * and directories. This method is equivalent to calling
29     * {@link #PathClassLoader(String, String, ClassLoader)} with a
30     * {@code null} value for the second argument (see description there).
31     *
32     * @param dexPath the list of jar/apk files containing classes and
33     * resources, delimited by {@code File.pathSeparator}, which
34     * defaults to {@code ":"} on Android
35     * @param parent the parent class loader
36     */
37    public PathClassLoader(String dexPath, ClassLoader parent) {
38        super(dexPath, null, null, parent);
39    }
40
41    /**
42     * Creates a {@code PathClassLoader} that operates on two given
43     * lists of files and directories. The entries of the first list
44     * should be one of the following:
45     *
46     * <ul>
47     * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
48     * well as arbitrary resources.
49     * <li>Raw ".dex" files (not inside a zip file).
50     * </ul>
51     *
52     * The entries of the second list should be directories containing
53     * native library files.
54     *
55     * @param dexPath the list of jar/apk files containing classes and
56     * resources, delimited by {@code File.pathSeparator}, which
57     * defaults to {@code ":"} on Android
58     * @param librarySearchPath the list of directories containing native
59     * libraries, delimited by {@code File.pathSeparator}; may be
60     * {@code null}
61     * @param parent the parent class loader
62     */
63    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
64        super(dexPath, null, librarySearchPath, parent);
65    }
66}

我们发现这个类里面居然只有构造函数,我们很失望,但我们发现这个类是继承BaseDexClassLoader,所以你懂的。

  • BaseDexClassLoader.class
public class BaseDexClassLoader extends ClassLoader {
  .....
 private final DexPathList pathList;
 .......
 @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        //这里就是在根据全类名找相应的类
        
        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;
    }
........
}

这里我们先关注一下这个方法,我们会用到,这里重写了ClassLoader 的findclass 方法。

  • ClassLoader.class(记得看程序中注释
public abstract class ClassLoader {
.....
 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
           // First, check if the class has already been loaded
           //这里的意思是,检查当前的类是否已经加载过。
           //为什么要这样呢,因为我们的apk里其实有很多.dex后缀文件,每个dex文件里面存放了我们写
           //好的class文件,所以每当我们需要创建相应的class,就去遍历这些dex文件,然后进行实例
           //化,但是我们考虑这样一个问题,难道每次创建都需要去dex查询创建,这样就很耗性能,所以
           //把已经加载过的类会缓存起来,这样放边下次直接拿出类就可以使用。所以这个findLoadedClass
           //就是在曾经加载过的类中的缓存里查找是否加载过,加载过就直接拿出来。
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
               } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    //如果找不到,就去加载
                    c = findClass(name);
                }
            }
            return c;
    }
    .......
}

Class class = getClassLoader().loadClass(“包名+类名”);
大家看这个代码,所有类的加载都是通过这样的方法加载的,大家可以看上面的代码,这是博主截取的部分代码,这就是加载类的核心源码了,大家可以看到其中有一个findClass()。
上面说过getClassLoader返回的是PathClassLoader(继承BaseDexClassLoader),本质上findClass就是调用了BaseDexClassLoader.findclass()

所以重点来了,我们现在只要关注在BaseDexClassLoader的findclass实现方法就行,这里代码就再贴一下吧。

public class BaseDexClassLoader extends ClassLoader {
  .....
 private final DexPathList pathList;
 .......
 @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        //这里就是在根据全类名找相应的类
       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;
    }
........
}

这里发现又掉用了DexPathList.findclass(),所以再深入一步。

  • DexPathList .class
final class DexPathList {
.....
/**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
     //怎么理解这个变量呢?
     //上面我们说过,我们的apk里有多个dex文件,这些文件里有许多的class,
     //我们每次查找class都需要到这些dex文件里面查找。
     //所以这个数组其实就是我们每个dex文件,googole把每个dex文件
     //分装成了Element,所以说每个Element就代表了一个dex文件。
    private Element[] dexElements;

    public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
        	//这里在循环查找dex文件里的每个element是否为目标class
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }
.....
}

恭喜你,看到这的话,你已经基本了解了Android类加载的过程

在这里插入图片描述
你还不能放松,现在才到一半,你会疑问为什么热修复跟这个有什么关系,其实不然,因为我们知道我们加载类是去dex文件里去查找相应的class文件然后实例化,但假如现在有个helloActivity.class里面存在bug,我们应该怎么办,当然第一步就是解决bug,然后把这个类打包成dex格式,当然也可以是library,apk都行,这里我们就先按照dex格式说明,然后让os先去我们这个刚修改过的dex文件里查找helloActivity.class,这样就把bug解决掉了,但是有个问题让os先去我们这个刚修改过的dex文件里查找?这就要讲到HOOK技术了,我们需要通过Hook的技术手段,使得我们修改好的dex文件也能被os所识别到。
接下来,重头戏来了。

二、进行代码的实现

  • 第一步 :
    我们需要首先创建一个文件夹(fix),然后切换成project模式,在app/intermediates/javac/debug/这个目录下面找到你修改好生成的class文件(注意这里要带上包名的文件夹,比如全路径为
    fix/com/example/oicq/helloActivity.class),然后进行打包,参考下面的博客

把修复好的class文件打包成修复包

  • 第二步:就是需要让用户在后台下载这个修复包
    这一步你可通过服务器也好,手动上传的方式也行,只要能上传到你的手机上sd卡根目录就行。
    当然你也可以放在别的地方,我们这里放在sd卡下面,只是做一个缓存,我们下面的程序,会把上传的dex修复包移动到我们app私密的路径里,这样就防止用户删除我们修复包。
  • 第三步:进行hook操作,实现热修复。(需要在Application里调用startFix())
public class FixManager {
    //存储所有修复包路径
    private static HashSet<File> loadedDex = new HashSet<>();

    static {
        loadedDex.clear();
    }

    public static void startFix(Context context){
        //将这个修复包先移动一个安全不易被用户删除的目录/data/data/包名/odex
        File odex = context.getDir("odex",Context.MODE_PRIVATE);
        String name = "fix.dex";
        File file = new File(odex.getAbsolutePath(),name);
        if(file.exists()) file.delete();
        String filePath = file.getAbsolutePath();
        FileInputStream is = null;
        FileOutputStream os = null;
        try{
            //这里我手动把补丁包上传到sd卡路径根目录下(为了安全考虑)
            is = new FileInputStream(new File(Environment.getExternalStorageDirectory(),name));
            os = new FileOutputStream(filePath);
            int len = 0;
            byte[] buffer = new byte[1024];
            while((len = is.read(buffer))!=-1){
                os.write(buffer,0,len);
            }
            //开始修复
            FixDex(context);
        }catch(IOException e){
            e.printStackTrace();
        }finally{
            try{
                if(is!= null) is.close();
                if(os!= null) os.close();
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }

    private static void FixDex(Context context){
        if(null == context) return;
        File odex = context.getDir("odex",Context.MODE_PRIVATE);
        File[] fileList = odex.listFiles();
        for(File file:fileList){
            if(!file.getName().endsWith(".dex")) continue;
            loadedDex.add(file);
        }

        //创建外部dex文件的缓存目录
        String temFileDir = odex.getAbsolutePath() + File.separator+"tempFile";
        File temFile = new File(temFileDir);
        if(!temFile.exists()) temFile.mkdirs();

        for(File file:loadedDex) {
            try {
                PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
                //反射开始(先得到BaseDexClassLoader)
                Class<?> superClass = pathClassLoader.getClass().getSuperclass();
                //得到成员变量 pathList
                Field pathListField = superClass.getDeclaredField("pathList");
                //设置可以访问
                pathListField.setAccessible(true);
                //得到成员变量pathList的值(得到当前应用的值)
                Object pathListValue = pathListField.get(pathClassLoader);
                //获取到dexElements的成员变量
                Field dexElementsField = pathListValue.getClass().getDeclaredField("dexElements");
                dexElementsField.setAccessible(true);
                Object dexElementsValue = dexElementsField.get(pathListValue);
                //以上我们获取当前应用下所有的dex
                //接下来我们要获取我们的补丁包
                //首先要用classloader加载外部的dex文件,我们这里使用DexClassLoader,
                //具体原因看上面的表格解释
                //创建DexClassLoder
                DexClassLoader dexClassLoader = new DexClassLoader(file.getAbsolutePath(),temFile.getAbsolutePath(),null,context.getClassLoader());
                Object dexPathListValue = pathListField.get(dexClassLoader);
                Object dexFixedElementsValue = dexElementsField.get(dexPathListValue);
                //上面获取了外部补丁包的dex文件
                //接下就是进行合并了
                //这里我们需要把我们的外部dex文件放在当前应用dexlist的开始位置
                //这是为了让系统在我们的已经修复好bug里面找,这样就不会找到之后
                //存在问题的class了,还不看不懂,我等会儿画图就懂了

                //接下来进行合并
                int length1 = Array.getLength(dexElementsValue);
                int length2 = Array.getLength(dexFixedElementsValue);
                int newLength = length1 + length2;
                //创建合并后的数组
                Class<?> dataType = dexFixedElementsValue.getClass().getComponentType();
                Object newDexElements = Array.newInstance(dataType,newLength);
                for(int i=0;i<newLength;i++){
                    if(i<length2){
                        //先放我们修改的补丁包dex文件
                        Array.set(newDexElements,i,Array.get(dexFixedElementsValue,i));
                    }else{
                        //再放应用原来的dex文件
                        Array.set(newDexElements,i,Array.get(dexElementsValue,i));
                    }
                }
                //将合并后的数组赋值给当前应用的dexlist
                dexElementsField.set(pathListValue,newDexElements);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

三、 总结

现在我们就完成了修复,现在考虑两个问题,
问: 上面这段代码需要每次应用都需bu要执行嘛,
答:是的,每次都要。
原因: 因为我们每次应用重启,我们ClassLoader的生命周期是跟Application生命周期一致的,所以我们每次都需要修复,所以上面这段代码我们需要在Appliation里的onCreate方法才行。
综上:我们这次实现的Tinker热修复是从java的角度通过反射实现,需要应用重启才可以进行修复,这也是我们这次的缺陷。但你想实现应用不重启实现热更新就需要从NDK的角度实现了。有时间会出一篇。

下记:
这里讲一下,我这里没有考虑应用的混淆和加固,请注意,如果有的话,需要进行代码混淆逻辑的解析的。

PS:纯属个人理解,希望指正!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值