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', 'CFDataCreateMutable',
'CFDataGetBytePtr', 'CFDataGetBytePtr',
'CFDataGetLength', 'CFDataGetLength',
// CFDictionary operations
'CFDictionaryCreate',
// CGImageSource operations (decoding) // CGImageSource operations (decoding)
'CGImageSourceCreateWithData', 'CGImageSourceCreateWithData',
'CGImageSourceCreateImageAtIndex', 'CGImageSourceCreateImageAtIndex',
@ -24,8 +26,23 @@ final config = FfiGenerator(
'CGImageDestinationCreateWithData', 'CGImageDestinationCreateWithData',
'CGImageDestinationAddImage', 'CGImageDestinationAddImage',
'CGImageDestinationFinalize', '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(); void main() => config.generate();

View File

@ -8,98 +8,210 @@ import 'package:objective_c/objective_c.dart' as objc;
@ffi.Native<ffi.Pointer<__CFAllocator>>() @ffi.Native<ffi.Pointer<__CFAllocator>>()
external final ffi.Pointer<__CFAllocator> kCFAllocatorDefault; 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.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<__CFAllocator>,
ffi.Pointer<ffi.UnsignedChar>, ffi.Pointer<ffi.UnsignedChar>,
ffi.Long, ffi.Long,
) )
>() >()
external ffi.Pointer<__CFData> CFDataCreate( external CFDataRef CFDataCreate(
ffi.Pointer<__CFAllocator> allocator, ffi.Pointer<__CFAllocator> allocator,
ffi.Pointer<ffi.UnsignedChar> bytes, ffi.Pointer<ffi.UnsignedChar> bytes,
int length, int length,
); );
@ffi.Native< @ffi.Native<CFMutableDataRef Function(ffi.Pointer<__CFAllocator>, ffi.Long)>()
ffi.Pointer<__CFData> Function(ffi.Pointer<__CFAllocator>, ffi.Long) external CFMutableDataRef CFDataCreateMutable(
>()
external ffi.Pointer<__CFData> CFDataCreateMutable(
ffi.Pointer<__CFAllocator> allocator, ffi.Pointer<__CFAllocator> allocator,
int capacity, int capacity,
); );
@ffi.Native<ffi.Long Function(ffi.Pointer<__CFData>)>() @ffi.Native<ffi.Long Function(CFDataRef)>()
external int CFDataGetLength(ffi.Pointer<__CFData> theData); external int CFDataGetLength(CFDataRef theData);
@ffi.Native<ffi.Pointer<ffi.UnsignedChar> Function(ffi.Pointer<__CFData>)>() @ffi.Native<ffi.Pointer<ffi.UnsignedChar> Function(CFDataRef)>()
external ffi.Pointer<ffi.UnsignedChar> CFDataGetBytePtr( external ffi.Pointer<ffi.UnsignedChar> CFDataGetBytePtr(CFDataRef theData);
ffi.Pointer<__CFData> theData,
@ffi.Native<CGImageSourceRef Function(CFDataRef, CFDictionaryRef)>()
external CGImageSourceRef CGImageSourceCreateWithData(
CFDataRef data,
CFDictionaryRef options,
); );
@ffi.Native< @ffi.Native<CGImageRef Function(CGImageSourceRef, ffi.Size, CFDictionaryRef)>()
ffi.Pointer<CGImageSource> Function( external CGImageRef CGImageSourceCreateImageAtIndex(
ffi.Pointer<__CFData>, CGImageSourceRef isrc,
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,
int index, int index,
ffi.Pointer<__CFDictionary> options, CFDictionaryRef options,
); );
@ffi.Native<ffi.Pointer<objc.CFString>>()
external ffi.Pointer<objc.CFString> kCGImageDestinationLossyCompressionQuality;
@ffi.Native< @ffi.Native<
ffi.Pointer<CGImageDestination> Function( CGImageDestinationRef Function(
ffi.Pointer<__CFData>, CFMutableDataRef,
ffi.Pointer<objc.CFString>, ffi.Pointer<objc.CFString>,
ffi.Size, ffi.Size,
ffi.Pointer<__CFDictionary>, CFDictionaryRef,
) )
>() >()
external ffi.Pointer<CGImageDestination> CGImageDestinationCreateWithData( external CGImageDestinationRef CGImageDestinationCreateWithData(
ffi.Pointer<__CFData> data, CFMutableDataRef data,
ffi.Pointer<objc.CFString> type, ffi.Pointer<objc.CFString> type,
int count, int count,
ffi.Pointer<__CFDictionary> options, CFDictionaryRef options,
); );
@ffi.Native< @ffi.Native<
ffi.Void Function( ffi.Void Function(CGImageDestinationRef, CGImageRef, CFDictionaryRef)
ffi.Pointer<CGImageDestination>,
ffi.Pointer<CGImage>,
ffi.Pointer<__CFDictionary>,
)
>() >()
external void CGImageDestinationAddImage( external void CGImageDestinationAddImage(
ffi.Pointer<CGImageDestination> idst, CGImageDestinationRef idst,
ffi.Pointer<CGImage> image, CGImageRef image,
ffi.Pointer<__CFDictionary> properties, CFDictionaryRef properties,
); );
@ffi.Native<ffi.Bool Function(ffi.Pointer<CGImageDestination>)>() @ffi.Native<ffi.Bool Function(CGImageDestinationRef)>()
external bool CGImageDestinationFinalize(ffi.Pointer<CGImageDestination> idst); external bool CGImageDestinationFinalize(CGImageDestinationRef idst);
final class __CFAllocator extends ffi.Opaque {} 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 {} final class __CFDictionary extends ffi.Opaque {}
typedef CFDictionaryRef = ffi.Pointer<__CFDictionary>;
final class __CFData extends ffi.Opaque {} final class __CFData extends ffi.Opaque {}
typedef CFDataRef = ffi.Pointer<__CFData>;
typedef CFMutableDataRef = ffi.Pointer<__CFData>;
final class CGImageSource extends ffi.Opaque {} final class CGImageSource extends ffi.Opaque {}
typedef CGImageSourceRef = ffi.Pointer<CGImageSource>;
final class CGImage extends ffi.Opaque {} final class CGImage extends ffi.Opaque {}
typedef CGImageRef = ffi.Pointer<CGImage>;
final class CGImageDestination extends ffi.Opaque {} final class CGImageDestination extends ffi.Opaque {}
typedef CGImageDestinationRef = ffi.Pointer<CGImageDestination>;

