Android增量编译3~5秒的背后

fuqj 8年前
   <p>这篇文章主要介绍freeline是如何实现快速增量编译的。</p>    <h2>Android 编译打包流程</h2>    <p>首先看一下android打包流程图,图片来源 Android开发学习笔记(二)——编译和运行原理</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/70946d57661fd2e656b6ae206b39ca5f.png"></p>    <p style="text-align:center">Paste_Image.png</p>    <ul>     <li><strong>R文件的生成</strong><br> R文件记录了每个资源的ID,之后要参与到java的编译过程,R文件是由aapt(Android Asset Package Tool)生成。</li>     <li><strong>java编译</strong><br> 我们知道有时app开发中会跨进程通信,这时可以通过aidl的方式定义接口,aidl工具可以根据aidl文件生成对应的java文件。<br> 之后R文件、aidl相关java文件、src中的java文件通过编译生成 .class文件</li>     <li><strong>dex生成</strong><br> 编译后的.class会又由dex工具打包成dex文件,freeline中用到了 Buck 中提取的dex工具, freeline 给出的数据是比原生的dex工具快了40%</li>     <li style="text-align:center"> <p>资源文件编译</p> <p>aapt(Android Asset Package Tool)工具对app中的资源文件进行打包。其流程如图</p> <img src="https://simg.open-open.com/show/e12a1442c12d6d27036208c0dd2db434.png"> <p>Paste_Image.png</p> <p style="text-align:left">Android应用程序资源的编译和打包过程分析 罗升阳老师的文章非常清晰地分析了应用资源的打包过程。</p> </li>     <li> <p>apk文件生成与签名</p> <p>apkbuild工具把编译后的资源文件和dex文件打包成为dex文件。jarsigner完成apk的签名,当然Android7.0之后可以通过apksigner工具进行签名。 了解Android Studio 2.2中的APK打包 中有介绍。</p> </li>    </ul>    <h2>增量编译原理</h2>    <p>Android增量编译分为代码增量和资源增量, <strong>资源增量是freeline的一个亮点</strong> ,instant-run开启时其实在资源上并不是增量的,而是把整个应用的资源打成资源包,推送至手机的。</p>    <ul>     <li> <h3>代码增量</h3> 谷歌在支持multidex之后,当方法数超过65535时,android打包后会存在多个dex文件,运行时加载类时,会从一个dexList依次查找,找到则返回,利用这个原理 可以把增量的代码打包成dex文件,插入到dexList的前边,这样就可以完成类的替换 。<br> 这里有一个问题是在非art的手机上存在兼容性问题,这也是instant-run只支持android5.0以上的原因,freeline在这里使用之前 <a href="http://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a&scene=0" rel="nofollow,noindex">安卓App热补丁动态修复技术介绍</a> 中提出的插桩方案做了兼容处理,这样在非art手机上也可以进行增量编译。</li>     <li> <h3>资源增量</h3> 资源增量是freeline的一个亮点,在第一部分我们知道是通过aapt工具对应用资源文件进行打包的,freeline开发了自己的incrementAapt工具(目前并没有开源)。我们知道aapt进行资源编译时,会生成 <strong>R文件和resources.arsc文件</strong> ,R文件是资源名称和资源id的一个对应表,用于java文件中对资源的引用,而resources.arsc文件描述了每个资源id对应的配置信息,也就是描述了如何根据一个资源id找到对应的资源。      <ul>       <li>pulbic.xml 和ids.xml文件<br> aapt进行资源编译时,如果两次编译之间资源文件进行了增删操作,则编译出的R文件即使资源名称没有变化,资源id值却可能发生变化,这样如果进行资源增量编译,则app在进行资源引用时可能发生资源引用错乱的情况。因此第二次编译时最好根据第一次编译的结果进行,public.xml和ids.xml文件就是完成这件事情的,freeline开发了 <strong>id-gen-tool</strong> 利用第一次编译的R文件来生成public.xml 和ids.xml,用于第二次的编译。</li>       <li style="text-align:center">客户端的处理<br> freeline 利用incrementAapt增量工具打包出增量的资源文件,然后客户端将文件放置在正确的位置,然后启动应用后,就可以正确访问应用资源了。<br> <img src="https://simg.open-open.com/show/9f084a6304a8cfc9624dd80a1eff9df0.png"> <p style="text-align:center">Paste_Image.png</p> </li>      </ul> </li>    </ul>    <h2>freeline实现分析</h2>    <p>freeline 在实现上借鉴了buck,layoutCast的思想,把整个过程构建成多个任务,多任务并发,同时缓存各个阶段的生成文件,以达到快速构建的目的。</p>    <ul>     <li style="text-align:center"> <h3>多任务并发</h3> <p>先来看一张图</p> <img src="https://simg.open-open.com/show/aa51916fec11bec5d04ba1fdc8b0c65c.png"> <p>Paste_Image.png</p> <p style="text-align:left">freeline这里借鉴了buck的思想,如果工程中有多个module,freeline会建立好各个工程构建的任务依赖。在build过程中同时可能会有多个module在构建,之后在合适的时间把构建后的文件进行合并。</p> </li>     <li> <h3>缓存</h3> <p>我们在debug时可能会进行多次代码修改,并运行程序看修改效果,也就是要进行多次的增量编译,freeline对每次对编译过程进行了缓存。比如我们进行了三次增量编译,freeline每次编译都是针对本次修改的文件,对比LayoutCast 和instant-run每次增量编译都是编译第一次全量编译之后的更改的文件,freeline速度快了很多,根据freeline官方给的数据,快了3~4倍,但是这样freeline进行增量编译时的复杂性增加了不少。</p> <p>另外freeline增量编译后可调试,这点相对于instant-run 和LayoutCast来说,优势很大。freeline官方介绍中提到的懒加载,个人认为只是锦上添花的作用,在实际中可能并没有太大作用。</p> <h2>代码分析</h2> <p>终于到了代码分析的环节,还是先贴一下freeline的github地址: <strong> freeline </strong> ,我们看一下其源码有哪些内容</p> </li>    </ul>    <p><img src="https://simg.open-open.com/show/6846656837448c8c5110a9ef98694dbf.png"></p>    <p style="text-align:center">Paste_Image.png</p>    <p>android-studio-plugin是android中的freeline插件源码</p>    <p>databinding-cli顾名思义是对dababinding的支持</p>    <p>freeline_core是我们今天分析的重点</p>    <p>gradle 是对gradle中freeline配置的支持</p>    <p>release-tools中是编译过程中用到的工具,如aapt工具等</p>    <p>runtime是增量编译后客户端处理的逻辑</p>    <p>sample是给出的demo</p>    <p>如果想编译调试freeline增量编译的源码,可以先clone下freeline的源码,然后导入sample工程,注意sample中其实就包含了freeline_core的源码,我这里用的ide是Pycharm。</p>    <p>freeline对于android的编译分为两个过程:全量编译和增量编译,我们先来看全量编译。</p>    <ul>     <li> <h3>全量编译</h3>      <ol>       <li> <p>代码入口</p> <p>代码入口当然是freeline.py,</p> <pre>  <code class="language-java">if sys.version_info > (3, 0):     print 'Freeline only support Python 2.7+ now. Please use the correct version of Python for freeline.'     exit()  parser = get_parser()  args = parser.parse_args()  freeline = Freeline()  freeline.call(args=args)</code></pre> <p>首先判断是否是python2.7,freeline是基于python2.7的,然后对命令进行解析:</p> <pre>  <code class="language-java">parser.add_argument('-v', '--version', action='store_true', help='show version')  parser.add_argument('-f', '--cleanBuild', action='store_true', help='force to execute a clean build')  parser.add_argument('-w', '--wait', action='store_true', help='make application wait for debugger')  parser.add_argument('-a', '--all', action='store_true',                     help="together with '-f', freeline will force to clean build all projects.")  parser.add_argument('-c', '--clean', action='store_true', help='clean cache directory and workspace')  parser.add_argument('-d', '--debug', action='store_true', help='enable debug mode')  parser.add_argument('-i', '--init', action='store_true', help='init freeline project')</code></pre> <p>之后创建了Freeline对象</p> <pre>  <code class="language-java">def __init__(self):     self.dispatcher = Dispatcher()    def call(self, args=None):     if 'init' in args and args.init:         print('init freeline project...')         init()         exit()       self.dispatcher.call_command(args)</code></pre> <p>freeline中创建了dispatcher,从名字可以就可以看出是进行命令分发的,就是在dispatcher中执行不同的编译过程。在dispatcher执行call方法之前,init方法中执行了checkBeforeCleanBuild命令,完成了部分初始化任务。</p> </li>       <li> <p style="text-align:left">关键模块说明</p> <p>dispatcher</p> 分发命令,根据freeline.py 中命令解析的结果执行不同的命令 <p>builder</p> 执行各种build命令</li>      </ol> </li>    </ul>    <p style="text-align:center"><br> <img src="https://simg.open-open.com/show/3f852ed76ace42b79c30533ce4651072.png"></p>    <p style="text-align:center">Paste_Image.png</p>    <p>这是其类继承图,可以看到最下边两个子类分别是gradleincbuilder和gradlecleanbuilder,分别用于增量编译和全量编译。</p>    <p>command</p>    <p><img src="https://simg.open-open.com/show/5bc04eccdc30f8c399ebdba524e51598.png"></p>    <p style="text-align:center">Paste_Image.png</p>    <p>利用build执行命令,可以组织多个command,在创建command时传入builder,则可以执行不同的任务。</p>    <p>task_engine</p>    <p>task_engine定义了一个线程池,TaskEngine会根据task的依赖关系,多线程执行任务。</p>    <p>task</p>    <p style="text-align:center">freeline中定义了多个task,分为完成不同的功能<br> <img src="https://simg.open-open.com/show/9d2d72dbebb6437b29b5af6b2878fbe5.png"></p>    <p style="text-align:center">Paste_Image.png</p>    <p>gradle_tools</p>    <p style="text-align:center">定义了一些公有的方法:<br> <img src="https://simg.open-open.com/show/1d1c71be13933ba9793568a6cb34cfc3.png"></p>    <p style="text-align:center">Paste_Image.png</p>    <ol>     <li> <p>命令分发</p> 在代码入口出可以发现对命令进行了解析,之后在dispatcher中对解析结果进行命令分发: <pre>  <code class="language-java">if 'cleanBuild' in args and args.cleanBuild:         is_build_all_projects = args.all         wait_for_debugger = args.wait         self._setup_clean_build_command(is_build_all_projects, wait_for_debugger)     elif 'version' in args and args.version:         version()     elif 'clean' in args and args.clean:         self._command = CleanAllCacheCommand(self._config['build_cache_dir'])     else:         from freeline_build import FreelineBuildCommand         self._command = FreelineBuildCommand(self._config, task_engine=self._task_engine)</code></pre> 我们重点关注最后一行,在这里创建了FreelineBuildCommand,接下来在这里进行全量编译和增量编译。</li>     <li> <p>FreelineBuildCommand</p> <p>首先需要判断时增量编译还是全量编译,全量编译则执行 CleanBuildCommand ,增量编译则执行 IncrementalBuildCommand</p> <pre>  <code class="language-java">if self._dispatch_policy.is_need_clean_build(self._config, file_changed_dict):         self._setup_clean_builder(file_changed_dict)         from build_commands import CleanBuildCommand         self._build_command = CleanBuildCommand(self._builder)     else:         # only flush changed list when your project need a incremental build.         Logger.debug('file changed list:')         Logger.debug(file_changed_dict)         self._setup_inc_builder(file_changed_dict)         from build_commands import IncrementalBuildCommand         self._build_command = IncrementalBuildCommand(self._builder)       self._build_command.execute()</code></pre> <p>我们看一下 is_need_clean_build 方法</p> <pre>  <code class="language-java">def is_need_clean_build(self, config, file_changed_dict):     last_apk_build_time = file_changed_dict['build_info']['last_clean_build_time']       if last_apk_build_time == 0:         Logger.debug('final apk not found, need a clean build.')         return True       if file_changed_dict['build_info']['is_root_config_changed']:         Logger.debug('find root build.gradle changed, need a clean build.')         return True       file_count = 0     need_clean_build_projects = set()       for dir_name, bundle_dict in file_changed_dict['projects'].iteritems():         count = len(bundle_dict['src'])         Logger.debug('find {} has {} java files modified.'.format(dir_name, count))         file_count += count           if len(bundle_dict['config']) > 0 or len(bundle_dict['manifest']) > 0:             need_clean_build_projects.add(dir_name)             Logger.debug('find {} has build.gradle or manifest file modified.'.format(dir_name))       is_need_clean_build = file_count > 20 or len(need_clean_build_projects) > 0       if is_need_clean_build:         if file_count > 20:             Logger.debug(                 'project has {}(>20) java files modified so that it need a clean build.'.format(file_count))         else:             Logger.debug('project need a clean build.')     else:         Logger.debug('project just need a incremental build.')       return is_need_clean_build</code></pre> <p>freelined的策略如下,如果有策略需求,可以通过更改这部分的代码来实现。</p> <p>1.在git pull 或 一次性修改大量</p> <p>2.无法依赖增量实现的修改:修改AndroidManifest.xml,更改第三方jar引用,依赖编译期切面,注解或其他代码预处理插件实现的功能等。</p> <p>3.更换调试手机或同一调试手机安装了与开发环境不一致的安装包。</p> </li>     <li style="text-align:center"> <p>CleanBuildCommand</p> <pre>  <code class="language-java">self.add_command(CheckBulidEnvironmentCommand(self._builder))     self.add_command(FindDependenciesOfTasksCommand(self._builder))     self.add_command(GenerateSortedBuildTasksCommand(self._builder))     self.add_command(UpdateApkCreatedTimeCommand(self._builder))     self.add_command(ExecuteCleanBuildCommand(self._builder))</code></pre> <p>可以看到,全量编译时实际时执行了如上几条command,我们重点看一下 GenerateSortedBuildTasksCommand ,这里创建了多条存在依赖关系的task,在task_engine启动按照依赖关系执行,其它command类似。</p> <img src="https://simg.open-open.com/show/a908909a1880691baa3da86fce87522b.png"> <p>Paste_Image.png</p> <p style="text-align:left">其依赖关系是通过 <strong>childTask</strong> 的关系进行确认,可参考gradle_clean_build模块中的generate_sorted_build_tasks方法:</p> <pre style="text-align:left">  <code class="language-java">build_task.add_child_task(clean_all_cache_task)     build_task.add_child_task(install_task)     clean_all_cache_task.add_child_task(build_base_resource_task)     clean_all_cache_task.add_child_task(generate_project_info_task)     clean_all_cache_task.add_child_task(append_stat_task)     clean_all_cache_task.add_child_task(generate_apt_file_stat_task)     read_project_info_task.add_child_task(build_task)</code></pre> <p>最后在 ExecuteCleanBuildCommand 中启动task_engine</p> <pre>  <code class="language-java">self._task_engine.add_root_task(self._root_task)  self._task_engine.start()</code></pre> </li>    </ol>    <ul>     <li> <h3>增量编译</h3> <p>增量编译与全量编译之前的步骤相同,在 FreelineBuildCommand 中创建了 IncrementalBuildCommand</p>      <ol>       <li> <p>IncrementalBuildCommand</p> <pre>  <code class="language-java">self.add_command(CheckBulidEnvironmentCommand(self._builder))  self.add_command(GenerateSortedBuildTasksCommand(self._builder))  self.add_command(ExecuteIncrementalBuildCommand(self._builder))</code></pre> 创建了三个command,我们重点看一下 GenerateSortedBuildTasksCommand 这里比全量编译更复杂一些。</li>       <li> <p>GenerateSortedBuildTasksCommand</p> <pre>  <code class="language-java">def generate_sorted_build_tasks(self):     """     sort build tasks according to the module's dependency     :return: None     """     for module in self._all_modules:         task = android_tools.AndroidIncrementalBuildTask(module, self.__setup_inc_command(module))         self._tasks_dictionary[module] = task       for module in self._all_modules:         task = self._tasks_dictionary[module]         for dep in self._module_dependencies[module]:             task.add_parent_task(self._tasks_dictionary[dep])</code></pre> <p>可以看到首先遍历每个module创建 <strong>AndroidIncrementalBuildTask</strong> ,之后遍历mudle创建任务依赖关系。创建 <strong>AndroidIncrementalBuildTask</strong> 时传入了 <strong>GradleCompileCommand</strong></p> </li>       <li> <p>GradleCompileCommand</p> <pre>  <code class="language-java">self.add_command(GradleIncJavacCommand(self._module, self._invoker))  self.add_command(GradleIncDexCommand(self._module, self._invoker))</code></pre> <p>查看一下GradleIncJavacCommand</p> <pre>  <code class="language-java">self._invoker.append_r_file()     self._invoker.fill_classpaths()     self._invoker.fill_extra_javac_args()     self._invoker.clean_dex_cache()     self._invoker.run_apt_only()     self._invoker.run_javac_task()     self._invoker.run_retrolambda()</code></pre> <p>执行了以上几个函数,具体的内容可以查看源码。</p> <p>以下简单说一下task_engine时如何解决task的依赖关系,这里根据task中的 parent_task列表定义了每个task的depth:</p> <pre>  <code class="language-java">def calculate_task_depth(task):     depth = []     parent_task_queue = Queue.Queue()     parent_task_queue.put(task)     while not parent_task_queue.empty():         parent_task = parent_task_queue.get()           if parent_task.name not in depth:             depth.append(parent_task.name)           for parent in parent_task.parent_tasks:             if parent.name not in depth:                 parent_task_queue.put(parent)       return len(depth)</code></pre> <p>在具体执行时根据depth对task进行了排序</p> <pre>  <code class="language-java">depth_array.sort()       for depth in depth_array:         tasks = self.tasks_depth_dict[depth]         for task in tasks:             self.debug("depth: {}, task: {}".format(depth, task))             self.sorted_tasks.append(task)       self._logger.set_sorted_tasks(self.sorted_tasks)       for task in self.sorted_tasks:         self.pool.add_task(ExecutableTask(task, self))</code></pre> <p>然后每个task执行时会判断parent是否执行完成</p> <pre>  <code class="language-java">while not self.task.is_all_parent_finished():        # self.debug('{} waiting...'.format(self.task.name))         self.task.wait()</code></pre> <p>只有parent任务执行完成后,task才可以开始执行。</p> <h2>总结</h2> <p>本文从增量编译的原理和代码角度简单分析了freeline的实现,其中原理部分主要参考了 中文原理说明 ,代码部分主要分析了大体框架,没有深入到每一个细节,如freeline如何支持apt、lambda等,可能之后会再继续写文分析。</p> <p>本人才疏学浅,如果有分析错误的地方,请指出。</p> </li>      </ol> </li>    </ul>    <h2>参考</h2>    <p><a href="/misc/goto?guid=4959009805721920577" rel="nofollow,noindex">https://github.com/alibaba/freeline</a></p>    <p><a href="/misc/goto?guid=4959729901619655468" rel="nofollow,noindex">https://yq.aliyun.com/articles/59122?spm=5176.8091938.0.0.1Bw3mU</a></p>    <p><a href="/misc/goto?guid=4959729901704381977" rel="nofollow,noindex">http://www.cnblogs.com/Pickuper/archive/2011/06/14/2078969.html</a></p>    <p><a href="/misc/goto?guid=4959729901784063604" rel="nofollow,noindex">http://blog.csdn.net/luoshengyang/article/details/8744683?spm=5176.100239.blogcont59122.10.pdZfgL</a></p>    <p> </p>    <p>来自:http://www.jianshu.com/p/37e31d924be9</p>    <p> </p>