Android增量編譯3~5秒的背后

前篇福利-Android增量編譯3~5秒介紹了增量編譯神器freeline的基本使用,這篇文章主要介紹freeline是如何實(shí)現(xiàn)快速增量編譯的。

Android 編譯打包流程

首先看一下android打包流程圖,圖片來源Android開發(fā)學(xué)習(xí)筆記(二)——編譯和運(yùn)行原理

Paste_Image.png

  • R文件的生成
    R文件記錄了每個(gè)資源的ID,之后要參與到j(luò)ava的編譯過程,R文件是由aapt(Android Asset Package Tool)生成。

  • java編譯
    我們知道有時(shí)app開發(fā)中會(huì)跨進(jìn)程通信,這時(shí)可以通過aidl的方式定義接口,aidl工具可以根據(jù)aidl文件生成對(duì)應(yīng)的java文件。
    之后R文件、aidl相關(guān)java文件、src中的java文件通過編譯生成 .class文件

  • dex生成
    編譯后的.class會(huì)又由dex工具打包成dex文件,freeline中用到了Buck中提取的dex工具,freeline給出的數(shù)據(jù)是比原生的dex工具快了40%

  • 資源文件編譯
    aapt(Android Asset Package Tool)工具對(duì)app中的資源文件進(jìn)行打包。其流程如圖(圖片來源

    Paste_Image.png

    Android應(yīng)用程序資源的編譯和打包過程分析羅升陽老師的文章非常清晰地分析了應(yīng)用資源的打包過程。

  • apk文件生成與簽名
    apkbuild工具把編譯后的資源文件和dex文件打包成為dex文件。jarsigner完成apk的簽名,當(dāng)然Android7.0之后可以通過apksigner工具進(jìn)行簽名。了解Android Studio 2.2中的APK打包中有介紹。

增量編譯原理

Android增量編譯分為代碼增量和資源增量,資源增量是freeline的一個(gè)亮點(diǎn),instant-run開啟時(shí)其實(shí)在資源上并不是增量的,而是把整個(gè)應(yīng)用的資源打成資源包,推送至手機(jī)的。

  • 代碼增量

谷歌在支持multidex之后,當(dāng)方法數(shù)超過65535時(shí),android打包后會(huì)存在多個(gè)dex文件,運(yùn)行時(shí)加載類時(shí),會(huì)從一個(gè)dexList依次查找,找到則返回,利用這個(gè)原理可以把增量的代碼打包成dex文件,插入到dexList的前邊,這樣就可以完成類的替換。
這里有一個(gè)問題是在非art的手機(jī)上存在兼容性問題,這也是instant-run只支持android5.0以上的原因,freeline在這里使用之前安卓App熱補(bǔ)丁動(dòng)態(tài)修復(fù)技術(shù)介紹中提出的插樁方案做了兼容處理,這樣在非art手機(jī)上也可以進(jìn)行增量編譯。

  • 資源增量

資源增量是freeline的一個(gè)亮點(diǎn),在第一部分我們知道是通過aapt工具對(duì)應(yīng)用資源文件進(jìn)行打包的,freeline開發(fā)了自己的incrementAapt工具(目前并沒有開源)。我們知道aapt進(jìn)行資源編譯時(shí),會(huì)生成R文件和resources.arsc文件,R文件是資源名稱和資源id的一個(gè)對(duì)應(yīng)表,用于java文件中對(duì)資源的引用,而resources.arsc文件描述了每個(gè)資源id對(duì)應(yīng)的配置信息,也就是描述了如何根據(jù)一個(gè)資源id找到對(duì)應(yīng)的資源。

  • pulbic.xml 和ids.xml文件
    aapt進(jìn)行資源編譯時(shí),如果兩次編譯之間資源文件進(jìn)行了增刪操作,則編譯出的R文件即使資源名稱沒有變化,資源id值卻可能發(fā)生變化,這樣如果進(jìn)行資源增量編譯,則app在進(jìn)行資源引用時(shí)可能發(fā)生資源引用錯(cuò)亂的情況。因此第二次編譯時(shí)最好根據(jù)第一次編譯的結(jié)果進(jìn)行,public.xml和ids.xml文件就是完成這件事情的,freeline開發(fā)了id-gen-tool利用第一次編譯的R文件來生成public.xml 和ids.xml,用于第二次的編譯。
  • 客戶端的處理
    freeline 利用incrementAapt增量工具打包出增量的資源文件,然后客戶端將文件放置在正確的位置,然后啟動(dòng)應(yīng)用后,就可以正確訪問應(yīng)用資源了。


    Paste_Image.png

freeline實(shí)現(xiàn)分析

freeline 在實(shí)現(xiàn)上借鑒了buck,layoutCast的思想,把整個(gè)過程構(gòu)建成多個(gè)任務(wù),多任務(wù)并發(fā),同時(shí)緩存各個(gè)階段的生成文件,以達(dá)到快速構(gòu)建的目的。

  • 多任務(wù)并發(fā)

先來看一張圖(圖片來源

Paste_Image.png

freeline這里借鑒了buck的思想,如果工程中有多個(gè)module,freeline會(huì)建立好各個(gè)工程構(gòu)建的任務(wù)依賴。在build過程中同時(shí)可能會(huì)有多個(gè)module在構(gòu)建,之后在合適的時(shí)間把構(gòu)建后的文件進(jìn)行合并。

  • 緩存

我們?cè)赿ebug時(shí)可能會(huì)進(jìn)行多次代碼修改,并運(yùn)行程序看修改效果,也就是要進(jìn)行多次的增量編譯,freeline對(duì)每次對(duì)編譯過程進(jìn)行了緩存。比如我們進(jìn)行了三次增量編譯,freeline每次編譯都是針對(duì)本次修改的文件,對(duì)比LayoutCast 和instant-run每次增量編譯都是編譯第一次全量編譯之后的更改的文件,freeline速度快了很多,根據(jù)freeline官方給的數(shù)據(jù),快了3~4倍,但是這樣freeline進(jìn)行增量編譯時(shí)的復(fù)雜性增加了不少。
另外freeline增量編譯后可調(diào)試,這點(diǎn)相對(duì)于instant-run 和LayoutCast來說,優(yōu)勢(shì)很大。freeline官方介紹中提到的懶加載,個(gè)人認(rèn)為只是錦上添花的作用,在實(shí)際中可能并沒有太大作用。

代碼分析

終于到了代碼分析的環(huán)節(jié),還是先貼一下freeline的github地址:freeline,我們看一下其源碼有哪些內(nèi)容

Paste_Image.png

android-studio-plugin是android中的freeline插件源碼
databinding-cli顧名思義是對(duì)dababinding的支持
freeline_core是我們今天分析的重點(diǎn)
gradle 是對(duì)gradle中freeline配置的支持
release-tools中是編譯過程中用到的工具,如aapt工具等
runtime是增量編譯后客戶端處理的邏輯
sample是給出的demo

如果想編譯調(diào)試freeline增量編譯的源碼,可以先clone下freeline的源碼,然后導(dǎo)入sample工程,注意sample中其實(shí)就包含了freeline_core的源碼,我這里用的ide是Pycharm。

freeline對(duì)于android的編譯分為兩個(gè)過程:全量編譯和增量編譯,我們先來看全量編譯。

  • 全量編譯

  1. 代碼入口

代碼入口當(dāng)然是freeline.py,

    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)

