打通Android Gradle编译过程的任督二脉

Posted QQ音乐技术团队

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了打通Android Gradle编译过程的任督二脉相关的知识,希望对你有一定的参考价值。


本文主要是基于自己在工作当中的一些android Gradle实践经验,对gradle相关知识做的一个简单总结和分享,希望对大家有帮助。

首先会讲Gradle大概的工作流程和实现原理,并以部分源码分析佐证。其中包括project中配置数据什么时候取,各个task的创建时机,如何自定义控制编译过程等。

然后着重会分析编译过程中class到dex这一步的具体过程,以及当初遇到的一些问题和解决方法。

主要工作流程

Gradle构建过程包括三个阶段:

  • 初始化阶段

读取根工程中的setting.gradle中的include信息,确定有多少工程加入构建并创建project实例,每个工程中的build.gradle对应一个project实例。

  • 配置阶段

根据每个工程目录下面的build.gradle,配置gradle对象,并构建好任务依赖有向图。

  • 执行阶段

根据配置阶段拿到的配置信息和任务依赖有向图执行对应的task。
如下图所示:

关于配置阶段和执行阶段可以看一个小例子:

task hello {
   print 'hello'}

区别与:

task hello {
   doLast{
   print 'hello'
   }
}

一个project有若干的task,而一个task有若干的action,上述例子当中,上面的是在配置阶段执行,而下面这个是在具体执行hello这个task的时候才会执行。
另外,从流程图中可以知道,我们可以在各个阶段通过hook去做一些事情,具体可以这样实现:
gradle里面有两个监听器接口:BuildListenerTaskExecutionListener

BuildListener

void buildStarted(Gradle gradle);
void settingsEvaluated(Settings settings);
void projectsLoaded(Gradle gradle);
void projectsEvaluated(Gradle gradle);
void buildFinished(BuildResult result);


TaskExecutionListener

void beforeExecute(Task task);
void afterExecute(Task task, TaskState state);


因此实现上面两个接口,并在srcipts中用gradle.addListener  YourListener中加入监听即可实现Hook功能,如我们可以自定义我们的task然后去替换系统默认的task,或者在task的依赖中去掉,插入某一个task等。
如在依赖链A到C之间插入B任务 ...->A->C->...可以用如下方式:

B.dependsOn A.taskDependencies.getDependencies(A)
A.dependsOn B

更多的使用技巧可以参考和

源码分析

Gradle源码当中主要有这么几个类,VariantManager,TaskManager,AndroidBuilder,ConfigAction:
 (1) VariantManager负责收集对应的变量数据,如build.gradle中的一些基本配置变量可以在AndroidSourceSet类中查看。
 (2) TaskManager负责管理task的创建和执行
 (3) AndroidBuilder负责具体执行Android构建的一些命令,如编译aidl,aapt,class转dex等。
 (4) ConfigAction负责task的具体表现行为,task是由若干Action组成的,gradle在创建每一个任务的时候会默认指定一个ConfigAction来指定task名字,输入输出等。

gradle进程启动的时候,VariantManager初始化的时候会收集对应的variantData,然后根据这些信息首先创建默认的的AndroidTask(想看默认有哪些AndroidTask,可以到类com.android.builder.profile.ExecutionType中查看),然后调用对应的TaskManager继承类如ApplicationTaskManagercreateTasksForVariantData创建所有相关的task,并构建依赖有向图。如下图所示:
 

createTasksForVariantData函数中创建任务的方式如下:

 ThreadRecorder.get().record(ExecutionType.APP_TASK_MANAGER_CREATE_COMPILE_TASK,
           new Recorder.Block<Void>() {
        @Override
        public Void call () {
            AndroidTask<JavaCompile> javacTask = createJavacTask(tasks, variantScope);
            if (variantData.getVariantConfiguration().getUseJack()) {
                createJackTask(tasks, variantScope);
            } else {
                setJavaCompilerTask(javacTask, tasks, variantScope);
                createJarTask(tasks, variantScope);
                createPostCompilationTasks(tasks, variantScope);
            }
            return null;
        }
    });

具体创建task方式如下:

 public synchronized AndroidTask<Task> create(
            TaskFactory taskFactory,
            String taskName,
            Closure configAction) {
        taskFactory.create(taskName, DefaultTask.class, new ClosureBackedAction<Task>(configAction));
        final AndroidTask<Task> newTask = new AndroidTask<Task>(taskName, Task.class);
        tasks.put(taskName, newTask);
        return newTask;
    }

这里用的是插件是com.android.application,因此调用ApplicationTaskManager中的createTasksForVariantData方法。
上面ThreadRecorder保证task之间是串行的,另外在具体创建每一个任务的时候的时候可以看到都传了一个ConfigAction,这个可以默认指定该任务执行某些行为,如指定该任务的输入输出等,获取AndroidBuilder中的工具等。

