flutter資源管理與自動化

當前 Flutter 版本: 1.9.1+hotfix.4java

項目資源管理一直都是應用開發領域煊赫一時的話題,資源和源碼是組成項目包的主要兩個部分,他們都直接影響到應用程序的包大小而且必定程度會影響應用程序的運行速度。此文主要介紹 Flutter 項目如何管理資源,一樣做爲開發者如何維持資源的可持續化管理node

1. 資源管理

1.1. 資源聲明

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/
複製代碼

1.1.1. 資源變種

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 的資源變種。

1.2. 資源在項目中的表現形式

直接看 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"
  ]
}
複製代碼

1.3. 資源使用

運行時使用 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 類型。

1.3.1. 直接訪問資源內容

import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;

Future<String> loadAsset() async {
  return await rootBundle.loadString('assets/config.json');
}
複製代碼

1.3.2. 訪問圖片

訪問圖片有兩種方式,第一種是直接使用 Image 視圖控件顯示,第二種是使用 AssetImageAssetImage 是一個 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,
  );
}
複製代碼

1.3.3. 深刻細節

以上說明圖片主要是由 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];
}
複製代碼

這兩個方法歸納一句話就是選擇當前存在的變種中最鄰近的變種。舉幾個例子來幫助理解:

  • 假如項目中只存在 1x/4x 圖,那麼 1/2/3/4 屏幕密度上分別選擇的是: 1/1/4/4
  • 假如項目中只存在 2x/3x 圖,那麼 1/2/3/4 屏幕密度上分別選擇的是: 2/2/3/3
  • 假如項目中只存在 1x/3x 圖,那麼 1/2/3/4 屏幕密度上分別選擇的是: 1/1/3/3
  • 假如項目中只存在 2x/4x 圖,那麼 1/2/3/4 屏幕密度上分別選擇的是: 2/2/2/4

能夠看到最合理的狀況下是隻提供 2x/3x 圖。

總結一下,Flutter 資源查找流程分爲 4 步:

  1. 獲取資源配置文件 AssetManifest.json 內容,找到資源標記與變種文件的關係。
  2. 根據當前資源標記獲取其全部的變種文件。
  3. 查找出當前存在的變種中與當前屏幕 scale 最鄰近的變種。
  4. 經過 Native API 獲取變種文件的內容,傳輸給 Flutter 端進行顯示。

1.4. 總結

以上即是 Flutter 主要的資源管理以及訪問的流程,至於還有其餘的例如訪問依賴包中的資源等等狀況,總體流程殊途同歸的,因此沒有一一列舉。

根據以上的流程咱們能夠總結出 Flutter 資源管理存在的問題:

  1. 以文件夾形式在 pubspec.yaml 中標明資源標記的狀況下,除非全部資源都有對應的一倍圖存在於根目錄下,資源的其餘變種纔會發揮做用。
  2. 資源是直接拷貝進 App.framework/flutter_assets/ 文件夾中,不在 App Bundle 的根目錄下,不會享受 Xcode 的自動 PNG 圖片壓縮。同時也不會享受 Assets Slicing

問題一有兩個思路去解決:第一個思路:只在 pubspec.yaml 聲明文件夾,每一個資源都提供一倍圖,可是如今 1x 的屏幕設備比例至關少,爲了防止安裝包體積的問題,僞造一個空的一倍圖來代替;第二個思路:每一個資源都在 pubspec.yaml 顯式聲明。實踐過程當中第一個思路會大大影響項目資源的管理過程,因此咱們選擇了第二個思路。

問題二解決思路就比較侷限了,咱們曾經的想法是資源仍是放在原生端,經過 MethodChannel 來獲取資源而且顯示,那樣資源能夠自動被壓縮而且切片。可是這樣管理起來會至關麻煩而且實現成本也比較大。因此咱們使用了退而求其次的方式:不考慮資源切片,可是必需要進行壓縮。

至此總體的資源添加流程能夠歸納爲:

  1. 設計師提供視覺資源
  2. 將視覺資源進行壓縮處理
  3. 將視覺資源添加進項目
  4. pubspec.yaml 中聲明資源標記

不得不說,太複雜了,並且隨着項目的增大,項目中的資源會愈來愈難以管理。因此就有了咱們資源自動化的方案。

2. 自動化

資源自動化管理工具:auto-assets

2.1. 工具由來

資源自動化要解決的問題很簡單,就是讓添加資源到項目這個過程變得簡單而且純粹。最簡單的流程是隻須要把資源添加到項目,其餘流程不用去關心。基於此需求上,自動化要解決的問題就變得明確了:

  • 自動生成 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";
}
複製代碼

2.2. 工具原理

工具原理其實很是簡單,這裏也不展開講了,主要概括爲如下幾個步驟:

  1. 監聽資源根目錄
  2. 資源修改/添加/刪除的事件後
  3. 從新生成 pubspec.yaml 聲明
  4. 從新壓縮資源
  5. 從新生成資源類型化代碼

2.3. 工具限制

  • 資源路徑字符集:[a-z0-9_/],建議使用 snake_case
  • 資源壓縮目前只支持 jpg/jpeg/png/svg

2.4. 工具安裝

在語言選型的過程當中也考慮過期候 Dart 來開發,可是 Dart Build Runner 還不夠完善和成熟,爲了開發速度最後仍是選擇了 nodejs

auto_assets 是使用 nodejs 開發的工具,安裝須要有 node 環境。

  1. 安裝 nodejs:nodejs.org/en/download…
  2. 執行命令:npm install auto_assets -g
  3. 安裝完以後便可使用 auto_assets 命令

2.5. 工具使用

在 Flutter 項目根目錄下新建 assets_config.json 文件,文件內容:

{
  "assets": "assets/",
  "code": "lib/assets/"
}
複製代碼
  • assets 表明項目中資源文件的根目錄,有多個的時候能夠傳入數組。
  • code 表明自動生成的代碼存放的目錄。

在命令行輸入:

auto_assets [Flutter項目根目錄]
複製代碼

2.6. VSCode Extension

使用 VSCode 開發的同窗能夠直接在插件商店裏面搜索 auto_assets

工具演示

相關文章
相關標籤/搜索