perf(ios): UIImage memory leaks (#9783)

This commit is contained in:
Igor Randjelovic
2022-02-18 23:55:14 +01:00
committed by Nathan Walker
parent f37b0160ed
commit 988f372788
6 changed files with 65 additions and 10 deletions

View File

@ -17,6 +17,7 @@ export class AccessibilityModel extends Observable {
accessibilityLiveRegions = AccessibilityLiveRegion; accessibilityLiveRegions = AccessibilityLiveRegion;
accessibilityRole = AccessibilityRole; accessibilityRole = AccessibilityRole;
accessibilityState = AccessibilityState; accessibilityState = AccessibilityState;
largeImageSrc = 'https://i.picsum.photos/id/669/5000/5000.jpg?hmac=VlpchW0ODhflKm0SKOYQrc8qysLWbqKmDS1MGT9apAc';
constructor() { constructor() {
super(); super();
@ -26,6 +27,11 @@ export class AccessibilityModel extends Observable {
const checked = (args.object as Switch).checked; const checked = (args.object as Switch).checked;
console.log(checked); console.log(checked);
this.set('switchCheckedText', `${this.labelText} ${checked}`); this.set('switchCheckedText', `${this.labelText} ${checked}`);
// prettier-ignore
this.set('largeImageSrc', checked ?
'https://i.picsum.photos/id/669/5000/5000.jpg?hmac=VlpchW0ODhflKm0SKOYQrc8qysLWbqKmDS1MGT9apAc' :
'https://i.picsum.photos/id/684/5000/5000.jpg?hmac=loiXO_OQ-y86XY_hc7p3qJdY39fSd9CuDM0iA_--P4Q');
} }
openModal() { openModal() {

View File

@ -13,12 +13,15 @@
<Image src="res://icon" width="50" class="view-item a11y" accessibilityLabel="Image with explicit attribute role" accessibilityRole="{{accessibilityRole.Image}}" /> <Image src="res://icon" width="50" class="view-item a11y" accessibilityLabel="Image with explicit attribute role" accessibilityRole="{{accessibilityRole.Image}}" />
<Image src="res://icon" width="50" class="view-item a11y a11y-role-image" accessibilityLabel="Image with css defined role" /> <Image src="res://icon" width="50" class="view-item a11y a11y-role-image" accessibilityLabel="Image with css defined role" />
<Image src="{{ largeImageSrc }}" width="50" class="view-item a11y a11y-role-image" accessibilityLabel="Image with css defined role" />
<Switch checked="true" class="view-item a11y" accessibilityLabel="Switch with attribute state" accessibilityState="{{accessibilityState.Checked}}" checkedChange="{{checkedChange}}" /> <Switch checked="true" class="view-item a11y" accessibilityLabel="Switch with attribute state" accessibilityState="{{accessibilityState.Checked}}" checkedChange="{{checkedChange}}" />
<Switch checked="true" class="view-item a11y a11y-state-checked" accessibilityLabel="Switch with css state" checkedChange="{{checkedChange}}" /> <Switch checked="true" class="view-item a11y a11y-state-checked" accessibilityLabel="Switch with css state" checkedChange="{{checkedChange}}" />
<TextView hint="TextView" text="{{switchCheckedText}}" class="view-item a11y" accessibilityLabel="TestView with a value" accessibilityLiveRegion="{{accessibilityLiveRegions.Polite}}"/> <TextView hint="TextView" text="{{switchCheckedText}}" class="view-item a11y" accessibilityLabel="TestView with a value" accessibilityLiveRegion="{{accessibilityLiveRegions.Polite}}" />
<TextField hint="TextField" class="view-item a11y" accessibilityLabel="Plain jane TextField" accessibilityHint="Tell us your real name Jane"/> <TextField hint="TextField" class="view-item a11y" accessibilityLabel="Plain jane TextField" accessibilityHint="Tell us your real name Jane" />
<TextView hint="TextView" class="view-item a11y" accessibilityLabel="Nice TextView" accessibilityHint="Tell us about yourself Jane"/> <TextView hint="TextView" class="view-item a11y" accessibilityLabel="Nice TextView" accessibilityHint="Tell us about yourself Jane" />
<GridLayout rows="25" columns="*" class="view-item" accessibilityLabel="No can go GridLayout" accessibilityHint="A grid that will not get bigger when increasing accessible text size"> <GridLayout rows="25" columns="*" class="view-item" accessibilityLabel="No can go GridLayout" accessibilityHint="A grid that will not get bigger when increasing accessible text size">
<Label text="IN-Accessible Grid" class="view-item text-center" /> <Label text="IN-Accessible Grid" class="view-item text-center" />
</GridLayout> </GridLayout>
@ -28,7 +31,7 @@
<Label rowSpan="2" col="1" text="Hi" /> <Label rowSpan="2" col="1" text="Hi" />
</GridLayout> </GridLayout>
<Button text="Open Modal" class="view-item" tap="{{openModal}}" /> <Button text="Open Modal" class="view-item" tap="{{openModal}}" />
<Slider value="10" minValue="0" maxValue="100" class="view-item a11y" accessibilityLabel="Slider" accessibilityHint="A smooth slider" accessibilityValue="10"/> <Slider value="10" minValue="0" maxValue="100" class="view-item a11y" accessibilityLabel="Slider" accessibilityHint="A smooth slider" accessibilityValue="10" />
</StackLayout> </StackLayout>
</ScrollView> </ScrollView>
</GridLayout> </GridLayout>

View File

@ -28,10 +28,16 @@ export abstract class ImageBase extends View implements ImageDefinition {
this.style.tintColor = value; this.style.tintColor = value;
} }
public disposeImageSource() {
// override in subclass
}
/** /**
* @internal * @internal
*/ */
public _createImageSourceFromSrc(value: string | ImageSource | ImageAsset): void { public _createImageSourceFromSrc(value: string | ImageSource | ImageAsset): void {
this.disposeImageSource();
const originalValue = value; const originalValue = value;
const sync = this.loadMode === 'sync'; const sync = this.loadMode === 'sync';
if (typeof value === 'string' || value instanceof String) { if (typeof value === 'string' || value instanceof String) {

View File

@ -2,7 +2,7 @@ import { ImageBase, stretchProperty, imageSourceProperty, tintColorProperty, src
import { ImageSource } from '../../image-source'; import { ImageSource } from '../../image-source';
import { Color } from '../../color'; import { Color } from '../../color';
import { Trace } from '../../trace'; import { Trace } from '../../trace';
import { layout } from '../../utils'; import { layout, queueGC } from '../../utils';
export * from './image-common'; export * from './image-common';
@ -24,21 +24,28 @@ export class Image extends ImageBase {
this._setNativeClipToBounds(); this._setNativeClipToBounds();
} }
public disposeNativeView(): void { public disposeImageSource() {
super.disposeNativeView(); if (this.nativeViewProtected?.image === this.imageSource?.ios) {
this.nativeViewProtected.image = null;
}
if (this.imageSource?.ios) { if (this.imageSource?.ios) {
this.imageSource.ios = null; this.imageSource.ios = null;
// causes crash currently:
// release the native UIImage
// CFRelease(this.imageSource.ios);
} }
this.imageSource = null; this.imageSource = null;
queueGC();
}
public disposeNativeView(): void {
super.disposeNativeView();
if (this.nativeViewProtected?.image) { if (this.nativeViewProtected?.image) {
this.nativeViewProtected.image = null; this.nativeViewProtected.image = null;
} }
this.disposeImageSource();
} }
private setTintColor(value: Color) { private setTintColor(value: Color) {
@ -46,15 +53,23 @@ export class Image extends ImageBase {
if (value && this.nativeViewProtected.image && !this._templateImageWasCreated) { if (value && this.nativeViewProtected.image && !this._templateImageWasCreated) {
this.nativeViewProtected.image = this.nativeViewProtected.image.imageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate); this.nativeViewProtected.image = this.nativeViewProtected.image.imageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate);
this._templateImageWasCreated = true; this._templateImageWasCreated = true;
queueGC();
} else if (!value && this.nativeViewProtected.image && this._templateImageWasCreated) { } else if (!value && this.nativeViewProtected.image && this._templateImageWasCreated) {
this._templateImageWasCreated = false; this._templateImageWasCreated = false;
this.nativeViewProtected.image = this.nativeViewProtected.image.imageWithRenderingMode(UIImageRenderingMode.Automatic); this.nativeViewProtected.image = this.nativeViewProtected.image.imageWithRenderingMode(UIImageRenderingMode.Automatic);
queueGC();
} }
this.nativeViewProtected.tintColor = value ? value.ios : null; this.nativeViewProtected.tintColor = value ? value.ios : null;
} }
} }
public _setNativeImage(nativeImage: UIImage) { public _setNativeImage(nativeImage: UIImage) {
if (this.nativeViewProtected?.image) {
this.nativeViewProtected.image = null;
queueGC();
}
if (this.nativeViewProtected) { if (this.nativeViewProtected) {
this.nativeViewProtected.image = nativeImage; this.nativeViewProtected.image = nativeImage;
} }
@ -169,6 +184,10 @@ export class Image extends ImageBase {
} }
[imageSourceProperty.setNative](value: ImageSource) { [imageSourceProperty.setNative](value: ImageSource) {
if (value !== this.imageSource) {
this.disposeImageSource();
}
this._setNativeImage(value ? value.ios : null); this._setNativeImage(value ? value.ios : null);
} }

View File

@ -187,6 +187,11 @@ export namespace ad {
*/ */
export function GC(); export function GC();
/**
* An utility function that queues a garbage collection, subseqent calls will be throttled and only one gc will be executed.
*/
export function queueGC();
/** /**
* Releases the reference to the wrapped native object * Releases the reference to the wrapped native object
* @param object The Java/Objective-C object to release. * @param object The Java/Objective-C object to release.

View File

@ -3,6 +3,8 @@ import { dispatchToMainThread, isMainThread } from './mainthread-helper';
import { sanitizeModuleName } from '../ui/builder/module-name-sanitizer'; import { sanitizeModuleName } from '../ui/builder/module-name-sanitizer';
import * as layout from './layout-helper'; import * as layout from './layout-helper';
import { GC } from './index';
export { layout }; export { layout };
export * from './mainthread-helper'; export * from './mainthread-helper';
export * from './macrotask-scheduler'; export * from './macrotask-scheduler';
@ -129,3 +131,17 @@ export function mainThreadify(func: Function): (...args: any[]) => void {
executeOnMainThread(() => func.apply(this, argsToPass)); executeOnMainThread(() => func.apply(this, argsToPass));
}; };
} }
let hasQueuedGC = false;
export function queueGC() {
if (hasQueuedGC) {
return;
}
hasQueuedGC = true;
setTimeout(() => {
hasQueuedGC = false;
GC();
}, 1000);
}