當前 Flutter 版本: 1.9.1+hotfix.4java
項目資源管理一直都是應用開發領域煊赫一時的話題,資源和源碼是組成項目包的主要兩個部分,他們都直接影響到應用程序的包大小而且必定程度會影響應用程序的運行速度。此文主要介紹 Flutter 項目如何管理資源
,一樣做爲開發者如何維持資源的可持續化管理
。node
Flutter 使用 pubspec.yaml
文件來定位項目的根目錄,任何項目中使用到的資源都須要在 pubspec.yaml
中進行聲明。pubspecl.yaml
是個YAML(/ˈjæməl/) 文件,資源聲明在該文件的 flutter->assets
層級上,例如:git
flutter:
assets:
- assets/my_icon.png
- assets/background.png
複製代碼
爲了統一表述,聲明的資源名稱咱們稱之爲
資源標記
。github
一樣,也能夠直接聲明文件夾。以文件夾形式聲明只會包含此文件夾根目錄
中的文件,二級目錄
須要另行聲明。npm
flutter:
assets:
- assets/
- assets/home/
複製代碼
Flutter 支持資源變種
:在不一樣上下文中使用資源變種的技術,例如 iOS 程序中的 2x/3x 圖。當在 pubspecl.yaml
中聲明資源時,Flutter 會自動查找全部子目錄中有關的變種文件而且關聯到一塊兒,運行時會根據上下文去獲取合適的那個資源。資源變種有幾個必須條件:json
一. Flutter 必須知道該資源的資源標記數組
假如項目中資源目錄結構爲:xcode
pubspec.yaml
assets/home/icon.png
assets/home/2x/icon.png
assets/home/3x/icon.png
assets/home/2x/other_icon.png
assets/home/3x/other_icon.png
複製代碼
假如以文件夾形式標明資源位置:緩存
flutter:
assets:
- assets/home/
複製代碼
Flutter 只會找到一個資源標記 assets/home/icon.png
,緣由上節已經說明,Flutter 只會查找聲明文件夾的根目錄中存在的文件。因此只能顯示聲明每一個資源才能達到預期效果:bash
flutter:
assets:
- assets/home/icon.png
- assets/home/other_icon.png
複製代碼
二. 變種標記必須在最後一個文件夾位置
假如項目中資源以下: 注意 2x/3x 的位置變化了。
pubspec.yaml
assets/home/icon.png
assets/2x/home/icon.png
assets/3x/home/icon.png
assets/3x/home/other_icon.png
assets/3x/home/other_icon.png
複製代碼
pubspec.yaml
中聲明以下:
flutter:
assets:
- assets/home/icon.png
- assets/home/other_icon.png
複製代碼
運行時使用資源標記不能查找到對應 2x/3x 的資源變種。
直接看 Flutter 生成文件 App.framework
的結構。
App.framework
├── App
├── Info.plist
└── flutter_assets
├── AssetManifest.json
├── assets
│ └── home
| ├── icon.png
│ ├── 2x
| | ├── icon.png
│ │ └── other_icon.png
│ └── 3x
| ├── icon.png
│ └── other_icon.png
複製代碼
結論是 Flutter 把項目中的全部資源拷貝進了 App.framework->flutter_assets
目錄中,同時生成了一個緩存文件,該緩存文件保存了資源標記與變種的對應關係
。內容以下:
{
"assets/home/icon.png": [
"assets/home/icon.png",
"assets/home/2x/icon.png",
"assets/home/3x/icon.png"
],
"assets/home/other_icon.png": [
"assets/home/2x/other_icon.png",
"assets/home/3x/other_icon.png"
]
}
複製代碼
運行時使用 AssetBundle
來訪問資源,AssetBundle
是個虛類,主要有三個方法:
/// 加載資源數據,返回字節流
Future<ByteData> load(String key);
/// 加載資源數據,返回 UTF-8 編碼的字符串
Future<String> loadString(String key, { bool cache = true });
/// 加載結構化數據,傳入解碼函數
Future<T> loadStructuredData<T>(String key, Future<T> parser(String value));
複製代碼
訪問項目資源基本都直接或者間接使用到 rootBundle
對象,該對象是 AssetBundle
類型。
import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;
Future<String> loadAsset() async {
return await rootBundle.loadString('assets/config.json');
}
複製代碼
訪問圖片有兩種方式,第一種是直接使用 Image
視圖控件顯示,第二種是使用 AssetImage
,AssetImage
是一個 ImageProvider
對象。
一. 使用
Image
顯示
@override
Widget build(BuildContext context) {
return Image.asset(
"home/icon.png",
width: 16,
height: 16,
);
}
複製代碼
二. 使用
AssetImage
顯示
@override
Widget build(BuildContext context) {
return Image(
image: AssetImage("home/icon.png"),
width: 16,
height: 16,
);
}
複製代碼
以上說明圖片主要是由
Image
或者AssetImage
訪問的,那麼系統是如何知道應該是使用哪一種資源變種的呢?咱們能夠深刻到源碼層面去研究。
首先咱們先看一下 Image.asset
的源碼:
Image.asset(
String name, {
/// 這部分省略
}) : image = scale != null
? ExactAssetImage(name, bundle: bundle, scale: scale, package: package)
: AssetImage(name, bundle: bundle, package: package),
loadingBuilder = null,
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
super(key: key);
複製代碼
能夠發現 Image.asset
構造方法中 image
最終仍是會賦值爲 ExactAssetImage/AssetImage
。
ExactAssetImage
是用來拿特定 scale 的資源的,不會涉及到變種。AssetImage
是根據當前屏幕 scale 自動獲取合適的變種。咱們主要將目光放在 AssetImage
上,AssetImage
實現了 ImageProvider
協議。ImageProvider
協議須要實現兩個方法:
/// 根據當前設備狀況,生成一個 key
Future<T> obtainKey(ImageConfiguration configuration);
/// 根據 Key 獲取圖片的數據流
ImageStreamCompleter load(T key);
複製代碼
再來看看 AssetImage
類中對應的實現,load
方法是調用原生接口讀取 obtainKey
返回的文件路徑並獲取內容,主要匹配邏輯在 obtainKey
方法中:
Future<AssetBundleImageKey> obtainKey(ImageConfiguration configuration) {
/// 找到當前的資源 bundle
final AssetBundle chosenBundle = bundle ?? configuration.bundle ?? rootBundle;
/// 加載 AssetManifest.json
chosenBundle.loadStructuredData<Map<String, List<String>>>(_kAssetManifestFileName, _manifestParser).then<void>(
(Map<String, List<String>> manifest) {
/// 找到當前資源標記的全部變種文件
/// 在全部變種文件中獲取最合適的變種
final String chosenName = _chooseVariant(
keyName,
configuration,
manifest == null ? null : manifest[keyName],
);
/// 根據變種地址獲取資源 scale
final double chosenScale = _parseScale(chosenName);
/// 生成 key
final AssetBundleImageKey key = AssetBundleImageKey(
bundle: chosenBundle,
name: chosenName,
scale: chosenScale,
);
}
}
複製代碼
能夠看到 Flutter 首先會加載 AssetManifest.json
文件,而後根據當前的資源標記
獲取到當前存在的全部資源變種,而後在全部變種選擇合適的變種,核心方法 _chooseVariant
源碼以下:
/// main 是資源標記
/// config 包含當前項目的屏幕 scale
/// candidates 指的是當前資源標記的全部變種文件,通過排序
String _chooseVariant(String main, ImageConfiguration config, List<String> candidates) {
if (config.devicePixelRatio == null || candidates == null || candidates.isEmpty)
return main;
final SplayTreeMap<double, String> mapping = SplayTreeMap<double, String>();
for (String candidate in candidates)
mapping[_parseScale(candidate)] = candidate;
return _findNearest(mapping, config.devicePixelRatio);
}
String _findNearest(SplayTreeMap<double, String> candidates, double value) {
if (candidates.containsKey(value))
return candidates[value];
/// 找到例當前變種左側最臨近的變種
final double lower = candidates.lastKeyBefore(value);
/// 找到例當前變種右側最臨近的變種
final double upper = candidates.firstKeyAfter(value);
/// 若是沒有左側鄰近的變種則選擇右側變種
if (lower == null)
return candidates[upper];
/// 若是沒有右側鄰近的變種則選擇左側變種
if (upper == null)
return candidates[lower];
/// 若是當前屏幕 scale 比左側和右側的平均值大,則選擇右側變種
if (value > (lower + upper) / 2)
return candidates[upper];
/// 選擇左側變種
else
return candidates[lower];
}
複製代碼
這兩個方法歸納一句話就是選擇當前存在的變種中最鄰近的變種
。舉幾個例子來幫助理解:
能夠看到最合理的狀況下是隻提供 2x/3x 圖。
總結一下,Flutter 資源查找流程分爲 4 步:
AssetManifest.json
內容,找到資源標記與變種文件的關係。以上即是 Flutter 主要的資源管理以及訪問的流程,至於還有其餘的例如訪問依賴包中的資源等等狀況,總體流程殊途同歸的,因此沒有一一列舉。
根據以上的流程咱們能夠總結出 Flutter 資源管理存在的問題:
pubspec.yaml
中標明資源標記的狀況下,除非全部資源都有對應的一倍圖存在於根目錄下,資源的其餘變種纔會發揮做用。App.framework/flutter_assets/
文件夾中,不在 App Bundle 的根目錄下,不會享受 Xcode 的自動 PNG 圖片壓縮。同時也不會享受 Assets Slicing。問題一有兩個思路去解決:第一個思路:只在 pubspec.yaml
聲明文件夾,每一個資源都提供一倍圖,可是如今 1x 的屏幕設備比例至關少,爲了防止安裝包體積的問題,僞造一個空的一倍圖來代替;第二個思路:每一個資源都在 pubspec.yaml
顯式聲明。實踐過程當中第一個思路會大大影響項目資源的管理過程,因此咱們選擇了第二個思路。
問題二解決思路就比較侷限了,咱們曾經的想法是資源仍是放在原生端,經過 MethodChannel
來獲取資源而且顯示,那樣資源能夠自動被壓縮而且切片。可是這樣管理起來會至關麻煩而且實現成本也比較大。因此咱們使用了退而求其次的方式:不考慮資源切片,可是必需要進行壓縮。
至此總體的資源添加流程能夠歸納爲:
pubspec.yaml
中聲明資源標記不得不說,太複雜了,並且隨着項目的增大,項目中的資源會愈來愈難以管理。因此就有了咱們資源自動化的方案。
資源自動化管理工具:auto-assets
資源自動化要解決的問題很簡單,就是讓添加資源到項目這個過程變得簡單而且純粹。最簡單的流程是隻須要把資源添加到項目,其餘流程不用去關心。基於此需求上,自動化要解決的問題就變得明確了:
pubspec.yaml
聲明至此,添加資源這個動做就已經變得簡單且純粹了。可是回想作 iOS 開發的過程當中,最難的每每是可持續化的資源管理。或許你們都遇到過如下狀況:
以上問題主要的緣由是 HardCode,解決的方式能夠參考 R.java
,主要思想是資源類型化
。例如每一個資源都生成一個對應的實例,使用資源改變爲使用該實例:
class Assets {
Assets._();
static const String homeIcon = "assets/home/icon.png";
static const String homeOtherIcon = "assets/home/other_icon.png";
}
複製代碼
工具原理其實很是簡單,這裏也不展開講了,主要概括爲如下幾個步驟:
資源根目錄
資源類型化
代碼jpg
/jpeg
/png
/svg
。在語言選型的過程當中也考慮過期候 Dart 來開發,可是 Dart Build Runner 還不夠完善和成熟,爲了開發速度最後仍是選擇了 nodejs。
auto_assets 是使用 nodejs 開發的工具,安裝須要有 node 環境。
npm install auto_assets -g
在 Flutter 項目根目錄下新建 assets_config.json
文件,文件內容:
{
"assets": "assets/",
"code": "lib/assets/"
}
複製代碼
assets
表明項目中資源文件的根目錄,有多個的時候能夠傳入數組。code
表明自動生成的代碼存放的目錄。在命令行輸入:
auto_assets [Flutter項目根目錄]
複製代碼
使用 VSCode 開發的同窗能夠直接在插件商店裏面搜索 auto_assets。