一、初识flutter image
在讲解源码之前,我们先看下面几个例子,回顾一下flutter加载图片资源的方式。
1.1、显示一张图片
Flutter应用程序包含两部分:代码和assets,其中assets最终会打包在apk的assets/flutter_assets目录下,可以在程序运行的过程中访问。我们知道flutter中是通过pubspec.xml文件来配置应用所需的依赖,图片资源也类似。比如想显示一个名为a.png的图片,只需要两步:
1、将图片a.png拷贝到xxx目录下,并在pubspec.yaml中配置assets依赖:
flutter:
assets:
- xxx/a.png
2、在代码中通过Image.asset构造函数加载图片,参数为在pubspec.xml配置的图片路径:
Image.asset("xxx/a.png");
1.2、加载不同分辨率的图片
flutter在加载图片时会根据手机的分辨率选择最合适的图片进行显示,要实现这种映射关系,需要根据特定的目录结构来存放图片:
…xxx/Mx/image.png
…xxx/Nx/image.png
其中M和N是数字标识符,对应于其中包含的图像的分辨率。
举个简单的例子,现在有两张a.png的图片分别对应着dpi为2.0和3.0,使用时,在图片目录下创建两个文件夹:2.0x和3.0x,将对应的图片拷贝进去,并在pubspec.yaml配置对应图片资源的key:
flutter:
assets:
- xxx/a.png
在flutter中会将对应资源项(这里是xxx/a.png)所在的目录的二级目录中与资源项名字相同的图片认为是统一资源,在打包的过程中会将资源项按照key-value的形式存入apk的assets/flutter_assets/AssetManifest.json文件中,flutter在加载资源是首先会先解析这个文件,后再选择最适合的图片进行显示,其中AssetManifest.json的具体格式如下:
{"xxx/a.png":["xxx/2.0x/a.png","xxx/3.0x/a.png"]}
在flutter中可以将通过目录的方式配置assets,需要注意的是它只会打包当前目录的文件,二级目录不会进行打包除非在assets中手动配置,例如想要要打包a目录下的所有文件,只需要pubspec.yaml配置目录即可:
flutter:
assets:
- a/
需要主要的这种用法只针对文件,对于目录是无效的,比如a目录下的b目录并不会被打入apk中,需要手动指定,具体的原因参照第三部分。
1.3、加载packages中的图片
假设开发的package test中包含应用需要的图片,那在应用程序中如何加载这些图片那?
有下面两种方式:
1、如果package的pubspec.yaml中已经声明了这张图片,在应用程序中不需要做任何操作,只需要在通过AssetImage加载时加入包名即可:
Image.asset("xxx/a.png",package:"test");
2、如果test的pubspec.yaml文件中没有声明对图片的依赖,flutter默认会将package的lib目录作为包的依赖目录,我们可以将图片放在lib/文件夹中,这种情况下,需要在应用程序的pubspec.yaml中进行声明:
flutter:
assets:
- packages/test/xxx/a.png
有一点需要注意,这两种方式引入package不能通过本地依赖的方式,这是因为flutter_tools 打包包资源的过程中会判断依赖的目录是否以file开头,如果不是就不进行解析,具体的逻辑参考第三章flutter打包解析。通过上面的介绍,我们大致知道在flutter中本地资源如何使用,下面从源码上分析资源的加载和打包过程。
二、资源加载源码分析
上面一节已经介绍过,加载本地图片需要用到Image控件的asset构造函数,flutter对Image控件提供了丰富的加载图片的api:
Image.network 从网络加载图片
Image.file 从file加载图片
Image.asset 从本地assets目录加载图片
Image.memory 从缓存中加载图片
这里只分析Image.asset的源码,其他的思路是一样的。
我们就看Image.asset干了什么。
Image.asset({...}) : image = scale != null
? new ExactAssetImage(name, bundle: bundle, scale: scale, package: package)
: new AssetImage(name, bundle: bundle, package: package);
Image继承于StatefulWidget类,构造函数比较简单,主要是用于设置成员变量,其中一个重要的变量是image,它是ImageProvider类型的对象,是加载图片资源的入口,对于不同的图片加载方式(网路、文件)对应着不同类型的ImageProvider对象,这里首先判断是不是需要对图片进行缩放操作来决定image是AssetImage还是ExactAssetImage类型,假设这里正常显示,image为AssetImage类型的对象。
由于Image继承于StatefulWidget类型的对象,所以widget的build的逻辑在对应的State中,接下来我们看一下Image对应的State类:
class _ImageState extends State<Image> {
//用于处理image resource,它里面存有两个成员变量:ImageStreamCompleter(图片解析器)和_ImageListenerPair(类似于java的Pair对象,有两个成员函数:listener和errorListener分别对应图片加载成功和失败的回调),用于存储图片解析完成和失败的回调。
ImageStream _imageStream;
//包含了图片的真正数据信息,成员变量有:scale(压缩比)、ui.Iamge(ARGB像素数据,通过Canvas.draw可以将其画到画布上)
ImageInfo _imageInfo;
bool _isListeningToStream = false;
//在initState后调用,在组件的初始化阶段调用
@override
void didChangeDependencies() {
//解析图片
_resolveImage();
//设置和移除监听图片成功的回调
if (TickerMode.of(context))
_listenToStream();
else
_stopListeningToStream();
super.didChangeDependencies();
}
//组件状态发生变化时调用(比如说调用了setState函数)
@override
void didUpdateWidget(Image oldWidget) {
super.didUpdateWidget(oldWidget);
//如果组件的图片来源发生了变化,则调用_resolveImage函数重新获取最新的图片
if (widget.image != oldWidget.image)
_resolveImage();
}
//debug期间组件重组时调用(比如热更新)
@override
void reassemble() {
_resolveImage();
super.reassemble();
}
void _resolveImage() {
//根据ImageConfiguration调用ImageProvider的resolve函数获得ImageStream对象,ImageStream
final ImageStream newStream =
widget.image.resolve(createLocalImageConfiguration(
context,
size: widget.width != null && widget.height != null ? new Size(widget.width, widget.height) : null
));
_updateSourceStream(newStream);
}
//图片加载成功的回调,更新数据信息,重绘界面
void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {
setState(() {
_imageInfo = imageInfo;
});
}
//更新state中的ImageStream对象
void _updateSourceStream(ImageStream newStream) {
if (_imageStream?.key == newStream?.key)
return;
if (_isListeningToStream)
_imageStream.removeListener(_handleImageChanged);
if (!widget.gaplessPlayback)
setState(() { _imageInfo = null; });
_imageStream = newStream;
if (_isListeningToStream)
_imageStream.addListener(_handleImageChanged);
}
void _listenToStream() {
if (_isListeningToStream)
return;
_imageStream.addListener(_handleImageChanged);
_isListeningToStream = true;
}
void _stopListeningToStream() {
if (!_isListeningToStream)
return;
_imageStream.removeListener(_handleImageChanged);
_isListeningToStream = false;
}
@override
void dispose() {
_stopListeningToStream();
super.dispose();
}
@override
Widget build(BuildContext context) {
//RawImage渲染图片的控件,用于显示ImageInfo中
final RawImage image = new RawImage(
image: _imageInfo?.image,
width: widget.width,
height: widget.height,
scale: _imageInfo?.scale ?? 1.0,
color: widget.color,
colorBlendMode: widget.colorBlendMode,
fit: widget.fit,
alignment: widget.alignment,
repeat: widget.repeat,
centerSlice: widget.centerSlice,
matchTextDirection: widget.matchTextDirection,
);
if (widget.excludeFromSemantics)
return image;
return new Semantics(
container: widget.semanticLabel != null,
image: true,
label: widget.semanticLabel == null ? '' : widget.semanticLabel,
child: image,
);
}
}
从上面的代码中可以看出,在控件刚创建或者是状态更新时都会调用ImageProvider的resolve函数获得ImageStream。
接着我们看一下AssetImage的resolve方法:
ImageStream resolve(ImageConfiguration configuration) {
final ImageStream stream = new ImageStream();
T obtainedKey;
obtainKey(configuration).then<void>((T key) {
obtainedKey = key;
stream.setCompleter(PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key)));
}).catchError(
(dynamic exception, StackTrace stack) async {
...
});
return stream;
}
obtainKey是一个抽象方法,在AssetImage中返回一个Future<AssetBundleImageKey>的对象,并作为PaintingBinding.instance.imageCache缓存的key,将load(key)加入到cache中。PaintingBinding.instance.imageCache是ImageCache类型的变量,是flutter的图片存储池,在dart在初始化时设置的,生命周期和整个dart虚拟机相同,默认缓存池的大小为100M,图片总量为1000个。通过上面的方法,知道在resolve函数最重要的两个方法为obtainKey和load,下面我们挨个分析,首先是AssetImage的obtainKey函数:
@override
Future<AssetBundleImageKey> obtainKey(ImageConfiguration configuration) {
final AssetBundle chosenBundle = bundle ?? configuration.bundle ?? rootBundle;
Completer<AssetBundleImageKey> completer;
Future<AssetBundleImageKey> result;
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]
);
final double chosenScale = _parseScale(chosenName);
final AssetBundleImageKey key = new AssetBundleImageKey(
bundle: chosenBundle,
name: chosenName,
scale: chosenScale
);
if (completer != null) {
completer.complete(key);
} else {
result = new SynchronousFuture<AssetBundleImageKey>(key);
}
}
).catchError((dynamic error, StackTrace stack) {
....
});
...
completer = new Completer<AssetBundleImageKey>();
return completer.future;
}
从上面的代码可知,首先从configuration得到AssetBundle对象,AssetBundle是flutter加载资源功能类,通过上面介绍_ImageState我们知道这个对象是通过createLocalImageConfiguration函数创建,通过跟踪源码可知我们bundle的类型为PlatformAssetBundle,由于代码比较简单,这就不做介绍有兴趣的同学可以自行看代码。接着会调用对应的AssetBundle的loadStructuredData函数,这个函数的功能是获取_kAssetManifestFileName字符串对应文件的内容,并交给_manifestParser函数进行解析,并将结果封装成future返回。其中_kAssetManifestFileName对应的字符串为AssetManifest.json,下面看源码:
@override
Future<T> loadStructuredData<T>(String key, Future<T> parser(String value)) {
//_structuredDataCache是一个map,用来缓存查找结果,如果如果已经缓存过数据,就将其返回,假设这里是第一次加载,这里为false,就会走到下面方法
if (_structuredDataCache.containsKey(key))
return _structuredDataCache[key];
Completer<T> completer;
Future<T> result;
//这里首先调用loadString函数获取key文件对应的文本信息的future,然后将加载的数据缓存到map中
loadString(key, cache: false).then<T>(parser).then<void>((T value) {
result = new SynchronousFuture<T>(value);
_structuredDataCache[key] = result;
});
....
return completer.future;
}
Future<String> loadString(String key, { bool cache = true }) async {
//这里调用load方法获取获取key对应的二进制数据流
final ByteData data = await load(key);
//将数据流转化为string对象
...
return compute(_utf8decode, data, debugLabel: 'UTF8 decode for "$key"');
}
@override
Future<ByteData> load(String key) async {
//将string转会为Uint8List对象,通过上面可知key为AssetManifest.json
final Uint8List encoded = utf8.encoder.convert(new Uri(path: Uri.encodeFull(key)).path);
//BinaryMessages通过platform plugins发送二进制数据,对应的plugin的名车个为flutter/assets,message为:AssetManifest.json
final ByteData asset =
await BinaryMessages.send('flutter/assets', encoded.buffer.asByteData());
return asset;
}
}
//BinaryMessages send方法继而调用_sendPlatformMessage方法
static Future<ByteData> send(String channel, ByteData message) {
...
return _sendPlatformMessage(channel, message);
}
//最后会进入ui.window.sendPlatformMessage方法
static Future<ByteData> _sendPlatformMessage(String channel, ByteData message) {
final Completer<ByteData> completer = new Completer<ByteData>();
ui.window.sendPlatformMessage(channel, message, ...);
return completer.future;
}
//最后会调到_sendPlatformMessage方法
String _sendPlatformMessage(String name,
PlatformMessageResponseCallback callback,
ByteData data) native 'Window_sendPlatformMessage';
_sendPlatformMessage函数是一个用native关键字声明的方法,后面紧跟一个字符串(这个是native声明的函数的标示),这种类型的函数用于dart调用native的方法,dart虚拟机在启动时会将调用DartLibraryNatives的Register方法将对应的函数名称和函数指针注册进去,这样dart端就可以根据函数名称调用对应的native方法,比如C++端Windows RegisterNatives方法:
void Window::RegisterNatives(tonic::DartLibraryNatives* natives) {
natives->Register({
{"Window_defaultRouteName", DefaultRouteName, 1, true},
{"Window_scheduleFrame", ScheduleFrame, 1, true},
{"Window_sendPlatformMessage", _SendPlatformMessage, 4, true},
{"Window_respondToPlatformMessage", _RespondToPlatformMessage, 3, true},
{"Window_render", Render, 2, true},
{"Window_updateSemantics", UpdateSemantics, 2, true},
});
所以上面的dart函数最终会调到native的Window下_SendPlatformMessage方法,接着进入C++中Window的SendPlatformMessage方法:
Dart_Handle
SendPlatformMessage(Dart_Handle window,
const std::string& name,
Dart_Handle callback,
const tonic::DartByteData& data) {
UIDartState* dart_state = UIDartState::Current();
...
const uint8_t* buffer = static_cast<const uint8_t*>(data.data());
dart_state->window()->client()->HandlePlatformMessage(
fml::MakeRefCounted<PlatformMessage>(
name, std::vector<uint8_t>(buffer, buffer + data.length_in_bytes()),
response));
return Dart_Null();
}
跟这源码一步步往下走,最后会走到Engine的HandlePlatformMessage方法,Engine类是flutter中很重要的一个类,dart和android交互的功能基本都在这个类中,HandlePlatformMessage函数:
void Engine::HandlePlatformMessage(
fml::RefPtr<blink::PlatformMessage> message) {
//kAssetChannel是一个字符串,值“flutter/assets”
if (message->channel() == kAssetChannel) {
HandleAssetPlatformMessage(std::move(message));
} else {
delegate_.OnEngineHandlePlatformMessage(*this, std::move(message));
}
}
void Engine::HandleAssetPlatformMessage(
fml::RefPtr<blink::PlatformMessage> message) {
fml::RefPtr<blink::PlatformMessageResponse> response = message->response();
if (!response) {
return;
}
const auto& data = message->data();
std::string asset_name(reinterpret_cast<const char*>(data.data()),
data.size());
if (asset_manager_) {
std::unique_ptr<fml::Mapping> asset_mapping =
asset_manager_->GetAsMapping(asset_name);
if (asset_mapping) {
response->Complete(std::move(asset_mapping));
return;
}
}
response->CompleteEmpty();
}
HandlePlatformMessage函数首先会判断channel是不是"flutter/assets",由上面dart的代码可知这里为true,接着就会调用HandleAssetPlatformMessage函数来读取相应的文件名的内容,这个函数比较简单会调用AssetManager的GetAsMapping函数读取AssetManifest.json文件的内容,其中AssetManager是一个资源读取的代理类,最终会走到APKAssetProvider的GetAsMapping函数:
std::unique_ptr<fml::Mapping> APKAssetProvider::GetAsMapping(
const std::string& asset_name) const {
std::stringstream ss;
ss << directory_.c_str() << "/" << asset_name;
AAsset* asset =
AAssetManager_open(assetManager_, ss.str().c_str(), AASSET_MODE_BUFFER);
if (!asset) {
return nullptr;
}
return std::make_unique<APKAssetMapping>(asset);
}
assetManager_是对应AssetManager的java指针,这里就是调用android虚拟机中 AAssetManager_open的方法读取asset目录下的文件,传入的名称为AssetManifest.json
综上所述,AssetBundle中loadStrig函数最终会通过Android的AssetManager读取assets目录下的AssetManifest.json文件并将结果返回。
接着AssetBundle会调用_manifestParser解析loadString的返回的内容,_manifestParser函数:
static Future<Map<String, List<String>>> _manifestParser(String jsonData) {
if (jsonData == null)
return null;
final Map<String, dynamic> parsedJson = json.decode(jsonData);
final Iterable<String> keys = parsedJson.keys;
final Map<String, List<String>> parsedManifest =
new Map<String, List<String>>.fromIterables(keys,
keys.map((String key) => new List<String>.from(parsedJson[key])));
return new SynchronousFuture<Map<String, List<String>>>(parsedManifest);
}
我们在第一节就介绍过AssetManifest.json的文件格式,它是一个json结构体,key为我们在pub配置的文件名,value对应这不同分辨率文件路径的列表。_manifestParser就是这些json对象解析成Map<String, List<String>的对象,其中key为对象我们在pub中配置的图片名称,value是对应不同分辨率对应的文件路径的列表。
回到AssetImage obtainKey函数,接着回调用_chooseVariant函数,根据key值获取最佳的图片路径:
String _chooseVariant(String main, ImageConfiguration config, List<String> candidates) {
...
//二叉排序树的map
final SplayTreeMap<double, String> mapping = new 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];
//先找出接近目标分辨率最近的两个候选者分别为lower和upper
final double lower = candidates.lastKeyBefore(value);
final double upper = candidates.firstKeyAfter(value);
//如果其中一个为空,则返回另外一个
if (lower == null)
return candidates[upper];
if (upper == null)
return candidates[lower];
//如果两个都为空就选择一个离目标分辨率最近的一个
if (value > (lower + upper) / 2)
return candidates[upper];
else
return candidates[lower];
}
这个方法首先通过_parseScale函数获取每一个候选图片路径对应的分辨率(这个方法比较简单,这里就不做讲解了,有兴趣的自行自行阅读),并存入到map中,后面会根据_findNearest函数和手机的分辨率得到最相近的图片路径,作为返回值。比如现在有两种2.0、3.0分辨率的图片,而手机的分辨率为1.6,这时系统会选择2.0的图片作为最终加载的图片资源。最后obtainKey函数会将得到的图片的路径、分辨率和AssetBundle封装成一个Key放回。整个obtainKey函数就分析完了。接着分析AssetImage的load函数:
@override
ImageStreamCompleter load(AssetBundleImageKey key) {
return new MultiFrameImageStreamCompleter(
codec: _loadAsync(key),
scale: key.scale,
informationCollector: (StringBuffer information) {
information.writeln('Image provider: $this');
information.write('Image key: $key');
}
);
}
这个函数创建MultiFrameImageStreamCompleter(ImageStreamCompleter的子类)对,并调用_loadAsync函数,设置codec成员变量:
@protected
Future<ui.Codec> _loadAsync(AssetBundleImageKey key) async {
final ByteData data = await key.bundle.load(key.name);
return await ui.instantiateImageCodec(data.buffer.asUint8List());
}
key.bundle就是上面介绍的AssetBundle类型的对象,load方法上面也介绍过,它通过platform读取android assets目录下的文件,在相当于读取目标图片的文件内容,接着调用ui.instantiateImageCodec获取Future<Codec>对象。最后调用ImageStreamCompleter的MultiFrameImageStreamCompleter构造函数:
MultiFrameImageStreamCompleter({@required Future<ui.Codec> codec,@required double scale, InformationCollector informationCollector}) {
...
codec.then<void>(_handleCodecReady, onError: (...)
}
void _handleCodecReady(ui.Codec codec) {
_codec = codec;
_decodeNextFrameAndSchedule();
}
Future<Null> _decodeNextFrameAndSchedule() async {
try {
_nextFrame = await _codec.getNextFrame();
} catch (exception, stack) {
...
}
if (_codec.frameCount == 1) {
_emitFrame(new ImageInfo(image: _nextFrame.image, scale: _scale));
return;
}
...
}
void _emitFrame(ImageInfo imageInfo) {
setImage(imageInfo);
_framesEmitted += 1;
}
通过codec.getNextFrame()获取下一帧图像,对于静态的图来说片frameCount始终是1,接着调用ImageInfo的构造函数组装image,并_emitFrame方法,这个方法里会调用setImage:
void setImage(ImageInfo image) {
_currentImage = image;
if (_listeners.isEmpty)
return;
final List<ImageListener> localListeners = _listeners.map<ImageListener>(
(_ImageListenerPair listenerPair) => listenerPair.listener
).toList();
for (ImageListener listener in localListeners) {
try {
listener(image, false);
} catch (exception, stack) {
...
}
}
}
这个方法会遍历监听图片完成的监听器,通知它们图片已经加载成功,可以刷新ui了。还记得在_ImageState调用ImageStream的addListener设置的回调吗?就是在这里被调用的,并将ImageInfo传入,后flutter会调用_ImageState的build方法,将图片渲染到界面上。到此整个图片加载过程就讲解完毕。
总结一下上面的过程:
1、Image继承于StatefulWidget,对应的_ImageState,它会在初始化和状态变化时调用AssetImage的resolve函数获取和解析图片,并且返回一个ImageStream对象给_ImageState,同时_ImageState也注册一个监听器给ImageStream,当图片加载完成会执行这个回调方法,更新ui。
2、在AssetImage的resolve函数中会调用obtainKey函数,在这个函数中会读取assets目录下AssetManifest.json文件,并根据手机的分辨率选择最合适的图片,接着将结果封装成 Future<AssetBundleImageKey>作为ImageCache的key。
3、AssetImage的load方法是真正加载图片的地方,创建MultiFrameImageStreamCompleter对象,并调用_loadAsync去加载assets目录下的图片。当图片加载完成就调用UI的回调方法,这里对应这_ImageState的_handleImageChanged方法,更新state的状态,重绘图片。
三、资源打包源码分析
在flutter中生成产物的入口为bundle.dart中bundle函数,这里只分析和图片资源相关函数,其余的有兴趣可以自行阅读:
final AssetBundle assets = await buildAssets(
manifestPath: manifestPath,
assetDirPath: assetDirPath,
packagesPath: packagesPath,
reportLicensedPackages: reportLicensedPackages,
);
if (assets == null)
throwToolExit('Error building assets', exitCode: 1);
await assemble(
assetBundle: assets,
kernelContent: kernelContent,
snapshotFile: snapshotFile,
privateKeyPath: privateKeyPath,
assetDirPath: assetDirPath,
buildSnapshot: buildSnapshot,
);
参数解析如下:
manifestPath:pubspec.yaml的路径
assetDirPath:资源生成的路径,例如在android debug模式下对应的目录为/build/intermediates/flutter/debug/flutter_assets
packagesPath:包依赖文件路径(也就是.packages文件的路径),在flutter中会根据pubspec.yaml的配置的包依赖信息生成.packages文件,来声明依赖包的文件路径。其中文件的格式为:包名:本地文件路径。默认拉取的离线包都存在根目录下的.pub-cache文件夹下。
reportLicensedPackages:是否报告包许可证,默认为false
该函数首先调用buildAssets获得AssetBundle(flutter资源管理类)对象,后调用assemble函数将AssetBundle中的内容写入各个文件,下面挨个分析这两个函数。
首先是buildAssets函数:
Future<AssetBundle> buildAssets({String manifestPath,String assetDirPath,String packagesPath,bool includeDefaultFonts = true,bool reportLicensedPackages = false}) async {
assetDirPath ??= getAssetBuildDirectory();
packagesPath ??= fs.path.absolute(PackageMap.globalPackagesPath);
final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
final int result = await assetBundle.build(
manifestPath: manifestPath,
assetDirPath: assetDirPath,
packagesPath: packagesPath,
includeDefaultFonts: includeDefaultFonts,
reportLicensedPackages: reportLicensedPackages
);
return assetBundle;
}
这个函数首先判断assetDirPath和packagesPath是否为null,如果为null就设置为默认的路径,接着调用AssetBundleFactory的createBundle函数创建AssetBundle的实例,这里用到了工厂模式,通过代码发现,AssetBundleFactory.instance实例的createBundle函数会创建_ManifestAssetBundle实例,接着调用_ManifestAssetBundle的build方法来创建资源的配置信息。
_ManifestAssetBundle的build方法:
@override
Future<int> build({String manifestPath = defaultManifestPath,String assetDirPath,String packagesPath,bool includeDefaultFonts = true,bool reportLicensedPackages = false
}) async {
...
FlutterManifest flutterManifest;
try {
flutterManifest = await FlutterManifest.createFromPath(manifestPath);
} catch (e) {
...
}
if (flutterManifest == null)
return 1;
if (flutterManifest.isEmpty) {
entries[_assetManifestJson] = new DevFSStringContent('{}');
return 0;
}
final String assetBasePath = fs.path.dirname(fs.path.absolute(manifestPath));
_lastBuildTimestamp = new DateTime.now();
final PackageMap packageMap = new PackageMap(packagesPath);
final Map<_Asset, List<_Asset>> assetVariants = _parseAssets(
packageMap,
flutterManifest,
assetBasePath,
excludeDirs: <String>[assetDirPath, getBuildDirectory()]
);
...
}
该函数首先调用createFromPath函数获得FlutterManifest对象,FlutterManifest类型是pubspec.yaml在内存中的表现形式,它读取pubspec.yaml文件的内容,并将文件的各个资源名解析到对应的各个变量中。如果pubspec.yaml没有配置任何依赖就将entries中key为AssetManifest.json的值设置为"{}"。假设我们配置了图片信息,这里就不为空。接着,利用packagesPath参数创建PackageMap实例,PackageMap是.packages文件在内存中的表现形式,它读取和解析.packages文件的内容,并作为参数传递给_parseAssets函数,_parseAssets函数用于解析pubspec.yaml中assets的每一项资源,并封装成Map<_Asset, List<_Asset>>返回,其中key为assets中的资源项,value具有相同名字的资源列表。
_parseAssets函数:
Map<_Asset, List<_Asset>> _parseAssets(
PackageMap packageMap,
FlutterManifest flutterManifest,
String assetBase, {
List<String> excludeDirs = const <String>[],
String packageName
}) {
final Map<_Asset, List<_Asset>> result = <_Asset, List<_Asset>>{};
final _AssetDirectoryCache cache = new _AssetDirectoryCache(excludeDirs);
for (Uri assetUri in flutterManifest.assets) {
if (assetUri.toString().endsWith('/')) {
_parseAssetsFromFolder(packageMap, flutterManifest, assetBase,
cache, result, assetUri,
excludeDirs: excludeDirs, packageName: packageName);
} else {
_parseAssetFromFile(packageMap, flutterManifest, assetBase,
cache, result, assetUri,
excludeDirs: excludeDirs, packageName: packageName);
}
}
...
return result;
}
该函数比较简单,遍历pubspec.yaml中assets的资源项,并根据资源是否为目录,分别调用_parseAssetsFromFolder和_parseAssetFromFile这两个方法,首先看_parseAssetFromFile方法:
void _parseAssetFromFile(PackageMap packageMap,FlutterManifest flutterManifest,
String assetBase,_AssetDirectoryCache cache,Map<_Asset, List<_Asset>> result,Uri assetUri, {
List<String> excludeDirs = const <String>[],
String packageName
}) {
final _Asset asset = _resolveAsset(
packageMap,
assetBase,
assetUri,
packageName,
);
final List<_Asset> variants = <_Asset>[];
for (String path in cache.variantsFor(asset.assetFile.path)) {
final String relativePath = fs.path.relative(path, from: asset.baseDir);
final Uri relativeUri = fs.path.toUri(relativePath);
final Uri entryUri = asset.symbolicPrefixUri == null
? relativeUri
: asset.symbolicPrefixUri.resolveUri(relativeUri);
variants.add(
new _Asset(
baseDir: asset.baseDir,
entryUri: entryUri,
relativeUri: relativeUri,
)
);
}
result[asset] = variants;
}
_parseAssetFromFile函数首先调用_resolveAsset获得资源的_Asset对象,_Asset是存储资源信息的实体类,它有三个成员变量,baseDir:资源所在的目录,relativeUri:相对目录,entryUri:在pubspec.yaml文件中资源名。接着调用variantsFor函数查找资源的变种列表资源,最后遍历第二步得到列表,根据每个资源的路径得到_Asset实例并加入到列表中。下面我们看一下variantsFor函数:
List<String> variantsFor(String assetPath) {
//获得资源的文件名
final String assetName = fs.path.basename(assetPath);
//获得资源的目录
final String directory = fs.path.dirname(assetPath);
if (!fs.directory(directory).existsSync())
return const <String>[];
//这里假设是第一次查找,_cache中自然是没有对应的缓存,所以判断为true
if (_cache[directory] == null) {
final List<String> paths = <String>[];
//遍历directory下的二级目录的文件路径,并将其加入到paths中
for (FileSystemEntity entity in fs.directory(directory).listSync(recursive: true)) {
final String path = entity.path;
if (fs.isFileSync(path) && !_excluded.any((String exclude) => path.startsWith(exclude)))
paths.add(path);
}
//从对应的文件列表中找出了相同文件的文件路径,并将其存入到variants中,variants是map类型的变量,其中key为文件名,value为拥有相同文件名的文件路径列表
final Map<String, List<String>> variants = <String, List<String>>{};
for (String path in paths) {
final String variantName = fs.path.basename(path);
if (directory == fs.path.dirname(path))
continue;
variants[variantName] ??= <String>[];
variants[variantName].add(path);
}
_cache[directory] = variants;
}
//返回assetName对应的文件路径的列表
return _cache[directory][assetName] ?? const <String>[];
}
}
通过上面的分析,可知上面的函数的作用就是在assetPath对应目录的二级目录中找到和assetPath相同文件名的文件,并分别加入到列表中返回。比如assets的路径如下:
assets/foo
assets/xxx/foo
assets/xxx/foo
assets/bar
那么variantsFor('assets/foo') 返回值为:['/assets/xxx/foo', '/assets/xxx/foo'],整个_parseAssetFromFile就解析完了,整个函数的功能就是根据assets中的资源项,找到其对应目录的二级目录中和其相同文件名的文件路径,加载到result变量中。下面接着看_parseAssetsFromFolder函数:
void _parseAssetsFromFolder(PackageMap packageMap,FlutterManifest flutterManifest,String assetBase,_AssetDirectoryCache cache,Map<_Asset, List<_Asset>> result,Uri assetUri, {List<String> excludeDirs = const <String>[],String packageName}) {
final String directoryPath = fs.path.join(
assetBase, assetUri.toFilePath(windows: platform.isWindows));
if (!fs.directory(directoryPath).existsSync()) {
printError('Error: unable to find directory entry in pubspec.yaml: $directoryPath');
return;
}
final List<FileSystemEntity> lister = fs.directory(directoryPath).listSync();
for (FileSystemEntity entity in lister) {
if (entity is File) {
final String relativePath = fs.path.relative(entity.path, from: assetBase);
final Uri uri = new Uri.file(relativePath, windows: platform.isWindows);
_parseAssetFromFile(packageMap, flutterManifest, assetBase, cache, result,
uri, packageName: packageName);
}
}
}
_parseAssetsFromFolder这个函数首先判断assets中配置的资源对应的目录是否存在,如果不存在直接抛出异常,这里为false,接着会遍历这个目录中的所有文件并调用_parseAssetFromFile得到以文件路径为key的列表信息。这个就是我们在第一部分中提到的在pubspec.yaml中可以配置目录资源对应的源码。但是需要注意的是在pubspec.yaml中只支持配置单级目录,对于二级目录却是无能为力。原因是上述的代码中只识别了文件,对于目录没有做任何处理。到这里整个_parseAssets函数的功能就介绍完了。接着回到build函数继续往下看:
if (assetVariants == null)
return 1;
...
for (String packageName in packageMap.map.keys) {
final Uri package = packageMap.map[packageName];
if (package != null && package.scheme == 'file') {
final String packageManifestPath = fs.path.fromUri(package.resolve('../pubspec.yaml'));
final FlutterManifest packageFlutterManifest = await FlutterManifest.createFromPath(packageManifestPath);
if (packageFlutterManifest == null)
continue;
if (packageFlutterManifest.appName == flutterManifest.appName)
continue;
final String packageBasePath = fs.path.dirname(packageManifestPath);
final Map<_Asset, List<_Asset>> packageAssets = _parseAssets(
packageMap,
packageFlutterManifest,
packageBasePath,
packageName: packageName,
);
if (packageAssets == null)
return 1;
assetVariants.addAll(packageAssets);
}
}
for (_Asset asset in assetVariants.keys) {
if (!asset.assetFileExists && assetVariants[asset].isEmpty) {
...
}
if (asset.assetFileExists) {
assert(!assetVariants[asset].contains(asset));
assetVariants[asset].insert(0, asset);
}
for (_Asset variant in assetVariants[asset]) {
assert(variant.assetFileExists);
entries[variant.entryUri.path] = new DevFSFileContent(variant.assetFile);
}
}
...
entries[_assetManifestJson] = _createAssetManifest(assetVariants);
...
return 0;
首先它会遍历.packages文件的依赖列表,找到每个包下的pubspec.yaml文件,并调用_parseAssets函数对依赖包的资源进行解析(需要注意的是在遍历依赖列表时首先会判断依赖包的路径是够以file开头,由于本地依赖的package路径是相对路径,并不是以file开头,所以应用在引用本地package时不能加载其资源文件)并将解析的资源加入到列表中,接着,遍历assetVariants列表,如果assets中的配置想的文件存在就将其插入列表的第一个位置(通过这里的代码我们可知,在assets配置资源时可以配置一个不存在的文件作为资源项的key),接着遍历特定资源项的变体列表,以资源路径为key,以DevFSFileContent为value加入到entries列表中,其中DevFSFileContent的参数是图片文件,DevFSFileContent继承于DevFSContent,用于读取文件内容。最后调用_createAssetManifest函数生成AssetManifest.json文件对应的DevFSContent对象:
DevFSContent _createAssetManifest(Map<_Asset, List<_Asset>> assetVariants) {
final Map<String, List<String>> jsonObject = <String, List<String>>{};
final List<_Asset> sortedKeys = assetVariants
.keys.toList()
..sort(_byBasename);
for (_Asset main in sortedKeys) {
final List<String> variants = <String>[];
for (_Asset variant in assetVariants[main])
variants.add(variant.entryUri.path);
jsonObject[main.entryUri.path] = variants;
}
return new DevFSStringContent(json.encode(jsonObject));
}
这个函数的主要功能是遍历assetVariants列表,并以资源为key,以图片列表为value形成一个json结构体,并将其封装成DevFSStringContent实例返回。整个buildAssets函数功能到此就结束了,接着回到bundle.dart的build函数看assemble函数,在这个函数中会调用writeBundle函数,将上面生成的entrys列表,写入到bundleDir目录中:
Future<void> writeBundle(
Directory bundleDir, Map<String, DevFSContent> assetEntries) async {
if (bundleDir.existsSync())
bundleDir.deleteSync(recursive: true);
bundleDir.createSync(recursive: true);
await Future.wait(
assetEntries.entries.map((MapEntry<String, DevFSContent> entry) async {
final File file = fs.file(fs.path.join(bundleDir.path, entry.key));
file.parent.createSync(recursive: true);
await file.writeAsBytes(await entry.value.contentsAsBytes());
}));
这个函数函数首先会判断文件输入目录bundleDir是否存在,如果不存在先建立该目录,接着,遍历assetEntries列表,将DevFSContent的contentsAsBytes()内容写入到对应的文件中。根据上面的资源介绍,我们知道这里写入的文件分别是各个分辨率对应的图片和AssetManifest.json文件。到此flutter资源加载和打包流程就分析完了。
网友评论