From 8b4a9b3c6bb62182d278a1d2d546a4c4a13e8037 Mon Sep 17 00:00:00 2001 From: Martin Bektchiev Date: Fri, 8 Feb 2019 10:42:51 +0200 Subject: [PATCH] feat: Provide API to release the native object wrapped by a JS one (#6873) * Add `releaseNativeObject` function in `utils` * Add tests * Add typings for the global `__releaseNativeCounterpart` functions provided by Android and iOS runtimes refs https://github.com/NativeScript/ios-runtime/issues/1062 and https://github.com/NativeScript/android-runtime/issues/1254 --- tests/app/TKUnit.ts | 19 ++++++-- tests/app/testRunner.ts | 5 ++- tests/app/utils/utils-tests.ts | 44 +++++++++++++++++++ tns-core-modules/utils/utils.android.ts | 4 ++ tns-core-modules/utils/utils.d.ts | 14 ++++-- tns-core-modules/utils/utils.ios.ts | 8 +++- .../android/android-declarations.d.ts | 10 ++++- tns-platform-declarations/ios/ios.d.ts | 3 +- tns-platform-declarations/ios/runtime.d.ts | 17 +++++++ tns-platform-declarations/typings-gen.sh | 4 +- 10 files changed, 111 insertions(+), 17 deletions(-) create mode 100644 tests/app/utils/utils-tests.ts create mode 100644 tns-platform-declarations/ios/runtime.d.ts diff --git a/tests/app/TKUnit.ts b/tests/app/TKUnit.ts index f7645a978..72e4ea164 100644 --- a/tests/app/TKUnit.ts +++ b/tests/app/TKUnit.ts @@ -7,7 +7,7 @@ 4. tests should use TKUnit.assert(condition, message) to mark error. If no assert fails test is successful 5. (if exists) at the end of each test tearDown() module function is called 6. (if exists) at the end of module test tearDownModule() module function is called - + */ import * as Application from "tns-core-modules/application"; @@ -317,6 +317,12 @@ export function assertAreClose(actual: number, expected: number, delta: number, } } +export function assertMatches(actual: string, expected: RegExp, message?: string) { + if (expected.test(actual) !== true) { + throw new Error(`"${actual}" doesn't match "${expected}". ${message}`); + } +} + export function arrayAssert(actual: Array, expected: Array, message?: string) { if (actual.length !== expected.length) { throw new Error(message + " Actual array length: " + actual.length + " Expected array length: " + expected.length); @@ -330,6 +336,11 @@ export function arrayAssert(actual: Array, expected: Array, message?: } export function assertThrows(testFunc: () => void, assertMessage?: string, expectedMessage?: string) { + const re = expectedMessage ? new RegExp(`^${expectedMessage}$`) : null; + return assertThrowsRegExp(testFunc, assertMessage, re); +} + +export function assertThrowsRegExp(testFunc: () => void, assertMessage?: string, expectedMessage?: RegExp) { let actualError: Error; try { testFunc(); @@ -341,8 +352,8 @@ export function assertThrows(testFunc: () => void, assertMessage?: string, expec throw new Error("Missing expected exception. " + assertMessage); } - if (expectedMessage && actualError.message !== expectedMessage) { - throw new Error("Got unwanted exception. Actual error: " + actualError.message + " Expected error: " + expectedMessage); + if (expectedMessage && !expectedMessage.test(actualError.message)) { + throw new Error("Got unwanted exception. Actual error: " + actualError.message + " Expected to match: " + expectedMessage); } } @@ -455,4 +466,4 @@ function doModalAndroid(quitLoop: () => boolean, timeoutSec: number, shouldThrow quit = true; } } -} \ No newline at end of file +} diff --git a/tests/app/testRunner.ts b/tests/app/testRunner.ts index b96bde95d..f79e2eb44 100644 --- a/tests/app/testRunner.ts +++ b/tests/app/testRunner.ts @@ -264,6 +264,9 @@ allTests["RESET-ROOT-VIEW"] = resetRootViewTests; import * as rootViewTests from "./ui/root-view/root-view-tests"; allTests["ROOT-VIEW"] = rootViewTests; +import * as utilsTests from "./utils/utils-tests"; +allTests["UTILS"] = utilsTests; + const testsSuitesWithLongDelay = { HTTP: 15 * 1000, } @@ -504,4 +507,4 @@ class TestInfo implements TKUnit.TestInfoEntry { this.testTimeout = testTimeout; this.duration = duration; } -} \ No newline at end of file +} diff --git a/tests/app/utils/utils-tests.ts b/tests/app/utils/utils-tests.ts new file mode 100644 index 000000000..10e07d498 --- /dev/null +++ b/tests/app/utils/utils-tests.ts @@ -0,0 +1,44 @@ +import * as TKUnit from "../TKUnit"; +import * as utils from "tns-core-modules/utils/utils"; +import { isIOS } from "tns-core-modules/platform"; + +export function test_GC_isDefined() { + TKUnit.assertNotEqual(utils.GC, undefined, "Method utils.GC() should be defined!"); +}; + +export function test_releaseNativeObject_isDefined() { + TKUnit.assertNotEqual(utils.releaseNativeObject, undefined, "Method utils.releaseNativeObject() should be defined!"); +}; + +export function test_releaseNativeObject_canBeCalledWithNativeObject() { + if (isIOS) { + test_releaseNativeObject_canBeCalledWithNativeObject_iOS(); + } else { + test_releaseNativeObject_canBeCalledWithNativeObject_Android(); + } +}; + +function test_releaseNativeObject_canBeCalledWithNativeObject_iOS() { + let deallocated = false; + const obj = new ((NSObject).extend({ + dealloc: function () { + deallocated = true; + } + })); + TKUnit.assertMatches(obj.description, /NSObject/, "Object description should match!") + + utils.releaseNativeObject(obj); + + // Need to sleep to make the delayed release get executed + NSThread.sleepForTimeInterval(0); + TKUnit.assertTrue(deallocated, "NativeObject must have been deallocated!"); +} + +function test_releaseNativeObject_canBeCalledWithNativeObject_Android() { + const obj = new java.lang.Object(); + TKUnit.assertMatches(obj.toString(), /java.lang.Object/, "Object description should match!") + + utils.releaseNativeObject(obj); + + TKUnit.assertThrowsRegExp(obj.toString.bind(obj), "Should throw an error!", /Failed calling toString on a java\/lang\/Object instance/); +} diff --git a/tns-core-modules/utils/utils.android.ts b/tns-core-modules/utils/utils.android.ts index 1407209d0..c2b9db852 100644 --- a/tns-core-modules/utils/utils.android.ts +++ b/tns-core-modules/utils/utils.android.ts @@ -206,6 +206,10 @@ export function GC() { gc(); } +export function releaseNativeObject(object: java.lang.Object) { + __releaseNativeCounterpart(object); +} + export function openUrl(location: string): boolean { const context = ad.getApplicationContext(); try { diff --git a/tns-core-modules/utils/utils.d.ts b/tns-core-modules/utils/utils.d.ts index 5f095b4d4..7ae9e8257 100644 --- a/tns-core-modules/utils/utils.d.ts +++ b/tns-core-modules/utils/utils.d.ts @@ -191,7 +191,7 @@ export module ios { */ export function getter(_this: any, propertyValue: T | {(): T}): T; - // Common properties between UILabel, UITextView and UITextField + // Common properties between UILabel, UITextView and UITextField export interface TextUIView { font: any; textAlignment: number; @@ -261,6 +261,12 @@ export module ios { */ export function GC(); +/** + * Releases the reference to the wrapped native object + * @param object The Java/Objective-C object to release. + */ +export function releaseNativeObject(object: any /*java.lang.Object | NSObject*/); + /** * Returns true if the specified path points to a resource or local file. * @param path The path. @@ -281,13 +287,13 @@ export function openUrl(url: string): boolean /** * Escapes special regex symbols (., *, ^, $ and so on) in string in order to create a valid regex from it. - * @param source The original value. + * @param source The original value. */ export function escapeRegexSymbols(source: string): string /** * Converts string value to number or boolean. - * @param value The original value. + * @param value The original value. */ export function convertString(value: any): any @@ -308,4 +314,4 @@ export function hasDuplicates(arr: Array): boolean; * Removes duplicate elements from array. * @param arr - The array. */ -export function eliminateDuplicates(arr: Array): Array; \ No newline at end of file +export function eliminateDuplicates(arr: Array): Array; diff --git a/tns-core-modules/utils/utils.ios.ts b/tns-core-modules/utils/utils.ios.ts index 8665fb40d..53c2c19a7 100644 --- a/tns-core-modules/utils/utils.ios.ts +++ b/tns-core-modules/utils/utils.ios.ts @@ -105,7 +105,7 @@ export module ios { // Strip part after tns_modules to obtain app root appPath = currentDir.substring(0, tnsModulesIndex); } - + return appPath; } @@ -140,6 +140,10 @@ export function GC() { __collect(); } +export function releaseNativeObject(object: NSObject) { + __releaseNativeCounterpart(object); +} + export function openUrl(location: string): boolean { try { var url = NSURL.URLWithString(location.trim()); @@ -175,4 +179,4 @@ class UIDocumentInteractionControllerDelegateImpl extends NSObject implements UI } } -mainScreenScale = ios.getter(UIScreen, UIScreen.mainScreen).scale; \ No newline at end of file +mainScreenScale = ios.getter(UIScreen, UIScreen.mainScreen).scale; diff --git a/tns-platform-declarations/android/android-declarations.d.ts b/tns-platform-declarations/android/android-declarations.d.ts index a58c10f5e..dc99141b3 100644 --- a/tns-platform-declarations/android/android-declarations.d.ts +++ b/tns-platform-declarations/android/android-declarations.d.ts @@ -3,10 +3,16 @@ declare function float(num: number): any; declare function long(num: number): any; +/** + * Triggers garbage collection in JavaScript + */ declare var gc: () => void; -declare function float(num: number): any; -declare function long(num: number): any; +/** + * Releases the reference to the wrapped native object + * @param object The Java object to release. + */ +declare function __releaseNativeCounterpart(object: java.lang.Object): void; interface ArrayConstructor { create(type: any, count: number): any; diff --git a/tns-platform-declarations/ios/ios.d.ts b/tns-platform-declarations/ios/ios.d.ts index 2d38f6e2c..9b3b38c92 100644 --- a/tns-platform-declarations/ios/ios.d.ts +++ b/tns-platform-declarations/ios/ios.d.ts @@ -1,4 +1,4 @@ -/// +/// /// /// /// @@ -126,4 +126,3 @@ /// /// /// -declare function __collect(): void; diff --git a/tns-platform-declarations/ios/runtime.d.ts b/tns-platform-declarations/ios/runtime.d.ts new file mode 100644 index 000000000..18d517e06 --- /dev/null +++ b/tns-platform-declarations/ios/runtime.d.ts @@ -0,0 +1,17 @@ +/// + +/** + * Triggers garbage collection in JavaScript + */ +declare function __collect(): void; + +/** + * Releases the reference to the wrapped native object + * @param object The Objective-C object to release. + */ +declare function __releaseNativeCounterpart(object: NSObject): void; + +/** + * Gets accurate system timestamp in ms. + */ +declare function __time(): Number; diff --git a/tns-platform-declarations/typings-gen.sh b/tns-platform-declarations/typings-gen.sh index f119013c9..22600ac46 100755 --- a/tns-platform-declarations/typings-gen.sh +++ b/tns-platform-declarations/typings-gen.sh @@ -42,11 +42,11 @@ mv ios-typings-prj/typings/x86_64/* ios/objc-x86_64/ echo "Emitting (ios/ios.d.ts)..." pushd ios -echo '/// ' > ios.d.ts + +echo '/// ' > ios.d.ts for i in `ls objc-x86_64/*.d.ts`; do echo "/// " >> ios.d.ts done -echo 'declare function __collect(): void;' >> ios.d.ts popd