贝壳Flutter瘦身实践

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"背景","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"贝壳找房内部大部分的App都已经接入了Flutter,而且公司在跨端方案的选择上在大力发展Flutter生态体系,越来越多的团队也在使用Flutter,Flutter虽然能带来高人效和高性能的体验,同时也导致包体积增加,包体积的增加会给我们的推广增大难度,所以我们迫切需要一套针对Flutter的通用瘦身方案。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"现状:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以一个空工程为例,Flutter产物主要包含两部分,App.framework和Flutter.framework这两个库,这两个库达到了16M,对我们的包体积优化会带来不小的压力,所以我们立项了Thin-Flutter项目,主要是为了所有App提供一套Flutter通用的瘦身方案。(由于安卓侧有比较多的手段来实现瘦身,所以本篇文章主要针对iOS)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对于包大小问题,Flutter官方也在持续跟进优化:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter V1.2 开始支持","attrs":{}},{"type":"link","attrs":{"href":"https://developers.googleblog.com/2019/02/launching-flutter-12-at-mobile-world.html","title":null,"type":null},"content":[{"type":"text","text":"Android App Bundles","attrs":{}}]},{"type":"text","text":",支持Dynamic Module下发。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter V1.12 ","attrs":{}},{"type":"link","attrs":{"href":"https://medium.com/flutter/announcing-flutter-1-12-what-a-year-22c256ba525d","title":null,"type":null},"content":[{"type":"text","text":"优化了2.6%","attrs":{}}]},{"type":"text","text":" Android平台Hello World App大小(3.8M -> 3.7M)。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter V1.17 通过优化","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/dart-lang/sdk/commit/a2bb7301c5795e6b28089a8dc96e6ab5ca798e22","title":null,"type":null},"content":[{"type":"text","text":"Dart PC Offset存储","attrs":{}}]},{"type":"text","text":"以减少StackMap大小等多个手段,再次优化了产物大小,实现","attrs":{}},{"type":"link","attrs":{"href":"https://medium.com/flutter/announcing-flutter-1-17-4182d8af7f8e","title":null,"type":null},"content":[{"type":"text","text":"18.5%的缩减","attrs":{}}]},{"type":"text","text":"。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter V1.20 通过","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/flutter/flutter/pull/49737","title":null,"type":null},"content":[{"type":"text","text":"Icon font tree shaking","attrs":{}}]},{"type":"text","text":"移除未用到的icon fonts,进一步优化了应用大小。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter V2.2.2 并没有明显的措施","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们以贝壳flutter产物为例(Flutter SDK : 1.22.4)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"App.framework 总大小20.8M","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/1a/1aa29fd6320b5e40828fc00a4421022f.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter.framework 总大小7.7M","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/3b/3b07df08c8e68f696cae8e78100765fc.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"
类型大小介绍
App17.75M包含Instructions代码段、数据段及其他
flutter_assets2.6M资源文件
info.plist较小配置文件,比较小,后面忽略
"}}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们先来分析Flutter的产物构成,通过对编译命令优化后,产物如下(Release模式):","attrs":{}}]},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"

类型

大小

介绍

App

17.75M

包含Instructions代码段、数据段及其他

flutter_assets

2.6M

资源文件

info.plist

较小

配置文件,比较小,后面忽略

