好久没有更新博客了,最近实在是太忙了,到了年底,好不容易有一个空下来的周末,决定写一篇文章记录一下最近学的东西。
大家都知道Google早些时间推出了Android新一代的编译器Jack,希望用它代替传统的编译方式。之前在网上看了一下,关于Jack的文章还是比较少的,所以花了一点时间对其进行了研究,这篇文章算是对它来一个简单全面的介绍吧~
在使用Jack进行编译之前,我们想一下之前的方式是怎样的。首先我们的“原材料”是.java格式的文件,而我们需要做的就是把这些文件编译成.class格式的文件,然后再进行转换,将其变为.dex结尾的文件,最后用dex文件配合aapt,jarsigner等工具进行打包,简单的.java —> .dex的过程可具体总结为:
javac (.java –> .class) –> dx (.class –> .dex)
当然,这只是最简单的过程,在具体的使用中,我们还需要使用类似proguard这样的工具,才可以变成一个我们想要的dex文件,所以真正的流程可以总结为:
javac (.java –> .class) -> proguard ...... -> (Java bytecode(.class) -> Optimized Java bytecode(.class)) –> dx (.class –> .dex)
可以看到整体的流程还是非常的长的,不过好在有AndroidStudio这样的帮我们做了这一切。
那Jack又是怎么做的呢?在我第一次接触Jack的时候,我手动的用它编译了一个Android项目,我发现一切真的太简单了,不需要dx,不需要proguard,只需要一个java -jar jack.jar命令,一切ok。具体的命令大家可以自行查找,可以说使用jack能非常简便的进行手动打包,具体流程总结为:
Jack (.java –> .dex)
先说一句,在AndroidStudio中使用Jack进行编译需要满足如下条件:
首先compileSdkVersion需要在24以上,其次,需要在jackOptions中开启enabled,最简单的配置如下:
apply plugin: 'com.android.application' android { compileSdkVersion 24 buildToolsVersion "24.0.0" defaultConfig { applicationId "com.zjutkz.jacktest" minSdkVersion 14 targetSdkVersion 23 versionCode 1 versionName "1.0" jackOptions { enabled true } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:24.2.1' testCompile 'junit:junit:4.12' }
对比Jack和传统的编译流程,我们可以发现一个最明显的区别,就是使用Jack进行编译的情况下,完全没有.class文件的出现。也就是说Jack抛弃了传统的Java字节码文件,这可谓是一个非常大的突破。当然这就带来了两个问题:第一个,抛弃.class文件之后,那些操作Java字节码的动态hook工具改如何使用呢?第二个问题,库的依赖我们还是要以jar/aar的形式依赖的,那么必然会存在.class文件,那如果使用了Jack之后,如果进行库的依赖呢?
对于第二个问题,Google使用了另外一个工具——Jill,下面就让我们来看看这个工具到底做了什么吧。
首先我们先看两张图:
通过第一张图我们知道,在进行编译的过程中,Jack使用了Jill工具将依赖库中的.class文件转换成了.jack文件,然后再使用Jack工具将这些文件和Android过程的源代码一起编译为.dex文件,当然.dex文件的数量取决于是否开启multiDex。
通过第二张图我们知道,Jill工具具体的工作就是将jar/aar中的.class文件转化成.jayce格式的文件,并且再使用Jack做preDex操作。至于preDex的作用,其实就是一个预编译的作用,起到缓存的作用。
我们真正需要关注的是,Jill是如何将.class文件转换成.jayce格式的文件的。
首先我们来翻一翻gradle的源码,其中有一个Task叫JillTask,名字一目了然,就是一个用来做Jill操作的gradle task。它分为两种类型——jillPackagedTask和jillRuntimeTask,我们看jillPackagedTask就好。
从图中可以看出它的输出目录为intermediates/jill/type+flavors/packaged/,下面让我们看看这个目录下有什么:
接着我们解压其中随意的一个jar包,看看里面是什么:
可以看到,里面其实就是该Android工程中依赖库的jayce格式文件,这也印证了前面的流程图:Jill的作用就是把依赖库中的.class文件转换成.jayce文件。那Jill具体是如何做的呢?还是看代码来的实在。
gradle task中执行任务是在@TaskAction这个注解下的方法中执行的,所以我们来看看JillTask的taskAction方法。
@TaskAction public void taskAction(IncrementalTaskInputs taskInputs) throws LoggedErrorException, InterruptedException, IOException { Revision revision = getBuilder().getTargetInfo().getBuildTools().getRevision(); if (revision.compareTo(JackTask.JACK_MIN_REV, Revision.PreviewComparison.IGNORE) < 0) { throw new RuntimeException( "Jack requires Build Tools " + JackTask.JACK_MIN_REV.toString() + " or later"); } final File outFolder = getOutputFolder(); // if we are not in incremental mode, then outOfDate will contain // all th files, but first we need to delete the previous output if (!taskInputs.isIncremental()) { FileUtils.emptyFolder(outFolder); } final Set<String> hashs = Sets.newHashSet(); final WaitableExecutor<Void> executor = new WaitableExecutor<Void>(); final List<File> inputFileDetails = Lists.newArrayList(); final AndroidBuilder builder = getBuilder(); taskInputs.outOfDate(new Action<InputFileDetails>() { @Override public void execute(InputFileDetails change) { inputFileDetails.add(change.getFile()); } }); for (final File file : inputFileDetails) { Callable<Void> action = new JillCallable(this, file, hashs, outFolder, builder); executor.execute(action); } taskInputs.removed(new Action<InputFileDetails>() { @Override public void execute(InputFileDetails change) { File jackFile = getJackFileName(outFolder, ((InputFileDetails) change).getFile()); //noinspection ResultOfMethodCallIgnored jackFile.delete(); } }); executor.waitForTasksWithQuickFail(false); }
其中最重要的就是异步执行了JillCallable这个Callable。
@Override public Void call() throws Exception { // TODO remove once we can properly add a library as a dependency of its test. String hash = getFileHash(fileToProcess); synchronized (hashs) { if (hashs.contains(hash)) { return null; } hashs.add(hash); } //noinspection GroovyAssignabilityCheck File jackFile = getJackFileName(outFolder, fileToProcess); //noinspection GroovyAssignabilityCheck builder.convertLibraryToJack(fileToProcess, jackFile, options, new LoggedProcessOutputHandler(builder.getLogger()), isJackInProcess()); return null; }
上面是JillCallable的call方法。步骤很简单,首先将输入文件,也就是依赖库进行哈希,然后使用这个哈希值作为输出文件的文件名,这也证明了为什么之前看到的jill目录下的输出文件的文件名都是哈希值了。然后,调用了AndroidBuilder的convertLibraryToJack,在这之中进行了真正的转换。
该方法最终调用了JackConversionCache的convertLibrary方法。在这个方法中,存在两种转换的方式:使用api或者命令行,我们就看api方式的转换,Jill具体的源码在 这里 。
最终会调用到Jill.java的process方法:
public static void process(@Nonnull Options options) { File binaryFile = options.getBinaryFile(); JavaTransformer jt = new JavaTransformer(getVersion().getVersion(), options); if (binaryFile.isFile()) { if (FileUtils.isJavaBinaryFile(binaryFile)) { List<File> javaBinaryFiles = new ArrayList<File>(); javaBinaryFiles.add(binaryFile); jt.transform(javaBinaryFiles); } else if (FileUtils.isJarFile(binaryFile)) { try { jt.transform(new JarFile(binaryFile)); } catch (IOException e) { throw new JillException("Fails to create jar file " + binaryFile.getName(), e); } } else { throw new JillException("Unsupported file type: " + binaryFile.getName()); } } else { List<File> javaBinaryFiles = new ArrayList<File>(); FileUtils.getJavaBinaryFiles(binaryFile, javaBinaryFiles); jt.transform(javaBinaryFiles); } }
其中调用了JavaTransformer的transform方法:
private void transform(@Nonnull ClassNode cn, @Nonnull OutputStream os) throws IOException { JayceWriter writer = createWriter(os); ClassNodeWriter asm2jayce = new ClassNodeWriter(writer, new SourceInfoWriter(writer), options); asm2jayce.write(cn); writer.flush(); }
可以看到,使用了asm2jayce去把.class转换成了.jayce文件。没想到,Jack也使用了asm,这个框架可谓无处不在啊!
到这里,我们就弄清楚了Jill这个工具的作用以及运作原理,其实很简单,就是为了让依赖库在保持jar/aar依赖方式不变的情况下,也能使用Jack进行编译。
说完了Jill,让我们再说下一个工具——Jack。通过上面的文章我们可以知道,Jill存在的原因其实就是为了Jack服务的,我们从gradle的TaskManager中也可以印证这一点:
看的很清楚,JaskTask是依赖于JillTask的,因为JaskTask需要获取JillTask中生成的依赖库的.jayce文件才行。那JaskTask又做了什么呢?这里我们从简讲,毕竟这篇文章只是全面简单的了解一下而已,之后有机会可以深入的讲一下其中的原理,包括它和jarjar,multidex等库的关系。
首先和JillTask一样,JaskTask会调用AndroidBuilder去进行操作,并且判断是使用api形式还是命令行形式,api形式具体的方法是convertByteCodeUsingJackApis。这其中我们主要关注第二个参数——jackOutputFile,最终我们可以追溯到VariantScopeImpl中的getJackClassesZip方法:
就是/intermediates/package/type+flavors/class.zip文件,让我们解压看看这个文件里面是什么:
这其中包含了.jayce格式的文件和prebuild目录,prebuild目录下就是经过predex生成的dex文件,用来加快下一次的编译速度。
最终,convertByteCodeUsingJackApis会调用到Jack.Java的run方法中,具体的源码在 这里 。在run方法中,就是去进行整个编译工作,其中会配合之前提到的jarjar,multidex等库一起进行。
对于class.zip这个文件,如果大家看过里面的内容,会发现它的jayce目录和prebuild目录下包含的内容是所有的依赖库和该android工程的文件,所以我们可以知道,Jack会把Jill所生成的所有依赖库的jayce文件做prebuild操作,生成jack文件,并且和工程文件一起合成最终的dex文件。最终的dex文件的产出目录是/intermediates/dex/type+flavors/classes1/2/3…./n.dex。
对于prebuild操作,生成的dex文件是可以复用的,意思就是如果两次编译之间的依赖库版本和数量不变,则Jack不会去对其进行重新的编译,而只专注于改变了的工程文件,从而加快整体的编译速度,这也就是我文章开头提到的[缓存]。
分析到这里,我们基本大致的走通了Jill和Jack的整体流程。最后,我在真实的项目环境中使用了传统的编译方式和Jack都编译了一次,发现clean build的情况下,一个传统编译只需要2分钟的项目,而在Jack编译器下编译则需要10分钟!gradle task很明显的卡在了app:compileDebugJavaWithJack这个task上。
为什么会这样呢,在没有仔细的研究Jask源码之前,我们可以从整体的编译流程上去猜测。首先,Jack需要Jill将依赖库转换成需要.jayce格式的jar包,以便于Jack做prebuild,也就是预先生成dex文件,这一步无疑是比较耗时的,而这些操作在传统的编译模式下都是不需要的,也就造成了Jack在clean build的情况下的耗时。但是在之后的编译中,由于prebuild的存在,jack的速度会快起来,这也是rebuild存在的意义。
总的来说,Jack现在还不是非常的成熟,想要大规模的使用,Google还得加把劲优化才行啊!