Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
## Unreleased
- Route HTTP(S) network images through a single Flutter codec provider by default.
- Keep an explicit native network backend for Drawable/Texture/Surface rendering.
- Remove placeholder codec metadata, nested image providers, URL-suffix format routing,
and completed encoded-handoff requests retained by Android.
- Split Flutter-codec and native Drawable/Surface animated WebP benchmarks.
- Add an injectable encoded-byte disk-cache boundary before direct codec decode,
without introducing a file-backed or nested image provider.
- Add a lightweight file implementation with an in-memory index, byte-capacity
LRU, same-key single-flight, atomic writes and background cleanup.
- Add direct-network headers, cache keys, per-attempt timeouts, transient retries
and cancellation.
- Coalesce equal raw-byte network misses and prioritize visible transfer/decode
work over queued prefetches.
- Batch raw-cache writes and LRU touches after the first displayed frame.
- Keep ordinary PNG, GIF and WebP on Flutter's codec by default; reserve the
native backend for explicit Drawable and private-decoder integrations.
- Submit normal Surface frames asynchronously and wait for presentation only at
teardown; scope release serialization to a TextureRegistry generation.
- Remove the runtime dependency on global `PowerImageBinding`/`ImageCacheExt`;
native requests now follow the stream completer's last listener.
- Add cold-load, raw-byte disk-hit, Flutter ImageCache-hit and 100-Surface
release/rebuild macrobenchmarks.

## 0.1.0-pre.2
- pre publish in github and flutter pub

Expand Down
97 changes: 81 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,37 +56,101 @@ dependency_overrides:
## Setup

### Flutter
#### 1. Replace `ImageCache` with `ImageCacheExt`.
#### 1. Use Flutter's standard binding and image cache

PowerImage no longer requires a global `PowerImageBinding` or
`ImageCacheExt`. Native texture resources are owned by their stream completer
and released when its last listener is removed. Use Flutter's standard binding:

```dart
/// call before runApp()
PowerImageBinding();
```
or
```dart
/// return ImageCacheExt in createImageCache(),
/// if you have extends with WidgetsFlutterBinding
class XXX extends WidgetsFlutterBinding {
@override
ImageCache createImageCache() {
return ImageCacheExt();
}
}
WidgetsFlutterBinding.ensureInitialized();
```



#### 2. Setup PowerImageLoader
Initialize and set the global default rendering mode, renderingTypeTexture is texture mode, renderingTypeExternal is ffi mode
In addition, there are exception reports in PowerImageSetupOptions, and the sampling rate of exception reports can be set.
```dart
PowerImageLoader.instance.setup(PowerImageSetupOptions(renderingTypeTexture,
debugLogging: true,
errorCallbackSamplingRate: 1.0,
errorCallback: (PowerImageLoadException exception) {

}));
```

On Android, `debugLogging: true` enables structured request, Surface lifecycle,
animated-frame coalescing and render-time logs. It is disabled by default. Read
the logs with `adb logcat -s PowerImage` while diagnosing performance.

### Android animated image adapters

Animated GIF and custom animated `Drawable` adapters draw directly into a
Flutter `SurfaceProducer`; frames are coalesced at VSync and stale frames are
dropped. Android HTTP(S) images now default to one Flutter `ImageProvider`: encoded
bytes go directly to Flutter's codec, whose content/magic detection does not
depend on a URL suffix. This avoids a native request, placeholder `ImageInfo`
and nested `Image.file` cache entry.

Use `networkBackend: PowerImageNetworkBackend.native` to opt into the separate
Glide/Drawable/Surface path. A custom native loader may still hand encoded data
or a cache-file path to Flutter; that compatibility path now publishes real
codec frames on the original stream and removes its Android request immediately
after successful delivery.

The direct codec path can use the built-in encoded-byte disk cache or another
implementation of the lightweight `PowerImageRawBytesCache` interface.
The cache returns `Uint8List`; a hit is passed straight to
`ImmutableBuffer`/`ui.Codec`, so no temporary `Image.file` or nested provider is
created. Equal concurrent misses share one byte download. Visible requests are
scheduled before queued prefetch work, with independent limits for transfer and
decode. Cache writes and LRU touches are batched after the first displayed frame,
so disk maintenance is not on the first-frame critical path.

