Merge pull request #2 from koji-1009/fix/memory_leak

Fix memory leak and code cleanup
This commit is contained in:
Koji Wakamiya 2025-12-08 19:45:19 +09:00 committed by GitHub
commit 05f2d1029a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 266 additions and 104 deletions

View File

@ -17,6 +17,8 @@ final config = FfiGenerator(
'CFDataCreateMutable',
'CFDataGetBytePtr',
'CFDataGetLength',
// CFDictionary operations
'CFDictionaryCreate',
// CGImageSource operations (decoding)
'CGImageSourceCreateWithData',
'CGImageSourceCreateImageAtIndex',
@ -24,8 +26,23 @@ final config = FfiGenerator(
'CGImageDestinationCreateWithData',
'CGImageDestinationAddImage',
'CGImageDestinationFinalize',
// Memory management
'CFRelease',
}),
globals: Globals.includeSet({
'kCFAllocatorDefault',
'kCGImageDestinationLossyCompressionQuality',
'kCFTypeDictionaryValueCallBacks',
'kCFTypeDictionaryKeyCallBacks',
}),
typedefs: Typedefs.includeSet({
'CFDataRef',
'CFDictionaryRef',
'CGImageRef',
'CGImageSourceRef',
'CFMutableDataRef',
'CGImageDestinationRef',
}),
globals: Globals.includeSet({'kCFAllocatorDefault'}),
);
void main() => config.generate();

View File

