当前 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。