"}}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/fb/fb8348ea2a4383ed8abb1a5d04352771.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"App.framework:其中两个文件占比较大,一个是App可执行文件,另一个是flutter_assets. App可执行文件是Dart侧业务代码AOT编译的产物,会随着业务量的增多而变大,flutter_assets包含图片、字体等资源文件。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter.framework:  引擎产物,大小是固定的,但是初始占比比较大。这部分能优化的空间很小,主要是通过裁剪引擎不需要的功能,减少体积。编译引擎时可以选择性编译skia和boringssl,收益大概只有几百K。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"经过对比,iOS和Flutter代码量增长对于包体积的影响是有很大区别的,由于Flutter的Tree Shaking机制,未被引用的代码都会被裁剪掉,这个机制iOS里是没有的,那么这个机制所造成的影响就是Flutter包体积在初期会极速增加,到一个临界点包体积的增加会趋于平缓。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"贝壳瘦身方案","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"一、方案调研:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"包体积瘦身方法论,大概就三种,要么删减,要么压缩,要么挪走,对于删减Flutter自带有tree-sharking机制,也就是没有用到的代码会自动裁剪,所以删减不会有太明显的效果,对于压缩,各个团队都会不定时压缩图片,所以不能作为主方案,那么想要有明显的瘦身效果,最好的方面很明显是挪了。下面是一些常用的瘦身方案:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"通过打包命令删减","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"—split-debug-info可以分离出debug info","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"—strip 去除无用符号","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"—dwarf_stack_trace 表示在生成的动态库文件中,不使用堆栈跟踪符号","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"—obfuscate 表示混淆,通过减少变量名/方法名的方式减小代码体积","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"减少显示类型转换操作","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"as  String/Bool等等,这类操作会导致App.framework体积显著增加,主要是他会增加类型检测及抛出异常的处理逻辑。","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Flutter引擎删减及符号化分离","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter引擎中包括了Dart、skia、boringssl、icu、libpng等多个模块,其中Dart和skia是必须的,其他模块如果用不到倒是可以考虑裁掉,能够带来几百k的瘦身收益。业务方可以根据业务诉求自定义裁剪。","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"无用代码及无用资源删除","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这个不用过多解释,这种直接删代码删资源的方案是最常见的,但投入回报比并不高。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了给各个业务线分配瘦身指标之外,通过其他方式达到了非常不错的瘦身效果,主要包括以下几方面:","attrs":{}}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"去除符号化文件","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter产物数据段及资源文件动态下发","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"其他方式:包括去除无用文件、无用资源等","attrs":{}}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"二、具体实现:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"贝壳希望有一套长期有效的瘦身方案,以及监控体系,所以贝壳的瘦身方案包括两方面,一是包大小分析及监控,二是通用的,对业务同学无感知的瘦身方案。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"1.监控","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为了让Flutter包大小结构更加一目了然,我们将Flutter包大小进行了线上可视化。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,我们对flutter_tools了修改,在打包过程中我们会收集各个Flutter组件中二进制和资源的大小并写入文件(这里的实现我们参考了flutter_tools中analyze_size.dart的代码),打包完成后会将包大小分析文件上传至服务器;然后,我们在后端对上传的包大小文件进行分析,并将各个组件对应到相应的业务线;最终,我们将分析过后的包大小文件在前端进行展示。除了展示各组件和业务线的大小之外,我们还提供了Flutter包大小的对比功能,这样就可以更清晰的看到各个组件和业务线的前后大小变化。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了包大小的分析,我们就可以根据各业务线和组件的不同情况制定不同的瘦身目标。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6f/6f2d19c35cafe63072ade5c94fa45f6e.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"与此同时,我们可以对包大小的变化有一个长期的监控,可以及时发觉增量大的组件或者业务方,及时做出调整。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.瘦身方案","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"2.1.去除符号化文件","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"贝壳通过podspec注入命令的方式,将debug符号信息剥离到指定目录,但这样会产生一个新的问题,Flutter侧error无法解析,因此,我们在编译的同时将符号文件和uuid唯一标识绑定上传后端归档,在App的Flutter页面发生异常时,动态获取当前运行app App.framework组件的uuid标识,连同异常堆栈上报,后端根据uuid匹配符号文件,并解析异常堆栈,这部分可以瘦身1.3M左右。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以下是Flutter error的解析流程图:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/be/be7941105be982e5183cf3ac297cfb31.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由各个业务方梳理无用页面及无用资源文件、图片压缩等等,其中无用代码及无用资源删减有2M的收益,图片压缩有800k的收益。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":" if File.exist?(\"./pubspec.yaml\")\n com_script_phase = { :name=>'build dart', :script => < rs(file);\n result = Dart_CreateVMAOTSnapshotAsAssembly(StreamingWriteCallback, file);\n CHECK_RESULT(result);\n break;\n }\n default:\n UNREACHABLE();\n }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"经过验证,release 模式下走的是 CreateAndWritePrecompiledSnapshot 编译流程,因此我们将其改造,将数据段回写到磁盘,需要重写 CreateAppAOTSnapshotAsAssembly 方法。至于回写文件,我们发现 debug 模式下会使用 WriteFile 方法写入文件,这里仿照 debug 模式,将回传的数据段写入./ios/Flutter/Resource/路径下。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"gen_snapshot.cc 文件","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"static void CreateAndWritePrecompiledSnapshot() {\n ASSERT(IsSnapshottingForPrecompilation());\n Dart_Handle result;\n\n // Precompile with specified embedder entry points\n result = Dart_Precompile();\n CHECK_RESULT(result);\n\n // Create a precompiled snapshot.\n if (snapshot_kind == kAppAOTAssembly) {\n if (strip && (debugging_info_filename == nullptr)) {\n Syslog::PrintErr(\n \"Warning: Generating assembly code without DWARF debugging\"\n \" information.\\n\");\n }\n if (loading_unit_manifest_filename == nullptr) {\n File* file = OpenFile(assembly_filename);\n RefCntReleaseScope rs(file);\n File* debug_file = nullptr;\n if (debugging_info_filename != nullptr) {\n debug_file = OpenFile(debugging_info_filename);\n }\n //在flutter编译目录下创建Resource目录,用于存放数据段产物\n FILE *fp = NULL;\n fp = fopen(\"./ios/Flutter/Resource/\", \"w\");\n \n if (!fp) {\n mkdir(\"./ios/Flutter/Resource/\", 0775);\n }\n else\n {\n fclose(fp);\n }\n //创建data_buffer对象用于回写数据段\n uint8_t* vm_snapshot_data_buffer = NULL;\n intptr_t vm_snapshot_data_size = 0;\n uint8_t* isolate_snapshot_data_buffer = NULL;\n intptr_t isolate_snapshot_data_size = 0;\n \n //对Dart_CreateAppAOTSnapshotAsAssembly方法改造,将数据 \n 段回写到磁盘上\n result = Dart_CreateAppAOTSnapshotAsAssembly(\n &vm_snapshot_data_buffer, \n &vm_snapshot_data_size,\n &isolate_snapshot_data_buffer, \n &isolate_snapshot_data_size,\n StreamingWriteCallback, file,\n strip, \n debug_file);\n //写入isolate_snapshot_data数据段\n WriteFile(\"./ios/Flutter/Resource/isolate_snapshot_data\", \n isolate_snapshot_data_buffer, \n isolate_snapshot_data_size);\n //写入vm_snapshot_data数据段 \n WriteFile(\"./ios/Flutter/Resource/vm_snapshot_data\", \n vm_snapshot_data_buffer, \n vm_snapshot_data_size); \n \n \n \n if (debug_file != nullptr) debug_file->Release();\n CHECK_RESULT(result);\n } else {\n }\n..... 此处省略无关代码 ....... \n}\n\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Dart_CreateAppAOTSnapshotAsAssembly 具体实现为在 dart_api_impl.cc 文件,gen_snapshot 编译器会将 dart 代码编译为 snapshot_assembly.S 文件,而 snapshot_assembly.S 文件实际上就包含了","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"kDartIsolateSnapshotData //数据段","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"kDarVmSnapshotData //数据段","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"kDartIsolateSnapshotInstructions //代码段","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"kDartVmSnapshotInstructions //代码段","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这几部分。那么我们找到如何将数据段和代码段写入 snapshot_assembly.S 文件,把数据段分离出来不就可以了吗?我们在 FullSnapshotWriter 里发现了整个 snapshot_assembly.S 的写入过程。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"DART_EXPORT Dart_Handle\n//增加四个参数用于回写数据段\nDart_CreateAppAOTSnapshotAsAssembly(uint8_t** vm_snapshot_data_buffer,\n intptr_t* vm_snapshot_data_size,\n uint8_t** isolate_snapshot_data_buffer,\n intptr_t* isolate_snapshot_data_size,\n Dart_StreamingWriteCallback callback,\n void* callback_data,\n bool strip,\n void* debug_callback_data) {\n#if defined(TARGET_ARCH_IA32)\n return Api::NewError(\"AOT compilation is not supported on IA32.\");\n#elif defined(TARGET_OS_WINDOWS)\n return Api::NewError(\"Assembly generation is not implemented for Windows.\");\n#elif !defined(DART_PRECOMPILER)\n return Api::NewError(\n \"This VM was built without support for AOT compilation.\");\n#else\n DARTSCOPE(Thread::Current());\n API_TIMELINE_DURATION(T);\n CHECK_NULL(callback);\n\n // Mark as not split.\n T->isolate_group()->object_store()->set_loading_units(Object::null_array());\n \n GrowableArray* units = nullptr;\n LoadingUnitSerializationData* unit = nullptr;\n uint32_t program_hash = 0;\n \n const bool generate_debug = debug_callback_data != nullptr;\n\n ZoneWriteStream vm_snapshot_data(T->zone(), FullSnapshotWriter::kInitialSize);\n ZoneWriteStream vm_snapshot_instructions(T->zone(), kInitialSize);\n ZoneWriteStream isolate_snapshot_data(T->zone(),\n FullSnapshotWriter::kInitialSize);\n ZoneWriteStream isolate_snapshot_instructions(T->zone(), kInitialSize);\n\n StreamingWriteStream assembly_stream(kAssemblyInitialSize, callback,\n callback_data);\n StreamingWriteStream debug_stream(generate_debug ? kInitialDebugSize : 0,\n callback, debug_callback_data);\n\n auto const elf = generate_debug\n ? new (Z) Elf(Z, &debug_stream, Elf::Type::DebugInfo,\n new (Z) Dwarf(Z))\n : nullptr;\n\n AssemblyImageWriter image_writer(T, &assembly_stream, strip, elf);\n FullSnapshotWriter writer(Snapshot::kFullAOT, &vm_snapshot_data,\n &isolate_snapshot_data, &image_writer,\n &image_writer);\n\n if (unit == nullptr || unit->id() == LoadingUnit::kRootId) {\n writer.WriteFullSnapshot(units);\n } else {\n writer.WriteUnitSnapshot(units, unit, program_hash);\n }\n \n image_writer.Finalize();\n //数据段大小与buffer\n *vm_snapshot_data_buffer = vm_snapshot_data.buffer();\n *vm_snapshot_data_size = vm_snapshot_data.bytes_written();\n *isolate_snapshot_data_buffer = isolate_snapshot_data.buffer();\n *isolate_snapshot_data_size = isolate_snapshot_data.bytes_written();\n\n return Api::Success();\n#endif\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到这一步我们已经将数据段回写到磁盘上了,Resource 下成功写入了两个数据段产物,如下图:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d5/d5afe099d579e986a0bdb11e4ed11e72.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"2.2.2、将数据段从可执行文件中剔除","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同理,找到写入数据段的位置,将其剔除,我们本着改动量最小的原则,分析原有写入逻辑,发现源码里已经将不同符号类型的数据归类,那么顺着原有逻辑,在写入符号的时候,将数据段类型剔除即可。具体源码如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"image_snapshot.cc","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"//判断符号类型\nconst char* ImageWriter::SectionSymbol(ProgramSection section, bool vm) const {\n switch (section) {\n case ProgramSection::Text:\n return vm ? kVmSnapshotInstructionsAsmSymbol\n : kIsolateSnapshotInstructionsAsmSymbol;\n case ProgramSection::Data:\n return vm ? kVmSnapshotDataAsmSymbol : kIsolateSnapshotDataAsmSymbol;\n case ProgramSection::Bss:\n return vm ? kVmSnapshotBssAsmSymbol : kIsolateSnapshotBssAsmSymbol;\n case ProgramSection::BuildId:\n return kSnapshotBuildIdAsmSymbol;\n }\n return nullptr;\n}\n\n//\nvoid AssemblyImageWriter::WriteROData(NonStreamingWriteStream* clustered_stream,\n bool vm) {\n ImageWriter::WriteROData(clustered_stream, vm);\n if (!EnterSection(ProgramSection::Data, vm, ImageWriter::kRODataAlignment)) {\n return;\n }\n WriteBytes(clustered_stream->buffer(), clustered_stream->bytes_written());\n ExitSection(ProgramSection::Data, vm, clustered_stream->bytes_written());\n}\n\nbool AssemblyImageWriter::EnterSection(ProgramSection section,\n bool vm,\n intptr_t alignment) {\n ASSERT(FLAG_precompiled_mode);\n ASSERT(current_section_symbol_ == nullptr);\n bool global_symbol = false;\n switch (section) {\n case ProgramSection::Text:\n assembly_stream_->WriteString(\".text\\n\");\n global_symbol = true;\n break;\n case ProgramSection::Data:\n#if defined(TARGET_OS_LINUX) || defined(TARGET_OS_ANDROID) || \\\n defined(TARGET_OS_FUCHSIA)\n assembly_stream_->WriteString(\".section .rodata\\n\");\n#elif defined(TARGET_OS_MACOS) || defined(TARGET_OS_MACOS_IOS)\n assembly_stream_->WriteString(\".const\\n\");\n#else\n UNIMPLEMENTED();\n#endif\n global_symbol = true;\n break;\n case ProgramSection::Bss:\n assembly_stream_->WriteString(\".bss\\n\");\n break;\n case ProgramSection::BuildId:\n break;\n }\n current_section_symbol_ = SectionSymbol(section, vm);\n ASSERT(current_section_symbol_ != nullptr);\n //SectionSymbol方法返回current_section_symbol对象是否是数据段类型,若为true,则返回false不写入。注:strcmp(str1,str2),若str1=str2,则返回零;若str1str2,则返回正数。 \n if (strcmp(current_section_symbol_, kVmSnapshotDataAsmSymbol) == 0 || strcmp(current_section_symbol_, kIsolateSnapshotDataAsmSymbol) == 0) {\n return false;\n }\n if (global_symbol) {\n assembly_stream_->Printf(\".globl %s\\n\", current_section_symbol_);\n }\n Align(alignment);\n assembly_stream_->Printf(\"%s:\\n\", current_section_symbol_);\n return true;\n}\n\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过nm命令验证App文件中是否只剩下代码段:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/02/02d0d14f4c42c1d9ee9b312cd483ec13.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对比剥离之前:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/52/5229c0161a4d99abe4429b32b040844d.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"数据段剥离之后,也就完成了我们瘦身的目的,但是App运行时,没有数据段是不行的,会造成App崩溃,因此我们还需要一套完善的方案,来保证数据段从远端下发之后,安全的被加载。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"2.2.3、如何加载分离后的数据段","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们来看下加载流程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/22/2210a6cf2bf621aff9a3b3e09024f928.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter 引擎启动的时候,会创建 DartVM,同时加载可执行文件中的代码段和数据段。具体方法可追溯到 ResolveVMData、ResolveVMInstructions、ResolveIsolateData、ResolveIsolateInstructions 等四个方法,分别加载了数据段与代码段,而这四个方法都指向了同一个方法,也就是 SearchMapping 方法,如下:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"static std::shared_ptr ResolveVMData(\n const Settings& settings) {\n#if DART_SNAPSHOT_STATIC_LINK\n return std::make_unique<:nonownedmapping>(kDartVmSnapshotData, 0);\n#else // DART_SNAPSHOT_STATIC_LINK\n return SearchMapping(\n settings.vm_snapshot_data, // embedder_mapping_callback\n settings.vm_snapshot_data_path, // file_path\n settings.application_library_path, // native_library_path\n DartSnapshot::kVMDataSymbol, // native_library_symbol_name\n false // is_executable\n );\n#endif // DART_SNAPSHOT_STATIC_LINK\n}\n\nstatic std::shared_ptr ResolveVMInstructions(\n const Settings& settings) {\n#if DART_SNAPSHOT_STATIC_LINK\n return std::make_unique<:nonownedmapping>(kDartVmSnapshotInstructions, 0);\n#else // DART_SNAPSHOT_STATIC_LINK\n return SearchMapping(\n settings.vm_snapshot_instr, // embedder_mapping_callback\n settings.vm_snapshot_instr_path, // file_path\n settings.application_library_path, // native_library_path\n DartSnapshot::kVMInstructionsSymbol, // native_library_symbol_name\n true // is_executable\n );\n#endif // DART_SNAPSHOT_STATIC_LINK\n}\n\nstatic std::shared_ptr ResolveIsolateData(\n const Settings& settings) {\n#if DART_SNAPSHOT_STATIC_LINK\n return std::make_unique<:nonownedmapping>(kDartIsolateSnapshotData, 0);\n#else // DART_SNAPSHOT_STATIC_LINK\n return SearchMapping(\n settings.isolate_snapshot_data, // embedder_mapping_callback\n settings.isolate_snapshot_data_path, // file_path\n settings.application_library_path, // native_library_path\n DartSnapshot::kIsolateDataSymbol, // native_library_symbol_name\n false // is_executable\n );\n#endif // DART_SNAPSHOT_STATIC_LINK\n}\n\nstatic std::shared_ptr ResolveIsolateInstructions(\n const Settings& settings) {\n#if DART_SNAPSHOT_STATIC_LINK\n return std::make_unique<:nonownedmapping>(\n kDartIsolateSnapshotInstructions, 0);\n#else // DART_SNAPSHOT_STATIC_LINK\n return SearchMapping(\n settings.isolate_snapshot_instr, // embedder_mapping_callback\n settings.isolate_snapshot_instr_path, // file_path\n settings.application_library_path, // native_library_path\n DartSnapshot::kIsolateInstructionsSymbol, // native_library_symbol_name\n true // is_executable\n );\n#endif // DART_SNAPSHOT_STATIC_LINK\n}\n\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"static std::shared_ptr SearchMapping(\n MappingCallback embedder_mapping_callback,\n const std::string& file_path,\n const std::vector<:string>& native_library_path,\n const char* native_library_symbol_name,\n bool is_executable) {\n // Ask the embedder. There is no fallback as we expect the embedders (via\n // their embedding APIs) to just specify the mappings directly.\n \n if (embedder_mapping_callback) {\n return embedder_mapping_callback();\n }\n //从settings.vm_snapshot_data_path或settings.isolate_snapshot_data_path加载\n // Attempt to open file at path specified.\n if (file_path.size() > 0) {\n if (auto file_mapping = GetFileMapping(file_path, is_executable)) {\n return file_mapping;\n }\n }\n// 从 settings.application_library_path 中加载\n // Look in application specified native library if specified.\n for (const std::string& path : native_library_path) {\n auto native_library = fml::NativeLibrary::Create(path.c_str());\n auto symbol_mapping = std::make_unique(\n native_library, native_library_symbol_name);\n if (symbol_mapping->GetMapping() != nullptr) {\n return symbol_mapping;\n }\n }\n\n // 从native_library_symbol_name加载\n {\n auto loaded_process = fml::NativeLibrary::CreateForCurrentProcess();\n auto symbol_mapping = std::make_unique(\n loaded_process, native_library_symbol_name);\n if (symbol_mapping->GetMapping() != nullptr) {\n return symbol_mapping;\n }\n }\n\n return nullptr;\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"从 SearchMapping 方法中可以判断,加载顺序为,先从 settings.vm_snapshot_data 或 settings.isolate_snapshot_data 加载,若不存在则从 settings.vm_snapshot_data_path 或 settings.isolate_snapshot_data_path 读取,再然后从 settings.application_library_path 中加载。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那么如果我们篡改 settings.vm_snapshot_data_path 和 settings.isolate_snapshot_data_path 的指向,是否可以将我们本地的数据段正确加载呢?答案是肯定的。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"struct Settings {\n Settings();\n\n Settings(const Settings& other);\n\n ~Settings();\n\n // VM settings\n std::string vm_snapshot_data_path; // deprecated\n MappingCallback vm_snapshot_data;\n std::string vm_snapshot_instr_path; // deprecated\n MappingCallback vm_snapshot_instr;\n\n std::string isolate_snapshot_data_path; // deprecated\n MappingCallback isolate_snapshot_data;\n std::string isolate_snapshot_instr_path; // deprecated\n MappingCallback isolate_snapshot_instr;\n std::string assets_path; //flutter_assets路径\n std::string icu_data_path; //icu_data国际化路径\n .... 此部分省略 .... \n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"从上面的加载流程图里可以看出来 Setting 类的初始化是在 FlutterDartProject 类里。我们在 FlutterDartProject 重设数据段路径、flutter_assets 路径和 icu_data 国际化文件路径。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我们在 App 启动时将分离产物下载到沙盒内的指定路径下:Document/flutter_resource/Resource/*","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然后将 setting 类中 path 指定到此路径下。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"FlutterDartProject.mm 文件","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"NSLog(@\"开始设置路径\");\n //设置沙盒路径\n NSArray * documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);\n NSString * documentDirectory = [documentPaths objectAtIndex:0];\n //设置vm_snapshot_data路径\n NSString *vm_snapshot_data_path = [NSString stringWithFormat:@\"%@/flutter_resource/Resource/vm_snapshot_data\",documentDirectory];\n if ([[NSFileManager defaultManager] fileExistsAtPath:vm_snapshot_data_path]) {\n settings.vm_snapshot_data_path = vm_snapshot_data_path.UTF8String;\n }\n \n //设置isolate_snapshot_data路径\n NSString *isolate_snapshot_data_path = [NSString stringWithFormat:@\"%@/flutter_resource/Resource/isolate_snapshot_data\",documentDirectory];\n if ([[NSFileManager defaultManager] fileExistsAtPath:isolate_snapshot_data_path]) {\n settings.isolate_snapshot_data_path = isolate_snapshot_data_path.UTF8String;\n }\n \n //设置资源路径\n NSString *assets_path = [NSString stringWithFormat:@\"%@/flutter_resource/Resource/flutter_assets\",documentDirectory];\n \n if ([[NSFileManager defaultManager] fileExistsAtPath:assets_path]) {\n settings.assets_path = assets_path.UTF8String;\n }\n else {\n NSString* assetsName = [FlutterDartProject flutterAssetsName:bundle];\n NSString* assetsPath = [bundle pathForResource:assetsName ofType:@\"\"];\n \n if (assetsPath.length == 0) {\n assetsPath = [mainBundle pathForResource:assetsName ofType:@\"\"];\n }\n \n if (assetsPath.length == 0) {\n NSLog(@\"Failed to find assets path for \\\"%@\\\"\", assetsName);\n } else {\n settings.assets_path = assetsPath.UTF8String;\n }\n }\n //设置icu_data_path\n NSString *icu_data_path = [NSString stringWithFormat:@\"%@/flutter_resource/Resource/icudtl.dat\",documentDirectory];\n if ([[NSFileManager defaultManager] fileExistsAtPath:icu_data_path]) {\n settings.icu_data_path = icu_data_path.UTF8String;\n }\n else {\n NSString* icuDataPath = [engineBundle pathForResource:@\"icudtl\" ofType:@\"dat\"];\n if (icuDataPath.length > 0) {\n settings.icu_data_path = icuDataPath.UTF8String;\n }\n }\n \n if ([[NSFileManager defaultManager] fileExistsAtPath:vm_snapshot_data_path] && [[NSFileManager defaultManager] fileExistsAtPath:isolate_snapshot_data_path] ) {\n NSLog(@\"data存在\");\n }\n else {\n NSLog(@\"data不存在\");\n }\n \n NSLog(@\"路径设置完毕\");\n\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"设置完成以后,DartVM 启动所需要的各种资源与二进制就可以正常加载了。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"工程化落地","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了初步的瘦身方案,具体落地还需要很多配套措施,比如持续集成,私有云及监控体系。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6d/6dc6e5d84d4e43b496548574866f59c4.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"工程化落地主要包括三部分:","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"第一部分:定制 Flutter SDK","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"一、将 Flutter.framework 文件和 gen_snapshot 文件进行归档,同时需要制作 dSYM 符号表文件。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"iOS 提供了两个工具,一个是用于 Flutter.framework 的规定及符号表导出,另一个是用于 gen_snapshot 文件的归档,他们位于 engine/src/flutter/sky/tools/create_ios_framework.py 和 engine/src/flutter/sky/tools/create_macos_gen_snapshots.py。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"cd /path/to/engine/src\n\n./flutter/sky/tools/create_ios_framework.py \\\n--arm64-out-dir /path/engine/src/out/ios_release \\\n--armv7-out-dir /path/engine/src/out/ios_release_arm \\\n--simulator-out-dir /path/engine/src/out/ios_debug_sim \\\n--dst /path/engine/src/out/flutter-engine/artifacts/ios-release \\\n--strip --dsym\n\n./flutter/sky/tools/create_ios_framework.py \\\n--arm64-out-dir /path/engine/src/out/ios_profile \\\n--armv7-out-dir /path/engine/src/out/ios_profile_arm \\\n--simulator-out-dir /path/engine/src/out/ios_debug_sim \\\n--dst /path/engine/src/out/flutter-engine/artifacts/ios-profile \\\n--dsym\n\n./flutter/sky/tools/create_ios_framework.py \\\n--arm64-out-dir /path/engine/src/out/ios_debug \\\n--armv7-out-dir /path/engine/src/out/ios_debug_arm \\\n--simulator-out-dir /path/engine/src/out/ios_debug_sim \\\n--dst /path/engine/src/out/flutter-engine/artifacts/ios \\\n--dsym\n\n./flutter/sky/tools/create_macos_gen_snapshots.py \\\n--arm64-out-dir /path/engine/src/out/ios_release \\\n--armv7-out-dir /path/engine/src/out/ios_release_arm \\\n--dst /path/engine/src/out/flutter-engine/artifacts/ios-release\n\n./flutter/sky/tools/create_macos_gen_snapshots.py \\\n--arm64-out-dir /path/engine/src/out/ios_profile \\\n--armv7-out-dir /path/engine/src/out/ios_profile_arm \\\n--dst /path/engine/src/out/flutter-engine/artifacts/ios-profile\n\n./flutter/sky/tools/create_macos_gen_snapshots.py \\\n--arm64-out-dir /path/engine/src/out/ios_debug \\\n--armv7-out-dir /path/engine/src/out/ios_debug_arm \\\n--dst /path/engine/src/out/flutter-engine/artifacts/ios\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最终如图所示","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/87/87cf2b3b0ede603d8887f32de1a79eb2.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ios-release 文件夹就是我们我最终改造完的产物,接下来就是定制 sdk 了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"实际上 Flutter sdk 里会根据不同的平台,不同的 build model 选择不同的编译器和 Flutter engine,如下图所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c5/c54d3400067de9f351576005921bdec2.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们只需要把刚刚归档出来的 ios-release 替换 Flutter sdk 里的 ios-release 文件夹,之后 release 模式下打 iOS 产物,App.framework 就会是剥离出数据段的产物。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"二、结合 flutterw 部署定制 sdk","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由于目前公司 Flutter sdk 存在多个版本,比如 1.12.13、1.22.4 等,因此我们开发了 Flutter sdk 自动化管理工具 flutterw,可以根据项目的不同配置,切换不同的 Flutter sdk,包括官方 sdk,并且自动同步官方新版本。因此我们借助 flutterw 的能力,部署定制的 Flutter sdk,在有瘦身需求的项目里配置 sdk 版本即可。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先发布定制 sdk:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"flutterm upload -l /path/flutter -n 2.2.2 -c other -v 2.2.2 --platform macos\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"结果如图:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d1/d1d89d5288f7fa37304e2abde56efb24.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接着在对应的项目中配置相应的 flutter sdk 版本,如下图:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ce/ce0ead0e5e607c686a395450b970d6f1.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"三、改造 xcode_backend.sh 编译脚本,将数据段、资源包等压缩","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"ljEmbedFlutterFrameworks() {\n project_path=$1\n if [ ! -d \"${project_path}/ios/Flutter\" ]; then\n return 0\n fi\n local build_product_path=\"${project_path}/ios/Flutter\"\n local app_framewok_path=\"${build_product_path}/App.framework\"\n local thin_resource_zip=\"${app_framewok_path}/flutter_resource.zip\"\n local thin_uuid_txt=\"${app_framewok_path}/uuid_app.txt\"\n local flutter_framewok_path=\"${build_product_path}/Flutter.framework\"\n\n RunCommand cp -rf \"${BUILT_PRODUCTS_DIR}/App.framework\" \"${build_product_path}\"\n RunCommand cp -rf \"${BUILT_PRODUCTS_DIR}/Flutter.framework\" \"${build_product_path}\"\n #当前编译uuid\n local uuid=`dwarfdump -u --arch=$ARCHS \"${app_framewok_path}/App\" | awk -F ' ' '{print $2}'`\n echo $uuid\n local old_uuid=\"\"\n #判断上一步产物uuid是否一致\n if [ -f $thin_uuid_txt ];then\n old_uuid=$(cat $thin_uuid_txt);\n fi\n\n local tmep_assets_path=\"$app_framewok_path/flutter_assets\"\n\n if [[ $uuid != \"\" && $uuid == $old_uuid ]];then\n\n if [ -f \"${tmep_assets_path}/NOTICES\" ];then\n RunCommand rm -rf \"${tmep_assets_path}/NOTICES\"\n fi\n\n if [ -d \"${tmep_assets_path}/fonts\" ];then\n RunCommand rm -rf \"${tmep_assets_path}/fonts\"\n fi\n\n unzip -o $thin_resource_zip -d $build_product_path\n if [ -d $tmep_assets_path ];then\n if [ -d \"$build_product_path/Resource/flutter_assets\" ];then\n RunCommand rm -rf \"$build_product_path/Resource/flutter_assets\"\n fi\n RunCommand cp -rf $tmep_assets_path \"$build_product_path/Resource\"\n fi\n\n local temp_resource_zip=\"$build_product_path/flutter_resource.zip\"\n RunCommand cd ${build_product_path} && zip -r flutter_resource.zip ./Resource\n RunCommand cd ${build_product_path} && zip -r $thin_resource_zip ./Resource\n RunCommand cp -rf $temp_resource_zip $app_framewok_path\n\n RunCommand rm -rf $tmep_assets_path\n RunCommand rm -rf $temp_resource_zip\n RunCommand rm -rf \"$build_product_path/Resource\"\n RunCommand rm -rf \"$flutter_framewok_path/icudtl.dat\"\n else\n if [[ ${thin_resource} != \"\" && -d ${thin_resource} ]];then\n if [ -f \"${tmep_assets_path}/NOTICES\" ];then\n RunCommand rm -rf \"${tmep_assets_path}/NOTICES\"\n fi\n\n if [ -d \"${tmep_assets_path}/fonts\" ];then\n RunCommand rm -rf \"${tmep_assets_path}/fonts\"\n fi\n\n RunCommand cp -rf ${tmep_assets_path} ${thin_resource}\n\n if [ -f \"$flutter_framewok_path/icudtl.dat\" ];then\n RunCommand cp -f \"$flutter_framewok_path/icudtl.dat\" ${thin_resource}\n fi\n uuid=`dwarfdump -u --arch=arm64 \"${app_framewok_path}/App\" | awk -F ' ' '{print $2}'`\n echo ${uuid}>\"$thin_uuid_txt\"\n\n local temp_resource_zip=\"$build_product_path/flutter_resource.zip\"\n RunCommand cd ${build_product_path} && zip -r flutter_resource.zip ./Resource\n RunCommand cp -f $temp_resource_zip $app_framewok_path\n\n RunCommand rm -rf $tmep_assets_path\n RunCommand rm -rf $temp_resource_zip\n RunCommand rm -rf \"$thin_resource\"\n RunCommand rm -rf \"$flutter_framewok_path/icudtl.dat\"\n else\n if [ -f ${thin_resource_zip} ];then\n RunCommand rm -rf $thin_resource_zip\n fi\n\n if [ -f ${thin_uuid_txt} ];then\n RunCommand rm -rf $thin_uuid_txt\n fi\n fi\n fi\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"编译之后产物被压缩成 flutter_resource.zip,同时为了标识产物的唯一性,将可执行文件的 uuid 作为唯一标识,每次下载完成之后需要先对比 uuid 是否一致。若不一致则更新产物。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/9e/9ef7bc862d5c5246e58cea0be23a747e.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"第二部分:上传产物平台或内置压缩","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到这一步我们准备了两种方案:","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"内置压缩方案:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"也就是将数据段和资源包统一压缩内置在 App.framework 内,应用安装启动后自动解压放在指定位置。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"动态下发方案:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对于其他小体量 App,可以采取远程下发的方案,也就是将 flutter_resource.zip 和 uuid_app.txt 上传到 s3 平台(资源服务器),同时在阿波罗平台(配置平台)增加新版本配置。应用启动后下载的方案。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"方案对比:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"两种方案对比之下,动态下发的瘦身效果最好,但成功率没有内置压缩高,内置压缩方案由于只增加了一个解压环节,因此成功率较高。不同的 APP 可以根据自己的需求采用不同的方案。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"第三部分:产物管理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"若使用远端下载方案,App 启动会首先拉取远端产物,并将版本信息生成缓存,校验 md5 通过后即可加载,当发现有新版本产物则拉取新版本产物并替换。至于内置压缩方案,则根据 App.framework 的 UUID 来判断是否是正确的产物。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"内置压缩方案成功率达到了 99.99%,极小部分失败原因在于内存空间不足。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对于远程下发方案,App 启动后下载相关资源并解压,成功率会受到网络因素影响,增加了重试逻辑之后成功率如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e0/e02341b46774e48da8e9a4b088aef7ea.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"尽管下载成功率达到了 99.4%但对于 C 端这种大体量的 app 来说,仍然会影响大量的用户,因此在 C 端使用的是成功率更高的内置压缩方案。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"收益","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过动态下发这种方式,虽然可以显著的减小 Flutter 包体积,但是也会带来其他问题,比如由于网络原因导致产物下载失败。因此我们提供了更加安全可靠的方式,将这些文件压缩然后内置在 app 包内。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"动态下发方案:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/11/11ba375f59a9498517e3e9862b4a0b86.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"压缩内置方案:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/60/60276dbd6dc39b22e66206eaea695488.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"优势:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1.通用的解决方案,任何接入 Flutter 的 APP 都可以用","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.只需集成一次,无需定时优化","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3.随着 Flutter 业务的增多,瘦身效果也会更明显","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"劣势:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"剥离出的产物需要通过网络下发,下载成功率取决于网络状况、内存空间等等因素制约。所以后续规划中,会结合 Flutter2web 来缓解由于下载失败,导致 Flutter 页面无法打开的情况。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"符号化剥离及混淆:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a7/a7f6c43b8c7dfdfe5085d777da04d98c.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注:Thin 模式就是数据段及资源动态下发或内置压缩的模式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"总体瘦身:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"经过上述方案的优化,Flutter 侧瘦身总大小达到了 7M 左右。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而经过各个业务方共同的努力,贝壳找房 app 包大小终于达标。以当时的 V2.47 版本为例:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“iPhone6-iPhoneX 系列”机型安装大小 149.2-149.8M,下载大小 110.5M","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“iPhone11-12”机型安装大小 139.4M,下载大小仅 53M","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter engine 的改造源码目前已经开源,如果想尝试贝壳方案的同学可以按照开源文档接入。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"开源地址:","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/LianjiaTech/flutter_beike_engine","title":"","type":null},"content":[{"type":"text","text":"GitHub - LianjiaTech/flutter_beike_engine","attrs":{}}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"后续规划","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基于以上的优势劣势,贝壳致力于更加高标准的目标, 那么有没有其他办法在不影响成功率的情况下最大程度的增加瘦身比例呢?答案是有的,那就是结合 Flutter for web 来做兜底方案。具体方案如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/0d/0db5846bc50caef6886b4d41fa682487.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们知道,Flutter 在三端一体化做了大量的工作,Flutter 页面可以很好的被转换为 web 页面,我们可以借助这个特性,在编译发版包的时候,同时将 Flutter 工程编译为 Web 产物并部署在远端,当 App 启动后 Flutter 产物由于种种原因最终都无法下载成功的时候,自动打开对应的 web 页面。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"关于 Fluttter for web 容灾降级更详细内容可参考:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://mp.weixin.qq.com/s/zIeU0z-4P5Pd9THVybnDFQ","title":"","type":null},"content":[{"type":"text","text":"https://mp.weixin.qq.com/s/zIeU0z-4P5Pd9THVybnDFQ","attrs":{}}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章