前篇福利-Android增量編譯3~5秒介紹了增量編譯神器freeline的基本使用呼股,這篇文章主要介紹freeline是如何實現(xiàn)快速增量編譯的。
Android 編譯打包流程
首先看一下android打包流程圖,圖片來源Android開發(fā)學(xué)習(xí)筆記(二)——編譯和運行原理
R文件的生成
R文件記錄了每個資源的ID都弹,之后要參與到j(luò)ava的編譯過程仁锯,R文件是由aapt(Android Asset Package Tool)生成。java編譯
我們知道有時app開發(fā)中會跨進程通信此洲,這時可以通過aidl的方式定義接口厂汗,aidl工具可以根據(jù)aidl文件生成對應(yīng)的java文件。
之后R文件呜师、aidl相關(guān)java文件面徽、src中的java文件通過編譯生成 .class文件dex生成
編譯后的.class會又由dex工具打包成dex文件,freeline中用到了Buck中提取的dex工具,freeline給出的數(shù)據(jù)是比原生的dex工具快了40%-
資源文件編譯
aapt(Android Asset Package Tool)工具對app中的資源文件進行打包趟紊。其流程如圖(圖片來源)
Android應(yīng)用程序資源的編譯和打包過程分析羅升陽老師的文章非常清晰地分析了應(yīng)用資源的打包過程氮双。 apk文件生成與簽名
apkbuild工具把編譯后的資源文件和dex文件打包成為dex文件。jarsigner完成apk的簽名霎匈,當(dāng)然Android7.0之后可以通過apksigner工具進行簽名戴差。了解Android Studio 2.2中的APK打包中有介紹。
增量編譯原理
Android增量編譯分為代碼增量和資源增量铛嘱,資源增量是freeline的一個亮點暖释,instant-run開啟時其實在資源上并不是增量的,而是把整個應(yīng)用的資源打成資源包墨吓,推送至手機的球匕。
-
代碼增量
谷歌在支持multidex之后,當(dāng)方法數(shù)超過65535時帖烘,android打包后會存在多個dex文件亮曹,運行時加載類時,會從一個dexList依次查找秘症,找到則返回照卦,利用這個原理可以把增量的代碼打包成dex文件,插入到dexList的前邊乡摹,這樣就可以完成類的替換役耕。
這里有一個問題是在非art的手機上存在兼容性問題,這也是instant-run只支持android5.0以上的原因聪廉,freeline在這里使用之前安卓App熱補丁動態(tài)修復(fù)技術(shù)介紹中提出的插樁方案做了兼容處理瞬痘,這樣在非art手機上也可以進行增量編譯。
-
資源增量
資源增量是freeline的一個亮點板熊,在第一部分我們知道是通過aapt工具對應(yīng)用資源文件進行打包的图云,freeline開發(fā)了自己的incrementAapt工具(目前并沒有開源)。我們知道aapt進行資源編譯時邻邮,會生成R文件和resources.arsc文件竣况,R文件是資源名稱和資源id的一個對應(yīng)表,用于java文件中對資源的引用筒严,而resources.arsc文件描述了每個資源id對應(yīng)的配置信息丹泉,也就是描述了如何根據(jù)一個資源id找到對應(yīng)的資源。
- pulbic.xml 和ids.xml文件
aapt進行資源編譯時鸭蛙,如果兩次編譯之間資源文件進行了增刪操作摹恨,則編譯出的R文件即使資源名稱沒有變化,資源id值卻可能發(fā)生變化娶视,這樣如果進行資源增量編譯晒哄,則app在進行資源引用時可能發(fā)生資源引用錯亂的情況睁宰。因此第二次編譯時最好根據(jù)第一次編譯的結(jié)果進行,public.xml和ids.xml文件就是完成這件事情的寝凌,freeline開發(fā)了id-gen-tool利用第一次編譯的R文件來生成public.xml 和ids.xml柒傻,用于第二次的編譯。 -
客戶端的處理
freeline 利用incrementAapt增量工具打包出增量的資源文件较木,然后客戶端將文件放置在正確的位置红符,然后啟動應(yīng)用后,就可以正確訪問應(yīng)用資源了伐债。
freeline實現(xiàn)分析
freeline 在實現(xiàn)上借鑒了buck预侯,layoutCast的思想,把整個過程構(gòu)建成多個任務(wù)峰锁,多任務(wù)并發(fā)萎馅,同時緩存各個階段的生成文件,以達到快速構(gòu)建的目的虹蒋。
-
多任務(wù)并發(fā)
先來看一張圖(圖片來源)
freeline這里借鑒了buck的思想糜芳,如果工程中有多個module,freeline會建立好各個工程構(gòu)建的任務(wù)依賴千诬。在build過程中同時可能會有多個module在構(gòu)建耍目,之后在合適的時間把構(gòu)建后的文件進行合并膏斤。
-
緩存
我們在debug時可能會進行多次代碼修改徐绑,并運行程序看修改效果,也就是要進行多次的增量編譯莫辨,freeline對每次對編譯過程進行了緩存傲茄。比如我們進行了三次增量編譯,freeline每次編譯都是針對本次修改的文件沮榜,對比LayoutCast 和instant-run每次增量編譯都是編譯第一次全量編譯之后的更改的文件盘榨,freeline速度快了很多,根據(jù)freeline官方給的數(shù)據(jù)蟆融,快了3~4倍草巡,但是這樣freeline進行增量編譯時的復(fù)雜性增加了不少。
另外freeline增量編譯后可調(diào)試型酥,這點相對于instant-run 和LayoutCast來說山憨,優(yōu)勢很大。freeline官方介紹中提到的懶加載弥喉,個人認(rèn)為只是錦上添花的作用郁竟,在實際中可能并沒有太大作用。
代碼分析
終于到了代碼分析的環(huán)節(jié)由境,還是先貼一下freeline的github地址:freeline,我們看一下其源碼有哪些內(nèi)容
android-studio-plugin是android中的freeline插件源碼
databinding-cli顧名思義是對dababinding的支持
freeline_core是我們今天分析的重點
gradle 是對gradle中freeline配置的支持
release-tools中是編譯過程中用到的工具棚亩,如aapt工具等
runtime是增量編譯后客戶端處理的邏輯
sample是給出的demo
如果想編譯調(diào)試freeline增量編譯的源碼蓖议,可以先clone下freeline的源碼,然后導(dǎo)入sample工程讥蟆,注意sample中其實就包含了freeline_core的源碼勒虾,我這里用的ide是Pycharm。
freeline對于android的編譯分為兩個過程:全量編譯和增量編譯攻询,我們先來看全量編譯从撼。
-
全量編譯
-
代碼入口
代碼入口當(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的低零,然后對命令進行解析:
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對象
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,從名字可以就可以看出是進行命令分發(fā)的拯杠,就是在dispatcher中執(zhí)行不同的編譯過程掏婶。在dispatcher執(zhí)行call方法之前,init方法中執(zhí)行了checkBeforeCleanBuild命令潭陪,完成了部分初始化任務(wù)雄妥。
-
關(guān)鍵模塊說明
dispatcher
分發(fā)命令,根據(jù)freeline.py 中命令解析的結(jié)果執(zhí)行不同的命令
builder
執(zhí)行各種build命令
這是其類繼承圖依溯,可以看到最下邊兩個子類分別是gradleincbuilder和gradlecleanbuilder老厌,分別用于增量編譯和全量編譯。
command
利用build執(zhí)行命令黎炉,可以組織多個command枝秤,在創(chuàng)建command時傳入builder,則可以執(zhí)行不同的任務(wù)慷嗜。
task_engine
task_engine定義了一個線程池淀弹,TaskEngine會根據(jù)task的依賴關(guān)系,多線程執(zhí)行任務(wù)庆械。
task
freeline中定義了多個task薇溃,分為完成不同的功能
gradle_tools
定義了一些公有的方法:
-
命令分發(fā)
在代碼入口出可以發(fā)現(xiàn)對命令進行了解析,之后在dispatcher中對解析結(jié)果進行命令分發(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)
我們重點關(guān)注最后一行缭乘,在這里創(chuàng)建了FreelineBuildCommand沐序,接下來在這里進行全量編譯和增量編譯。
-
FreelineBuildCommand
首先需要判斷時增量編譯還是全量編譯堕绩,全量編譯則執(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的策略如下策幼,如果有策略需求,可以通過更改這部分的代碼來實現(xiàn)逛尚。
1.在git pull 或 一次性修改大量
2.無法依賴增量實現(xiàn)的修改:修改AndroidManifest.xml垄惧,更改第三方j(luò)ar引用,依賴編譯期切面,注解或其他代碼預(yù)處理插件實現(xiàn)的功能等绰寞。
3.更換調(diào)試手機或同一調(diào)試手機安裝了與開發(fā)環(huán)境不一致的安裝包到逊。
-
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))
可以看到铣口,全量編譯時實際時執(zhí)行了如上幾條command,我們重點看一下GenerateSortedBuildTasksCommand
,這里創(chuàng)建了多條存在依賴關(guān)系的task觉壶,在task_engine啟動按照依賴關(guān)系執(zhí)行脑题,其它command類似。
其依賴關(guān)系是通過childTask的關(guā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
中啟動task_engine
self._task_engine.add_root_task(self._root_task)
self._task_engine.start()
-
增量編譯
增量編譯與全量編譯之前的步驟相同叔遂,在FreelineBuildCommand
中創(chuàng)建了IncrementalBuildCommand
-
IncrementalBuildCommand
self.add_command(CheckBulidEnvironmentCommand(self._builder))
self.add_command(GenerateSortedBuildTasksCommand(self._builder))
self.add_command(ExecuteIncrementalBuildCommand(self._builder))
創(chuàng)建了三個command,我們重點看一下GenerateSortedBuildTasksCommand
這里比全量編譯更復(fù)雜一些争剿。
-
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])
可以看到首先遍歷每個module創(chuàng)建AndroidIncrementalBuildTask已艰,之后遍歷mudle創(chuàng)建任務(wù)依賴關(guān)系。創(chuàng)建AndroidIncrementalBuildTask時傳入了GradleCompileCommand
-
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í)行了以上幾個函數(shù)蚕苇,具體的內(nèi)容可以查看源碼哩掺。
以下簡單說一下task_engine時如何解決task的依賴關(guān)系,這里根據(jù)task中的 parent_task列表定義了每個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í)行時根據(jù)depth對task進行了排序
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))
然后每個task執(zhí)行時會判斷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é)
本文從增量編譯的原理和代碼角度簡單分析了freeline的實現(xiàn),其中原理部分主要參考了中文原理說明蹬碧,代碼部分主要分析了大體框架舱禽,沒有深入到每一個細節(jié),如freeline如何支持apt恩沽、lambda等誊稚,可能之后會再繼續(xù)寫文分析。
本人才疏學(xué)淺飒筑,如果有分析錯誤的地方片吊,請指出绽昏。
參考
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)注公眾號wutongke协屡,每天推送移動開發(fā)前沿技術(shù)文章:
推薦閱讀: