flutter图片加载和打包源码分析

一、初识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资源加载和打包流程就分析完了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容