美文网首页Android开发经验谈程序员android
Android multidex 主dex是怎么来的?

Android multidex 主dex是怎么来的?

作者: IMSk | 来源:发表于2018-05-25 14:08 被阅读88次

    先提一下(gradle assembleDebug)编译过程中关键产物
    build/intermediates/multi-dex/debug 目录中,可以看到如下几个文件


    image.png

    componentClasses.jar ----> 经过shrinkWithProguard得到(jarOfRoots.jar)
    components.flags
    maindexlist.txt ----> 通过一些列操作,计算出来的一个列表,记录放入主dex中的所有class (说个不好听的如果你能hook掉这个文件的写入,那么想让谁在主dex, 谁就在主dex)
    manifest_keep.txt

    一: 依赖两个核心模块

    1. gradle 插件 (本文基于3.0.0)(编译项目)

    https://android.googlesource.com/platform/tools/gradle/+/gradle_3.0.0

    后面会讨论两个地方:

      1. AndroidBuilder.createMainDexList
      1. MultiDexTransform.transform

    2. dalvik-dx (处理和dex相关逻辑)

    https://android.googlesource.com/platform/dalvik/+/6a8e552/dx

    后面会讨论一个地方

      1. ClassReferenceListBuilder

    二: 生成主dex的核心流程

    1. gradle 插件篇

    • AndroidBuilder

    收集我们项目配置的build.gradle等基本编译信息,以及后续的 createMainDexList

    • MultiDexTransform -> transform :

    核心代码入口

         @Override
        public void transform(@NonNull TransformInvocation invocation)
                throws IOException, TransformException, InterruptedException {
            // Re-direct the output to appropriate log levels, just like the official ProGuard task.
            LoggingManager loggingManager = invocation.getContext().getLogging();
            loggingManager.captureStandardOutput(LogLevel.INFO);
            loggingManager.captureStandardError(LogLevel.WARN);
    
            try {
                File input = verifyInputs(invocation.getReferencedInputs());
                //-->1 把所有的class文件经过 Proguard (runProguard)处理之后,得到 jarOfRoots.jar 
                shrinkWithProguard(input);
               //-->2 通过上一步生成的 rootJars.jar, 计算出mainDexList
                computeList(input);
            } catch (ParseException | ProcessException e) {
                throw new TransformException(e);
            }
        }
    
    • MultiDexTransform -> transform -> shrinkWithProguard

    把所有的class文件经过 Proguard (runProguard)处理之后,得到 jarOfRoots.jar ,即:variantScope.getProguardComponentsJarFile()

        private void shrinkWithProguard(@NonNull File input) throws IOException, ParseException {
             //-->1 一大堆混淆的配置。。。
            configuration.obfuscate = false;
            configuration.optimize = false;
            configuration.preverify = false;
            dontwarn();
            dontnote();
            forceprocessing();
    
           //-->2 把manifest_keep.txt中的内容加过来
            applyConfigurationFile(manifestKeepListProguardFile);
            if (userMainDexKeepProguard != null) {
                 //-->3 如果在项目中有自定义想放入主dex的keep(multiDexKeepProguard file('./maindex-rules.pro')),也追加进来
                applyConfigurationFile(userMainDexKeepProguard);
            }
            //-->4  3.0.0的插件默认就帮我们keep了一些
            // add a couple of rules that cannot be easily parsed from the manifest.
            keep("public class * extends android.app.Instrumentation { <init>(); }");
            keep("public class * extends android.app.Application { "
                    + "  <init>(); "
                    + "  void attachBaseContext(android.content.Context);"
                    + "}");
            keep("public class * extends android.app.backup.BackupAgent { <init>(); }");
            keep("public class * extends java.lang.annotation.Annotation { *;}");
            keep("class com.android.tools.ir.** {*;}"); // Instant run.
           
            //-->5 把Android的jar引入进来放入path中
            // handle inputs
            libraryJar(findShrinkedAndroidJar());
            //-->6 把所有的class引入进来放入path中
            inJar(input, null);
            //-->7 设置产物的路径
            // outputs.
            outJar(variantScope.getProguardComponentsJarFile());
            printconfiguration(configFileOut);
           
             //-->8 最终执行混淆
            // run proguard
            runProguard();
        }
    
    • MultiDexTransform -> transform -> computeList

    把上一步生成 jarOfRoots.jar 以及所有的class 通过 callDx 处理之后,计算出mainDexClasses, 如果项目中自己配置了需要放在主dex的类(MainDexKeepFile),这里会读取出来追加到mainDexClasses中, 最终写到一个mainDexListFile文件中

        private void computeList(File _allClassesJarFile) throws ProcessException, IOException {
            // manifest components plus immediate dependencies must be in the main dex.
            Set<String> mainDexClasses = callDx(
                    _allClassesJarFile,
                    variantScope.getProguardComponentsJarFile());
    
            if (userMainDexKeepFile != null) {
                mainDexClasses = ImmutableSet.<String>builder()
                        .addAll(mainDexClasses)
                        .addAll(Files.readLines(userMainDexKeepFile, Charsets.UTF_8))
                        .build();
            }
    
            String fileContent = Joiner.on(System.getProperty("line.separator")).join(mainDexClasses);
    
            Files.write(fileContent, mainDexListFile, Charsets.UTF_8);
    
        }
    
    • MultiDexTransform -> transform -> computeList -> callDx

    callDex 顾名思义就是调用 Dex 返回一个需要放在主dex的列表, 其实最终又会调用刚才提到的AndroidBuilder -> createMainDexList

      private Set<String> callDx(File allClassesJarFile, File jarOfRoots) throws ProcessException {
            EnumSet<AndroidBuilder.MainDexListOption> mainDexListOptions =
                    EnumSet.noneOf(AndroidBuilder.MainDexListOption.class);
            if (!keepRuntimeAnnotatedClasses) {
                mainDexListOptions.add(
                        AndroidBuilder.MainDexListOption.DISABLE_ANNOTATION_RESOLUTION_WORKAROUND);
                Logging.getLogger(MultiDexTransform.class).warn(
                        "Not including classes with runtime retention annotations in the main dex.\n"
                                + "This can cause issues with reflection in older platforms.");
            }
          //这里 最终又会调用刚才提到的`AndroidBuilder -> createMainDexList`
            return variantScope.getGlobalScope().getAndroidBuilder().createMainDexList(
                    allClassesJarFile, jarOfRoots, mainDexListOptions);
        }
    
    • AndroidBuilder -> createMainDexList

    调用 dex.jar 中的 ClassReferenceListBuilder ,找出哪些需要放在主dex中的class,需要传入的参数是所有的class文件、通过 shrinkWithProguard 之后得到的jarOfRoots.jar 以及一个MainDexListOption配置

        public Set<String> createMainDexList(
                @NonNull File allClassesJarFile,
                @NonNull File jarOfRoots,
                @NonNull EnumSet<MainDexListOption> options) throws ProcessException {
    
            BuildToolInfo buildToolInfo = mTargetInfo.getBuildTools();
            ProcessInfoBuilder builder = new ProcessInfoBuilder();
    
            String dx = buildToolInfo.getPath(BuildToolInfo.PathId.DX_JAR);
            if (dx == null || !new File(dx).isFile()) {
                throw new IllegalStateException("dx.jar is missing");
            }
    
            builder.setClasspath(dx);
            builder.setMain("com.android.multidex.ClassReferenceListBuilder");
    
            if (options.contains(MainDexListOption.DISABLE_ANNOTATION_RESOLUTION_WORKAROUND)) {
                builder.addArgs("--disable-annotation-resolution-workaround");
            }
    
            builder.addArgs(jarOfRoots.getAbsolutePath());
            builder.addArgs(allClassesJarFile.getAbsolutePath());
    
            CachedProcessOutputHandler processOutputHandler = new CachedProcessOutputHandler();
    
            mJavaProcessExecutor.execute(builder.createJavaProcess(), processOutputHandler)
                    .rethrowFailure()
                    .assertNormalExitValue();
    
            LineCollector lineCollector = new LineCollector();
            processOutputHandler.getProcessOutput().processStandardOutputLines(lineCollector);
            return ImmutableSet.copyOf(lineCollector.getResult());
        }
    

    2. dalvik-dx 篇

    gradle 插件中已经准备好了jarOfRoots和所有class文件,现在该dex上场了的。

    • ClassReferenceListBuilder

    其实在我们的SDK环境中的build-tools下能找到一个脚本 mainDexClasses ,比如build-tools/26.0.2。里面最后一行就有调用,调用方式和刚才的gradle插件类似。
    java -cp "$jarpath" com.android.multidex.MainDexListBuilder ${disableKeepAnnotated} "${tmpOut}" ${@} || exit 11

    这个类的作用是什么呢?顾名思义找出class的引用。那么怎么做到的呢?

    • ClassReferenceListBuilder -> main
      先来看主入口,main函数做了三件事情:
    1. 拿到刚才传入的 jarOfRoots.jar
    2. 拿到刚才传入的所有class文件
    3. 开始干活了的 构建了一个ClassReferenceListBuilder,调用addRoots

    代码如下:

       public static void main(String[] args) {
             ......
          // 1. 拿到刚才传入的 jarOfRoots.jar
            ZipFile jarOfRoots;
            try {
                jarOfRoots = new ZipFile(args[0]);
            } catch (IOException e) {
                System.err.println("\"" + args[0] + "\" can not be read as a zip archive. ("
                        + e.getMessage() + ")");
                System.exit(STATUS_ERROR);
                return;
            }
    
        
            Path path = null;
            try {
              // 2. 拿到刚才传入的所有class文件
                path = new Path(args[1]);
              // 3. 开始干活了的 构建了一个ClassReferenceListBuilder
                ClassReferenceListBuilder builder = new ClassReferenceListBuilder(path);
                builder.addRoots(jarOfRoots);
    
                printList(builder.toKeep);
            } catch (IOException e) {
                System.err.println("A fatal error occured: " + e.getMessage());
                System.exit(STATUS_ERROR);
                return;
            } finally {
                try {
                    jarOfRoots.close();
                } catch (IOException e) {
                    // ignore
                }
                if (path != null) {
                    for (ClassPathElement element : path.elements) {
                        try {
                            element.close();
                        } catch (IOException e) {
                            // keep going, lets do our best.
                        }
                    }
                }
            }
        }
    
    • ClassReferenceListBuilder -> main -> addRoots

    把 jarOfRoots中的class文件都keep住,以及这些class直接依赖的class文件从刚才传入的所有class文件中找出来并且也keep住。这样就构成了一个主的需要keep的class列表用以生成主dex.

        public void addRoots(ZipFile jarOfRoots) throws IOException {
    
            // keep roots
            for (Enumeration<? extends ZipEntry> entries = jarOfRoots.entries();
                    entries.hasMoreElements();) {
                ZipEntry entry = entries.nextElement();
                String name = entry.getName();
                if (name.endsWith(CLASS_EXTENSION)) {
                    toKeep.add(name.substring(0, name.length() - CLASS_EXTENSION.length()));
                }
            }
    
            // keep direct references of roots (+ direct references hierarchy)
            for (Enumeration<? extends ZipEntry> entries = jarOfRoots.entries();
                    entries.hasMoreElements();) {
                ZipEntry entry = entries.nextElement();
                String name = entry.getName();
                if (name.endsWith(CLASS_EXTENSION)) {
                    DirectClassFile classFile;
                    try {
                        classFile = path.getClass(name);
                    } catch (FileNotFoundException e) {
                        throw new IOException("Class " + name +
                                " is missing form original class path " + path, e);
                    }
    
                    addDependencies(classFile.getConstantPool());
                }
            }
        }
    

    相关文章

      网友评论

      • 我才是孙悟空:比如我自己有个apk包,也知道秘钥什么的。 我想往里面加java逻辑代码, 是反编译,往dex里面加.class吗,有没有类似的博文或者解决思路推荐一下 ,大神
        我才是孙悟空:@IMSk apk内应该会预留一些接口。如果需要这个功能,拿到apk,反编译,加入资源文件等,然后就是java逻辑代码
        我才是孙悟空:@IMSk 不能说是篡改吧,因为是自己的apk,有些部分是公共的,我想提取出来,做个一键加入这个功能,使功能脱离apk。之后需要这个功能apk,一键操作一下 ,差不多是这个意思,
        IMSk:@我才是孙悟空 听着意思是反编译之后篡改apk?
      • IT人故事会:太感谢了,写的不错!
        IMSk:@IT人故事会 后面还会补充一些 这个比较大概的流程

      本文标题:Android multidex 主dex是怎么来的?

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