Flutter图片库CachedNetworkImage鸿蒙适配:从原理到实践
写在前面
鸿蒙(HarmonyOS)生态的快速发展,给跨平台框架带来了新的适配需求。对于很多采用Flutter的团队来说,一个现实的问题摆在眼前:那些在Android和iOS上成熟的三方库,如何在鸿蒙上继续工作?
CachedNetworkImage 作为Flutter社区里最常用的图片加载与缓存库,其能力深深扎根于原生平台------它依赖系统网络栈进行下载,依赖本地文件系统进行缓存。当运行环境切换到鸿蒙,这些依赖就需要被重新实现。本文将分享我们如何对CachedNetworkImage进行鸿蒙端的适配,包括背后的设计思路、具体实现代码以及一些调优经验,希望能帮助更多开发者更顺畅地完成鸿蒙迁移。
适配的价值与挑战
为什么值得做这件事? 首先,这意味着团队已有的Flutter代码资产和开发经验能在鸿蒙生态中得到延续。其次,一套高效的缓存机制是图片加载流畅度的关键,在鸿蒙上复现这套机制对用户体验至关重要。最后,为开发者提供一个透明的、与之前保持一致的API,能大大降低多平台开发的心智负担。
需要面对的主要难题:
- 网络层不同 :鸿蒙使用自己的
ohos.net.http框架,其API设计和回调机制与Android上的HttpURLConnection或OkHttp有显著区别。 - 文件访问方式不同 :鸿蒙的应用沙箱路径和文件操作API(
ohos.file.fs)与Android不同,缓存文件的存储策略需要调整。 - 平台通信需要重写 :必须在鸿蒙端完整实现Flutter Plugin要求的
MethodChannel和EventChannel接口。 - 内存管理需协调:图片解码后的内存缓存,需要与鸿蒙系统的内存管理及应用生命周期妥善配合。
下面,我们就来逐一拆解这些挑战,构建一个稳定可用的鸿蒙端适配层。
一、原理剖析:Flutter插件机制如何在鸿蒙上工作
1.1 Flutter Plugin 的鸿蒙架构重构
Flutter插件通过Platform Channel实现Dart与原生代码的通信。在鸿蒙上,我们需要基于鸿蒙的API重新搭建这座桥梁。
两边对比一下:
- 在Android/iOS上 :插件通过
MethodChannel调用HttpURLConnection或NSURLSession进行网络请求,用File类读写缓存文件。 - 在鸿蒙上 :我们必须改用
ohos.net.http发起请求,用ohos.file.fs操作文件,并通过鸿蒙的PlatformChannel实现与Flutter引擎的交互。
一个直观的代码对比:
java
// Android端的典型实现(作为参考)
public class ImageLoader {
public void load(String url, Callback callback) {
// 使用HttpURLConnection或OkHttp
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
// ... 处理响应
}
}
// 鸿蒙端需要实现的对应逻辑
public class HarmonyImageLoader {
public void load(String url, Callback callback) {
// 必须使用ohos.net.http.HttpClient
HttpRequest request = new HttpRequest(url);
HttpClient client = new HttpClient();
// ... 处理响应
}
}
1.2 CachedNetworkImage 的核心流程
要完成适配,得先吃透它的工作原理。CachedNetworkImage 的核心流程可以概括为五步:
- 查缓存:先查内存(LRU Cache),再查磁盘。
- 下网络:缓存没有,就发起网络请求。
- 解图片:将下载的原始数据解码成可渲染的位图。
- 存缓存:把解码后的图片放入内存和磁盘缓存。
- 渲染 :通过
ImageProvider将图片显示到界面。
我们的适配工作,重点在于重写第2步(网络请求)和第4步(缓存存储),并对第1、3步的参数配置进行适配。
二、完整实现:鸿蒙端插件代码拆解
2.1 项目结构与依赖
鸿蒙模块建议结构:
cached_network_image_harmony/
├── entry/
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/com/example/cachednetworkimage/
│ │ │ │ ├── CacheManager.java # 缓存管理核心
│ │ │ │ ├── HarmonyImageLoader.java # 鸿蒙网络加载器
│ │ │ │ ├── ImageCache.java # 图片缓存实现
│ │ │ │ └── CachedNetworkImagePlugin.java # 插件主入口
│ │ │ └── resources/
│ │ │ └── config.json
│ └── build.gradle
└── build.gradle
关键依赖配置(entry/build.gradle):
gradle
dependencies {
implementation 'io.openharmony.tpc.thirdlib:okhttp:1.0.2'
implementation 'io.openharmony.tpc.thirdlib:glide:1.0.2'
implementation fileTree(dir: 'libs', include: ['*.jar', '*.har'])
// Flutter引擎
compileOnly project(path: ':flutter')
}
2.2 插件主类:通信的枢纽
java
package com.example.cachednetworkimage;
import ohos.ace.ability.AceAbility;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.PluginRegistry;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedNetworkImagePlugin implements MethodChannel.MethodCallHandler {
private static final String CHANNEL_NAME = "cached_network_image";
private final ohos.app.AbilityContext context;
private final ExecutorService executorService;
private final CacheManager cacheManager;
private final HarmonyImageLoader imageLoader;
private static MethodChannel channel;
public static void registerWith(PluginRegistry registry, AceAbility ability) {
if (channel != null) return;
channel = new MethodChannel(registry.registrarFor(CHANNEL_NAME).messenger(), CHANNEL_NAME);
channel.setMethodCallHandler(new CachedNetworkImagePlugin(ability));
}
private CachedNetworkImagePlugin(AceAbility ability) {
this.context = ability.getContext();
this.executorService = Executors.newFixedThreadPool(4);
this.cacheManager = CacheManager.getInstance(context);
this.imageLoader = new HarmonyImageLoader(cacheManager);
}
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
switch (call.method) {
case "loadImage":
loadImage(call, result);
break;
case "clearCache":
clearCache(call, result);
break;
case "getCacheSize":
getCacheSize(result);
break;
default:
result.notImplemented();
}
}
private void loadImage(MethodCall call, final MethodChannel.Result result) {
final String url = call.argument("url");
if (url == null || url.isEmpty()) {
result.error("INVALID_URL", "URL cannot be null or empty", null);
return;
}
final int maxWidth = call.argument("maxWidth");
final int maxHeight = call.argument("maxHeight");
final Map<String, String> headers = call.argument("headers");
executorService.submit(() -> {
try {
ImageResult imageResult = imageLoader.loadImage(url, maxWidth, maxHeight, headers);
Map<String, Object> response = new HashMap<>();
response.put("filePath", imageResult.getFilePath());
response.put("width", imageResult.getWidth());
response.put("height", imageResult.getHeight());
response.put("fromCache", imageResult.isFromCache());
result.success(response);
} catch (Exception e) {
result.error("LOAD_FAILED", "Failed to load image: " + e.getMessage(), e.toString());
}
});
}
private void clearCache(MethodCall call, MethodChannel.Result result) {
boolean memoryOnly = call.argument("memoryOnly");
try {
if (memoryOnly) cacheManager.clearMemoryCache();
else cacheManager.clearAllCache();
result.success(true);
} catch (Exception e) {
result.error("CLEAR_FAILED", "Failed to clear cache: " + e.getMessage(), e.toString());
}
}
private void getCacheSize(MethodChannel.Result result) {
try {
Map<String, Long> sizes = new HashMap<>();
sizes.put("memory", cacheManager.getMemoryCacheSize());
sizes.put("disk", cacheManager.getDiskCacheSize());
result.success(sizes);
} catch (Exception e) {
result.error("SIZE_ERROR", "Failed to get cache size: " + e.getMessage(), e.toString());
}
}
public void onDetachedFromEngine() {
if (channel != null) channel.setMethodCallHandler(null);
channel = null;
if (executorService != null && !executorService.isShutdown()) executorService.shutdown();
}
}
2.3 鸿蒙网络加载器实现
这是适配的核心,我们基于ohos.net.http重新实现了网络请求和图片解码流程。
java
package com.example.cachednetworkimage;
import ohos.net.http.*;
import ohos.media.image.ImageSource;
import ohos.media.image.PixelMap;
import java.io.*;
public class HarmonyImageLoader {
private final CacheManager cacheManager;
private final HttpClient httpClient;
public HarmonyImageLoader(CacheManager cacheManager) {
this.cacheManager = cacheManager;
this.httpClient = new HttpClient();
this.httpClient.setConnectTimeout(15, java.util.concurrent.TimeUnit.SECONDS);
this.httpClient.setReadTimeout(30, java.util.concurrent.TimeUnit.SECONDS);
this.httpClient.setMaxRetryCount(3);
}
public ImageResult loadImage(String url, Integer maxWidth, Integer maxHeight, Map<String, String> headers)
throws IOException, HttpClientException {
// 1. 生成缓存Key(包含尺寸信息)
String cacheKey = generateCacheKey(url, maxWidth, maxHeight);
// 2. 优先检查内存缓存
ImageResult cachedResult = cacheManager.getFromMemoryCache(cacheKey);
if (cachedResult != null) return cachedResult.setFromCache(true);
// 3. 检查磁盘缓存
File cachedFile = cacheManager.getCachedFile(cacheKey);
if (cachedFile != null && cachedFile.exists()) {
PixelMap pixelMap = decodeFile(cachedFile, maxWidth, maxHeight);
if (pixelMap != null) {
ImageResult result = new ImageResult(cachedFile.getAbsolutePath(),
pixelMap.getImageInfo().size.width,
pixelMap.getImageInfo().size.height);
cacheManager.putToMemoryCache(cacheKey, result); // 回填内存缓存
return result.setFromCache(true);
}
}
// 4. 缓存未命中,发起网络请求
HttpRequest request = new HttpRequest(url);
request.setRequestMethod(HttpRequest.Method.GET);
if (headers != null) headers.forEach(request::setHeader);
HttpResponse response = httpClient.execute(request);
if (response.getResponseCode() != 200) {
throw new IOException("HTTP error: " + response.getResponseCode());
}
// 5. 下载到临时文件
File tempFile = downloadToFile(response, cacheKey);
// 6. 解码图片
PixelMap pixelMap = decodeFile(tempFile, maxWidth, maxHeight);
if (pixelMap == null) throw new IOException("Failed to decode image");
// 7. 构造结果并存入内存缓存
ImageResult result = new ImageResult(tempFile.getAbsolutePath(),
pixelMap.getImageInfo().size.width,
pixelMap.getImageInfo().size.height);
cacheManager.putToMemoryCache(cacheKey, result);
pixelMap.release(); // 及时释放原生资源
return result.setFromCache(false);
}
private File downloadToFile(HttpResponse response, String cacheKey) throws IOException {
File cacheFile = cacheManager.getCacheFileForWrite(cacheKey);
try (InputStream is = response.getInputStream();
FileOutputStream os = new FileOutputStream(cacheFile)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) os.write(buffer, 0, bytesRead);
os.flush();
} finally {
response.close();
}
return cacheFile;
}
private PixelMap decodeFile(File file, Integer maxWidth, Integer maxHeight) throws IOException {
ImageSource.SourceOptions srcOptions = new ImageSource.SourceOptions();
srcOptions.formatHint = "image/png";
ImageSource imageSource = ImageSource.create(file.getAbsolutePath(), srcOptions);
if (imageSource == null) return null;
ImageSource.DecodingOptions decodingOptions = new ImageSource.DecodingOptions();
if (maxWidth != null && maxHeight != null) {
decodingOptions.desiredSize = new ohos.media.image.common.Size(maxWidth, maxHeight);
}
decodingOptions.desiredPixelFormat = ohos.media.image.common.PixelFormat.ARGB_8888;
try {
return imageSource.createPixelmap(decodingOptions);
} finally {
imageSource.release();
}
}
private String generateCacheKey(String url, Integer maxWidth, Integer maxHeight) {
StringBuilder key = new StringBuilder().append(url.hashCode());
if (maxWidth != null) key.append("_w").append(maxWidth);
if (maxHeight != null) key.append("_h").append(maxHeight);
return key.toString();
}
}
2.4 缓存管理器的实现
缓存管理器负责内存LRU缓存和磁盘文件缓存的管理。
java
package com.example.cachednetworkimage;
import ohos.app.Context;
import java.io.File;
import java.util.LinkedHashMap;
import java.util.Map;
public class CacheManager {
private static final long MAX_MEMORY_CACHE_SIZE = 50 * 1024 * 1024; // 50MB
private static final long MAX_DISK_CACHE_SIZE = 200 * 1024 * 1024; // 200MB
private static final String CACHE_DIR_NAME = "cached_network_image";
private static volatile CacheManager instance;
private final Context context;
private final LinkedHashMap<String, ImageResult> memoryCache;
private long currentMemoryCacheSize = 0;
private File cacheDir;
private CacheManager(Context context) {
this.context = context;
// 创建一个按访问顺序排序、并在超过容量时自动移除最老元素的LRU缓存
this.memoryCache = new LinkedHashMap<String, ImageResult>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, ImageResult> eldest) {
if (currentMemoryCacheSize > MAX_MEMORY_CACHE_SIZE) {
currentMemoryCacheSize -= calculateSize(eldest.getValue());
return true;
}
return false;
}
};
initializeCacheDir();
}
public static CacheManager getInstance(Context context) {
if (instance == null) {
synchronized (CacheManager.class) {
if (instance == null) instance = new CacheManager(context);
}
}
return instance;
}
private void initializeCacheDir() {
File filesDir = context.getFilesDir();
cacheDir = new File(filesDir, CACHE_DIR_NAME);
if (!cacheDir.exists()) cacheDir.mkdirs();
}
public synchronized ImageResult getFromMemoryCache(String key) {
return memoryCache.get(key);
}
public synchronized void putToMemoryCache(String key, ImageResult result) {
long size = calculateSize(result);
// 若已存在相同key,先移除旧数据
ImageResult old = memoryCache.remove(key);
if (old != null) currentMemoryCacheSize -= calculateSize(old);
// 存入新数据
memoryCache.put(key, result);
currentMemoryCacheSize += size;
}
public File getCachedFile(String cacheKey) {
return new File(cacheDir, cacheKey + ".cache");
}
public File getCacheFileForWrite(String cacheKey) throws IOException {
File file = getCachedFile(cacheKey);
File parent = file.getParentFile();
if (parent != null && !parent.exists()) parent.mkdirs();
return file;
}
public synchronized void clearMemoryCache() {
memoryCache.clear();
currentMemoryCacheSize = 0;
}
public void clearAllCache() {
clearMemoryCache();
clearDiskCache();
}
private void clearDiskCache() {
if (cacheDir.exists() && cacheDir.isDirectory()) {
File[] files = cacheDir.listFiles();
if (files != null) for (File f : files) f.delete();
}
}
public synchronized long getMemoryCacheSize() { return currentMemoryCacheSize; }
public long getDiskCacheSize() {
if (!cacheDir.exists() || !cacheDir.isDirectory()) return 0;
long total = 0;
File[] files = cacheDir.listFiles();
if (files != null) for (File f : files) if (f.isFile()) total += f.length();
return total;
}
private long calculateSize(ImageResult result) {
// 估算内存占用:宽 × 高 × 4字节(ARGB_8888格式)
return (long) result.getWidth() * result.getHeight() * 4;
}
}
2.5 Dart层适配代码
最后,我们需要一个Dart层的封装,为Flutter业务代码提供与原有CachedNetworkImage相似的API。
dart
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'dart:io';
class CachedNetworkImageHarmony extends ImageProvider<CachedNetworkImageHarmony> {
final String url;
final Map<String, String>? headers;
final double? scale;
CachedNetworkImageHarmony(this.url, {this.headers, this.scale = 1.0});
static const MethodChannel _channel = MethodChannel('cached_network_image');
@override
ImageStreamCompleter loadImage(CachedNetworkImageHarmony key, ImageDecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key),
scale: key.scale ?? 1.0,
debugLabel: key.url,
informationCollector: () sync* {
yield DiagnosticsProperty<ImageProvider>('Image provider', this);
yield DiagnosticsProperty<CachedNetworkImageHarmony>('Image key', key);
},
);
}
Future<ui.Codec> _loadAsync(CachedNetworkImageHarmony key) async {
assert(key == this);
try {
final result = await _channel.invokeMethod('loadImage', <String, dynamic>{
'url': url,
'headers': headers,
});
if (result == null) throw Exception('Failed to load image: result is null');
final String filePath = result['filePath'] as String;
// 尝试读取文件字节
Uint8List bytes;
try {
final data = await rootBundle.load(filePath);
bytes = data.buffer.asUint8List();
} catch (_) {
final file = File(filePath);
if (await file.exists()) {
bytes = await file.readAsBytes();
} else {
throw Exception('File not found: $filePath');
}
}
return await ui.instantiateImageCodec(bytes);
} on PlatformException catch (e) {
debugPrint('''
Platform error while loading image:
Code: ${e.code}
Message: ${e.message}
Details: ${e.details}
''');
rethrow;
}
}
@override
Future<CachedNetworkImageHarmony> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<CachedNetworkImageHarmony>(this);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is CachedNetworkImageHarmony &&
other.url == url &&
other.scale == scale &&
mapEquals(other.headers, headers);
}
@override
int get hashCode => Object.hash(url.hashCode, scale?.hashCode, headers?.hashCode ?? 0);
}
// 工具函数
Future<void> clearCache({bool memoryOnly = false}) async {
try {
await CachedNetworkImageHarmony._channel.invokeMethod(
'clearCache', {'memoryOnly': memoryOnly});
} on PlatformException catch (e) {
debugPrint('Failed to clear cache: ${e.message}');
}
}
Future<Map<String, int>> getCacheSize() async {
try {
final result = await CachedNetworkImageHarmony._channel.invokeMethod('getCacheSize');
if (result != null) return {'memory': result['memory'] as int, 'disk': result['disk'] as int};
} on PlatformException catch (e) {
debugPrint('Failed to get cache size: ${e.message}');
}
return {'memory': 0, 'disk': 0};
}
小结
通过以上的步骤,我们基本完成了CachedNetworkImage在鸿蒙系统上的核心适配。这个过程的核心思路是:保持Dart层API不变,在鸿蒙原生层实现其依赖的网络、文件与缓存能力。
实际集成时,还需要注意性能调优,比如根据应用图片尺寸调整内存缓存大小、设置合理的网络超时和重试策略等。希望这篇从原理到代码的梳理,能为你将其他Flutter插件迁移到鸿蒙提供一些可行的参考。