美文网首页
AndroidMultidex热修复CLASS_ISPREVER

AndroidMultidex热修复CLASS_ISPREVER

作者: ModestStorm | 来源:发表于2020-06-12 17:10 被阅读0次

    1.为什么会出现这个问题呢?

    注意:在5.0之前会有这个问题,5.0之后没有了

    1.在apk安装的时候,虚拟机会将dex优化成odex后才拿去执行。在这个过程中会对所有class一个校验。
    2.校验方式:假设A类在它的static方法,private方法,构造函数,override方法中直接引用到B类。如果A类和B类在同一个dex中,那么A类就会被打上CLASS_ISPREVERIFIED标记
    被打上这个标记的类不能引用其他dex中的类,否则就会报CLASS_ISPREVERIFIED

    2.如何防止我们源代码中所有的类被打上CLASS_ISPPREVERIFIED标记?

    理论上,一个android工程中所有的java类(除了Application之外)都有可能需要热修复。如果让这些类都去引用一个另一个dex文件之下的class,就能防止在dex解析的时候被打CLASS_ISPPREVERIFIED标记。
    但是这样有一个弊端,就是 CLASS_ISPPREVERIFIED带来的性能提升将会消失。但是既然出现bug,要解决,总要付出一点代价。

    2.1:创建一个空白java类 AntilazyLoad。编译它,得到 AntilazyLoad.class 然后用dx命令,将它打包成hack.dex

    2.2:使用gradle插桩的方式,干涉gradle打包流程,在生成javac命令之后,在dx命令之前,在所有我们编写的所有class里面的构造函数内部,加上 AntilazyLoad 的直接使用(反射引用是不行的)。

    2.3:gradle执行项目构建,是通过一个一个的task来进行。比如 将java文件用javac命令编译为 class,任务名字叫做::app:compileDebugJavaWithJavac


    task任务执行图

    我们进行插桩的时机,便是上图中javac之后,dx之前。 另外,任何一个Task,都有input元素和output元素,以及可以设置doFirst闭包,表示执行任务之前先执行一段逻辑,设置doLast,表示执行任务执行之后再执行一段逻辑。

    task input

    2.4:HotfixPlugin.java 作为gradle插件的核心类,其关键代码如下:

    project.afterEvaluate(new Action<Project>() {
        @Override
            public void execute(Project project) {
           //找到额外属性
                final HotfixExt hotfixExt = project.getExtensions().findByType(HotfixExt.class);
            // 找到系统属性
                AppExtension appExtension = project.getExtensions().findByType(AppExtension.class);
                DomainObjectSet<ApplicationVariant> applicationVariants = appExtension.getApplicationVariants();
                for (ApplicationVariant var : applicationVariants) {
                final String variantName = var.getName();
           //debug  release 因为任务的名字是release/debug有关,我们要找到确切的切入点,就必须拿到这个值
                        final String myTaskName = "transformClassesWithDexBuilderFor" + 
                        firstCharUpperCase(variantName);
                        final Task task = project.getTasks().findByName(myTaskName);
                        task.doFirst(new Action<Task>() {
          @Override
               public void execute(Task task) {
                 System.out.println("\n\n\n=================task.doFirst=================\n\n\n");
                 Set<File> files = task.getInputs().getFiles().getFiles();
                         for (File file : files) {
                         String filePath = file.getAbsolutePath();
                         if (filePath.endsWith(".jar")) {
                          processJar(file);
                          } else if (filePath.endsWith(".class")) {
                           processClass(variantName, file); //对于class的处理完毕
                         }
                        }
                 System.out.println("\n\n\n=================task.doFirst   end=================\n\n\n");
                        }
                      });
                    }
                 System.out.println("=================end=================");
               }
             });
          }
    

    我们的思路是 在java变成class之后,在class变成 dex之前,将class进行ASM插桩。所以,我们要找的 gradle task 是 : transformClassesWithDexBuilderForRelease 或者 transformClassesWithDexBuilderForDebug 给它重写doFirst。 也可以 找到 gradle task : compileReleaseJavaWithJavac 或者 compileDebugJavaWithJavac. 给它重写 doLast。效果相同。

    2.5:开始重写doFirst,所有task都有input输入和output输出。我们这里获取它的输入getInputs(). 然后进行文件遍历。发现,既有jar文件也有class文件。jar文件是class的压缩包。要进行插装,必须分别处理。class文件直接插桩。jar文件解压缩之后插桩。


    获取输出

    2.6: class文件的插桩。
    注:有些class,不需要热修复,也就不需要插桩,比如android support包,或者androidx兼容包。比如MyApplication类。

    private void processClass(String variantName, File file) {
            String path = file.getAbsolutePath();//拿到完整路径,如下:
            // D:\studydemo\hotfix\HotUpdateDemo\app\build\intermediates\classes\debug\com\example\administrator\myapplication\MainActivity.class
            // 这么一大串,包括三个部分,以debug为分界。
            // D:\studydemo\hotfix\HotUpdateDemo\app\build\intermediates\classes\ 是目录
            // debug\ 是编译变体名
            // com\example\administrator\myapplication\MainActivity.class 类完整路径
            //将他进行分割
            String className = path.split(variantName)[1].substring(1);
    //        System.out.println("className:" + className);//拿到完整类名 com\example\administrator\myapplication\MainActivity.class
            // 由于有些class我们不用执行插桩,包括Application,也包括 androidx和support包
            if (isAndroidClz(className) || isApplicationClz(className)) {
                return;
            }
            // 能走到这里的,都是需要插桩的,那么,在这个任务执行时,我需要:
            // 使用文件流
            try {
                FileInputStream fis = new FileInputStream(path);
                byte[] byteCode = referHackWhenInit(fis);
                fis.close();
    
                FileOutputStream fos = new FileOutputStream(path);
                fos.write(byteCode);
                fos.close();
    
                //成功给class加了一行代码
                System.out.println("className:" + className + "植入hack成功");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    jar文件的插桩:

    private void processJar(File file) {
            try {
                // 先预备一个备份文件
                File bakJar = new File(file.getParent(), file.getName() + ".bak");
                JarOutputStream jos = new JarOutputStream(new FileOutputStream(bakJar));
                JarFile jarFile = new JarFile(file);
                Enumeration<JarEntry> entries = jarFile.entries(); // 准备遍历
                while (entries.hasMoreElements()) {
                    JarEntry jarEntry = entries.nextElement(); // 迭代器遍历
    
                    jos.putNextEntry(new JarEntry(jarEntry.getName()));
                    InputStream is = jarFile.getInputStream(jarEntry);
    
                    String className = jarEntry.getName();
                    if (className.endsWith(".class") && !isApplicationClz(className)
                            && !isAndroidClz(className)) {
                        byte[] byteCode = referHackWhenInit(is);
                        jos.write(byteCode);
                    } else {
                        //输出到临时文件
                        jos.write(IOUtils.toByteArray(is));
                    }
                    jos.closeEntry();
                }
                jos.close();
                jarFile.close();
                file.delete();
                bakJar.renameTo(file);
                //成功给class加了一行代码
                System.out.println("jarName:" + file.getAbsolutePath() + "植入hack成功");
            } catch (Exception e) {
    
            }
        }
    

    2.7:使用javassit或者asm插桩,在构造函数中插入:

    System.out.println(AntilazyLoad.class);这行代码
    

    2.8:将生成的hack.dex放入assets目录下,在app启动的时候(因为applicatio是启动类所以它里面不能加入System.out.println(AntilazyLoad.class);不然会报找不到,那时hackDex还没加载呢)优先加载hack.dex到pathClassLoader中的成员变量dexPathList中的Elements[]数组中的第一个元素中。这样下面所有的class.dex就可以加载了。

    相关文章

      网友评论

          本文标题:AndroidMultidex热修复CLASS_ISPREVER

          本文链接:https://www.haomeiwen.com/subject/wyhntktx.html