View File

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

View File

@ -35,11 +35,18 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
OutputFormat format = OutputFormat.jpeg, OutputFormat format = OutputFormat.jpeg,
int quality = 100, int quality = 100,
}) async { }) async {
final inputPtr = calloc<Uint8>(inputData.length); Pointer<Uint8>? inputPtr;
CFDataRef? cfData;
CGImageSourceRef? imageSource;
CGImageRef? cgImage;
CFMutableDataRef? outputData;
CGImageDestinationRef? destination;
CFDictionaryRef? properties;
try { try {
inputPtr = calloc<Uint8>(inputData.length);
inputPtr.asTypedList(inputData.length).setAll(0, inputData); inputPtr.asTypedList(inputData.length).setAll(0, inputData);
final cfData = CFDataCreate( cfData = CFDataCreate(
kCFAllocatorDefault, kCFAllocatorDefault,
inputPtr.cast(), inputPtr.cast(),
inputData.length, inputData.length,
@ -48,17 +55,17 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
throw Exception('Failed to create CFData from input data.'); throw Exception('Failed to create CFData from input data.');
} }
final imageSource = CGImageSourceCreateWithData(cfData, nullptr); imageSource = CGImageSourceCreateWithData(cfData, nullptr);
if (imageSource == nullptr) { if (imageSource == nullptr) {
throw Exception('Failed to create CGImageSource. Invalid image data.'); throw Exception('Failed to create CGImageSource. Invalid image data.');
} }
final cgImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nullptr); cgImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nullptr);
if (cgImage == nullptr) { if (cgImage == nullptr) {
throw Exception('Failed to decode image.'); throw Exception('Failed to decode image.');
} }
final outputData = CFDataCreateMutable(kCFAllocatorDefault, 0); outputData = CFDataCreateMutable(kCFAllocatorDefault, 0);
if (outputData == nullptr) { if (outputData == nullptr) {
throw Exception('Failed to create output CFData.'); throw Exception('Failed to create output CFData.');
} }
@ -81,7 +88,7 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
.retainAndAutorelease() .retainAndAutorelease()
.cast<CFString>(); .cast<CFString>();
final destination = CGImageDestinationCreateWithData( destination = CGImageDestinationCreateWithData(
outputData, outputData,
cfString, cfString,
1, 1,
@ -91,7 +98,8 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
throw Exception('Failed to create CGImageDestination.'); throw Exception('Failed to create CGImageDestination.');
} }
CGImageDestinationAddImage(destination, cgImage, nullptr); properties = _createPropertiesForFormat(format, quality);
CGImageDestinationAddImage(destination, cgImage, properties ?? nullptr);
final success = CGImageDestinationFinalize(destination); final success = CGImageDestinationFinalize(destination);
if (!success) { if (!success) {
@ -106,7 +114,45 @@ final class ImageConverterDarwin implements ImageConverterPlatform {
return Uint8List.fromList(bytePtr.cast<Uint8>().asTypedList(length)); return Uint8List.fromList(bytePtr.cast<Uint8>().asTypedList(length));
} finally { } 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 | | | /// | jpeg | | |
/// | png | | | /// | png | | |
/// | webp | | | /// | webp | | |
/// | heic | | |
/// ///
/// **Notes:** /// **Notes:**
/// - [jpeg]: Good compression with adjustable quality /// - [jpeg]: Good compression with adjustable quality