```dart
final temporaryDirectory = await getTemporaryDirectory();
final rawBytesCache = PowerImageFileRawBytesCache(
// Give each cache instance its own directory.
directory: Directory(
'${temporaryDirectory.path}${Platform.pathSeparator}power_image_raw_bytes',
),
maxSizeBytes: 200 * 1024 * 1024,
);
await rawBytesCache.warmUp();

PowerImageLoader.instance.setup(PowerImageSetupOptions(
renderingTypeTexture,
rawBytesCache: rawBytesCache,
));

final cancelToken = PowerImageCancellationToken();

PowerImage.network(
imageUrl,
headers: const {'Authorization': 'Bearer token'},
cacheKey: 'user-42-avatar-v3',
timeout: const Duration(seconds: 5), // applied to every attempt
retryCount: 2, // two retries after the first attempt
retryDelay: const Duration(milliseconds: 100),
cancellationToken: cancelToken,
);

// For example, when the owning screen is disposed:
cancelToken.cancel();
```

The built-in cache keeps only metadata in memory. It uses capacity-bounded LRU,
same-key read/write single-flight, same-directory atomic replacement and
background cleanup. Its file modification times preserve an approximate LRU
order after a restart. Use one instance per dedicated directory.

`cacheKey` defaults to the URL. This cache deliberately has no TTL or HTTP
revalidation: use it only for immutable CDN URLs or explicitly versioned cache
keys, including when headers can change the bytes returned by the same URL.
Retries are limited to I/O/timeouts and HTTP 408, 429, and 5xx responses;
cancellation is never retried. Cache read/write failures fall back to the
network or the already decoded image. Set `cacheRawBytes: false` to bypass the
injected cache for one request.



### iOS
Expand Down Expand Up @@ -766,6 +830,7 @@ network image:
String src, {
Key? key,
String? renderingType,
PowerImageNetworkBackend networkBackend = PowerImageNetworkBackend.auto,
double? imageWidth,
double? imageHeight,
this.width,
Expand Down
22 changes: 5 additions & 17 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,28 +57,16 @@ dependency_overrides:

### Flutter

#### 1. 用 `ImageCacheExt`替换 `ImageCache` .
#### 1. 使用 Flutter 标准 Binding 和图片缓存

```dart
/// call before runApp()
PowerImageBinding();
```

or
PowerImage 不再依赖全局 `PowerImageBinding` 或 `ImageCacheExt`。原生纹理资源由
对应的 stream completer 管理,并在最后一个监听者移除时释放。应用只需使用
Flutter 标准 Binding:

```dart
/// return ImageCacheExt in createImageCache(),
/// if you have extends with WidgetsFlutterBinding
class XXX extends WidgetsFlutterBinding {
@override
ImageCache createImageCache() {
return ImageCacheExt();
}
}
WidgetsFlutterBinding.ensureInitialized();
```



#### 2. 初始化 PowerImageLoader

初始化并设置全局的默认的渲染方式,renderingTypeTexture为texture方式,renderingTypeExternal为ffi方式
Expand Down
21 changes: 16 additions & 5 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,34 @@ version '1.0'
buildscript {
repositories {
google()
jcenter()
mavenCentral()
}

dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath 'com.android.tools.build:gradle:8.11.1'
}
}

rootProject.allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}

apply plugin: 'com.android.library'

android {
compileSdkVersion 29
namespace 'com.taobao.power_image'
compileSdkVersion 36

compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}