dex过程分析

在包含多dex的项目工程中,class转dex的过程(release参考),主要涉及到如下几个,如下图:

其中:

  1. collectReleaseMultidexCompents任务也就是图中的mainfestKeepListTask会调用CreateManifestKeepList这个类分析AndroidMainFest.xml文件,将默认的 activityservicereceiverproviderinstrumenter等继承类加进来, 生成mainfest_keep.txt。

  2. 接下来shrinkReleaseMultidexCompents任务也就是图中的proguardComponentsTask输入class.jar,shrinkAndroid.jar,mainfest_keep.txt等参数,然后得到componentClass.jar。

  3. 接下来是createReleaseMainDexClassList类会根据componentClass.jar中的入口类调用dx工具中的MainDexListBuilder类分析入口类的依赖集,并从类全集class.jar中过滤出我们的主dex中的类,生成maindexlist.txt。

  4. retraceReleaseMainDexClassList还原maindexlist.txt的混淆结果,得到不混淆的map文件。

  5. dexRelease任务根据maindexlist.txt以及所有类文件全集调用dx工具中的入口类com.android.dx.command.Main调用processAllFiles函数生成对应的主dex和从dex。

在我们的分包过程中我们遇到了如下几个问题:

  1. 工程达到一定规模,如果主dex当中的method和field达到65536数目的限制,编译打包就会失败,在com.android.dx.command.dexer类的processClaas函数中有下面一段:

if (args.multiDex
         // Never switch to the next dex if current dex is already empty
         && (outputDex.getClassDefs().items().size() > 0)
         && ((maxMethodIdsInDex > args.maxNumberOfIdxPerDex) ||
             (maxFieldIdsInDex > args.maxNumberOfIdxPerDex))) {
         DexFile completeDex = outputDex;
         createDexFile();
         assert  (completeDex.getMethodIds().items().size() <= numMethodIds +
                 MAX_METHOD_ADDED_DURING_DEX_CREATION) &&
                 (completeDex.getFieldIds().items().size() <= numFieldIds +
                 MAX_FIELD_ADDED_DURING_DEX_CREATION);
     }

......

if (dexOutputArrays.size() > 0) {
            throw new DexException("Too many classes in " + Arguments.MAIN_DEX_LIST_OPTION
                    + ", main dex capacity exceeded");
        }

在dexmerger阶段,会调用processAllfiles->processOne->processFileBytes->processClass等函数,如果我们的maindex_list.txt过大导致主dex放不下,在processClass函数里面就会调用createDexFile函数生成新dex,然而在processAllfiles函数的最后阶段如果发现主dex放不下就会抛出异常,导致编译失败(不同dx版本略有不同,这里参考的是build-tools 19.1.0版本)。
至此可以认为系统默认生成的主dex依赖集过大,可以考虑优化系统默认的生成maindex_list.txt的过程。

由上面对dex过程分析,我们可以知道有两种方法可以解决:

  1. 控制入口类的数目,就是缩减mainfest_keep.txt的数目,从而达到控制maindex_list的数目。

  2. 直接控制maindex_list的数目

方法1:
早期采用的方法是在配置阶段,通过反射动态修改CreateManifestKeepList中类的字段KEEP_SPECS,这个字段默认控制入口类规则,但是这里有一个坑是当gradle版本升级到2.0.0之后,该字段变成是immutable类型了,反射修改的方式被禁止(还有另外一个坑是2.0.0之后默认的dex开头的task任务没有了,改为对应的transfrom任务了,而且取消了从配置阶段向dx传参数的功能,如下面这种用法不再生效

dx.additionalParameters += "--set-max-idx-number=58000"

而为了兼容2.3手机,需要通过控制方法数来控制线性内存的大小,怎么办?官方回应是后续会加上支持,不过后续随着5.0手机比重逐渐增多,该功能也没啥必要了)。
最终这里我们采取的方案与CreateManifestKeepList的实现方式是类似的,写一个task读取AndroidMainFest.xml,自动分析并加入主入口类然后生成对应的maindest_keep.txt并替换系统默认的keep文件即可。

