Flutter系統(tǒng)網(wǎng)絡(luò)圖片加載流程解析
Flutter原生支持在Image組件上顯示網(wǎng)絡(luò)圖片,最簡單的使用方式如下,調(diào)用Image的命名構(gòu)造方法Image.network即可實現(xiàn)網(wǎng)絡(luò)圖片的下載顯示。
Widget image = Image.network(imageUrl);
那么,它內(nèi)部是如何實現(xiàn)的呢?是否有做緩存處理或其他優(yōu)化操作呢?帶著疑問,我們一起來看下它的底層究竟是如何實現(xiàn)的。
一、從構(gòu)造函數(shù)開始
我們以最簡單的調(diào)用方式舉例,當(dāng)我們使用Image.network(imageUrl)這種方式來顯示圖片時,Image組件內(nèi)部image屬性就會被賦值NetworkImage。
// 此為簡化過的Image組件類結(jié)構(gòu)
class Image extends StatefulWidget {
Image.network(
String src,
) : image = NetworkImage(src);
// 圖片數(shù)據(jù)處理的基類
final ImageProvider image;
}這里引出了一個類叫NetworkImage,它是ImageProvider的子類,專門實現(xiàn)網(wǎng)絡(luò)圖片的下載和解析邏輯。當(dāng)然你直接點進(jìn)去看到的其實是個抽象類,并不是真正實現(xiàn)下載邏輯的地方,真正實現(xiàn)網(wǎng)絡(luò)圖片下載解析的在'_network_image_io.dart’這個文件下。構(gòu)造函數(shù)知道這些就夠了。接下來就看Image是在何時觸發(fā)網(wǎng)絡(luò)圖片的下載的。
二、圖片下載入口
Image是一個StatefulWidget,它又一個對應(yīng)的State叫_ImageState。在這個_ImageState的生命周期中,控制著圖片的下載過程。
State的生命周期可以簡單的分為:構(gòu)造函數(shù) → initState → didChangeDependencies → build
因此,我們順著這個順序找,很快看到一個可疑的地方,didChangeDependencies中的_resolveImage方法。而TickerMode則是用于控制動畫的,在這里被用于判斷是否禁用了動畫。關(guān)于TickerMode的相關(guān)介紹,可以看下這篇文章
// 完整源碼
@override
void didChangeDependencies() {
_updateInvertColors();
// 處理圖片的入口
_resolveImage();
// 當(dāng)動畫被禁用時,圖片也是無法顯示的,這個
if (TickerMode.of(context))
// 添加圖片流處理的監(jiān)聽
_listenToStream();
else
_stopListeningToStream(keepStreamAlive: true);
super.didChangeDependencies();
}我們進(jìn)入到_resolveImage方法中去。
void _resolveImage() {
// ScrollAwareImageProvider包裝了我們的NetworkImage
final ScrollAwareImageProvider provider = ScrollAwareImageProvider<Object>(
context: _scrollAwareContext,
imageProvider: widget.image,
);
// 新建圖片流
final ImageStream newStream =
provider.resolve(createLocalImageConfiguration(
context,
size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null,
));
assert(newStream != null);
// 更新圖片流
_updateSourceStream(newStream);
}_resolveImage方法就做了三件事。
1、用ScrollAwareImageProvider包裝了NetworkImage
2、創(chuàng)建圖片流對象ImageStream
3、更新圖片流
2.1、ScrollAwareImageProvider
ScrollAwareImageProvider也是ImageProvider的子類,它的作用很簡單,就是防止在快速滑動的時候加載圖片,當(dāng)存在快速滑動時,會將解析圖片的工作放到下一幀處理。至于具體如何實現(xiàn),我們放在后面再提。
2.2、ImageConfiguration
ImageConfiguration由方法createLocalImageConfiguration創(chuàng)建,保存了圖片的基本配置信息,如Bundle,屏幕項目比devicePixelRatio,本地化local,尺寸size,平臺platform等。
2.3、ImageStream
表示一個圖片流,可以添加觀察者ImageStreamCompleter來監(jiān)聽圖片是否處理完成。一個圖片流可以添加多個觀察者。
ImageStream由provider的resolve方法調(diào)用后創(chuàng)建。通過源碼可知,此處的provider就是ScrollAwareImageProvider對象。但是它內(nèi)部并沒有實現(xiàn)resolve方法,因此此處調(diào)用的是父類ImageProvider的resolve方法。
三、圖片流和Key
以下代碼截取自ImageProvider,并且刪減了無關(guān)代碼。
ImageStream resolve(ImageConfiguration configuration) {
// 創(chuàng)建流,這里直接調(diào)用了ImageStream的構(gòu)造函數(shù),并沒有用到configuration
final ImageStream stream = createStream(configuration);
// 關(guān)鍵在這里,這里會根據(jù)configuration創(chuàng)建一個唯一key
_createErrorHandlerAndKey(
configuration,
// 成功的回調(diào)
(T key, ImageErrorListener errorHandler) {
resolveStreamForKey(configuration, stream, key, errorHandler);
},
// 下面是錯誤回調(diào),可以不關(guān)注
(T? key, Object exception, StackTrace? stack) async {
await null; // wait an event turn in case a listener has been added to the image stream.
InformationCollector? collector;
if (stream.completer == null) {
stream.setCompleter(_ErrorImageCompleter());
}
stream.completer!.reportError(
exception: exception,
stack: stack,
context: ErrorDescription('while resolving an image'),
silent: true, // could be a network error or whatnot
informationCollector: collector,
);
},
);
return stream;
}resolve方法的作用是創(chuàng)建圖片流對象ImageStream,并根據(jù)傳入的圖片配置信息configuration,創(chuàng)建對應(yīng)的Key,這個Key用于圖片緩存。
那么這個key到底是怎么創(chuàng)建的呢,我們進(jìn)入到_createErrorHandlerAndKey方法中查看。關(guān)鍵代碼如下,已刪除無關(guān)代碼。
Future<T> key;
try {
key = obtainKey(configuration);
} catch (error, stackTrace) {
handleError(error, stackTrace);
return;
}
key.then<void>((T key) {
obtainedKey = key;
try {
successCallback(key, handleError);
} catch (error, stackTrace) {
handleError(error, stackTrace);
}
}).catchError(handleError);可以看到方法實現(xiàn)中調(diào)用了ImageProvider的obtainKey方法,而這個方法在ImageProvider并沒有具體實現(xiàn),需要子類完成對應(yīng)的實現(xiàn)。
Future<T> obtainKey(ImageConfiguration configuration);
還記得上文的分析不,我們說傳入的imageProvider實例是ScrollAwareImageProvider對象,因此對應(yīng)的實現(xiàn)也要到這個類中去查找。很快,我們找到obtainKey方法的實現(xiàn),可以看到它做了個透傳,具體是由它包裝的類也就是NetworkImage來實現(xiàn)的。
@override Future<T> obtainKey(ImageConfiguration configuration) => imageProvider.obtainKey(configuration);
那么,我們就去NetworkImage找obtainKey。
注意下真正的NetworkImage實現(xiàn)是在_network_image_io.dart文件下的。

Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
return SynchronousFuture<NetworkImage>(this);
}到這,我們就知道了NetworkImage的key為SynchronousFuture。
獲取到key后的下一步就是調(diào)用_createErrorHandlerAndKey方法的successCallback回調(diào)。從而觸發(fā)了下一個流程resolveStreamForKey。
_createErrorHandlerAndKey(
configuration,
(T key, ImageErrorListener errorHandler) {
// 拿到Key之后的回調(diào)
resolveStreamForKey(configuration, stream, key, errorHandler);
}
)四、根據(jù)key來處理圖片流
還是回到子類ScrollAwareImageProvider中,它重寫了父類的resolveStreamForKey方法,前文提到,ScrollAwareImageProvider是用來防止列表在快速滑動的時候來加載圖片的,那么它是如何實現(xiàn)的?我們就從resolveStreamForKey這個方法中來一探究竟。
// 以下代碼已去掉無關(guān)邏輯
@override
void resolveStreamForKey(
ImageConfiguration configuration,
ImageStream stream,
T key,
ImageErrorListener handleError,
) {
// 滑動過快
if (Scrollable.recommendDeferredLoadingForContext(context.context!)) {
SchedulerBinding.instance!.scheduleFrameCallback((_) {
// 放入下一幀再嘗試處理,如果下一幀還是過快,那么將一直被推遲
scheduleMicrotask(() => resolveStreamForKey(configuration, stream, key, handleError));
});
return;
}
// 當(dāng)前可以加載,那么透傳給包裝的imageProvider來處理,這里是NetworkImage
imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
}Scrollable用于滑動組件,它有個方法叫recommendDeferredLoadingForContext,表示是否建議延遲加載。內(nèi)部最終是根據(jù)滑動速度和當(dāng)前設(shè)備的最大物理尺寸的邊去比較,如果大于,表示速度過快,那么就建議延遲。具體邏輯在scroll_physics.dart文件下。這里不多做介紹。
一旦當(dāng)前應(yīng)用處于滑動狀態(tài),并且速度過快,那么,圖片的加載將會被推遲到下一幀再進(jìn)行嘗試。因此我們說,當(dāng)處于快速滑動時,圖片是無法加載的。
當(dāng)判斷可以加載圖片時,操作流將會被移交給被包裝類imageProvider,這里是NetworkImage來處理。但是,NetworkImage沒有實現(xiàn)resolveStreamForKey方法,因此最終還是跑到了ImageProvider類中的resolveStreamForKey方法下。
@protected
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
// 第一次進(jìn)來還沒有設(shè)置completer,因此不會進(jìn)入這個分支中
if (stream.completer != null) {
final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
key,
() => stream.completer!,
onError: handleError,
);
assert(identical(completer, stream.completer));
return;
}
final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
key,
() => load(key, PaintingBinding.instance!.instantiateImageCodec),
onError: handleError,
);
if (completer != null) {
stream.setCompleter(completer);
}
}當(dāng)?shù)谝淮渭虞d網(wǎng)絡(luò)圖的時候,會直接走到下面這個邏輯中。這里涉及到一個很重要的類,ImageCache。它是做圖片緩存用的。
final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
key,
() => load(key, PaintingBinding.instance!.instantiateImageCodec),
onError: handleError,
);4.1、ImageCache
圖片緩存類,只做了內(nèi)存緩存。它由PaintingBinding持有,是一個單利。它的內(nèi)部通過三個Map來緩存圖片。
// 加載中的圖片
final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
// 緩存中的圖片
final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
// 正在使用的圖片
final Map<Object, _LiveImage> _liveImages = <Object, _LiveImage>{}從圖片緩存器中獲取圖片的邏輯集中在putIfAbsent方法中。以下代碼已經(jīng)去掉無關(guān)代碼。
ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter Function() loader, { ImageErrorListener? onError }) {
TimelineTask? timelineTask;
TimelineTask? listenerTask;
ImageStreamCompleter? result = _pendingImages[key]?.completer;
// 正在加載,直接返回
if (result != null) {
return result;
}
// 這邊有個小知識,dart中的Map是有順序的,因此利用這點可以實現(xiàn)LRU算法。
// 最近用到了這圖片,因此刪除對應(yīng)鍵值對,并更新,就能讓它的位置處于前面
final _CachedImage? image = _cache.remove(key);
if (image != null) {
// 更新 _liveImages
_trackLiveImage(
key,
image.completer,
image.sizeBytes,
);
_cache[key] = image;
return image.completer;
}
final _LiveImage? liveImage = _liveImages[key];
if (liveImage != null) {
// 更新 _cache,這里還會根據(jù)最大緩存數(shù)量和大小來最限制
_touch(
key,
_CachedImage(
liveImage.completer,
sizeBytes: liveImage.sizeBytes,
),
timelineTask,
);
return liveImage.completer;
}
// 加載流程,這是個回調(diào),由各ImageProvider子類來實現(xiàn)
try {
result = loader();
// 下載完更新 _liveImages
_trackLiveImage(key, result, null);
} catch (error, stackTrace) {
if (onError != null) {
onError(error, stackTrace);
return null;
} else {
rethrow;
}
}
bool listenedOnce = false;
_PendingImage? untrackedPendingImage;
// 設(shè)置圖片加載監(jiān)聽,一旦加載完畢,那么會刪除_pendingImages下對應(yīng)的圖片,并移除監(jiān)聽。同時更新_cache和_liveImages
void listener(ImageInfo? info, bool syncCall) {
int? sizeBytes;
if (info != null) {
sizeBytes = info.sizeBytes;
info.dispose();
}
final _CachedImage image = _CachedImage(
result!,
sizeBytes: sizeBytes,
);
_trackLiveImage(key, result, sizeBytes);
if (untrackedPendingImage == null) {
_touch(key, image, listenerTask);
} else {
image.dispose();
}
final _PendingImage? pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
if (pendingImage != null) {
pendingImage.removeListener();
}
listenedOnce = true;
}
// 設(shè)置加載監(jiān)聽,主要用來管理_pendingImages
final ImageStreamListener streamListener = ImageStreamListener(listener);
if (maximumSize > 0 && maximumSizeBytes > 0) {
_pendingImages[key] = _PendingImage(result, streamListener);
} else {
untrackedPendingImage = _PendingImage(result, streamListener);
}
result.addListener(streamListener);
return result;
}4.2、 load
一旦在ImageCache中找不到緩存的圖片,就會通過loader回調(diào)出來,走真正的下載流程。
final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
key,
// 本地圖片找不到,需要去對應(yīng)的ImageProvider子類里實現(xiàn)加載邏輯
() => load(key, PaintingBinding.instance!.instantiateImageCodec),
onError: handleError,
);還是先看ScrollAwareImageProvider類,里面實現(xiàn)了load方法,并透傳給了NetworkImage來實現(xiàn)。
@override ImageStreamCompleter load(T key, DecoderCallback decode) => imageProvider.load(key, decode);
在NetworkImage下,可以找到對應(yīng)的load方法實現(xiàn)。里面有個_loadAsync方法,它就是我們要找的圖片下載核心代碼。
@override
ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
// 多幀圖片流管理器
return MultiFrameImageStreamCompleter(
// 核心異步加載邏輯
codec: _loadAsync(key as NetworkImage, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: key.scale,
debugLabel: key.url,
informationCollector: () => <DiagnosticsNode>[
DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
],
);
}五、圖片下載
饒了一大圈,終于來到了下載圖片的地方了??梢钥吹较螺d圖片的邏輯很簡單,創(chuàng)建一個下載的http請求,設(shè)置header,下載圖片。一旦下載成功,就會通過decode這個回調(diào)將圖片的二進(jìn)制數(shù)據(jù)返回出去decode(bytes)。
Future<ui.Codec> _loadAsync(
NetworkImage key,
StreamController<ImageChunkEvent> chunkEvents,
image_provider.DecoderCallback decode,
) async {
try {
assert(key == this);
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) {
// The network may be only temporarily unavailable, or the file will be
// added on the server later. Avoid having future calls to resolve
// fail to check the network again.
await response.drain<List<int>>(<int>[]);
throw image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);
}
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 decode(bytes);
} catch (e) {
// Depending on where the exception was thrown, the image cache may not
// have had a chance to track the key in the cache at all.
// Schedule a microtask to give the cache a chance to add the key.
scheduleMicrotask(() {
PaintingBinding.instance!.imageCache!.evict(key);
});
rethrow;
} finally {
chunkEvents.close();
}
}而回調(diào)出去的這些二進(jìn)制數(shù)據(jù),是在MultiFrameImageStreamCompleter中被處理的。MultiFrameImageStreamCompleter是ImageStreamCompleter的子類,用于管理多幀圖片的加載。
在MultiFrameImageStreamCompleter的構(gòu)造方法中,我們可以看到它對codec做了回調(diào)處理。而這個codec就是前面提到的_loadAsync異步方法。
MultiFrameImageStreamCompleter({
required Future<ui.Codec> codec,
required double scale,
String? debugLabel,
Stream<ImageChunkEvent>? chunkEvents,
InformationCollector? informationCollector,
}) : assert(codec != null),
_informationCollector = informationCollector,
_scale = scale {
this.debugLabel = debugLabel;
// 這里處理了_loadAsync的回調(diào)
codec.then<void>(_handleCodecRead);_handleCodecRead方法中回判斷是否有觀察者,有就進(jìn)入解碼流程。
void _handleCodecReady(ui.Codec codec) {
_codec = codec;
assert(_codec != null);
if (hasListeners) {
// 存在觀察者,開始解碼
_decodeNextFrameAndSchedule();
}
}_decodeNextFrameAndSchedule方法可以看成是圖片的解碼方法,當(dāng)然實際解碼的地方位于更底層的Native。圖片解碼后會將信息保存在FrameInfo中,由_nextFrame持有。這里我們只考慮單幀圖片,不考慮gif圖。解碼后的信息會被封裝在ImageInfo中,其中image就是真正的圖片數(shù)據(jù)。并調(diào)用_emitFrame方法,更新圖片信息。而_emitFrame方法則主要是調(diào)用了setImage來通知觀察者更新。我們直接看setImage方法即可。
Future<void> _decodeNextFrameAndSchedule() async {
_nextFrame?.image.dispose();
_nextFrame = null;
try {
// 解碼得到一幀圖片信息,保存在FrameInfo中
_nextFrame = await _codec!.getNextFrame();
} catch (exception, stack) {
return;
}
// 當(dāng)幀圖片就將數(shù)據(jù)封裝在ImageInfo中回調(diào)出去
if (_codec!.frameCount == 1) {
if (!hasListeners) {
return;
}
_emitFrame(ImageInfo(
image: _nextFrame!.image.clone(),
scale: _scale,
debugLabel: debugLabel,
));
_nextFrame!.image.dispose();
_nextFrame = null;
return;
}
// 多幀則繼續(xù)往下走
_scheduleAppFrame();
}通過ImageStreamListener通知更新,刷新界面展示。
void setImage(ImageInfo image) {
_checkDisposed();
_currentImage?.dispose();
_currentImage = image;
if (_listeners.isEmpty)
return;
// Make a copy to allow for concurrent modification.
final List<ImageStreamListener> localListeners =
List<ImageStreamListener>.of(_listeners);
for (final ImageStreamListener listener in localListeners) {
try {
// 設(shè)置新圖篇,通知更新界面展示
listener.onImage(image.clone(), false);
} catch (exception, stack) {
reportError(
context: ErrorDescription('by an image listener'),
exception: exception,
stack: stack,
);
}
}
}說到這里,我們好像還沒提到過什么時候設(shè)置的觀察者,好,我們再次回到最初的入口,_ImageState組件的didChangeDependencies方法中。
六、添加觀察者實現(xiàn)界面更新
這個觀察者就是通過_listenToStream方法添加的。
@override
void didChangeDependencies() {
_updateInvertColors();
_resolveImage();
if (TickerMode.of(context))
// 添加觀察者
_listenToStream();
else
_stopListeningToStream(keepStreamAlive: true);
super.didChangeDependencies();
}并且在創(chuàng)建觀察者ImageStreamListener的時候,設(shè)置了onImage的回調(diào)。
// 這里是獲取觀察者的入口
ImageStreamListener _getListener({bool recreateListener = false}) {
if(_imageStreamListener == null || recreateListener) {
_lastException = null;
_lastStack = null;
_imageStreamListener = ImageStreamListener(
// 這個就是onImage的回調(diào)
_handleImageFrame,
onChunk: widget.loadingBuilder == null ? null : _handleImageChunk,
onError: widget.errorBuilder != null || kDebugMode
? (Object error, StackTrace? stackTrace) {
setState(() {
_lastException = error;
_lastStack = stackTrace;
});
}
: null,
);
}
return _imageStreamListener!;
}onImage的入?yún)⒈辉O(shè)置了_handleImageFrame,因此當(dāng)下載完圖片后調(diào)用的就是_handleImageFrame方法。
void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
setState(() {
// 更新圖片信息,實現(xiàn)圖片加載
_replaceImage(info: imageInfo);
_loadingProgress = null;
_lastException = null;
_lastStack = null;
_frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1;
_wasSynchronouslyLoaded = _wasSynchronouslyLoaded | synchronousCall;
});
}
void _replaceImage({required ImageInfo? info}) {
_imageInfo?.dispose();
_imageInfo = info;
}到此,圖片下載和更新的流程已經(jīng)都串起來了。下載完的圖片存放在ImageInfo中,在setState后,會被設(shè)置進(jìn)RawImage組件中實現(xiàn)渲染。
總結(jié)
網(wǎng)絡(luò)圖片的加載邏輯可以分為以下幾個步驟:
1、根據(jù)圖片類型,生成對應(yīng)的key
2、根據(jù)key去全局的ImageCache下查找圖片緩存,命中則直接返回刷新
3、圖片緩存沒有命中,調(diào)用Http去下載圖片
4、下載完圖片后,將圖片的二進(jìn)制數(shù)據(jù)回調(diào)出去觸發(fā)界面刷新,同時會做內(nèi)存緩存
5、在RawImage中顯示網(wǎng)絡(luò)圖片
到此這篇關(guān)于Flutter系統(tǒng)網(wǎng)絡(luò)圖片加載過程解析的文章就介紹到這了,更多相關(guān)Flutter圖片加載流程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android?`%d`?與?`1$%d`?格式化的區(qū)別解析
本文詳細(xì)解析了Android開發(fā)中`%d`和`1$%d`格式化占位符的區(qū)別,并通過Kotlin代碼示例幫助理解,`%d`按順序填充參數(shù),而`1$%d`按指定索引填充參數(shù),后者在多語言場景下更靈活,感興趣的朋友一起看看吧2025-03-03
RecyclerView優(yōu)雅實現(xiàn)復(fù)雜列表布局
這篇文章主要為大家詳細(xì)介紹了RecyclerView優(yōu)雅實現(xiàn)復(fù)雜列表布局,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-11-11
Android Studio實現(xiàn)簡易進(jìn)制轉(zhuǎn)換計算器
這篇文章主要為大家詳細(xì)介紹了Android Studio實現(xiàn)簡易進(jìn)制轉(zhuǎn)換計算器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-05-05
解決webview內(nèi)的iframe中的事件不可用的問題
這篇文章主要介紹了解決webview內(nèi)的iframe中的事件不可用的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03
如何通過Battery Historian分析Android APP耗電情況
Android 從兩個層面統(tǒng)計電量的消耗,分別為軟件排行榜及硬件排行榜。它們各有自己的耗電榜單,軟件排行榜為機器中每個 App 的耗電榜單,硬件排行榜則為各個硬件的耗電榜單。這兩個排行榜的統(tǒng)計是互為獨立,互不干擾的2021-06-06