defaultConfig {
minSdkVersion 16
minSdkVersion 21
// externalNativeBuild {
// cmake {
// cppFlags '-std=c++14'
Expand All @@ -42,3 +48,8 @@ android {
// }
// }
}

dependencies {
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:5.12.0'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.taobao.power_image;

import android.os.Debug;
import android.os.SystemClock;
import android.util.Log;

/** Structured, opt-in diagnostics for request and animated-frame performance. */
public final class PowerImageDiagnostics {
public static final String TAG = "PowerImage";

private static volatile boolean enabled;

private PowerImageDiagnostics() {
}

/** Enables verbose diagnostics at runtime. Disabled by default. */
public static void setEnabled(boolean value) {
if (enabled == value) {
return;
}
enabled = value;
Log.i(TAG, "event=diagnostics enabled=" + value);
}

public static boolean isDebugEnabled() {
return enabled || Log.isLoggable(TAG, Log.DEBUG);
}

public static boolean isVerboseEnabled() {
return enabled || Log.isLoggable(TAG, Log.VERBOSE);
}

public static void debug(String event, String requestId, String details) {
if (!isDebugEnabled()) {
return;
}
Log.d(TAG, format(event, requestId, details));
}

public static void verbose(String event, String requestId, String details) {
if (!isVerboseEnabled()) {
return;
}
Log.v(TAG, format(event, requestId, details));
}

public static void error(
String event, String requestId, String details, Throwable throwable) {
String message = format(event, requestId, details);
if (throwable == null) {
Log.e(TAG, message);
} else {
Log.e(TAG, message, throwable);
}
}

/** Avoids putting URLs and file paths from the request key into logcat. */
public static String requestToken(String requestId) {
return valueToken(requestId);
}

/** Returns a correlation token without exposing the original text. */
public static String valueToken(String value) {
return value == null ? "none" : Integer.toHexString(value.hashCode());
}

public static long elapsedMillis(long startedAtNanos) {
return (SystemClock.elapsedRealtimeNanos() - startedAtNanos) / 1_000_000L;
}

/** Lightweight process memory counters suitable for lifecycle logs. */
public static String memorySummary() {
Runtime runtime = Runtime.getRuntime();
long javaUsedBytes = runtime.totalMemory() - runtime.freeMemory();
return "javaUsedBytes=" + Math.max(0L, javaUsedBytes)
+ " javaCommittedBytes=" + runtime.totalMemory()
+ " nativeHeapBytes=" + Debug.getNativeHeapAllocatedSize();
}

private static String format(String event, String requestId, String details) {
StringBuilder message = new StringBuilder(96)
.append("event=").append(event)
.append(" request=").append(requestToken(requestId))
.append(" thread=").append(Thread.currentThread().getName());
if (details != null && !details.isEmpty()) {
message.append(' ').append(details.replace('\n', ' '));
}
return message.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import androidx.annotation.NonNull;

import com.taobao.power_image.request.PowerImageRequestManager;
import com.taobao.power_image.request.PowerImageBaseRequest;

import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -66,6 +67,10 @@ public void sendImageStateEvent(Map<String, Object> event, boolean success) {
}
}

public void releaseCompletedRequest(PowerImageBaseRequest request) {
powerImageRequestManager.releaseCompletedRequest(request);
}

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
if ("startImageRequests".equals(call.method)) {
Expand All @@ -85,12 +90,42 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
} else {
throw new IllegalArgumentException("stopImageRequests require List arguments");
}
} else if ("setImageAnimationActive".equals(call.method)) {
if (call.arguments instanceof Map) {
Map arguments = (Map) call.arguments;
Object uniqueKeyValue = arguments.get("uniqueKey");
String uniqueKey = uniqueKeyValue instanceof String
? (String) uniqueKeyValue : null;
Object activeValue = arguments.get("active");
if (uniqueKey != null && activeValue instanceof Boolean) {
powerImageRequestManager.setAnimationActive(
uniqueKey, (Boolean) activeValue);
result.success(true);
} else {
result.error("invalid_arguments",
"setImageAnimationActive requires uniqueKey and active", null);
}
} else {
result.error("invalid_arguments",
"setImageAnimationActive requires Map arguments", null);
}
} else if ("setPowerImageDebugLogging".equals(call.method)) {
if (call.arguments instanceof Boolean) {
PowerImageDiagnostics.setEnabled((Boolean) call.arguments);
result.success(true);
} else {
result.error("invalid_arguments",
"setPowerImageDebugLogging requires a boolean", null);
}
} else {
result.notImplemented();
}
}

public void onDetached() {
// Flutter calls plugin detach before FlutterJNI detach. Surface textures
// must therefore be unregistered synchronously in this callback.
powerImageRequestManager.releaseAllRequestsForEngineDetach();
if (methodChannel != null) {
methodChannel.setMethodCallHandler(null);
}
Expand Down
Loading