Freeline source文件的增量原理梳理
为什么要写这个呢?
因为我觉得…适配kotlin之前要弄通java的增量逻辑然后适配其实也不是很难
还有就是... source增量比资源增量好理解些(个人感觉)
先放个图
切入正题
官方已经很细致的讲述了freeline增量构建的原理,欢迎时刻回味官方介绍,贴个地址
这次我的分析作为一个第三方分析,主要来源于一个修轮子的人对于源码一层层的理解。因为要适配kotlin嘛,所以要先把java增量理顺。
所谓增量?
增量 ——> 对症下药
python freeline.py -f
第一步跑项目,生成一些基本信息
在 app/build/freeline目录中我们可以发现这些文件
~/AndroidStudioProjects/One/app/build/freeline vlayout*
❯ ls
app freeline-backup jar_dependencies.json public_keeper.xml
freeline-assets freeline_annotation_info.json project_info_cache.json stat_cache.json
- jar_dependencies.json : 记录项目所用第三方依赖jar包的地址,classpath会用到
- project_info_cache.json : 记录项目相关的配置,不如说assets在哪啊,java环境变量啊什么的
- stat_cache.json : 这就是本节重点了 记录了各个文件的状态,记录了什么状态呢???
让我们来看看啊~
{
"app": {
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png": {
"md5": "4ad08251ddecfdd6e2a2c53e79b8d8de",
"mtime": 1491468325.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/layout/activity_vlayout_main.xml": {
"md5": "27764e0bfc8b1eceb487270b5f81077f",
"mtime": 1491671113.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/java/com/twtstudio/one/view/InfoView.java": {
"md5": "ca64e944f5f77b3d9d963e6b5f7a5b1b",
"mtime": 1491468325.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/values/dimens.xml": {
"md5": "77708536bdf75969da02b4a5687d1a9b",
"mtime": 1491468325.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/mipmap-xxhdpi/ic_launcher.png": {
"md5": "40d69d20dd057b4a331d03f2f3f2667e",
"mtime": 1491468325.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/AndroidManifest.xml": {
"md5": "58017fd1d190500d73de049906a5c5c6",
"mtime": 1491663464.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/values/strings.xml": {
"md5": "5a7370677c86e0d1f9172ad12d9b07c8",
"mtime": 1491468325.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/layout/info_fragment.xml": {
"md5": "66fa79125bcec1303742ef02a1e00b91",
"mtime": 1491468325.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/layout/item.xml": {
"md5": "d9ac17df542927edd7e87d686827a68e",
"mtime": 1491661862.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/java/com/twtstudio/one/OneApp.java": {
"md5": "2540855b975841251027d551e06e6c39",
"mtime": 1491468325.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/mipmap-mdpi/ic_launcher.png": {
"md5": "5ff8945dee0f56d079db8ce6691aaaeb",
"mtime": 1491468325.0
},
"/Users/retrox/AndroidStudioProjects/One/app/build.gradle": {
"md5": "5ca79f9228d06c1548bded1b926b62b4",
"mtime": 1503676918.0
},
"/Users/retrox/AndroidStudioProjects/One/app/libs/Depth_lib_android.jar": {
"md5": "725d9cbc54865c1fe5ccfcec13255753",
"mtime": 1491468325.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/layout/activity_welcome.xml": {
"md5": "7bc85b0c5224fef021a037f4b057cffb",
"mtime": 1491663032.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/layout/footer.xml": {
"md5": "55dcb32dc6a806382ddfc05505e7c42c",
"mtime": 1491468325.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/layout/activity_content.xml": {
"md5": "70bcbcd6fe9ee665e4e83ee172936246",
"mtime": 1491468325.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/layout/activity_main.xml": {
"md5": "f5b8721e3a23069a6f9f945cdf8551ac",
"mtime": 1491536877.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/mipmap-xhdpi/ic_launcher.png": {
"md5": "93fd2eadf162fd41695cab262540184f",
"mtime": 1491468325.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/values/styles.xml": {
"md5": "ef65245d9b65dc4e791395cc5351de59",
"mtime": 1491468325.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/mipmap-hdpi/ic_launcher.png": {
"md5": "357539403cded61d96161198a0773ffc",
"mtime": 1491468325.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/drawable/one_pic.jpeg": {
"md5": "d4adae21678a36795006a1d9d6ad2fb3",
"mtime": 1491468325.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/values-w820dp/dimens.xml": {
"md5": "f06a662d076312e2271749c093cf40d3",
"mtime": 1491468325.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/java/com/twtstudio/one/presenter/InfoBeanPresenter.java": {
"md5": "d89984c2adf024bcd39104bdec13d131",
"mtime": 1491527284.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/values/colors.xml": {
"md5": "5891b38465c5cfd7020d91693388aea6",
"mtime": 1491468325.0
},
"/Users/retrox/AndroidStudioProjects/One/app/src/main/res/layout/item_simple_title.xml": {
"md5": "18b47cab0db0c34cdde68598d2c41552",
"mtime": 1491671160.0
}
},
"/Users/retrox/AndroidStudioProjects/One/settings.gradle": {
"md5": "a7fe1ac39169b5e5285a5e53662f251b"
},
"/Users/retrox/AndroidStudioProjects/One/build.gradle": {
"md5": "413db5cee66d5c922059d2ab9b0f9163"
}
}
所以看得出来他是扫描了项目代码,资源相关目录的文件,并且标注了修改日期和md5值
写这篇博客的时候我已经适配了kotlin...所以我故意把json里面的的kt文件一个个删掉...
宝宝心里苦啊
继续说,其实看到这个json,心里还是有一点小头绪的,那我们去看代码吧!
def __check_changes(self, module_name, fpath, module_cache):
if not fpath:
return False
if fpath not in module_cache:
self.debug('find new file {}'.format(fpath))
return True
stat = module_cache[fpath]
mtime = os.path.getmtime(fpath)
if mtime > stat['mtime']:
self.debug('find {} has modification.'.format(fpath))
stat['mtime'] = mtime
self._stat_cache[module_name][fpath] = stat
md5 = get_md5(fpath)
if md5 != stat['md5']:
self.debug('find {} has modification.'.format(fpath))
stat['md5'] = md5
self._stat_cache[module_name][fpath] = stat
return True
return False
这就是检查文件变化的代码(json缓存层的代码我就不贴了 大家意会 也可以看代码)
如果在json cache文件里找不到的话 直接返回true
如果找到了,那就检查修改时间 ,如果修改时间有变化 然后就判断md5 最后综合得出结论
个人感觉:用修改时间比较来避免大量的md5
运算然后用md5校验最大限度的减小不必要增量
然后我们再回去看增量文件的扫描方法
# scan src
src_dirs = self._config['project_source_sets'][module_name]['main_src_directory']
for src_dir in src_dirs:
if os.path.exists(src_dir):
for dirpath, dirnames, files in os.walk(src_dir):
if re.findall(r'[/\\+]androidTest[/\\+]', dirpath) or '/.' in dirpath:
continue
for fn in files:
if fn.endswith('java'):
if fn.endswith('package-info.java') or fn.endswith('BuildConfig.java'):
continue
fpath = os.path.join(dirpath, fn)
if self.__check_changes(module_name, fpath, module_cache):
self._changed_files[module_name]['src'].append(fpath)
# 添加kotlin的增量检查
# elif fn.endswith('kt'):
# fpath = os.path.join(dirpath, fn)
# if self.__check_changes(module_name, fpath, module_cache):
# self._changed_files[module_name]['kotlin'].append(fpath)
就是对相应的文件夹遍历而已 然后判断修改 如果修改就把它的路基保存下来 秋后问斩~
我注释掉了自己加的kotlin判断部分 当然你也可以推断出 之前的代码对于kotlin文件是不作处理的
对! 直接跳过了......
self._changed_files[module_name]['src'].append(fpath)
这里放着修改过的文件,然后对它们做什么操作呢
当然是javac编译他们了啊!
# 删减了一些处理兼容性 apt什么的代码 为了方便阅读
def _generate_java_compile_args(self, extra_javac_args_enabled=False):
javacargs = [self._javac]
arguments = ['-encoding', 'UTF-8', '-g']
if not self._is_retrolambda_enabled:
arguments.extend(['-target', '1.7', '-source', '1.7'])
arguments.append('-cp')
arguments.append(os.pathsep.join(self._classpaths))
# 睁大眼睛
for fpath in self._changed_files['src']:
arguments.append(fpath)
arguments.append('-d')
arguments.append(self._finder.get_patch_classes_cache_dir())
javacargs.extend(arguments)
return javacargs
随意感受 java增量过程
class GradleIncJavacCommand(IncJavacCommand):
def __init__(self, module_name, invoker):
IncJavacCommand.__init__(self, module_name, invoker)
def execute(self):
self._invoker.check_r_md5() # check if R.java has changed
# self._invoker.check_other_modules_resources()
should_run_javac_task = self._invoker.check_javac_task()
if not should_run_javac_task:
self.debug('no need to execute')
return
self.debug('start to execute javac command...')
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() # 在这个方法里面调用了_generate_java_compile_args
self._invoker.run_retrolambda()
然后呢? 打包增量Dex推送到手机
官方文档对原理有解析 就不再细说 (很多热修复框架也在说这个)
追查到freeline源码
class GradleIncDexCommand(IncDexCommand):
def __init__(self, module_name, invoker):
IncDexCommand.__init__(self, module_name, invoker)
def execute(self):
should_run_dex_task = self._invoker.check_dex_task()
if not should_run_dex_task:
self.debug('no need to execute')
return
self.debug('start to execute dex command...')
self._invoker.run_dex_task()
# 根据字节码打包成dex
def run_dex_task(self):
patch_classes_cache_dir = self._finder.get_patch_classes_cache_dir()
# dex_path = self._finder.get_dst_dex_path()
dex_path = self._finder.get_patch_dex_dir()
add_path = None
if is_windows_system():
add_path = str(os.path.abspath(os.path.join(self._javac, os.pardir)))
dex_args = [self._dx, '--dex', '--multi-dex', '--output=' + dex_path, patch_classes_cache_dir]
else:
dex_args = [self._dx, '--dex', '--no-optimize', '--force-jumbo', '--multi-dex', '--output=' + dex_path,
patch_classes_cache_dir]
self.debug('dex exec: ' + ' '.join(dex_args))
output, err, code = cexec(dex_args, add_path=add_path)
if code != 0:
raise FreelineException('incremental dex compile failed.', '{}\n{}'.format(output, err))
else:
mark_restart_flag(self._cache_dir)
这部分就是检查是否需要进行dex操作然后...
生成增量的部分dex然后插到DebugApp的上面 实现dex的热更新
设置一些标志位什么的(用来表示要不要重启app 要不要重启activity什么的)