本篇文章你將得到?html
一、Flutter 圖片加載方式java
二、Flutter 圖片加載源碼實現流程android
三、Flutter 圖片加載優化點有什麼objective-c
在 Flutter 中 Image 是展現圖片的 widget ,用於從 ImageProvider 獲取圖像。Image 支持的圖片格式有 JPEG、WebP、GIF、animated WebP/GIF 、PNG 、BMP、 and WBMP。json
Image 結構以下:swift
能夠看到圖片上部有多個加載方式。緩存
兩種方式以下:markdown
Image(height: 100, width: 100, image: AssetImage(happy.png), )
複製代碼
Image.asset( happy.png, width: 100, height: 100,)
複製代碼
固然這一方式,須要在 pubspec.yaml 文件中配置圖片路徑。網絡
Image.network('https://p0.ssl.qhimg.com/t0183421f63f84fccaf.gif',fit: BoxFit.fill);
複製代碼
Image.file(File('/sdcard/happy.png')),
複製代碼
new Image.memory(Uint8List bytes),
複製代碼
bytes
指內存中的圖片數據,將其轉化爲圖片對象。數據結構
Flutter 中 Unit8List 與其餘語言數據結構類比:
flutter | java | swift | C |
---|---|---|---|
Uint8List | byte[] | FlutterStandardTypedData | char[] |
new CachedNetworkImage(
fit:BoxFit.fill,
width:200,
height:100,
imageUrl:'https://p0.ssl.qhimg.com/t0183421f63f84fccaf.gif',
placeholder:(context, url) => new ProgressView(),
errorWidget:(context, url, error) => new Icon(Icons.error),
);
複製代碼
import 'package:transparent_image/transparent_image.dart';
FadeInImage.memoryNetwork(
placeholder: kTransparentImage, //kTransparentImage 屬於 transparent_image 庫
image: 'https://p0.ssl.qhimg.com/t0183421f63f84fccaf.gif',
);
複製代碼
new Icon(Icons.android,size: 200,);
複製代碼
Flutter 能夠爲當前設備加載適合其分辨率的圖像。指定不一樣素設備像比例的圖片能夠這樣分配asset文件夾:
主資源默認對應於 1.0 倍的分辨率圖片;在設備像素比率爲 1.8 的設備上會選用 .../2.0x/happy.png
;對於在像素比率 2.7 的設備上 ,會選用 .../3.0x/happy.png
。
pubspec.yaml
中 asset 聲明中每一項都標識與實際文件對應。可是主資源缺乏時,會按分辨率從低到高的順序尋找加載。這裏的加載方案,能夠參考 Android 系統中圖片加載的邏輯做對比。
Flutter 打包應用時,資源會按照 key-value 的形式存入 apk 的 assets/flutter_assets/AssetManifest.json 文件中,加載資源時先解析 json 文件,選擇最適合的圖片進行加載顯示,其中 AssetManifest.json 的具體內容簡介如:
{
"assets/happy.png":[
"assets/2.0x/happy.png",
"assets/3.0x/happy.png"
]
}
複製代碼
android 上能夠經過 AssetManager 獲取 asset, 根據 key 查找到 openFd 。
key 是由 PluginRegistry.Registrar 的 lookupKeyForAsset 與 FlutterView 的 getLookupKeyForAsset 獲得;
PluginRegistry.Registrar 用於開發插件,而 FlutterView 則用於開發平臺 app 的 view。
flutter:
assets:
- icons/happy.png
複製代碼
AssetManager assetManager = registrar.context().getAssets();
String key = registrar.lookupKeyForAsset("icons/happy.png");
AssetFileDescriptor fd = assetManager.openFd(key);
複製代碼
iOS 開發使用 mainbundle 獲取 assets。
使用 FlutterPluginRegistrar 的 lookupKeyForAsset 和 lookupKeyForAsset:fromPackage: 方法獲取文件路徑 ;FlutterViewController 的 lookupKeyForAsset 和lookupKeyForAsset:fromPackage: 方法獲取文件路徑 ;
而後 FlutterPluginRegistrar 用於開發插件,而 FlutterViewController 則用於開發平臺 app 的 view 。
NSString* key = [registrar lookupKeyForAsset:@"icons/happy.png"];
NSString* path = [[NSBundle mainBundle] pathForResource:key ofType:nil];
複製代碼
固然 pubspec.yaml 配置都是一致的。
圖片加載方式中有四種方式,接下來咱們一塊兒看看 framework 層加載圖片是如何實現的。咱們就以 Image.network 爲例,跟進一下相關源碼實現。
Image.network 的方法以下:
Image.network(
String src, {
Key key,
double scale = 1.0,
this.frameBuilder,
this.loadingBuilder,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width,
this.height,
this.color,
this.colorBlendMode,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.centerSlice,
this.matchTextDirection = false,
this.gaplessPlayback = false,
this.filterQuality = FilterQuality.low,
Map<String, String> headers,
}) : image = NetworkImage(src, scale: scale, headers: headers),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null),
super(key: key);
複製代碼
這方法的做用就是建立一個 用於顯示從網絡獲得的 ImageStream 的 image 小部件,加載網絡圖片的 image 是由 NetworkImage 建立出來的,其中參數 src, scale, headers 是不能爲空的,其餘的參數能夠不作要求。NetworkImage 又是繼承自 ImageProvider,因此 image 就是 ImageProvider 。ImageProvider 是個抽象類,它的實現類包括:NetworkImage、FileImage、ExactAssetImage、AssetImage、MemoryImage、AssetBundleImageProvider。
Image 源碼部分以下:
class Image extends StatefulWidget {
/// 用於顯示的 image
final ImageProvider image;
..........
@override
_ImageState createState() => _ImageState();
}
複製代碼
_ImageState 類
class _ImageState extends State<Image> with WidgetsBindingObserver {
ImageStream _imageStream;
ImageInfo _imageInfo;
.......
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeDependencies() {
_updateInvertColors();
_resolveImage();//解析圖片從這裏開始
//設置和移除監聽圖片變化的回調
if (TickerMode.of(context))
_listenToStream();
else
_stopListeningToStream();
super.didChangeDependencies();
}
void _resolveImage() {
//根據 ImageConfiguration 調用 ImageProvider 的 resolve 函數得到 ImageStream 對象
final ImageStream newStream = widget.image.resolve(createLocalImageConfiguration(
context,
size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null,
));
_updateSourceStream(newStream);
}
......
}
複製代碼
它的生命週期方法方法包括initState()
、didChangeDependencies()
、build()
、deactivate()
、dispose()
、didUpdateWidget()
等等。當它插入到渲染樹時,先調用initState()
函數,再調用 didChangeDependencies()
。代碼中能夠看到調用了方法 _resolveImage(),這個方法中建立了 ImageStream 的新對象 newStream 。widget.image 就是 ImageProvider,調用resolve方法,代碼以下:
ImageStream resolve(ImageConfiguration configuration) {
final ImageStream stream = ImageStream();
T obtainedKey;
bool didError = false;
Future<void> handleError(dynamic exception, StackTrace stack) async {
if (didError) {
return;
}
didError = true;
await null; // 等待事件輪詢,以防偵聽器被添加到圖像流中。
final _ErrorImageCompleter imageCompleter = _ErrorImageCompleter();
stream.setCompleter(imageCompleter);
......
}
......
Future<T> key;
try {
key = obtainKey(configuration);
} catch (error, stackTrace) {
return;
}
key.then<void>((T key) {
obtainedKey = key;
final ImageStreamCompleter completer =
PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key), onError: handleError);
if (completer != null) {
stream.setCompleter(completer);
}
}).catchError(handleError);
return stream;
複製代碼
ImageStreamCompleter 用於管理 dart:ui 加載的類的基類。ImageStreams 的對象不多直接構造,而是由 ImageStreamCompleter 自動配置它。ImageStream 中的圖片管理者 ImageStreamCompleter 經過方法建立,imageCache 是 Flutter 框架中實現的用於圖片緩存的單例,它這 Dart 虛擬機加載時就已經建立。imageCache 最多可緩存 1000 張圖像和 100MB 內存空間。可使用 [maximumSize] 和 [maximumSizeBytes]調整最大大小。
PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key), onError: handleError);
複製代碼
根據源碼能夠看到兩個關鍵方法 :putIfAbsent 和 load。
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), {ImageErrorListener onError }) {
ImageStreamCompleter result = _pendingImages[key]?.completer;
// 由於圖像尚未加載,不須要作任何事情。
if (result != null)
return result;
// 從緩存列表中根據Key刪除對應的 imageprovider,便於將它移動到下面最近使用位置。
final _CachedImage image = _cache.remove(key);
if (image != null) {
_cache[key] = image;
return image.completer;
}
try {
result = loader();
} catch (error, stackTrace) {
......
}
void listener(ImageInfo info, bool syncCall) {
// 沒法加載的圖像不會佔用緩存大小。
final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
final _CachedImage image = _CachedImage(result, imageSize);
// 若是圖像大於最大緩存大小,且緩存大小不爲零,則將緩存大小增長到圖像大小加上 1000。
// 思考點:一直這麼加何時引發崩潰?
if (maximumSizeBytes > 0 && imageSize > maximumSizeBytes) {
_maximumSizeBytes = imageSize + 1000;
}
_currentSizeBytes += imageSize;
final _PendingImage pendingImage = _pendingImages.remove(key);
if (pendingImage != null) {
pendingImage.removeListener();
}
_cache[key] = image;
_checkCacheSize();
}
if (maximumSize > 0 && maximumSizeBytes > 0) {
final ImageStreamListener streamListener = ImageStreamListener(listener);
_pendingImages[key] = _PendingImage(result, streamListener);
// 移除 [_PendingImage.removeListener] 上的監聽
result.addListener(streamListener);
}
return result;
}
複製代碼
/// 拉取網絡圖片的 image_provider.NetworkImage 具體實現.
class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage> implements image_provider.NetworkImage {
......................
@override
ImageStreamCompleter load(image_provider.NetworkImage key) {
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, chunkEvents),
chunkEvents: chunkEvents.stream,
scale: key.scale,
informationCollector: () {
return <DiagnosticsNode>[
DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
];
},
);
}
複製代碼
Future<ui.Codec> _loadAsync(
NetworkImage key,
StreamController<ImageChunkEvent> chunkEvents,
) async {
try {
final Uri resolved = Uri.base.resolve(key.url);
final HttpClientRequest request = await _httpClient.getUrl(resolved);
headers?.forEach((String name, String value) {
request.headers.add(name, value);
});
final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok)
throw image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);
//將網絡返回的 response 信息,轉換成內存中的 Uint8List bytes。這裏面有解壓 gzip 的邏輯。
final Uint8List bytes = await consolidateHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int total) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: cumulative,
expectedTotalBytes: total,
));
},
);
if (bytes.lengthInBytes == 0)
throw Exception('NetworkImage is an empty file: $resolved');
return PaintingBinding.instance.instantiateImageCodec(bytes);
} finally {
chunkEvents.close();
}
}
複製代碼
將網絡返回的response信息,轉換成內存中的 Uint8List bytes,最終返回一個實例化圖像編解碼器對象Codec,此處 Codec 能夠移步到 painting.dart 文件的 _instantiateImageCodec 看出來它是調用了native方法去處理了。
這個對象就是 ImageStreamCompleter 的具體實現,見名知意,多幀圖片流管理,做用管理圖像幀的解碼和調度。
這個類處理兩種類型的幀:
圖像幀 :動畫圖像的圖像幀。
app 幀 :Flutter 引擎繪製到屏幕的幀,顯示到應用程序 GUI。
這就不貼全部代碼了,在 image_stream.dart 文件中 可見 class MultiFrameImageStreamCompleter。
MultiFrameImageStreamCompleter({
@required Future<ui.Codec> codec,
@required double scale,
Stream<ImageChunkEvent> chunkEvents,
InformationCollector informationCollector,
}) : assert(codec != null),
_informationCollector = informationCollector,
_scale = scale {
codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
..........
});
複製代碼
這裏 codec 異步回調次方法
void _handleCodecReady(ui.Codec codec) {
_codec = codec;
if (hasListeners) {
_decodeNextFrameAndSchedule();
}
}
複製代碼
codec 解碼獲取到圖片的幀數,判斷圖片是隻有一幀的話,就是png、jpg這樣靜態圖片。
Future<void> _decodeNextFrameAndSchedule() async {
try {
_nextFrame = await _codec.getNextFrame();
} catch (exception, stack) {
........
return;
}
if (_codec.frameCount == 1) { // 此處判斷圖片是隻有一幀的邏輯.
_emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
return;
}
_scheduleAppFrame();
}
void _scheduleAppFrame() {
if (_frameCallbackScheduled) {
return;
}
_frameCallbackScheduled = true;
SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame);
}
複製代碼
_emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
void _emitFrame(ImageInfo imageInfo) {
setImage(imageInfo);
_framesEmitted += 1;
}
@protected
void setImage(ImageInfo image) {
_currentImage = image;
if (_listeners.isEmpty)
return;
// 複製一份以容許併發修改。
final List<ImageStreamListener> localListeners = List<ImageStreamListener>.from(_listeners);
for (ImageStreamListener listener in localListeners) {
try {
listener.onImage(image, false);
} catch (exception, stack) {
..........
}
}
}
複製代碼
setImage 核心邏輯就是通知全部註冊上的監聽,表示圖片發生了變化能夠更新啦。此時咱們回到 開始提到的_ImageState 類中 didChangeDependencies 方法調用的 _listenToStream 方法,最終調用方法 _handleImageFrame ,改變 圖片信息 _imageInfo 和 圖片幀數變化 _frameNumber ,最終執行 setState(() {}) 來刷新了 UI。
void _listenToStream() {
if (_isListeningToStream)
return;
_imageStream.addListener(_getListener());
_isListeningToStream = true;
}
ImageStreamListener _getListener([ImageLoadingBuilder loadingBuilder]) {
loadingBuilder ??= widget.loadingBuilder;
return ImageStreamListener(
_handleImageFrame,
onChunk: loadingBuilder == null ? null : _handleImageChunk,
);
}
void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
setState(() {
_imageInfo = imageInfo;
_loadingProgress = null;
_frameNumber = _frameNumber == null ? 0 : _frameNumber + 1;
_wasSynchronouslyLoaded |= synchronousCall;
});
}
複製代碼
這樣就結束了一個網絡圖片的加載過程。
此處應該有流程圖就更加簡潔明瞭的表達啦。
圖片加載顯示的方式 framework 提供了多種方式,咱們就圖片網絡加載進行了分析。從源碼角度對網絡圖片加載過程有了大體的瞭解。發現的能夠優化點,這裏先提出來優化的點:
一、看到網絡圖片只是在 ImageCache 管理類中進行了內存緩存,當應用進程從新啓動後仍是要從新下載圖片,此處是能夠優化的,好比保存到本地磁盤外存。
二、拿到圖片加載到內存裏面的時候,是否有對圖片進行壓縮處理,這種處理最好既適應當前平臺又不過度地改變圖片的清晰度。
期待下一篇的迭代優化點。