首先判斷是否是python2.7,freeline是基于python2.7的,然后對(duì)命令進(jìn)行解析:

    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')

之后創(chuàng)建了Freeline對(duì)象

    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)

freeline中創(chuàng)建了dispatcher,從名字可以就可以看出是進(jìn)行命令分發(fā)的,就是在dispatcher中執(zhí)行不同的編譯過程。在dispatcher執(zhí)行call方法之前,init方法中執(zhí)行了checkBeforeCleanBuild命令,完成了部分初始化任務(wù)。

  1. 關(guān)鍵模塊說明

dispatcher

分發(fā)命令,根據(jù)freeline.py 中命令解析的結(jié)果執(zhí)行不同的命令

builder

執(zhí)行各種build命令


Paste_Image.png

這是其類繼承圖,可以看到最下邊兩個(gè)子類分別是gradleincbuilder和gradlecleanbuilder,分別用于增量編譯和全量編譯。

command
Paste_Image.png

利用build執(zhí)行命令,可以組織多個(gè)command,在創(chuàng)建command時(shí)傳入builder,則可以執(zhí)行不同的任務(wù)。

task_engine

task_engine定義了一個(gè)線程池,TaskEngine會(huì)根據(jù)task的依賴關(guān)系,多線程執(zhí)行任務(wù)。

task

freeline中定義了多個(gè)task,分為完成不同的功能


Paste_Image.png
gradle_tools

定義了一些公有的方法:


Paste_Image.png
  1. 命令分發(fā)

在代碼入口出可以發(fā)現(xiàn)對(duì)命令進(jìn)行了解析,之后在dispatcher中對(duì)解析結(jié)果進(jìn)行命令分發(fā):

        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)

我們重點(diǎn)關(guān)注最后一行,在這里創(chuàng)建了FreelineBuildCommand,接下來在這里進(jìn)行全量編譯和增量編譯。

  1. FreelineBuildCommand

首先需要判斷時(shí)增量編譯還是全量編譯,全量編譯則執(zhí)行CleanBuildCommand,增量編譯則執(zhí)行IncrementalBuildCommand

        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()

我們看一下is_need_clean_build方法

    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

freelined的策略如下,如果有策略需求,可以通過更改這部分的代碼來實(shí)現(xiàn)。

1.在git pull 或 一次性修改大量
2.無法依賴增量實(shí)現(xiàn)的修改:修改AndroidManifest.xml,更改第三方j(luò)ar引用,依賴編譯期切面,注解或其他代碼預(yù)處理插件實(shí)現(xiàn)的功能等。
3.更換調(diào)試手機(jī)或同一調(diào)試手機(jī)安裝了與開發(fā)環(huán)境不一致的安裝包。

  1. CleanBuildCommand

        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))

