Gradle1.5.0之后如何控制dex包内的方法数上限?

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lizhen3125/article/details/51911989
最近项目方法数量超过6w了,该考虑分包的实现了。
参考各位大神的文章,结合目前项目的情况,最后决定按照FB的思路来实现分包策略。具体的不在这里详述。
参考文章地址:

Android Dex分包之旅–鱼动动是一只码农BBB

dex 分包变形记–李金涛

其实你不知道MultiDex到底有多坑–总悟君

分包的问题解决之后,在通过控制dex包内方法数量来解决LinearAlloc4M缓存问题的时候遇到了困难。
之前大家的思路都是在dextask执行的时候通过传入参数来实现,代码如下:
afterEvaluate {
tasks.matching {
    it.name.startsWith('dex')
}.each { dx ->
    if (dx.additionalParameters == null) {
        dx.additionalParameters = []
    }
    dx.additionalParameters += '--multi-dex'
    dx.additionalParameters += '--set-max-idx-number=48000'
}
但在我们项目里报以下错误:
Error:(32, 0) Access to the dex task is now impossible, starting with 1.4.0
1.4.0 introduces a new Transform API allowing manipulation of the .class files.
See more information: http://tools.android.com/tech-docs/new-build-system/transform-api
Open File

后来通过查询gradle-build system的1.5.0-beta1的changelog发现了以下的信息:

Starting with 1.5.0-beta1, the Gradle plugin includes a Transform
API allowing 3rd party plugins to manipulate compiled class files
before they are converted to dex files. (The API existed in
1.4.0-beta2 but it’s been completely revamped in 1.5.0-beta1)

The goal of this API is to simplify injecting custom class
manipulations without having to deal with tasks, and to offer more
flexibility on what is manipulated. The internal code processing
(jacoco, progard, multi-dex) have all moved to this new mechanism
already in 1.5.0-beta1. Note: this applies only to the javac/dx code
path. Jack does not use this API at the moment.

The API doc is here.

To insert a transform into a build, you simply create a new class
implementing one of the Transform interfaces, and register it with
android.registerTransform(theTransform) or
android.registerTransform(theTransform, dependencies).

Important notes: The Dex class is gone. You cannot access it anymore
through the variant API (the getter is still there for now but will
throw an exception) Transform can only be registered globally which
applies them to all the variants. We’ll improve this shortly. There’s
no way to control ordering of the transforms. We’re looking for
feedback on the API. Please file bugs or email us on our adt-dev
mailing list.

链接:http://tools.android.com/tech-docs/new-build-system/transform-api

简单来说就是gradle官方从1.5.0-beta1这个版本开始修改了关于分包参数的API,不再允许通过dex task传递参数的方式来控制dex内方法的数量,也无法指定主dex内保存的类了。那这个版本上有没有替代的方法呢。。通过查看gradle源码可以看到:
DexProcessBuilder内的build方法:

    @NonNull
    public JavaProcessInfo build(
            @NonNull BuildToolInfo buildToolInfo,
            @NonNull DexOptions dexOptions) throws ProcessException {

        checkState(
                !mMultiDex
                        || buildToolInfo.getRevision().compareTo(MIN_MULTIDEX_BUILD_TOOLS_REV) >= 0,
                "Multi dex requires Build Tools " +
                        MIN_MULTIDEX_BUILD_TOOLS_REV.toString() +
                        " / Current: " +
                        buildToolInfo.getRevision().toShortString());


        ProcessInfoBuilder builder = new ProcessInfoBuilder();
        builder.addEnvironments(mEnvironment);

        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.dx.command.Main");

        if (dexOptions.getJavaMaxHeapSize() != null) {
            builder.addJvmArg("-Xmx" + dexOptions.getJavaMaxHeapSize());
        } else {
            builder.addJvmArg("-Xmx1024M");
        }

        builder.addArgs("--dex");

        if (mVerbose) {
            builder.addArgs("--verbose");
        }

        if (dexOptions.getJumboMode()) {
            builder.addArgs("--force-jumbo");
        }

        if (mIncremental) {
            builder.addArgs("--incremental", "--no-strict");
        }

        if (mNoOptimize) {
            builder.addArgs("--no-optimize");
        }

        if (mNoStrict) {
            builder.addArgs("--no-strict");
        }

        // only change thread count is build tools is 22.0.2+
        if (buildToolInfo.getRevision().compareTo(MIN_MULTI_THREADED_DEX_BUILD_TOOLS_REV) >= 0) {
            Integer threadCount = dexOptions.getThreadCount();
            if (threadCount == null) {
                builder.addArgs("--num-threads=4");
            } else {
                builder.addArgs("--num-threads=" + threadCount);
            }
        }

        if (mMultiDex) {
            builder.addArgs("--multi-dex");

            if (mMainDexList != null ) {
                builder.addArgs("--main-dex-list", mMainDexList.getAbsolutePath());
            }
        }

        if (mAdditionalParams != null) {
            for (String arg : mAdditionalParams) {
                builder.addArgs(arg);
            }
        }


        builder.addArgs("--output", mOutputFile.getAbsolutePath());

        // input
        builder.addArgs(getFilesToAdd(buildToolInfo));

        return builder.createJavaProcess();
    }

我们可以通过mAdditionalParams的值的传入来控制,继续往上看:
AndroidBuilder的convertByteCode方法:

public void convertByteCode(
            @NonNull Collection<File> inputs,
            @NonNull File outDexFolder,
                     boolean multidex,
            @Nullable File mainDexList,
            @NonNull DexOptions dexOptions,
            @Nullable List<String> additionalParameters,
            boolean incremental,
            boolean optimize,
            @NonNull ProcessOutputHandler processOutputHandler)
            throws IOException, InterruptedException, ProcessException {
        checkNotNull(inputs, "inputs cannot be null.");
        checkNotNull(outDexFolder, "outDexFolder cannot be null.");
        checkNotNull(dexOptions, "dexOptions cannot be null.");
        checkArgument(outDexFolder.isDirectory(), "outDexFolder must be a folder");
        checkState(mTargetInfo != null,
                "Cannot call convertByteCode() before setTargetInfo() is called.");

        ImmutableList.Builder<File> verifiedInputs = ImmutableList.builder();
        for (File input : inputs) {
            if (checkLibraryClassesJar(input)) {
                verifiedInputs.add(input);
            }
        }

        BuildToolInfo buildToolInfo = mTargetInfo.getBuildTools();
        DexProcessBuilder builder = new DexProcessBuilder(outDexFolder);

        builder.setVerbose(mVerboseExec)
                .setIncremental(incremental)
                .setNoOptimize(!optimize)
                .setMultiDex(multidex)
                .setMainDexList(mainDexList)
                .addInputs(verifiedInputs.build());

        if (additionalParameters != null) {
            builder.additionalParameters(additionalParameters);
        }

        JavaProcessInfo javaProcessInfo = builder.build(buildToolInfo, dexOptions);

        ProcessResult result = mJavaProcessExecutor.execute(javaProcessInfo, processOutputHandler);
        result.rethrowFailure().assertNormalExitValue();
    }

方法参数中传入了additionalParameters,继续找:

@Override
    public void transform(
            @NonNull Context context,
            @NonNull Collection<TransformInput> inputs,
            @NonNull Collection<TransformInput> referencedInputs,
            @Nullable TransformOutputProvider outputProvider,
            boolean isIncremental) throws TransformException, IOException, InterruptedException {
        checkNotNull(outputProvider, "Missing output object for transform " + getName());

        // Gather a full list of all inputs.
        List<JarInput> jarInputs = Lists.newArrayList();
        List<DirectoryInput> directoryInputs = Lists.newArrayList();
        for (TransformInput input : inputs) {
            jarInputs.addAll(input.getJarInputs());
            directoryInputs.addAll(input.getDirectoryInputs());
        }

        try {
            // if only one scope or no per-scope dexing, just do a single pass that
            // runs dx on everything.
            if ((jarInputs.size() + directoryInputs.size()) == 1 || !dexOptions.getPreDexLibraries()) {
                File outputDir = outputProvider.getContentLocation("main",
                        getOutputTypes(), getScopes(),
                        Format.DIRECTORY);
                FileUtils.mkdirs(outputDir);

                // first delete the output folder where the final dex file(s) will be.
                FileUtils.emptyFolder(outputDir);

                // gather the inputs. This mode is always non incremental, so just
                // gather the top level folders/jars
                final List<File> inputFiles = Lists.newArrayList();
                for (JarInput jarInput : jarInputs) {
                    inputFiles.add(jarInput.getFile());
                }

                for (DirectoryInput directoryInput : directoryInputs) {
                    inputFiles.add(directoryInput.getFile());
                }

                androidBuilder.convertByteCode(
                        inputFiles,
                        outputDir,
                        multiDex,
                        mainDexListFile,
                        dexOptions,
                        null,
                        false,
                        true,
                        new LoggedProcessOutputHandler(logger));
            } else {
                // Figure out if we need to do a dx merge.
                // The ony case we don't need it is in native multi-dex mode when doing debug
                // builds. This saves build time at the expense of too many dex files which is fine.
                // FIXME dx cannot receive dex files to merge inside a folder. They have to be in a jar. Need to fix in dx.
                boolean needMerge = !multiDex || mainDexListFile != null;// || !debugMode;

                // where we write the pre-dex depends on whether we do the merge after.
                // If needMerge changed from one build to another, we'll be in non incremental
                // mode, so we don't have to deal with changing folder in incremental mode.
                File perStreamDexFolder = null;
                if (needMerge) {
                    perStreamDexFolder = intermediateFolder;

                    if (!isIncremental) {
                        FileUtils.deleteFolder(perStreamDexFolder);
                    }
                } else if (!isIncremental) {
                    // in this mode there's no merge and we dex it all separately into different
                    // output location so we have to delete everything.
                    outputProvider.deleteAll();
                }

                // dex all the different streams separately, then merge later (maybe)
                // hash to detect duplicate jars (due to isse with library and tests)
                final Set<String> hashs = Sets.newHashSet();
                // input files to output file map
                final Map<File, File> inputFiles = Maps.newHashMap();
                // stuff to delete. Might be folders.
                final List<File> deletedFiles = Lists.newArrayList();

                // first gather the different inputs to be dexed separately.
                for (DirectoryInput directoryInput : directoryInputs) {
                    File rootFolder = directoryInput.getFile();
                    // The incremental mode only detect file level changes.
                    // It does not handle removed root folders. However the transform
                    // task will add the TransformInput right after it's removed so that it
                    // can be detected by the transform.
                    if (!rootFolder.exists()) {
                        // if the root folder is gone we need to remove the previous
                        // output
                        File preDexedFile = getPreDexFile(outputProvider, needMerge, perStreamDexFolder,
                                directoryInput);
                        if (preDexedFile.exists()) {
                            deletedFiles.add(preDexedFile);
                        }
                    } else if (!isIncremental || !directoryInput.getChangedFiles().isEmpty()) {
                        // add the folder for re-dexing only if we're not in incremental
                        // mode or if it contains changed files.
                        File preDexFile = getPreDexFile(outputProvider, needMerge, perStreamDexFolder,
                                directoryInput);
                        inputFiles.put(rootFolder, preDexFile);
                    }
                }

                for (JarInput jarInput : jarInputs) {
                    switch (jarInput.getStatus()) {
                        case NOTCHANGED:
                            if (isIncremental) {
                                break;
                            }
                            // intended fall-through
                        case CHANGED:
                        case ADDED: {
                            File preDexFile = getPreDexFile(outputProvider, needMerge, perStreamDexFolder,
                                    jarInput);
                            inputFiles.put(jarInput.getFile(), preDexFile);
                            break;
                        }
                        case REMOVED: {
                            File preDexedFile = getPreDexFile(outputProvider, needMerge, perStreamDexFolder,
                                    jarInput);
                            if (preDexedFile.exists()) {
                                deletedFiles.add(preDexedFile);
                            }
                            break;
                        }
                    }
                }

                WaitableExecutor<Void> executor = new WaitableExecutor<Void>();
                ProcessOutputHandler outputHandler = new LoggedProcessOutputHandler(logger);

                for (Map.Entry<File, File> entry : inputFiles.entrySet()) {
                    Callable<Void> action = new PreDexTask(
                            entry.getKey(),
                            entry.getValue(),
                            hashs,
                            outputHandler);
                    executor.execute(action);
                }

                for (final File file : deletedFiles) {
                    executor.execute(new Callable<Void>() {
                        @Override
                        public Void call() throws Exception {
                            FileUtils.deleteFolder(file);
                            return null;
                        }
                    });
                }

                executor.waitForTasksWithQuickFail(false);

                if (needMerge) {
                    File outputDir = outputProvider.getContentLocation("main",
                            TransformManager.CONTENT_DEX, getScopes(),
                            Format.DIRECTORY);
                    FileUtils.mkdirs(outputDir);

                    // first delete the output folder where the final dex file(s) will be.
                    FileUtils.emptyFolder(outputDir);
                    mkdirs(outputDir);

                    // find the inputs of the dex merge.
                    // they are the content of the intermediate folder.
                    List<File> outputs = null;
                    if (!multiDex) {
                        // content of the folder is jar files.
                        File[] files = intermediateFolder.listFiles(new FilenameFilter() {
                            @Override
                            public boolean accept(File file, String name) {
                                return name.endsWith(SdkConstants.DOT_JAR);
                            }
                        });
                        if (files != null) {
                            outputs = Arrays.asList(files);
                        }
                    } else {
                        File[] directories = intermediateFolder.listFiles(new FileFilter() {
                            @Override
                            public boolean accept(File file) {
                                return file.isDirectory();
                            }
                        });
                        if (directories != null) {
                            outputs = Arrays.asList(directories);
                        }
                    }

                    if (outputs == null) {
                        throw new RuntimeException("No dex files to merge!");
                    }

                    androidBuilder.convertByteCode(
                            outputs,
                            outputDir,
                            multiDex,
                            mainDexListFile,
                            dexOptions,
                            null,
                            false,
                            true,
                            new LoggedProcessOutputHandler(logger));
                }
            }
        } catch (LoggedErrorException e) {
            throw new TransformException(e);
        } catch (ProcessException e) {
            throw new TransformException(e);
        }
    }

DexTransform类中两个调用androidBuilder.convertByteCode的地方,additionalParameters的值均为null,到底为止已经无计可施。。
几天之后突然出现了转机,偶然看到gradle-build system的更新日志:

2.2.0-alpha4 (2016/6/23)
Fixed instant-run regressions introduced in previous alpha builds related to code refactoring.
Turned off new packager by default due to some signing issues.
versionNameSuffix can now be specified for product flavors.
DexOptions now has an additionalParameters property for specifying custom flags for dx.
ProGuard files returned by getDefaultProguardFile are distributed with the plugin now, the ones in $ANDROID_HOME are no longer used.
SDK auto-download: Gradle will attempt to download missing SDK packages that a project depends on.
Added support for annotation processors. By default, annotation processors on your classpath, such as any compile dependency, will be automatically applied. You can also specify an annotation processor in your build and pass arguments by using the following DSL in your module-level build.gradle file:
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
className ‘com.example.MyProcessor’

// Arguments are optional.
arguments = [ foo : ‘bar’ ]
}
}
}
}

这个参数又开放了。。不过改到了DexOptions里面,在DexOptions里增加了additionalParameters 参数来配置dx的参数。
我们来试下:
首先在app的build.gradle里的DexOptions里面增加以下内容:

dexOptions {
        javaMaxHeapSize "4g"
        preDexLibraries = false
        additionalParameters = ['--multi-dex',
                                '--set-max-idx-number=40000']
}

然后把整个项目的gradle依赖改为:

dependencies {
        classpath 'com.android.tools.build:gradle:2.2.0-alpha4'
}

build的时候提示需要升级gradle,于是把gradle升级到2.10,在Terminal里执行gradlew命令,之后仍然报错,信息如下:

Gradle 'CreditPerson' project refresh failed
Error:Cause: com/android/build/gradle/internal/model/DefaultAndroidProject : Unsupported major.minor version 52.0

查了原因,需要升级jdk,把jdk升级到1.8,终于打包成功。
测试了打出的包,在指定dex内方法数量40000的情况下,打出两个dex,主dex内方法数量为39642,第二个dex内方法数量为18113,搞定收工。

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页