android nio debug模式正常 release包crash_Flutter包大小治理上的探索与实践
Flutter作為一種全新的響應(yīng)式、跨平臺(tái)、高性能的移動(dòng)開發(fā)框架,在性能、穩(wěn)定性和多端體驗(yàn)一致上都有著較好的表現(xiàn),自開源以來,已經(jīng)受到越來越多開發(fā)者的喜愛。
但是,Flutter的引入往往帶來包體積的增大,給很多研發(fā)團(tuán)隊(duì)帶來了很大的困擾。美團(tuán)外賣前端團(tuán)隊(duì)對(duì)Flutter的包大小問題進(jìn)行了調(diào)研和實(shí)踐,設(shè)計(jì)并實(shí)現(xiàn)了一套基于動(dòng)態(tài)下發(fā)的包大小優(yōu)化方案,希望對(duì)從事Flutter開發(fā)相關(guān)的同學(xué)能夠帶來一些啟發(fā)或者幫助。
一、背景
隨著Flutter框架的不斷發(fā)展和完善,業(yè)內(nèi)越來越多的團(tuán)隊(duì)開始嘗試并落地Flutter技術(shù)。不過在實(shí)踐過程中我們發(fā)現(xiàn),Flutter的接入會(huì)給現(xiàn)有的應(yīng)用帶來比較明顯的包體積增加。不論是在Android還是在iOS平臺(tái)上,僅僅是接入一個(gè)Flutter Demo頁面,包體積至少要增加5M,這對(duì)于那些包大小敏感的應(yīng)用來說其實(shí)是很難接受的。
對(duì)于包大小問題,Flutter官方也在持續(xù)跟進(jìn)優(yōu)化:
- Flutter V1.2 開始支持Android App Bundles,支持Dynamic Module下發(fā)。
- Flutter V1.12 優(yōu)化了2.6% Android平臺(tái)Hello World App大小(3.8M -> 3.7M)。
- Flutter V1.17 通過優(yōu)化Dart PC Offset存儲(chǔ)以減少StackMap大小等多個(gè)手段,再次優(yōu)化了產(chǎn)物大小,實(shí)現(xiàn)18.5%的縮減。
- Flutter V1.20 通過Icon font tree shaking移除未用到的icon fonts,進(jìn)一步優(yōu)化了應(yīng)用大小。
除了Flutter SDK內(nèi)部或Dart實(shí)現(xiàn)的優(yōu)化,我們是否還有進(jìn)一步優(yōu)化的空間呢?答案是肯定的。為了幫助業(yè)務(wù)方更好的接入和落地Flutter技術(shù),MTFlutter團(tuán)隊(duì)對(duì)Flutter的包大小問題進(jìn)行了調(diào)研和實(shí)踐,設(shè)計(jì)并實(shí)現(xiàn)了一套基于動(dòng)態(tài)下發(fā)的包大小優(yōu)化方案,瘦身效果也非??捎^。這里分享給大家,希望對(duì)大家能有所幫助或者啟發(fā)。
二、Flutter包大小問題分析
在Flutter官方的優(yōu)化文檔中,提到了減少應(yīng)用尺寸的方法:在V1.16.2及以上使用—split-debug-info選項(xiàng)(可以分離出debug info);移除無用資源,減少從庫中帶入的資源,控制適配的屏幕尺寸,壓縮圖片文件。這些措施比較直接并容易理解,但為了探索進(jìn)一步瘦身空間并讓大家更好的理解技術(shù)方案,我們先從了解Flutter的產(chǎn)物構(gòu)成開始,然后再一步步分析有哪些可行的方案。
2.1 Flutter產(chǎn)物介紹
我們首先以官方的Demo為例,介紹一下Flutter的產(chǎn)物構(gòu)成及各部分占比。不同F(xiàn)lutter版本以及打包模式下,產(chǎn)物有所不同,本文均以Flutter 1.9 Release模式下的產(chǎn)物為準(zhǔn)。
2.1.1 iOS側(cè)Flutter產(chǎn)物
圖1 Flutter iOS 產(chǎn)物組成示意圖
iOS側(cè)的Flutter產(chǎn)物主要由四部分組成(info.plist 比較小,對(duì)包體積的影響可忽略,這里不作為重點(diǎn)介紹),表格1中列出了各部分的詳細(xì)信息。
表1 Flutter產(chǎn)物組成
2.1.2 Android側(cè)Flutter產(chǎn)物
圖2 Flutter Android 產(chǎn)物組成示意圖
Android側(cè)的Flutter產(chǎn)物總共5.16MB,由四部分組成,表格2中列出了各部分的詳細(xì)信息。
表2 Flutter Android產(chǎn)物組成
2.1.3 各部分產(chǎn)物的變化趨勢(shì)
無論是Android還是iOS,Flutter的產(chǎn)物大體可以分為三部分:
下圖3展示了Flutter各資源變化的趨勢(shì):
圖3 Flutter各資源大小變化的趨勢(shì)圖
2.2 不同優(yōu)化思路分析
上面我們對(duì)Flutter產(chǎn)物進(jìn)行了分析,接下來看一下官方提供的優(yōu)化思路如何應(yīng)用于Flutter產(chǎn)物,以及對(duì)應(yīng)的困難與收益如何。
1. 刪減法
Flutter引擎中包括了Dart、skia、boringssl、icu、libpng等多個(gè)模塊,其中Dart和skia是必須的,其他模塊如果用不到倒是可以考慮裁掉,能夠帶來幾百k的瘦身收益。業(yè)務(wù)方可以根據(jù)業(yè)務(wù)訴求自定義裁剪。
Flutter業(yè)務(wù)產(chǎn)物,因?yàn)镕lutter的Tree Shaking機(jī)制,該部分產(chǎn)物從代碼的角度已經(jīng)是精簡(jiǎn)過的,要想繼續(xù)精簡(jiǎn)只能從業(yè)務(wù)的角度去分析。
Flutter資源中占比較多的一般是圖片,對(duì)于圖片可以根據(jù)業(yè)務(wù)場(chǎng)景,適當(dāng)降低圖片分辨率,或者考慮替換為網(wǎng)絡(luò)圖片。
2. 壓縮法
因?yàn)闊o論是Android還是iOS,安裝包本身已經(jīng)是壓縮包了,對(duì)Flutter產(chǎn)物再次壓縮的收益很低,所以該方法并不適用。
3. 動(dòng)態(tài)下發(fā)
對(duì)于靜態(tài)資源,理論上是Android和iOS都可以做到動(dòng)態(tài)下發(fā)。而對(duì)于代碼邏輯部分的編譯產(chǎn)物,在Android平臺(tái)支持可執(zhí)行產(chǎn)物的動(dòng)態(tài)加載,iOS平臺(tái)則不允許執(zhí)行動(dòng)態(tài)下發(fā)的機(jī)器指令。
經(jīng)過上面的分析可以發(fā)現(xiàn),除了刪減、壓縮,對(duì)所有業(yè)務(wù)適用、可行且收益明顯的進(jìn)一步優(yōu)化空間重點(diǎn)在于動(dòng)態(tài)下發(fā)了。能夠動(dòng)態(tài)下發(fā)的部分越多,包大小的收益越大。因此我們決定從動(dòng)態(tài)下發(fā)入手來設(shè)計(jì)一套Flutter包大小優(yōu)化方案。
三、基于動(dòng)態(tài)下發(fā)的Flutter包大小優(yōu)化方案
我們?cè)贏ndroid和iOS上實(shí)現(xiàn)的包大小優(yōu)化方案有所不同,區(qū)別在于Android側(cè)可以做到so和Flutter資源的全部動(dòng)態(tài)下發(fā),而iOS側(cè)由于系統(tǒng)限制無法動(dòng)態(tài)下發(fā)可執(zhí)行產(chǎn)物,所以需要對(duì)產(chǎn)物的組成和其加載邏輯進(jìn)行分析,將其中非必須和動(dòng)態(tài)鏈接庫一起加載的部分進(jìn)行動(dòng)態(tài)下發(fā)、運(yùn)行時(shí)加載。
當(dāng)將產(chǎn)物動(dòng)態(tài)下發(fā)后,還需要對(duì)引擎的初始化流程做修改,這樣才能保證產(chǎn)物的正常加載。由于兩端技術(shù)棧的不同,在很多具體實(shí)現(xiàn)上都采用了不同的方式,下面就分別來介紹下兩端的方案。
3.1 iOS側(cè)方案
在iOS平臺(tái)上,由于系統(tǒng)的限制無法實(shí)現(xiàn)在運(yùn)行時(shí)加載并運(yùn)行可執(zhí)行文件,而在上文產(chǎn)物介紹中可以看到,占比較高的App及Flutter這兩個(gè)均是可執(zhí)行文件,理論上是不能進(jìn)行動(dòng)態(tài)下發(fā)的,實(shí)際上對(duì)于Flutter可執(zhí)行文件我們能做的確實(shí)不多,但對(duì)于App這個(gè)可執(zhí)行文件,其內(nèi)部組成的四個(gè)模塊并不是在鏈接時(shí)都必須存在的,可以考慮部分移出,進(jìn)而來實(shí)現(xiàn)包體積的縮減。
因此,在該部分我們首先介紹Flutter產(chǎn)物的生成和加載的流程,通過對(duì)流程細(xì)節(jié)的分析來挖掘出產(chǎn)物可以被拆分出動(dòng)態(tài)下發(fā)的部分,然后基于實(shí)現(xiàn)原理來設(shè)計(jì)實(shí)現(xiàn)工程化的方案。
3.1.1 實(shí)現(xiàn)原理簡(jiǎn)析
為了實(shí)現(xiàn)App的拆分,我們需要了解下App.framework是怎樣生成以及各部分資源時(shí)如何加載的。如下圖4所示,Dart代碼會(huì)使用gen_snapshot工具來編譯成.S文件,然后通過xcrun工具來進(jìn)行匯編和鏈接最終生成App.framework。其中g(shù)en_snapshot是Dart編譯器,采用了Tree Shaking等技術(shù),用于生成匯編形式的機(jī)器代碼。
圖4 App.framework生成流程示意圖
產(chǎn)物加載流程:
圖5 Flutter產(chǎn)物加載流程圖
如上圖5所示,Flutter engine在初始化時(shí)會(huì)從根據(jù) FlutterDartProject 的settings中配置資源路徑來加載可執(zhí)行文件(App)、flutter_assets等資源,具體settings的相關(guān)配置如下:
//?settings{...??//?snapshot?文件地址或內(nèi)存地址??std::string?vm_snapshot_data_path;????MappingCallback?vm_snapshot_data;??std::string?vm_snapshot_instr_path;????MappingCallback?vm_snapshot_instr;??std::string?isolate_snapshot_data_path;????MappingCallback?isolate_snapshot_data;??std::string?isolate_snapshot_instr_path;????MappingCallback?isolate_snapshot_instr;??//?library?模式下的lib文件路徑??std::string?application_library_path;??//?icudlt.dat?文件路徑??std::string?icu_data_path;??//?flutter_assets?資源文件夾路徑??std::string?assets_path;??//?...}以加載vm_snapshot_data為例,它的加載邏輯如下:
load vm_snapshot_data
std::unique_ptr?ResolveVMData(const?Settings&?settings)?{??//?從?settings.vm_snapshot_data?中取??if?(settings.vm_snapshot_data)?{????...??}??//?從?settings.vm_snapshot_data_path?中取??if?(settings.vm_snapshot_data_path.size()?>?0)?{????...??}??//?從?settings.application_library_path?中取??if?(settings.application_library_path.size()?>?0)?{????...??}??auto?loaded_process?=?fml::NativeLibrary::CreateForCurrentProcess();??//?根據(jù)?kVMDataSymbol?從native?library中加載??return?DartSnapshotBuffer::CreateWithSymbolInLibrary(??????loaded_process,?DartSnapshot::kVMDataSymbol);}對(duì)于iOS來說,它默認(rèn)會(huì)根據(jù)kVMDataSymbol來從App中加載對(duì)應(yīng)資源,而其實(shí)settings是給提供了通過path的方式來加載資源和snapshot入口,那么對(duì)于 flutter_assets、icudtl.dat這些靜態(tài)資源,我們完全可以將其移出托管到服務(wù)端,然后動(dòng)態(tài)下發(fā)。
而由于iOS系統(tǒng)的限制,整個(gè)App可執(zhí)行文件則不可以動(dòng)態(tài)下發(fā),但在第二部分的介紹中我們了解到,其實(shí)App是由kDartVmSnapshotData、kDartVmSnapshotInstructions、kDartIsolateSnapshotData、kDartIsolateSnapshotInstructions等四個(gè)部分組成的,其中kDartIsolateSnapshotInstructions、kDartVmSnapshotInstructions為指令段,不可通過動(dòng)態(tài)下發(fā)的方式來加載,而kDartIsolateSnapshotData、kDartVmSnapshotData為數(shù)據(jù)段,它們?cè)诩虞d時(shí)不存在限制。
到這里,其實(shí)我們就可以得到iOS側(cè)Flutter包大小的優(yōu)化方案:將flutter_assets、icudtl.dat等靜態(tài)資源及kDartVmSnapshotData、kDartIsolateSnapshotData兩部分在編譯時(shí)拆分出去,通過動(dòng)態(tài)下發(fā)的方式來實(shí)現(xiàn)包大小的縮減。但此方案有個(gè)問題,kDartVmSnapshotData、kDartIsolateSnapshotData是在編譯時(shí)就寫入到App中了,如何實(shí)現(xiàn)自動(dòng)化地把此部分拆分出去是一個(gè)待解決的問題。為了解決此問題,我們需要先了解kDartVmSnapshotData、kDartIsolateSnapshotData的寫入時(shí)機(jī)。接下來,我們通過下圖6來簡(jiǎn)單地介紹一下該過程:
圖6 Flutter Data段寫入時(shí)序圖
代碼通過gen_snapshot工具來進(jìn)行編譯,它的入口在gen_snapshot.cc文件,通過初始化、預(yù)編譯等過程,最終調(diào)用Dart_CreateAppAOTSnapshotAsAssembly方法來寫入snapshot。因此,我們可以通過修改此流程,在寫入snapshot時(shí)只將instructions寫入,而將data重定向輸入到文件,即可實(shí)現(xiàn) kDartVmSnapshotData、kDartIsolateSnapshotData與App的分離。此部分流程示意圖如下圖7所示:
圖7 Flutter產(chǎn)物拆分流程示意圖
3.1.2 工程化方案
在完成了App數(shù)據(jù)段與代碼段分離的工作后,我們就可以將數(shù)據(jù)段及資源文件通過動(dòng)態(tài)下發(fā)、運(yùn)行時(shí)加載的方式來實(shí)現(xiàn)包體積的縮減。由此思路衍生的iOS側(cè)整體方案的架構(gòu)如下圖8所示;其中定制編譯產(chǎn)物階段主要負(fù)責(zé)定制Flutter engine及Flutter SDK,以便完成產(chǎn)物的“瘦身”工作;發(fā)布集成階段則為產(chǎn)物的發(fā)布和工程集成提供了一套標(biāo)準(zhǔn)化、自動(dòng)化的解決方案;而運(yùn)行階段的使命是保證“瘦身”的資源在engine啟動(dòng)的時(shí)候能被安全穩(wěn)定地加載。
圖8 架構(gòu)設(shè)計(jì)
注:圖例中MTFlutterRoute為Flutter路由容器,MWS指的是美團(tuán)云。
3.1.2.1 定制編譯產(chǎn)物階段
雖然我們不能把App.framework及Flutter.framework通過動(dòng)態(tài)下發(fā)的方式完全拆分出去,但可以剝離出部分非安裝時(shí)必須的產(chǎn)物資源,通過動(dòng)態(tài)下發(fā)的方式來達(dá)到Flutter包體積縮減的目的,因此在該階段主要工作包括三部分。
1. 新增編譯command
在將Flutter包瘦身工程化時(shí),我們必須保證現(xiàn)有的流程的編譯規(guī)則不會(huì)被影響,需要考慮以下兩點(diǎn):
- 增加編譯“瘦身”的Flutter產(chǎn)物構(gòu)建模式, 該模式應(yīng)能編譯出AOT模式下的瘦身產(chǎn)物。
- 不對(duì)常規(guī)的編譯模式(debug、profile、release)引入影響。
對(duì)于iOS平臺(tái)來說,AOT模式Flutter產(chǎn)物編譯的關(guān)鍵工作流程圖如下圖9所示。runCommand會(huì)將編譯所需參數(shù)及環(huán)境變量封裝傳遞給編譯后端(gen_snapshot負(fù)責(zé)此部分工作),進(jìn)而完成產(chǎn)物的編譯工作:
圖9 AOT模式Flutter產(chǎn)物編譯的關(guān)鍵工作流程圖
為了實(shí)現(xiàn)“瘦身”的工作流,工具鏈在圖9的流程中新增了buildwithoutdata的編譯command,該命令針對(duì)通過傳遞相應(yīng)參數(shù)(without-data=true)給到編譯后端(gen_snapshot),為后續(xù)編譯出剝離data段提供支撐:
xcode_backend.sh
if?[[?$#?==?0?]];?then??#?Backwards-compatibility:?if?no?args?are?provided,?build.??BuildAppelse??case?$1?in????"build")??????BuildApp?;;????"buildWithoutData")??????BuildAppWithoutData?;;????"thin")??????ThinAppFrameworks?;;????"embed")??????EmbedFlutterFrameworks?;;??esacfibuild_aot.dart
..addFlag('without-data',????????negatable:?false,????????defaultsTo:?false,????????hide:?true,??)2. 編譯后端定制
該部分主要對(duì)gen_snapshot工具進(jìn)行定制,當(dāng)gen_snapshot工具在接收到Dart層傳來的“瘦身”命令時(shí),會(huì)解析參數(shù)并執(zhí)行我們定制的方法Dart_CreateAppAOTSnapshotAsAssembly,該部分主要做了兩件事:
- 定制產(chǎn)物編譯過程,生成剝離data段的編譯產(chǎn)物。
- 重定向data段到文件中,以便后續(xù)進(jìn)行使用。
具體到處理的細(xì)節(jié),首先我們需要在gen_sanpshot的入口處理傳參,并指定重定向data文件的地址:
gen_snapshot.cc
??CreateAndWritePrecompiledSnapshot()?{????...????if?(snapshot_kind?==?kAppAOTAssembly)?{?//?常規(guī)release模式下產(chǎn)物的編譯流程??????...????}?else?if?(snapshot_kind?==?kAppAOTAssemblyDropData)?{???????...??????result?=?Dart_CreateAppAOTSnapshotAsAssembly(StreamingWriteCallback,????????????????????????????????????????????????????file,????????????????????????????????????????????????????&vm_snapshot_data_buffer,???????????????????????????????????????????????????&vm_snapshot_data_size,???????????????????????????????????????????????????&isolate_snapshot_data_buffer,???????????????????????????????????????????????????&isolate_snapshot_data_size,???????????????????????????????????????????????????true);?//?定制產(chǎn)物編譯過程,生成剝離data段的編譯產(chǎn)物snapshot_assembly.S??????...????}?else?if?(...)?{??????...????}????...??}在接受到編譯“瘦身”模式的命令后,將會(huì)調(diào)用定制的FullSnapshotWriter類來實(shí)現(xiàn)Snapshot_assembly.S的生成,該類會(huì)將原有編譯過程中vm_snapshot_data、isolate_snapshot_data的寫入過程改寫成緩存到buff中,以便后續(xù)寫入到獨(dú)立的文件中:
dart_api_imp.cc
//?drop_data=true,?表示后瘦身模式的編譯過程//?vm_snapshot_data_buffer、isolate_snapshot_data_buffer用于保存?vm_snapshot_data、isolate_snapshot_data以便后續(xù)寫入文件Dart_CreateAppAOTSnapshotAsAssembly(Dart_StreamingWriteCallback?callback,????????????????????????????????????void*?callback_data,?????????????????????????????????????bool?drop_data,????????????????????????????????????uint8_t**?vm_snapshot_data_buffer,????????????????????????????????????uint8_t**?isolate_snapshot_data_buffer)?{??...??FullSnapshotWriter?writer(Snapshot::kFullAOT,?&vm_snapshot_data_buffer,????????????????????????????&isolate_snapshot_data_buffer,?ApiReallocate,????????????????????????????&image_writer,?&image_writer);??if?(drop_data)?{????writer.WriteFullSnapshotWithoutData();?//?分離出數(shù)據(jù)段??}?else?{????writer.WriteFullSnapshot();??}??...}當(dāng)data段被緩存到buffer中后,便可以使用gen_snapshot提供的文件寫入的方法 WriteFile來實(shí)現(xiàn)數(shù)據(jù)段以文件形式從編譯產(chǎn)物中分離:
gen_snapshot.cc
static?void?WriteFile(const?char*?filename,?const?uint8_t*?buffer,?const?intptr_t?size);//?寫data到指定文件中{??...??????WriteFile(vm_snapshot_data_filename,?vm_snapshot_data_buffer,?vm_snapshot_data_size);?//?寫入vm_snapshot_data??????WriteFile(isolate_snapshot_data_filename,?isolate_snapshot_data_buffer,?isolate_snapshot_data_size);?//?寫入isolate_snapshot_data??...}3. engine定制
編譯參數(shù)修改
iOS側(cè)使用-0z參數(shù)可以獲得包體積縮減的收益(大約為700KB左右的收益),但會(huì)有相應(yīng)的性能損耗,因此該部分作為一個(gè)可選項(xiàng)提供給業(yè)務(wù)方,工具鏈提供相應(yīng)版本的Flutter engine的定制。
資源加載方式定制
對(duì)于engine的定制,主要圍繞如何“手動(dòng)”引入拆分出的資源來展開,好在engine提供了settings接口讓我們可以實(shí)現(xiàn)自定義引入文件的path,因此我們需要做的就是對(duì)Flutter engine初始化的過程進(jìn)行相應(yīng)改造:
shell/platform/darwin/ios/framework/Headers/FlutterDartProject.h
/**?*?custom?icudtl.dat?path?*/@property(nonatomic,?copy)?NSString*?icuDataPath;/**?*?custom?flutter_assets?path?*/@property(nonatomic,?copy)?NSString*?assetPath;/**?*?custom?isolate_snapshot_data?path?*/@property(nonatomic,?copy)?NSString*?isolateSnapshotDataPath;/**?*custom?vm_snapshot_data?path?*/@property(nonatomic,?copy)?NSString*?vmSnapshotDataPath;在運(yùn)行時(shí)“手動(dòng)”配置上述路徑,并結(jié)合上述參數(shù)初始化FlutterDartProject,從而達(dá)到engine啟動(dòng)時(shí)從配置路徑加載相應(yīng)資源的目的。
engine編譯自動(dòng)化
在完成engine的定制和改造后,還需要手動(dòng)編譯一下engine源碼,生成各平臺(tái)、架構(gòu)、模式下的產(chǎn)物,并將其集成到Flutter SDK中,為了讓引擎定制的流程標(biāo)準(zhǔn)化、自動(dòng)化,MTFlutter工具鏈提供了一套engine自動(dòng)化編譯發(fā)布的工具。如流程圖10所示,在完成engine代碼的自定義修改之后,工具鏈會(huì)根據(jù)engine的patch code編譯出各平臺(tái)、架構(gòu)及不同模式下的engine產(chǎn)物,然后自動(dòng)上傳到美團(tuán)云上,在開發(fā)和打包時(shí)只需要通簡(jiǎn)單的命令,即可安裝和使用定制后的Flutter engine:
圖10 Flutter engine自動(dòng)化編譯發(fā)布流程
3.1.2.2 發(fā)布集成階段
當(dāng)完成Dart代碼編譯產(chǎn)物的定制后,我們下一步要做的就是改造MTFlutter工具鏈現(xiàn)有的產(chǎn)物發(fā)布流程,支持打出“瘦身”模式的產(chǎn)物,并將瘦身模式下的產(chǎn)物進(jìn)行合理的組織、封裝、托管以方便產(chǎn)物的集成。從工具鏈的視角來看,該部分的流程示如下圖11所示:
圖11 Flutter產(chǎn)物發(fā)布集成流程示意圖
自動(dòng)化發(fā)布與版本管理
MTFlutter工具鏈將“瘦身”集成到產(chǎn)物發(fā)布的流水線中,新增一種thin模式下的產(chǎn)物,在iOS側(cè)該產(chǎn)物包括release模式下瘦身后的App.framework、Flutter.framework以及拆分出的數(shù)據(jù)、資源等文件。當(dāng)開發(fā)者提交了代碼并使用Talos(美團(tuán)內(nèi)部前端持續(xù)交付平臺(tái))觸發(fā)Flutter打包時(shí),CI工具會(huì)自動(dòng)打出瘦身的產(chǎn)物包及需要運(yùn)行時(shí)下載的資源包、生成產(chǎn)物相關(guān)信息的校驗(yàn)文件并自動(dòng)上傳到美團(tuán)云上。
對(duì)于產(chǎn)物資源的版本管理,我們則復(fù)用了美團(tuán)云提供資源管理的能力。在美團(tuán)云上,產(chǎn)物資源以文件目錄的形式來實(shí)現(xiàn)各版本資源的相互隔離,同時(shí)對(duì)“瘦身”資源單獨(dú)開一個(gè)bucket進(jìn)行單獨(dú)管理,在集成產(chǎn)物時(shí),集成插件只需根據(jù)當(dāng)前產(chǎn)物module的名稱及版本號(hào)便可獲取對(duì)應(yīng)的產(chǎn)物。
自動(dòng)化集成
針對(duì)瘦身模式MTFlutter工具鏈對(duì)集成插件也進(jìn)行了相應(yīng)的改造,如下圖12所示。我們對(duì)Flutter集成插件進(jìn)行了修改,在原有的產(chǎn)物集成模式的基礎(chǔ)上新增一種thin模式,該模式在表現(xiàn)形式與原有的debug、release、profile類似,區(qū)別在于:為了方便開發(fā)人員調(diào)試,該模式會(huì)依據(jù)當(dāng)前工程的buildconfigration來做相應(yīng)的處理,即在debug模式下集成原有的debug產(chǎn)物,而在release模式下才集成“瘦身”產(chǎn)物包。
圖12 Flutter iOS端集成插件修改
3.1.2.3 運(yùn)行階段
運(yùn)行階段所處理的核心問題包括資源下載、緩存、解壓、加載及異常監(jiān)控等。一個(gè)典型的瘦身模式下的engine啟動(dòng)的過程如圖13所示。
該過程包括:
- 資源下載:讀取工程配置文件,得到當(dāng)前Flutter module的版本,并查詢和下載遠(yuǎn)程資源。
- 資源解壓和校驗(yàn):對(duì)下載資源進(jìn)行完整性校驗(yàn),校驗(yàn)完成則進(jìn)行解壓和本地緩存。
- 啟動(dòng)engine:在engine啟動(dòng)時(shí)加載下載的資源。
- 監(jiān)控和異常處理:對(duì)整個(gè)流程可能出現(xiàn)的異常情況進(jìn)行處理,相關(guān)數(shù)據(jù)情況進(jìn)行監(jiān)控上報(bào)。
圖13 iOS側(cè)瘦身模式下engine啟動(dòng)流程圖
為了方便業(yè)務(wù)方的使用、減少其接入成本,MTFlutter將該部分工作集成至MTFlutterRoute中,業(yè)務(wù)方僅需引入MTFlutterRoute即可將“瘦身”功能接入到項(xiàng)目中。
3.2 Android側(cè)方案
3.2.1 整體架構(gòu)
在Android側(cè),我們做到了除Java代碼外的所有Flutter產(chǎn)物都動(dòng)態(tài)下發(fā)。完整的優(yōu)化方案概括來說就是:動(dòng)態(tài)下發(fā)+自定義引擎初始化+自定義資源加載。方案整體分為打包階段和運(yùn)行階段,打包階段會(huì)將Flutter產(chǎn)物移除并生成瘦身的APK,運(yùn)行階段則完成產(chǎn)物下載、自定義引擎初始化及資源加載。其中產(chǎn)物的上傳和下載由DynLoader完成,這是由美團(tuán)平臺(tái)迭代工程組提供的一套so與assets的動(dòng)態(tài)下發(fā)框架,它包括編譯時(shí)和運(yùn)行時(shí)兩部分的操作:
我們?cè)贒ynLoader的基礎(chǔ)上,通過對(duì)Flutter引擎初始化及資源加載流程進(jìn)行定制,設(shè)計(jì)了整體的Flutter包大小優(yōu)化方案:
圖14 Android側(cè)Flutter包大小優(yōu)化方案整體架構(gòu)
打包階段:我們?cè)谠械腁PK打包流程中,加入一些自定義的gradle plugin來對(duì)Flutter產(chǎn)物進(jìn)行處理。在預(yù)處理流程,我們將一些無用的資源文件移除,然后將flutter_assets中的文件打包為bundle.zip。然后通過DynLoader提供的上傳插件將libflutter.so、libapp.so和flutter_assets/bundle.zip從APK中移除,并上傳到動(dòng)態(tài)發(fā)布系統(tǒng)托管。其中對(duì)于多架構(gòu)的so,我們通過在build.gradle中增加abiFilters進(jìn)行過濾,只保留單架構(gòu)的so。最終打包出來的APK即為瘦身后的APK。
不經(jīng)處理的話,瘦身后的APK一進(jìn)到Flutter頁面肯定會(huì)報(bào)錯(cuò),因?yàn)榇藭r(shí)so和flutter_assets可能都還沒下載下來,即使已經(jīng)下載下來,其位置也發(fā)生了改變,再使用原來的加載方式肯定會(huì)找不到。所以我們?cè)谶\(yùn)行階段需要做一些特殊處理:
1. Flutter路由攔截
首先要使用Flutter路由攔截器,在進(jìn)到Flutter頁面之前,要確保so和flutter_assets都已經(jīng)下載完成,如果沒有下載完,則顯示loading彈窗,然后調(diào)用DynLoader的方法去異步下載。當(dāng)下載完成后,再執(zhí)行原來的跳轉(zhuǎn)邏輯。
2. 自定義引擎初始化
第一次進(jìn)到Flutter頁面,需要先初始化Flutter引擎,其中主要是將libflutter.so和libapp.so的路徑改為動(dòng)態(tài)下發(fā)的路徑。另外還需要將flutter_assets/bundle.zip進(jìn)行解壓。
3. 自定義資源加載
當(dāng)引擎初始化完成后,開始執(zhí)行Dart代碼的邏輯。此時(shí)肯定會(huì)遇到資源加載,比如字體或者圖片。原有的資源加載器是通過method channel調(diào)用AssetManager的方法,從APK中的assets中進(jìn)行加載,我們需要改成從動(dòng)態(tài)下發(fā)的路徑中加載。
下面我們?cè)敿?xì)介紹下某些部分的具體實(shí)現(xiàn)。
3.2.2 自定義引擎初始化
原有的Flutter引擎初始化由FlutterMain類的兩個(gè)方法完成,分別為startInitialization和ensureInitializationComplete,一般在Application初始化時(shí)調(diào)用startInitialization(懶加載模式會(huì)延遲到啟動(dòng)Flutter頁面時(shí)再調(diào)用),然后在Flutter頁面啟動(dòng)時(shí)調(diào)用ensureInitializationComplete確保初始化的完成。
圖15 Android側(cè)Flutter引擎初始化流程圖
在startInitialization方法中,會(huì)加載libflutter.so,在ensureInitializationComplete中會(huì)構(gòu)建shellArgs參數(shù),然后將shellArgs傳給FlutterJNI.nativeInit方法,由jni側(cè)完成引擎的初始化。其中shellArgs中有個(gè)參數(shù)AOT_SHARED_LIBRARY_NAME可以用來指定libapp.so的路徑。
自定義引擎初始化,主要要修改兩個(gè)地方,一個(gè)是System.loadLibrary("flutter"),一個(gè)是shellArgs中l(wèi)ibapp.so的路徑。有兩種辦法可以做到:
- 直接修改FlutterMain的源碼,這種方式簡(jiǎn)單直接,但是需要修改引擎并重新打包,業(yè)務(wù)方也需要使用定制的引擎才可以。
- 繼承FlutterMain類,重寫startInitialization和ensureInitializationComplete的邏輯,讓業(yè)務(wù)方使用我們的自定義類來初始化引擎。當(dāng)自定義類完成引擎的初始化后,通過反射的方式修改sSettings和sInitialized,從而使得原有的初始化邏輯不再執(zhí)行。
本文使用第二種方式,需要在FlutterActivity的onCreate方法中首先調(diào)用自定義的引擎初始化方法,然后再調(diào)用super的onCreate方法。
3.2.3 自定義資源加載
Flutter中的資源加載由一組類完成,根據(jù)數(shù)據(jù)源的不同分為了網(wǎng)絡(luò)資源加載和本地資源加載,其類圖如下:
圖16 Flutter 資源加載相關(guān)類圖
AssetBundle為資源加載的抽象類,網(wǎng)絡(luò)資源由NetworkAssetBundle加載,打包到Apk中的資源由PlatformAssetBundle加載。
PlatformAssetBundle通過channel調(diào)用,最終由AssetManager去完成資源的加載并返回給Dart層。
我們無法修改PlatformAssetBundle原有的資源加載邏輯,但是我們可以自定義一個(gè)資源加載器對(duì)其進(jìn)行替換:在widget樹的頂層通過DefaultAssetBundle注入。
自定義的資源加載器DynamicPlatformAssetBundle,通過channel調(diào)用,最終從動(dòng)態(tài)下發(fā)的flutter_assets中加載資源。
3.2.4 字體動(dòng)態(tài)加載
字體屬于一種特殊的資源,其有兩種加載方式:
- 靜態(tài)加載:在pubspec.yaml文件中聲明的字體及為靜態(tài)加載,當(dāng)引擎初始化的時(shí)候,會(huì)自動(dòng)從AssetManager中加載靜態(tài)注冊(cè)的字體資源。
- 動(dòng)態(tài)加載:Flutter提供了FontLoader類來完成字體的動(dòng)態(tài)加載。
當(dāng)資源動(dòng)態(tài)下發(fā)后,assets中已經(jīng)沒有字體文件了,所以靜態(tài)加載會(huì)失敗,我們需要改為動(dòng)態(tài)加載。
3.2.5 運(yùn)行時(shí)代碼組織結(jié)構(gòu)
整個(gè)方案的運(yùn)行時(shí)部分涉及多個(gè)功能模塊,包括產(chǎn)物下載、引擎初始化、資源加載和字體加載,既有Native側(cè)的邏輯,也有Dart側(cè)的邏輯。如何將這些模塊合理的加以整合呢?平臺(tái)團(tuán)隊(duì)的同學(xué)給了很好的答案,并將其實(shí)現(xiàn)為一個(gè)Flutter Plugin:flutter_dynamic(美團(tuán)內(nèi)部庫)。其整體分為Dart側(cè)和Android側(cè)兩部分,Dart側(cè)提供字體和資源加載方法,方法內(nèi)部通過method channel調(diào)到Android側(cè),在Android側(cè)基于DynLoader提供的接口實(shí)現(xiàn)產(chǎn)物下載和資源加載的邏輯。
圖17 FlutterDynamic結(jié)構(gòu)圖
四、方案的接入與使用
為了讓大家了解上述方案使用層面的設(shè)計(jì),我們?cè)诖税衙缊F(tuán)內(nèi)部的使用方式介紹給大家,其中會(huì)涉及到一些內(nèi)部工具細(xì)節(jié)我們暫不展開,重點(diǎn)解釋設(shè)計(jì)和使用體驗(yàn)部分。由于Android和iOS的實(shí)現(xiàn)方案有所區(qū)別,故在接入方式相應(yīng)的也會(huì)有些差異,下面針對(duì)不同平臺(tái)分開進(jìn)行介紹:
4.1 iOS
在上文方案的設(shè)計(jì)中,我們介紹到包瘦身功能已經(jīng)集成進(jìn)入美團(tuán)內(nèi)部MTFlutter工具鏈中,因此當(dāng)業(yè)務(wù)方在使用了MTFlutter后只需簡(jiǎn)單的幾步配置便可實(shí)現(xiàn)包瘦身功能的接入。iOS 的接入使用上總體分為三步:
1. 引入Flutter集成插件(cocoapods-flutter-plugin 美團(tuán)內(nèi)部Cocoapods插件,進(jìn)一步封裝Flutter模塊引入,使之更加清晰便捷):
Gemfile
gem?'cocoapods-flutter-plugin',?'~>?1.2.0'2. 接入MTFlutterRoute混合業(yè)務(wù)容器(美團(tuán)內(nèi)部pod庫,封裝了Flutter初始化及全局路由等能力),實(shí)現(xiàn)基于“瘦身”產(chǎn)物的初始化:
Flutter 業(yè)務(wù)工程中引入 mt_flutter_route:
pubspec.yaml
dependencies:??mt_flutter_route:?^2.4.03. 在iOS Native工程中引入MTFlutterRoute pod:
podfile
binary_pod?'MTFlutterRoute',?'2.4.1.8'經(jīng)過上面的配置后,正常Flutter業(yè)務(wù)發(fā)版時(shí)就會(huì)自動(dòng)產(chǎn)生“瘦身”后的產(chǎn)物,此時(shí)只需在工程中配置瘦身模式即可完成接入:
podfile
flutter?'your_flutter_project',?'x.x.x',?:thin?=>?true4.2 Android
4.2.1 Flutter側(cè)修改
1. 在Flutter工程pubspec.yaml中添加flutter_dynamic(美團(tuán)內(nèi)部Flutter Plugin,負(fù)責(zé)Dart側(cè)的字體、資源加載)依賴。
2. 在main.dart中添加字體動(dòng)態(tài)加載邏輯,并替換默認(rèn)資源加載器。
main.dart
void?main()?async?{???//?動(dòng)態(tài)加載字體??await?dynFontInit();??//?自定義資源加載器??runApp(DefaultAssetBundle(????bundle:?dynRootBundle,????child:?MyApp(),??));}4.2.2 Native 側(cè)修改
1. 打包腳本修改
在App模塊的build.gradle中通過apply特定plugin完成產(chǎn)物的刪減、壓縮以及上傳。
2. 在Application的onCreate方法中初始化FlutterDynamic。
3. 添加Flutter頁面跳轉(zhuǎn)攔截。
在跳轉(zhuǎn)到Flutter頁面之前,需要使用FlutterDynamic提供的接口來確保產(chǎn)物已經(jīng)下載完成,在下載成功的回調(diào)中來執(zhí)行真正的跳轉(zhuǎn)邏輯。
class?FlutterRouteUtil?{????public?static?void?startFlutterActivity(final?Context?context,?Intent?intent)?{????????FlutterDynamic.getInstance().ensureLoaded(context,?new?LoadCallback()?{????????????@Override????????????public?void?onSuccess()?{??????????????//?在下載成功的回調(diào)中執(zhí)行跳轉(zhuǎn)邏輯????????????????context.startActivity(intent);????????????}????????});????}}備注:如果App有使用類似WMRoute之類的路由組件的話,可以自定義一個(gè)UriHandler來統(tǒng)一處理所有的Flutter頁面跳轉(zhuǎn),同樣在ensureLoaded方法回調(diào)中執(zhí)行真正的跳轉(zhuǎn)邏輯。
4. 添加引擎初始化邏輯
我們需要重寫FlutterActivity的onCreate方法,在super.onCreate之前先執(zhí)行自定義的引擎初始化邏輯。
MainFlutterActivity.java
public?class?MainFlutterActivity?extends?FlutterActivity?{????@Override????protected?void?onCreate(Bundle?savedInstanceState)???????//?確保自定義引擎初始化完成????????FlutterDynamic.getInstance().ensureFlutterInit(this);????????super.onCreate(savedInstanceState);????}}五、總結(jié)展望
目前,動(dòng)態(tài)下發(fā)的方案已在美團(tuán)內(nèi)部App上線使用,Android包瘦身效果到達(dá)95%,iOS包瘦身效果達(dá)到30%+。動(dòng)態(tài)下發(fā)的方案雖然能顯著減少Flutter的包體積,但其收益是通過運(yùn)行時(shí)下載的方式置換回來的。當(dāng)Flutter業(yè)務(wù)的不斷迭代增長時(shí),Flutter產(chǎn)物包也會(huì)隨之不斷變大,最終導(dǎo)致需下載的產(chǎn)物變大,也會(huì)對(duì)下載成功率帶來壓力。
未來,我們還會(huì)探索Flutter的分包邏輯,通過將不同的業(yè)務(wù)模塊拆分來降低單個(gè)產(chǎn)物包的大小,來進(jìn)一步保障包瘦身功能的可用性。
六、作者簡(jiǎn)介
艷東,2018年加入美團(tuán),到家平臺(tái)前端工程師。
宗文,2019年加入美團(tuán),到家平臺(tái)前端高級(jí)工程師。
會(huì)超,2014年加入美團(tuán),到家平臺(tái)前端技術(shù)專家。
---------- END ----------
招聘信息
美團(tuán)外賣長期招聘Android、iOS、FE 高級(jí)/資深工程師和技術(shù)專家。歡迎感興趣的同學(xué)投遞簡(jiǎn)歷至:tech@meituan.com(郵件標(biāo)題請(qǐng)注明:美團(tuán)外賣技術(shù)團(tuán)隊(duì))。
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
以上是生活随笔為你收集整理的android nio debug模式正常 release包crash_Flutter包大小治理上的探索与实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 信用卡逾期对配偶有影响吗?爱他/她就别这
- 下一篇: go语言一天入门(上)