可以看到,全量編譯時(shí)實(shí)際時(shí)執(zhí)行了如上幾條command,我們重點(diǎn)看一下GenerateSortedBuildTasksCommand,這里創(chuàng)建了多條存在依賴關(guān)系的task,在task_engine啟動(dòng)按照依賴關(guān)系執(zhí)行,其它c(diǎn)ommand類似。

Paste_Image.png

其依賴關(guān)系是通過childTask的關(guān)系進(jìn)行確認(rèn),可參考gradle_clean_build模塊中的generate_sorted_build_tasks方法:

        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)

最后在ExecuteCleanBuildCommand中啟動(dòng)task_engine

self._task_engine.add_root_task(self._root_task)
self._task_engine.start()
  • 增量編譯

增量編譯與全量編譯之前的步驟相同,在FreelineBuildCommand中創(chuàng)建了IncrementalBuildCommand

  1. IncrementalBuildCommand

self.add_command(CheckBulidEnvironmentCommand(self._builder))
self.add_command(GenerateSortedBuildTasksCommand(self._builder))
self.add_command(ExecuteIncrementalBuildCommand(self._builder))

創(chuàng)建了三個(gè)command,我們重點(diǎn)看一下GenerateSortedBuildTasksCommand這里比全量編譯更復(fù)雜一些。

  1. GenerateSortedBuildTasksCommand


    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])

可以看到首先遍歷每個(gè)module創(chuàng)建AndroidIncrementalBuildTask,之后遍歷mudle創(chuàng)建任務(wù)依賴關(guān)系。創(chuàng)建AndroidIncrementalBuildTask時(shí)傳入了GradleCompileCommand

  1. GradleCompileCommand

self.add_command(GradleIncJavacCommand(self._module, self._invoker))
self.add_command(GradleIncDexCommand(self._module, self._invoker))

查看一下GradleIncJavacCommand

        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()

執(zhí)行了以上幾個(gè)函數(shù),具體的內(nèi)容可以查看源碼。
以下簡(jiǎn)單說一下task_engine時(shí)如何解決task的依賴關(guān)系,這里根據(jù)task中的 parent_task列表定義了每個(gè)task的depth:

    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)

在具體執(zhí)行時(shí)根據(jù)depth對(duì)task進(jìn)行了排序

        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))

然后每個(gè)task執(zhí)行時(shí)會(huì)判斷parent是否執(zhí)行完成

while not self.task.is_all_parent_finished():   
        # self.debug('{} waiting...'.format(self.task.name))    
        self.task.wait()

只有parent任務(wù)執(zhí)行完成后,task才可以開始執(zhí)行。

總結(jié)

本文從增量編譯的原理和代碼角度簡(jiǎn)單分析了freeline的實(shí)現(xiàn),其中原理部分主要參考了中文原理說明,代碼部分主要分析了大體框架,沒有深入到每一個(gè)細(xì)節(jié),如freeline如何支持apt、lambda等,可能之后會(huì)再繼續(xù)寫文分析。
本人才疏學(xué)淺,如果有分析錯(cuò)誤的地方,請(qǐng)指出。

參考

https://github.com/alibaba/freeline
https://yq.aliyun.com/articles/59122?spm=5176.8091938.0.0.1Bw3mU
http://www.cnblogs.com/Pickuper/archive/2011/06/14/2078969.html
http://blog.csdn.net/luoshengyang/article/details/8744683?spm=5176.100239.blogcont59122.10.pdZfgL

Other

歡迎關(guān)注公眾號(hào)wutongke,每天推送移動(dòng)開發(fā)前沿技術(shù)文章:

wutongke

推薦閱讀:

神兵利器-Android性能調(diào)優(yōu)工具Hugo

神兵利器-內(nèi)存調(diào)試插件

炫酷的懸浮操作欄-谷歌出品

ViewPager倒計(jì)時(shí)播放

Android保存私密信息-強(qiáng)大的keyStore(譯)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 參考:https://github.com/alibaba/freeline/blob/master/freeli...
    才兄說閱讀 6,854評(píng)論 1 9
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,688評(píng)論 19 139
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,319評(píng)論 25 708
  • 說起我童年時(shí)候的那只老花貓,其實(shí)它到我家時(shí),還只是幼兒的年紀(jì)。 鄰居家的哥哥在外面玩,不知從哪里把它捉了回來。 那...
    kyran閱讀 1,421評(píng)論 4 8
  • 真實(shí)的生活無法歸于口頭表達(dá)或書面寫出的言語,誰都做不到,從來做不到。真實(shí)的生活開始于我們獨(dú)處之時(shí),獨(dú)自思考、獨(dú)自感...
    滟新閱讀 549評(píng)論 9 1

友情鏈接更多精彩內(nèi)容