源:51cto 作者:王群,李東,偉國
技術的發展使得作為一名Web前端工程師實現Native APP需求更加容易,可以考慮使用Flutter做可攻可守的混合開發,隨著業務的穩定逐漸將穩定業務原生化,借助 web 高性價比的開發成本又不會因為開發效率措施商機。
導讀:近些年來前端技術不斷推陳出新,使得web前端工程師開發Native App的成本逐漸降低,跨端技術也使得應用開發成本大幅度降低。這種背景下,越來越多的web前端工程師團隊正在逐漸嘗試獨立開發App,使用各種綜合性方案做著性能與效率上的平衡。
開發一款Native APP往往都是一件復雜的事情,現在市面上大多數的APP都不是單純使用某種技術研發而成,而是隨著項目需求的復雜程度、迭代效率、團隊具體情況等諸多原因的影響逐步發展成為各種技術混合開發的APP架構。
技術無分好壞,適合自己團隊的才是最合適的技術選型。啥意思?我來說說吧,比如你的團隊幾乎都使用vue開發,你非要選擇使用React Native全員開發APP,成本就很大也很難推動;又比如你的團隊客戶端研發工程師很多,那就沒必要非得用React Native,特別不人道。這就好像這么多年來,人們一直倡導通過優化工具提升效率,而不是直接通過技術干掉那些人。
那么,什么是合適的技術選型?我覺得對于一個團隊,一方面能夠滿足需求優化體驗,另一方面就是降本提效簡單靈活。
為了幫助Web前端工程師實現APP需求理清楚知識脈絡,本文將簡單介紹開發Native APP常用的技術方法,再介紹如何用flutter做可攻可守的混合開發。
移動端APP能夠帶來很好的體驗,能夠利用很多系統能力加持使之密閉生態發展迅速。但是使用原生方式(Native)來開發 App,不僅要求分別針對 iOS 和 Android 平臺,使用不同的語言實現同樣的產品功能,還要對不同的終端設備和不同的操作系統進行功能適配,并承擔由此帶來的測試維護升級工作。
圖:原生開發App語言
為了減少開發維護成本,近年來很多“一套代碼,多端運行”的跨平臺方案猶如雨后春筍般的涌現出來,跨端方案針對不同業務目標,有Native APP的跨端,有小程序的跨端,也有橫向的PC端移動端和web的跨端,這些技術方案都有各自的優缺點,其中 React Native 和 Flutter 都是非常具有代表性的技術方案。
圖:跨端視圖效果
RN 希望開發者能夠在性能、展示、交互能力和迭代交付效率之間做到平衡。它在 Web 容器方案的基礎上,優化了加載、解析和渲染這三大過程,以相對簡單的方式支持了構建移動端頁面必要的 Web 標準,保證了便捷的前端開發體驗。并且在保留基本渲染能力的基礎上,用原生自帶的 UI 組件實現代替了核心的渲染引擎,從而保證了良好的渲染性能。
圖:RN 原理圖
但是,由于 React Native 的技術方案所限,使用原生控件承載界面渲染,在犧牲了部分 Web 標準靈活性的同時,固然解決了不少性能問題,但也引入了新的問題。除開通過 JavaScript 虛擬機進行原生接口的調用,JavaScript只能解釋執行,而帶來的通信低效不談,由于框架本身不負責渲染,而是由原生代理,因此我們還需要面對大量平臺相關的邏輯。要用好 React Native,除了掌握這個框架外,開發者還必須同時熟悉 iOS 和 Android 系統。
在 Google 的強力帶動下,Flutter 開辟了全新的思路,提供了一整套從底層渲染邏輯到上層開發語言的完整解決方案。視圖渲染完全閉環在其框架內部,一切皆widget,不依賴于底層操作系統提供的任何組件,依靠底層圖像渲染引擎 Skia從根本上保證了視圖渲染在 Android 和 iOS 上的高度一致性。
圖:Flutter 架構圖
Flutter 的開發語言 Dart,是 Google 專門為大前端開發量身打造的專屬語言,借助于先進的工具鏈和編譯器,成為了少數同時支持JIT和AOT的語言之一,開發期調試效率高,發布期運行速度快、執行性能好,在代碼執行效率上可以媲美原生 App。Dart 避免了搶占式調度和共享內存,可以在沒有鎖的情況下進行對象分配和垃圾回收,在性能方面表現相當不錯。雖然 Dart 開發語言有一定的學習成本,但是學習成本并不高,很容易上手。
2022年3月 Flutter 2 發布,聲稱使用相同的代碼庫為 iOS、Android、Windows、macOS 和 Linux 五種操作系統構建原生應用,甚至可以嵌入到汽車、電視和智能家電,為環境計算提供最普適、可移植的體驗。
圖:跨端方案的對比圖
從 Web 容器時代到以 React Native 為代表的泛 Web 容器時代,最后再到以 Flutter 為代表的自繪引擎時代,這些優秀的跨平臺開發框架們慢慢抹平了各個平臺的差異,使得操作系統的邊界變得越來越模糊。
混合開發也稱為Hybird開發方式,是一種非常高效的APP開發方式,主要通過多種技術共同研發同一個APP應用,讓專業的人做專業的部分,在技術、效率、能力、目標之間做平衡,達到核心部分體驗優秀、部分功能迭代迅速成本低的開發方案。目前大多數APP也都是采用的多技術共用的開發方式,比如淘寶、美團、百度等APP都有混合開發的影子。
下面介紹幾種常見的混合開發方案。
這個方案主要是通過原生開發出Native APP的外殼,內部的頁面和功能主要通過常規的web技術實現,系統端能力通過原生殼子暴露出API給web實現能力調用。
這種技術對于不同的業務側重點會有些不同,如果客戶端主導的業務native會重一些,如果是純web前端的團隊web實現會非常多。這個主要是根據團隊現狀和業務需要做實現上的調整,如果一個部分非??赡軙蔀槲磥砗诵臉I務則大概率會使用Native及時實現或者重構。
圖:原生 + Webview 方案
原生APP的開發成本是非常高的,就像前文所說的同一個APP需要分別針對 iOS 和 Android 平臺進行開發,也就是說有幾種平臺就需要幾個技術方向的人力,這樣的產品實現是非常消耗人力的。所以人們一直在尋找合適的方案一次開發就能夠運行在不同的平臺上,Flutter 就是 Google 開發的優秀的跨端開發Native APP技術方案,通過使用統一的編程語言 Dart ,根據一切皆 widget 的結構組織,通過觸發自研渲染引擎Skia在系統應用中繪制頁面,能夠達到無差異的渲染效果。
無差異的繪制UI和交互表現,這也是Flutter最強大的地方,對于UI視圖的渲染和交互體驗基本上與原生開發的表現差距不大,但是能夠節省大約1 / 2的人力,這樣的降本提效方法十分受到歡迎。
圖:原生 + Flutter 通信交互圖
圖:Flutter原生混編
當然,不同項目的面臨的情況也不同,我們主要介紹的是以flutter為主的開發結構,但是很多業務原來使用原生開發的,后來為了實現一些低成本的需求開發,所以在原生開發的結構上接入 Flutter 技術進行混編開發。
如果團隊中有很多易變的需求,或者說團隊中純web前端成員占據大多數,那么引入 webview 是一個不錯的方案,雖然 webview 也存在很多問題,但是相對來說比起 RN 還是容易定位和解決的。
圖:原生 + Flutter + Webview
常見webview插件依賴于Flatter嵌入Android和 iOS本機視圖的機制,底層使用AndroidView和UiKitView。iOS12版本已經廢棄UIWebView強推WKWebView,WKWebView 在獨立進程中加載網頁。其中 webview_flutter 是官方維護的 WebView插件,特性是基于原生和 Flutter SDK封裝,繼承 StatefulWidget,因此支持內嵌于 flutter Widget 樹中,這是比較靈活的。
圖:Flutter Webview插件對比
我們的項目包括APP必備的開屏,然后是登陸頁面,登錄成功后是加載頁面,然后打開首頁,在首頁可以打開直播浮窗,跳轉到大多數頁面直播浮窗都懸浮在那里,包括首頁和其他頁面都是迭代頻率非常高的。
圖:項目需求
假設我們有一個 APP 項目,你的團隊大部分人都是 vue 的深度依賴開發者,項目初期很多需求可能都會不斷底推倒重來,需求迭代頻率非常高,但是也需要一定程度的保證用戶體驗,并且支持多個平臺。這種背景下,使用 flutter + webview + 原生 的開發方案確實能夠順利的完成任務。
那么,根據項目需求我們可以簡單劃分各個部分設計。例如開屏、登錄和加載頁面的變更頻率不高而且用戶體驗要求平穩順滑,這里可以采用 flutter 技術來實現;對于1對1視頻通話部分可以考慮使用 RTC 和 webRTC 技術,而原生方式接入 sdk 和實現需求的穩定性和延時是比較可靠的,所以使用原生方式實現;其他的頁面如果面臨業務初級階段、變化頻率高等情況,我們可以考慮使用 web 技術實現(H5),如果存在對于客戶端能力或者系統能力的調用,可以使用 Jsbridge 中間層實現。
圖:設計簡圖
因為是從 0 到 1 的項目,我們選擇創建一個flutter項目,按照設計的結構進行研發。項目入口在 main.dart 中實現;通過 routers.dart 的業務邏輯實現頁面間的切換;具體的頁面實現在 pages 目錄中相應的同名文件;UI 組件位于 widgets 中,可以通過 import 進行使用;熟悉客戶端研發的同學們會發現,在flutter項目里有 Android 和 iOS 目錄,原生開發在相應的原生目錄中實現就可以;flutter擴展插件可以通過引用放置到plugins目錄中,具體的配置在 pubspec.yaml 文件中實現。
復制app-project
|----android // Android原生目錄
| |--gradle
| |--build.gradle // Android配置文件
| |--key.jks
| |--key.properties
| |--app
| |--libs
| |--src
| |--profile
| | |--AndroidManifest.xml
| |--main
| |--kotlin
| |--res
| |--AndroidManifest.xml
|----ios
| |--Gemfile
| |--Podfile // ios 的pod 依賴文件
| |--Flutter
| |--scripts
| | |--AppIcon.sh
| | |--flutterbuild.sh
| | |--setup.sh
| |--Runner
| |--Info.plist
| |--main.m
| |--Assets.xcassets
| |--Base.lproj
| |--AppDelegate.h
| |--AppDelegate.m
|----lib // Flutter 工作區
| |--assets // 本地的資源工作區
| |--config
| |--jsBridge // 提供webview jsbrige
| | |--live
| | |--login
| | |--cache
| |--pages // flutter 頁面
| | |--live
| | |--loading
| | |--webViewPage
| | |--login
| |--routers
| | |--routers.dart
| |--utils
| | |--cache.dart
| | |--color.dart
| | |--network.dart
| | |--events.dart
| |--widgets
| | |--routers.dart
| |--main.dart // 入口文件
|----plugins
| |--flutter-inappwebview //webview庫
| |--flutter-login
|----test
|----web
|----pubspec.yaml // flutter 配置文件
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.
如果你有過原生系統(Android、iOS)或原生 JavaScript 開發經驗的話,應該知道視圖開發是命令式的,需要精確地告訴操作系統或瀏覽器用何種方式去做事情。與此不同的是,Flutter 的視圖開發是聲明式的,其核心設計思想就是將視圖和數據分離,這與 React 的設計思路完全一致。例如代碼實現如下:
復制// 頁面實現事例
import 'package:flutter/widgets.dart';
class MyAPP extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const Center(child: Text('Hello Qun'));
}
}
void main()=> runApp(new MyAPP());
1.2.3.4.5.6.7.8.9.
當你所要構建的用戶界面不隨任何狀態信息的變化而變化時,需要選擇使用 StatelessWidget,反之則選用 StatefulWidget。 渲染也非常有意思,Widget 是 Flutter 世界里對視圖的一種結構化描述,里面存儲的是有關視圖渲染的配置信息; Element 則是 Widget 的一個實例化對象,將 Widget 樹的變化做了抽象,能夠做到只將真正需要修改的部分同步到真實的 Render Object 樹中,最大程度地優化了從結構化的配置信息到完成最終渲染的過程; 而 RenderObject,則負責實現視圖的最終呈現,通過布局、繪制完成界面的展示。
圖:界面生成的“三棵樹”
Dart 是單線程的,這意味著代碼是有序的,按照在 main 函數出現的次序一個接一個地執行,不會被其他代碼中斷。關于組件層面的原始指針事件的監聽,Flutter 提供了 Listener Widget,可以監聽其子 Widget 的原始指針事件。
復制// 事件響應
Listener(
child: Container(
color: Colors.yellow,// 背景色
width: 700,
height: 700,
),
// 手勢按下回調
onPointerDown: (event)=> print("down $event"),
// 手勢移動回調
onPointerMove: (event)=> print("move $event"),
// 手勢抬起回調
onPointerUp: (event)=> print("up $event")
);
1.2.3.4.5.6.7.8.9.10.11.12.13.14.
如果說 UI 框架的視圖元素的基本單位是組件,那應用程序的基本單位就是頁面了。對于擁有多個頁面的應用程序而言,需要有一個統一的機制來管理頁面之間的跳轉,通常被稱為路由管理或導航管理。
在 Flutter 中,頁面之間的跳轉是通過 Route 和 Navigator 來管理的。根據是否需要提前注冊頁面標識符,Flutter 中的路由管理可以分為兩種方式:
復制// 基本路由
class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(
// 打開頁面
onPressed: ()=> Navigator.push(context, MaterialPageRoute(
builder: (context)=> SecondPage()
));
);
}
}
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(
// 回退頁面
onPressed: ()=> Navigator.pop(context)
);
}
}
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.
復制// 命名路由
MaterialApp(
...
// 注冊路由
routes:{
"uri_page":(context)=>UriPage(),
},
);
// 使用名字打開頁面
Navigator.pushNamed(context,"uri_page");
1.2.3.4.5.6.7.8.9.10.
依賴庫的依賴是通過pubspec.xml中配置完成。
復制// 版本配置,iOS最終是以identify中的版本配置為準
version: 1.0.30+1
// flutter sdk環境配置
environment:
sdk: ">=2.12.0-0 <3.0.0"
flutter: ">=1.22.2"
// 依賴配置
dependencies:
flutter:
sdk: flutter
permission_handler: ^7.1.0
flutter_inappwebview:
path: ./plugins/flutter_inappwebview
joymo_app_upgrade:
path: ./plugins/joymo_app_upgrade
tal_login:
path: ./plugins/tal_login
student_101_live:
path: ./plugins/student_101_live
dev_dependencies:
flutter_test:
sdk: flutter
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.
開發過程不免需要原生底層能力的支持,上文已經介紹了使用platform channel方式完成對接,這里用代碼示例下(原生的直播能力如何對接,以單獨的插件StudentLive為例子)。
復制// 定義一個 flutter 側的 名稱為"student_101_live"的MethodChannel
static const MethodChannel _channel=const MethodChannel('student_101_live');
// 展示原生端直播UI
static Future<void> showLive(Map<String,dynamic>? map) async {
try{
// 調用原生方法名為:"showLiveDialog"方法
await _channel.invokeMethod('showLiveDialog',map);
}on MissingPluginException catch(e){
print('MissingPluginException, please check plugin has been registered or not,error message:${e.message}');
}on PlatformException catch(e){
print('invoke method log---showLiveDialog failed,error message:${e.message}');
}
}
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.
iOS側的代碼,每個plugin自動編譯會生成一個對應的Student101LivePlugin類:
復制// 這個plugin類是模版編譯生成,繼承自FlutterPlugin
@implementation Student101LivePlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
// 定義一個和flutter側相同名稱的"student_101_live" 的FlutterMethodChannel
FlutterMethodChannel* channel=[FlutterMethodChannel
methodChannelWithName:@"student_101_live"
binaryMessenger:[registrar messenger]];
Student101LivePlugin* instance=[[Student101LivePlugin alloc] init];
// 設置一個對應dart的methodDelegate
[registrar addMethodCallDelegate:instance channel:channel];
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
// 判斷是否和flutter側的方法對應起來
if ([@"showLiveDialog" isEqualToString:call.method]) {
// 這里method delegate的處理,完成原生方法的調用
if (call.arguments !=nil) {
[self initLiveView:call.arguments];
}
result(@"iOS showLiveDialog");
}else {
result(FlutterMethodNotImplemented);
}
}
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.
上面就已經完成了原生的接入,那這個studen101LivePlugin 如何完成注冊的呢,系統已經幫忙做了,查看ios/Runner/GeneratedPluginRegistrant.m,可以看到依賴的插件都被注冊,這樣你只用關注業務代碼的編寫即可。
復制@implementation GeneratedPluginRegistrant
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
[Student101LivePlugin registerWithRegistrar:[registry registrarForPlugin:@"Student101LivePlugin"]];
[FLTURLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTURLLauncherPlugin"]];
}
1.2.3.4.5.
總結一下: 在fluttter 側和原生側分別創建一個名稱相同的channel,然后再根據channel下的各個方法完成對接的調用,是不是很簡單。
在 APP 中使用H5進行頁面編寫的方案往往包括 web 資源離線方案和 server資源方案,這兩種方案最大的區別就是前者的靜態資源已經下發到APP中可以離線使用,后者需要通過向 web 服務端請求資源返回后通過webview進行渲染展現。
通過為 webview 指定訪問網站的 URL ,通過向服務端請求頁面進行渲染。
離線方案往往與在線方案相結合,能夠提供更好的用戶體驗和也不需要服務端承載大量的訪問壓力。
圖:離線化web方案
前端應用(Web)保持原有的開發部署流程,僅通過webpack plugin修改打包流程,建立與配置平臺的聯系,并生成壓縮文件,上傳到CDN。這就讓現有的H5應用方便的具備離線包的能力,后續新開發的離線應用也可以同時具備靜態發布的能力。
離線包配置平臺(Configuration Platform)用于管理各個離線包應用,包括對應用的增刪改查,版本管理,配置查看,設置檢查更新的url等功能,由webpack plugin生成的配置會通過接口入庫。每次版本更新都會存儲下來,通過上線操作生效。
APP后端(APP Server)主要提供APP側離線包配置查詢的接口,也包含ios和android離線包開啟與否的開關,在配置平臺設置生效的離線包應用會以列表的形式提供給APP使用。
客戶端(APP)通過接口獲取離線包清單,如果某個應用版本需要更新,先將本地包資源刪除,再進行更新。通過整體考量,目前我們沒有做增量更新和差量包的維護,為了 減少應用體積過大帶來的更新問題,我們提供分包的配置,可以分步實現離線化。在開啟離線包功能的WebView中,對所有資源請求進行反向代理,如果資源在本地有緩存,走本地緩存,沒有則請求在線資源。
Flutter 開屏頁面(也稱為啟動頁)也是在各家平臺上自己設置的,iOS 和 Android 都不相同。
iOS:由于 iOS 必須使用 Xcode storyboard 提供應用啟動頁面,在項目根項目下執行 open ios/Runner.xcworkspace 打開Flutter應用程序的Xcode項目,然后選擇 Runner/Assets.xcassets,將需要的啟動圖拖到 LaunchImage 圖像集中即可。
Android:因 Android 啟動頁面是個 AndroidView,同時Flutter第一幀也在繪制,這時候兩者之間會有空隙。Flutter 2.5之后做了優化(將Android啟動頁保持到Flutter的第一幀渲染完成),也改變了啟動頁面的設置方式。簡單介紹下2.5以后的設置方式,在Android AndroidManifest.xml配置
復制<activity
android:name=".MyActivity"
// 1、配置啟動的style
android:theme="@style/LaunchTheme"
// ...
>
<meta-data
// 2、配置普通的style,系統會從啟動style直接過渡到這個style
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.
Flutter 雖然實現多端的 UI 統一,最終的發布還是交給了各家平臺發布流程。
通常的流程如下:全程通過Android studio完成。
Flutter clean 清理自動編譯生成的緩存文件,避免因為緩存文件導致的編譯錯誤。
復制Flutter clean=========執行輸出=========Cleaning Xcode workspace... 4.1s
Deleting build... 1,116ms
Deleting .dart_tool... 6ms
Deleting .packages... 0ms
Deleting Generated.xcconfig... 1ms
Deleting flutter_export_environment.sh... 0ms
Deleting .flutter-plugins-dependencies... 0ms
Deleting .flutter-plugins... 0ms
1.2.3.4.5.6.7.8.9.10.11.12.
Flutter pub get 拉取工程依賴庫。
復制Flutter pub get=========部分執行輸出=========Running "flutter pub get" in studentflutter...
executing: [/Users/xx/talworkproject/xx/studentflutter/]
/Users/xx/talworkproject/xx/fluttersdk/bin/cache/dart-sdk/bin/pub --verbose get --no-precompile
FINE: Pub 2.14.4
MSG : Resolving dependencies...
SLVR: fact: app_student_flutter is 1.0.30+1
SLVR: derived: app_student_flutter
SLVR: fact: app_student_flutter depends on flutter any from sdk
SLVR: fact: app_student_flutter depends on permission_handler ^7.1.0
SLVR: fact: app_student_flutter depends on limiting_direction_csx ^0.1.0
SLVR: fact: app_student_flutter depends on url_launcher ^6.0.0-nullsafety.4
SLVR: fact: app_student_flutter depends on package_info ^2.0.0
SLVR: fact: app_student_flutter depends on shared_preferences ^2.0.5
SLVR: fact: app_student_flutter depends on connectivity_plus ^1.0.2
SLVR: fact: app_student_flutter depends on dio ^4.0.0
SLVR: fact: app_student_flutter depends on app_settings ^4.1.0
SLVR: fact: app_student_flutter depends on fluttertoast ^8.0.7
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.
Flutter build apk 最終產生一個可以發布的APK文件,發布出去。
復制Flutter build apk --release/--debug
1.
通常流程如下:需要Xcode+Android studio/VScode完成,前兩步同Android 。
復制Flutter clean
Flutter pub get
1.2.
Flutter build ipa 生成一個構建歸檔。
復制Flutter build ipa --release/--debug/--profile
1.
后面的操作和純原生的 iOS 發布流程相同,使用 xcode 完成歸檔校驗,upload to App store等。
flutter 雖屏蔽了平臺的編譯流程,但是依然脫離不了各平臺編譯流程,那它是如何做到呢?因篇幅有限以 Android Fluttter(純Flutter項目) 編譯幾個重要編譯步驟來說明:
圖:Flutter在Android上的編譯
App模塊下的 build.gradle,這個是安卓的編譯配置文件,承載編譯的重點內容。
復制...省略
apply plugin: FlutterPlugin
class FlutterPlugin implements Plugin<Project> {
// ......
// 重點入口
@Override
void apply(Project project) {
//......
project.extensions.create("flutter", FlutterExtension)
// 重點:添加flutter構建相關的各種task
this.addFlutterTasks(project)
//......
// flutter shell 命令
String flutterExecutableName=Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
flutterExecutable=Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile();
String flutterProguardRules=Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools",
"gradle", "flutter_proguard_rules.pro")
// 給所有的buildtypes 添加依賴
project.android.buildTypes.all this.&addFlutterDependencies
}
/**
* Adds the dependencies required by the Flutter project.
* This includes:
* 1. The embedding
* 2. libflutter.so
*/
// 重要,看上面注釋就可以看出主要是The embedding和libflutter.so
void addFlutterDependencies(buildType) {
String flutterBuildMode=buildModeFor(buildType)
//.....
platforms.each { platform ->
String arch=PLATFORM_ARCH_MAP[platform].replace("-", "_")
// Add the `libflutter.so` dependency.
addApiDependencies(project, buildType.name,
"io.flutter:${arch}_$flutterBuildMode:$engineVersion")
}
}
//......
// 重點:整個編譯過程中的重點和難點,最終是產出flutter層的產物 app.so和libs.jar,篇幅有限
private void addFlutterTasks(Project project) {
//......
String taskName=toCammelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name])
FlutterTask compileTask=project.tasks.create(name: taskName, type: FlutterTask) {
//......
}
// 中間產物lib.jar
File libJar=project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/libs.jar")
Task packFlutterAppAotTask=project.tasks.create(name: "packLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", type: Jar) {
destinationDir libJar.parentFile
archiveName libJar.name
dependsOn compileTask
targetPlatforms.each { targetPlatform ->
String abi=PLATFORM_ARCH_MAP[targetPlatform]
from("${compileTask.intermediateDir}/${abi}") {
include "*.so"
// Move `app.so` to `lib/<abi>/libapp.so`
rename { String filename ->
return "lib/${abi}/lib${filename}"
}
}
}
}
if (isFlutterAppProject()) {
project.android.applicationVariants.all { variant ->
// assemble task任務,最終走到 Android assemble 任務中
Task assembleTask=getAssembleTask(variant)
Task copyFlutterAssetsTask=addFlutterDeps(variant)
def variantOutput=variant.outputs.first()
def processResources=variantOutput.hasProperty("processResourcesProvider") ?
variantOutput.processResourcesProvider.get() : variantOutput.processResources
processResources.dependsOn(copyFlutterAssetsTask)
// Copy the output APKs into a known location, so `flutter run` or `flutter build apk`
// flutter build apk的歸檔
variant.outputs.all { output ->
assembleTask.doLast {
// .......
project.copy {
from new File("$outputDirectoryStr/${output.outputFileName}")
into new File("${project.buildDir}/outputs/flutter-apk");
rename {
return "${filename}.apk"
}
}
}
}
}
// ...
}
// ...
}
}
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78.79.80.81.82.83.84.85.86.87.88.89.90.91.92.93.
flutter shell -> flutter dart command -> flutter 中的 gradle 腳本(最終和平臺編譯方式結合起來)-> flutter shell 將各種編譯腳本鏈路化。
技術的發展使得作為一名Web前端工程師實現Native APP需求更加容易,可以考慮使用flutter做可攻可守的混合開發,隨著業務的穩定逐漸將穩定業務原生化,借助 web 高性價比的開發成本又不會因為開發效率措施商機。整個混合開發的全鏈路還是比較長的,從各種技術的功能開發、聯調、編譯和發布都會積累非常多的實戰經驗,這份經歷也是非常寶貴的。
漏洞描述
近期,互聯網爆出Android WebView存在跨域訪問漏洞。攻擊者利用該漏洞,可遠程獲取用戶隱私數據(包括手機應用數據、照片、文檔等敏感信息),還可竊取用戶登錄憑證,在受害者毫無察覺的情況下實現對APP用戶賬戶的完全控制。由于該組件廣泛應用于Android平臺,導致大量APP受影響,構成較為嚴重的攻擊威脅。該漏洞危害程度為高危(High)。
2 影響范圍
漏洞影響使用WebView控件,開啟file域訪問并且未按安全策略開發的Android應用APP。
3 漏洞原理
WebView是Android用于顯示網頁的控件,是一個基于Webkit引擎、展現web頁面的控件。WebView控件功能除了具有一般View的屬性和設置外,還可對URL請求、頁面加載、渲染、頁面交互進行處理。
該漏洞產生的原因是在Android應用中,WebView開啟了file域訪問,允許file域訪問http域,且未對file域的路徑進行嚴格限制所致。攻擊者通過URL Scheme的方式,可遠程打開并加載惡意HTML文件,遠程獲取APP中包括用戶登錄憑證在內的所有本地敏感數據。
漏洞觸發成功前提條件如下:
1.WebView中setAllowFileAccessFromFileURLs 或setAllowUniversalAccessFromFileURLsAPI配置為true;
2.WebView可以直接被外部調用,并能夠加載外部可控的HTML文件。
4 漏洞建議
廠商暫未發布解決方案,臨時解決方案如下:
1.file域訪問為非功能需求時,手動配置setAllowFileAccessFromFileURLs或setAllowUniversalAccessFromFileURLs兩個API為false。(Android4.1版本之前這兩個API默認是true,需要顯式設置為false)
2. 若需要開啟file域訪問,則設置file路徑的白名單,嚴格控制file域的訪問范圍,具體如下:
(1)固定不變的HTML文件可以放在assets或res目錄下,file:///android_asset和file:///android_res 在不開啟API的情況下也可以訪問;
(2)可能會更新的HTML文件放在/data/data/(app) 目錄下,避免被第三方替換或修改;
(3)對file域請求做白名單限制時,需要對“../../”特殊情況進行處理,避免白名單被繞過。
3. 避免App內部的WebView被不信任的第三方調用。排查內置WebView的Activity是否被導出、必須導出的Activity是否會通過參數傳遞調起內置的WebView等。
4. 建議進一步對APP目錄下的敏感數據進行保護。客戶端APP應用設備相關信息(如IMEI、IMSI、Android_id等)作為密鑰對敏感數據進行加密。使攻擊者難以利用相關漏洞獲得敏感信息。
Visaul Studio 2019中的Xamarin開發Android App應用,選用Android 9.0-API 28做調試運行模擬器。用WebView控件顯示網站內容,出錯!
代碼片斷:
調試運行出錯:
經上網查閱資料:
原來,google在Android 9.0開始,WebView中的網頁地址默認不支持明文傳送,即WebView.Source不能用"http://"打頭的網址,只能用“https://”打頭的網址。
網上解答截圖如下:
我在項目文件中進行了對應設置,問題真的就解決了!解決過程圖如下:
1、在解決方案中Android部分,展開Properties
2、選中并打開:AndroidManifest.xml,在<application>節中,增加 android:usesCleartextTraffic="true" 意思是允許明文傳送。
3、保存,重新調試運行App程序,網站頁面就正常顯示出來了,效果圖如下:
*請認真填寫需求信息,我們會在24小時內與您取得聯系。