美文网首页flutter探索
flutter图片加载和打包源码分析

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

作者: 五月_f6d4 | 来源:发表于2018-09-25 12:02 被阅读0次

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

    相关文章

      网友评论

        本文标题:flutter图片加载和打包源码分析

        本文链接:https://www.haomeiwen.com/subject/rlokoftx.html