方法2:
直接控制maindex_list的数目,这里需要自己写一个类依赖集分析工具,然后生成对应的maindex_list.txt。系统默认dx工具分析依赖集的时候,首先按照入口类jar包componentClass.jar中的zipEntry顺序找到第一级依赖,然后根据常量池中的信息(包括类引用,方法参数引用,字段引用等),通过获取到的description信息来判定是否加入类的依赖链,最后在加入父类和接口,然后递归此过程得到类的主dex全依赖集。
核心代码如下:

  private void addDependencies(ConstantPool pool) {
        for (Constant constant : pool.getEntries())
            if ((constant instanceof CstType)) {
                checkDescriptor(((CstType) constant).getClassType());
            } else if ((constant instanceof CstFieldRef)) {
                checkDescriptor(((CstFieldRef) constant).getType());
            } else if ((constant instanceof CstMethodRef)) {
                Prototype proto = ((CstMethodRef) constant).getPrototype();
                checkDescriptor(proto.getReturnType());
                StdTypeList args = proto.getParameterTypes();
                for (int i = 0; i < args.size(); i++)
                    checkDescriptor(args.get(i));
            }
    }

    private void checkDescriptor(Type type) {
        String descriptor = type.getDescriptor();
        if (descriptor.endsWith(";")) {
            int lastBrace = descriptor.lastIndexOf('[');
            if (lastBrace < 0) {
                addClassWithHierachy(descriptor.substring(1, descriptor.length() - 1));
            } else {
                assert ((descriptor.length() > lastBrace + 3) && (descriptor.charAt(lastBrace + 1) == 'L'));
                addClassWithHierachy(descriptor.substring(lastBrace + 2, descriptor.length() - 1));
            }
        }
    }

    private void addClassWithHierachy(String classBinaryName) {
        if (this.classNames.contains(classBinaryName)) {
            return;
        }
        try {
            DirectClassFile classFile = this.path.getClass(classBinaryName + ".class");
            this.classNames.add(classBinaryName);
            CstType superClass = classFile.getSuperclass();
            if (superClass != null) {
                addClassWithHierachy(superClass.getClassType().getClassName());
            }
            TypeList interfaceList = classFile.getInterfaces();
            int interfaceNumber = interfaceList.size();
            for (int i = 0; i < interfaceNumber; i++)
                addClassWithHierachy(interfaceList.getType(i).getClassName());
        } catch (FileNotFoundException e) {
        }
    }

目前音乐工程中已经做了异步加载的方案,主dex的依赖集必须充分完全,否则就会出现NoClassDefError。我们知道在初始化载入主dex当中一些类的时候,会去加载所有的静态内部类和匿名内部类以及校验所有的方法,如果对应的类所在的dex还未加载进来,就会进行指令替换从而在真正运行到该类代码的时候发生NoClassDefError。

采用dx工具默认的分包方案,我根据java -verbose的方式查看了对应的常量池信息,通过分析可以发现对于一些匿名内部类以及方法内部中的一些类信息可能会有遗漏,而事实上我们也的确遇到了类似情况,有时候改了相关代码之后,发现主dex中依赖集中的一个类被放到了第二个dex中去了,从而影响启动。

之前通过在build.gradle配置文件中添加keep文件的方式可以手动添加入口类以及对应的依赖集到主dex当中:

multiDexKeepProguard file('multiDexKeep.pro')

这个手动添加依赖集的方式需要完善,因此自己的类分析依赖工具就应运而生了。

主要的实现方式就是在系统默认的构造链规则中在加一些规则(确保我们主dex的依赖集都包含进来),通过用ASM框架主动加入必要的匿名内部类以及方法类信息,然后在gradle里面自定义task替换系统默认的类依赖分析过程即可,如加入内部类信息:

public static void  anylsisInnerClassDependence(Set<String>toKeep, String fullClassName,InputStream inputStream) throws IOException {
        if (!toKeep.contains(fullClassName)) {
            ClassReader classReader = new ClassReader(inputStream);
            ClassNode classNode = new ClassNode();
            classReader.accept(classNode, 0);
            for(int i=0;i<classNode.innerClasses.size();i++){
                InnerClassNode innerClassNode=(InnerClassNode)classNode.innerClasses.get(i);
                String innerClassName=innerClassNode.name;
                if (PisaClassReferenceBuilder.isAnysisClass(innerClassName)) {
                    PisaClassReferenceBuilder.getDefault().addClassWithHierachy(innerClassName);
                    System.out.println("innerClassNode name=" + innerClassNode.name);
                }
            }
       }
    }

总结

本文主要讲了工作当中对Gradle相关知识的一些实践心得和体会,通过简单的源码分析了编译中创建task的过程,并重点讲述dex过程的相关流程,试图打通 android gradle编译工程链的任督二脉。

以上是关于打通Android Gradle编译过程的任督二脉的主要内容,如果未能解决你的问题,请参考以下文章

12-5打通Flutter与Android的任督二脉Flutter Plugin开发指南-Android端实现-1

开篇词 | 打通“容器技术”的任督二脉

K2 BPM_当K2遇上医药,用流程打通企业的任督二脉_业务流程管理系统

10段代码打通js学习的任督二脉

一文打通Seata源码的任督二脉

“智慧”交通打通城市的任督二脉