貝殼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":{}}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章