@ -8,98 +8,210 @@ import 'package:objective_c/objective_c.dart' as objc;
@ffi.Native<ffi.Pointer<__CFAllocator>>()
external final ffi.Pointer<__CFAllocator> kCFAllocatorDefault;
@ffi.Native<ffi.Void Function(ffi.Pointer<ffi.Void>)>()
external void CFRelease(ffi.Pointer<ffi.Void> cf);
@ffi.Native<CFDictionaryKeyCallBacks>()
external final CFDictionaryKeyCallBacks kCFTypeDictionaryKeyCallBacks;
@ffi.Native<CFDictionaryValueCallBacks>()
external final CFDictionaryValueCallBacks kCFTypeDictionaryValueCallBacks;
@ffi.Native<
ffi.Pointer<__CFData> Function(
CFDictionaryRef Function(
ffi.Pointer<__CFAllocator>,
ffi.Pointer<ffi.Pointer<ffi.Void>>,
ffi.Pointer<ffi.Pointer<ffi.Void>>,
ffi.Long,
ffi.Pointer<CFDictionaryKeyCallBacks>,
ffi.Pointer<CFDictionaryValueCallBacks>,
)
>()
external CFDictionaryRef CFDictionaryCreate(
ffi.Pointer<__CFAllocator> allocator,
ffi.Pointer<ffi.Pointer<ffi.Void>> keys,
ffi.Pointer<ffi.Pointer<ffi.Void>> values,
int numValues,
ffi.Pointer<CFDictionaryKeyCallBacks> keyCallBacks,
ffi.Pointer<CFDictionaryValueCallBacks> valueCallBacks,
);
@ffi.Native<
CFDataRef Function(
ffi.Pointer<__CFAllocator>,
ffi.Pointer<ffi.UnsignedChar>,
ffi.Long,
)
>()
external ffi.Pointer<__CFData> CFDataCreate(
external CFDataRef CFDataCreate(
ffi.Pointer<__CFAllocator> allocator,
ffi.Pointer<ffi.UnsignedChar> bytes,
int length,
);
@ffi.Native<
ffi.Pointer<__CFData> Function(ffi.Pointer<__CFAllocator>, ffi.Long)
>()
external ffi.Pointer<__CFData> CFDataCreateMutable(
@ffi.Native<CFMutableDataRef Function(ffi.Pointer<__CFAllocator>, ffi.Long)>()
external CFMutableDataRef CFDataCreateMutable(
ffi.Pointer<__CFAllocator> allocator,
int capacity,
);
@ffi.Native<ffi.Long Function(ffi.Pointer<__CFData>)>()
external int CFDataGetLength(ffi.Pointer<__CFData> theData);
@ffi.Native<ffi.Long Function(CFDataRef)>()
external int CFDataGetLength(CFDataRef theData);
@ffi.Native<ffi.Pointer<ffi.UnsignedChar> Function(ffi.Pointer<__CFData>)>()
external ffi.Pointer<ffi.UnsignedChar> CFDataGetBytePtr(
ffi.Pointer<__CFData> theData,
@ffi.Native<ffi.Pointer<ffi.UnsignedChar> Function(CFDataRef)>()
external ffi.Pointer<ffi.UnsignedChar> CFDataGetBytePtr(CFDataRef theData);
@ffi.Native<CGImageSourceRef Function(CFDataRef, CFDictionaryRef)>()
external CGImageSourceRef CGImageSourceCreateWithData(
CFDataRef data,
CFDictionaryRef options,
);
@ffi.Native<
ffi.Pointer<CGImageSource> Function(
ffi.Pointer<__CFData>,
ffi.Pointer<__CFDictionary>,
)
>()
external ffi.Pointer<CGImageSource> CGImageSourceCreateWithData(
ffi.Pointer<__CFData> data,
ffi.Pointer<__CFDictionary> options,
);
@ffi.Native<
ffi.Pointer<CGImage> Function(
ffi.Pointer<CGImageSource>,
ffi.Size,
ffi.Pointer<__CFDictionary>,
)
>()
external ffi.Pointer<CGImage> CGImageSourceCreateImageAtIndex(
ffi.Pointer<CGImageSource> isrc,
@ffi.Native<CGImageRef Function(CGImageSourceRef, ffi.Size, CFDictionaryRef)>()
external CGImageRef CGImageSourceCreateImageAtIndex(
CGImageSourceRef isrc,
int index,
ffi.Pointer<__CFDictionary> options,
CFDictionaryRef options,
);
@ffi.Native<ffi.Pointer<objc.CFString>>()
external ffi.Pointer<objc.CFString> kCGImageDestinationLossyCompressionQuality;
@ffi.Native<
ffi.Pointer<CGImageDestination> Function(
ffi.Pointer<__CFData>,
CGImageDestinationRef Function(
CFMutableDataRef,
ffi.Pointer<objc.CFString>,
ffi.Size,
ffi.Pointer<__CFDictionary>,
CFDictionaryRef,
)
>()
external ffi.Pointer<CGImageDestination> CGImageDestinationCreateWithData(
ffi.Pointer<__CFData> data,
external CGImageDestinationRef CGImageDestinationCreateWithData(
CFMutableDataRef data,
ffi.Pointer<objc.CFString> type,
int count,
ffi.Pointer<__CFDictionary> options,
CFDictionaryRef options,
);
@ffi.Native<
ffi.Void Function(
ffi.Pointer<CGImageDestination>,
ffi.Pointer<CGImage>,
ffi.Pointer<__CFDictionary>,
)
ffi.Void Function(CGImageDestinationRef, CGImageRef, CFDictionaryRef)
>()
external void CGImageDestinationAddImage(
ffi.Pointer<CGImageDestination> idst,
ffi.Pointer<CGImage> image,
ffi.Pointer<__CFDictionary> properties,
CGImageDestinationRef idst,
CGImageRef image,
CFDictionaryRef properties,
);
@ffi.Native<ffi.Bool Function(ffi.Pointer<CGImageDestination>)>()
external bool CGImageDestinationFinalize(ffi.Pointer<CGImageDestination> idst);
@ffi.Native<ffi.Bool Function(CGImageDestinationRef)>()
external bool CGImageDestinationFinalize(CGImageDestinationRef idst);
final class __CFAllocator extends ffi.Opaque {}
final class CFDictionaryKeyCallBacks extends ffi.Struct {
@ffi.Long()
external int version;
external ffi.Pointer<
ffi.NativeFunction<
ffi.Pointer<ffi.Void> Function(
ffi.Pointer<__CFAllocator> allocator,
ffi.Pointer<ffi.Void> value,
)
>
>
retain;
external ffi.Pointer<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<__CFAllocator> allocator,
ffi.Pointer<ffi.Void> value,
)
>
>
release;
external ffi.Pointer<
ffi.NativeFunction<
ffi.Pointer<objc.CFString> Function(ffi.Pointer<ffi.Void> value)
>
>
copyDescription;
external ffi.Pointer<
ffi.NativeFunction<
ffi.UnsignedChar Function(
ffi.Pointer<ffi.Void> value1,
ffi.Pointer<ffi.Void> value2,
)
>
>
equal;
external ffi.Pointer<
ffi.NativeFunction<ffi.UnsignedLong Function(ffi.Pointer<ffi.Void> value)>
>
hash;
}
final class CFDictionaryValueCallBacks extends ffi.Struct {
@ffi.Long()
external int version;
external ffi.Pointer<
ffi.NativeFunction<
ffi.Pointer<ffi.Void> Function(
ffi.Pointer<__CFAllocator> allocator,
ffi.Pointer<ffi.Void> value,
)
>
>
retain;
external ffi.Pointer<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<__CFAllocator> allocator,
ffi.Pointer<ffi.Void> value,
)
>
>
release;
external ffi.Pointer<
ffi.NativeFunction<
ffi.Pointer<objc.CFString> Function(ffi.Pointer<ffi.Void> value)
>
>
copyDescription;
external ffi.Pointer<
ffi.NativeFunction<
ffi.UnsignedChar Function(
ffi.Pointer<ffi.Void> value1,
ffi.Pointer<ffi.Void> value2,
)
>
>
equal;
}
final class __CFDictionary extends ffi.Opaque {}
typedef CFDictionaryRef = ffi.Pointer<__CFDictionary>;
final class __CFData extends ffi.Opaque {}
typedef CFDataRef = ffi.Pointer<__CFData>;
typedef CFMutableDataRef = ffi.Pointer<__CFData>;
final class CGImageSource extends ffi.Opaque {}
typedef CGImageSourceRef = ffi.Pointer<CGImageSource>;
final class CGImage extends ffi.Opaque {}
typedef CGImageRef = ffi.Pointer<CGImage>;
final class CGImageDestination extends ffi.Opaque {}
typedef CGImageDestinationRef = ffi.Pointer<CGImageDestination>;

View File

@ -34,60 +34,46 @@ final class ImageConverterAndroid implements ImageConverterPlatform {
OutputFormat format = OutputFormat.jpeg,
int quality = 100,
}) async {
final inputJBytes = JByteArray.from(inputData);
JByteArray? inputJBytes;
Bitmap? bitmap;
Bitmap$CompressFormat? compressFormat;
ByteArrayOutputStream? outputStream;
JByteArray? outputJBytes;
try {
final bitmap = BitmapFactory.decodeByteArray(
inputJBytes,
0,
inputData.length,
);
inputJBytes = JByteArray.from(inputData);
bitmap = BitmapFactory.decodeByteArray(inputJBytes, 0, inputData.length);
if (bitmap == null) {
throw Exception('Failed to decode image. Invalid image data.');
}
try {
final compressFormat = switch (format) {
OutputFormat.jpeg => Bitmap$CompressFormat.JPEG,
OutputFormat.png => Bitmap$CompressFormat.PNG,
OutputFormat.webp => Bitmap$CompressFormat.WEBP_LOSSY,
OutputFormat.heic => throw UnsupportedError(
'HEIC output format is not supported on Android.',
),
};
compressFormat = switch (format) {
OutputFormat.jpeg => Bitmap$CompressFormat.JPEG,
OutputFormat.png => Bitmap$CompressFormat.PNG,
OutputFormat.webp => Bitmap$CompressFormat.WEBP_LOSSY,
OutputFormat.heic => throw UnsupportedError(
'HEIC output format is not supported on Android.',
),
};
try {
final outputStream = ByteArrayOutputStream();
try {
final success = bitmap.compress(
compressFormat,
quality,
outputStream,
);
if (!success) {
throw Exception('Failed to compress bitmap.');
}
final outputJBytes = outputStream.toByteArray();
if (outputJBytes == null) {
throw Exception('Failed to get byte array from output stream.');
}
try {
final outputList = outputJBytes.toList();
return Uint8List.fromList(outputList);
} finally {
outputJBytes.release();
}
} finally {
outputStream.release();
}
} finally {
compressFormat.release();
}
} finally {
bitmap.release();
outputStream = ByteArrayOutputStream();
final success = bitmap.compress(compressFormat, quality, outputStream);
if (!success) {
throw Exception('Failed to compress bitmap.');
}
outputJBytes = outputStream.toByteArray();
if (outputJBytes == null) {
throw Exception('Failed to get byte array from output stream.');
}
return Uint8List.fromList(outputJBytes.toList());
} finally {
inputJBytes.release();
inputJBytes?.release();
bitmap?.recycle();
bitmap?.release();
compressFormat?.release();
outputStream?.release();
outputJBytes?.release();
}
}
}

View File

@ -35,11 +35,18 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
OutputFormat format = OutputFormat.jpeg,
int quality = 100,
}) async {
final inputPtr = calloc<Uint8>(inputData.length);
Pointer<Uint8>? inputPtr;
CFDataRef? cfData;
CGImageSourceRef? imageSource;
CGImageRef? cgImage;
CFMutableDataRef? outputData;
CGImageDestinationRef? destination;
CFDictionaryRef? properties;
try {
inputPtr = calloc<Uint8>(inputData.length);
inputPtr.asTypedList(inputData.length).setAll(0, inputData);
final cfData = CFDataCreate(
cfData = CFDataCreate(
kCFAllocatorDefault,
inputPtr.cast(),
inputData.length,
@ -48,17 +55,17 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
throw Exception('Failed to create CFData from input data.');
}
final imageSource = CGImageSourceCreateWithData(cfData, nullptr);
imageSource = CGImageSourceCreateWithData(cfData, nullptr);
if (imageSource == nullptr) {
throw Exception('Failed to create CGImageSource. Invalid image data.');
}
final cgImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nullptr);
cgImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nullptr);
if (cgImage == nullptr) {
throw Exception('Failed to decode image.');
}
final outputData = CFDataCreateMutable(kCFAllocatorDefault, 0);
outputData = CFDataCreateMutable(kCFAllocatorDefault, 0);
if (outputData == nullptr) {
throw Exception('Failed to create output CFData.');
}
@ -81,7 +88,7 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
.retainAndAutorelease()
.cast<CFString>();
final destination = CGImageDestinationCreateWithData(
destination = CGImageDestinationCreateWithData(
outputData,
cfString,
1,
@ -91,7 +98,8 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
throw Exception('Failed to create CGImageDestination.');
}
CGImageDestinationAddImage(destination, cgImage, nullptr);
properties = _createPropertiesForFormat(format, quality);
CGImageDestinationAddImage(destination, cgImage, properties ?? nullptr);
final success = CGImageDestinationFinalize(destination);
if (!success) {
@ -106,7 +114,45 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
return Uint8List.fromList(bytePtr.cast<Uint8>().asTypedList(length));
} finally {
calloc.free(inputPtr);
if (inputPtr != null) calloc.free(inputPtr);
if (cfData != null) CFRelease(cfData.cast());
if (imageSource != null) CFRelease(imageSource.cast());
if (cgImage != null) CFRelease(cgImage.cast());
if (outputData != null) CFRelease(outputData.cast());
if (destination != null) CFRelease(destination.cast());
if (properties != null) CFRelease(properties.cast());
}
}
CFDictionaryRef? _createPropertiesForFormat(
OutputFormat format,
int quality,
) {
if (format == OutputFormat.png || format == OutputFormat.webp) {
return null;
}
return using((arena) {
final keys = arena<Pointer<CFString>>(1);
final values = arena<Pointer<Void>>(1);
keys[0] = kCGImageDestinationLossyCompressionQuality;
values[0] = (quality / 100.0)
.toNSNumber()
.ref
.retainAndAutorelease()
.cast<Void>();
final keyCallBacks = arena<CFDictionaryKeyCallBacks>();
final valueCallBacks = arena<CFDictionaryValueCallBacks>();
return CFDictionaryCreate(
kCFAllocatorDefault,
keys.cast(),
values.cast(),
1,
keyCallBacks,
valueCallBacks,
);
});
}
}

View File

@ -7,7 +7,8 @@
/// |--------|-----------|---------|
/// | jpeg | | |
/// | png | | |
/// | webp | | |
/// | webp | | |
/// | heic | | |
///
/// **Notes:**
/// - [jpeg]: Good compression with adjustable quality