From c60f74d4eb4e37c97fc92fd3065f8966e37a0bb7 Mon Sep 17 00:00:00 2001 From: Martin Bektchiev Date: Wed, 8 May 2019 13:56:45 +0300 Subject: [PATCH] fix(devtools-ios): Ensure UI modifications run on main thread Modifications to the UI can only be made from the main thread. Since {N} 5.3.0 all debugger protocol messages are processed by the worker thread that receives them in iOS. refs #7219, https://github.com/NativeScript/ios-runtime/pull/1101 --- tests/app/utils/utils-tests.ts | 32 +++++++++++++++++++ .../debugger/devtools-elements.common.ts | 15 +++++---- tns-core-modules/utils/utils-common.ts | 8 +++++ tns-core-modules/utils/utils.android.ts | 11 +++++++ tns-core-modules/utils/utils.d.ts | 20 ++++++++++++ tns-core-modules/utils/utils.ios.ts | 8 +++++ 6 files changed, 87 insertions(+), 7 deletions(-) diff --git a/tests/app/utils/utils-tests.ts b/tests/app/utils/utils-tests.ts index 10e07d498..3dba797a2 100644 --- a/tests/app/utils/utils-tests.ts +++ b/tests/app/utils/utils-tests.ts @@ -18,6 +18,38 @@ export function test_releaseNativeObject_canBeCalledWithNativeObject() { } }; + +export function test_executeOnMainThread_Works(done: Function) { + utils.executeOnMainThread(() => { + try { + TKUnit.assertTrue(utils.isMainThread()); + done(); + } catch (e) { + done(e); + } + }); +} + +export function test_mainThreadify_PassesArgs(done: Function) { + const expectedN = 434; + const expectedB = true; + const expectedS = "string"; + const f = utils.mainThreadify(function (n: number, b: boolean, s: string) { + try { + TKUnit.assertTrue(utils.isMainThread()); + TKUnit.assertEqual(n, expectedN); + TKUnit.assertEqual(b, expectedB); + TKUnit.assertEqual(s, expectedS); + done(); + } catch (e) { + done(e); + } + }); + + f(expectedN, expectedB, expectedS); +} + + function test_releaseNativeObject_canBeCalledWithNativeObject_iOS() { let deallocated = false; const obj = new ((NSObject).extend({ diff --git a/tns-core-modules/debugger/devtools-elements.common.ts b/tns-core-modules/debugger/devtools-elements.common.ts index 186cfe320..c0b7ac43f 100644 --- a/tns-core-modules/debugger/devtools-elements.common.ts +++ b/tns-core-modules/debugger/devtools-elements.common.ts @@ -2,6 +2,7 @@ import { getNodeById } from "./dom-node"; // Needed for typings only import { ViewBase } from "../ui/core/view-base"; +import { mainThreadify } from "../utils/utils"; // Use lazy requires for core modules const frameTopmost = () => require("../ui/frame").topmost(); @@ -11,7 +12,7 @@ function unsetViewValue(view, name) { if (!unsetValue) { unsetValue = require("../ui/core/properties").unsetValue; } - + view[name] = unsetValue; } @@ -30,10 +31,10 @@ export function getDocument() { if (!topMostFrame) { return undefined; } - + try { topMostFrame.ensureDomNode(); - + } catch (e) { console.log("ERROR in getDocument(): " + e); } @@ -49,7 +50,7 @@ export function getComputedStylesForNode(nodeId): Array<{ name: string, value: s return []; } -export function removeNode(nodeId) { +export const removeNode = mainThreadify(function removeNode(nodeId) { const view = getViewById(nodeId); if (view) { // Avoid importing layout and content view @@ -63,9 +64,9 @@ export function removeNode(nodeId) { console.log("Can't remove child from " + parent); } } -} +}); -export function setAttributeAsText(nodeId, text, name) { +export const setAttributeAsText = mainThreadify(function setAttributeAsText(nodeId, text, name) { const view = getViewById(nodeId); if (view) { // attribute is registered for the view instance @@ -93,4 +94,4 @@ export function setAttributeAsText(nodeId, text, name) { view.domNode.loadAttributes(); } -} +}); diff --git a/tns-core-modules/utils/utils-common.ts b/tns-core-modules/utils/utils-common.ts index 97829e166..d94022b0f 100644 --- a/tns-core-modules/utils/utils-common.ts +++ b/tns-core-modules/utils/utils-common.ts @@ -1,4 +1,5 @@ import * as types from "./types"; +import { executeOnMainThread } from "./utils" export const RESOURCE_PREFIX = "res://"; export const FILE_PREFIX = "file:///"; @@ -154,3 +155,10 @@ export function hasDuplicates(arr: Array): boolean { export function eliminateDuplicates(arr: Array): Array { return Array.from(new Set(arr)); } + +export function mainThreadify(func: Function): (...args: any[]) => void { + return function () { + const argsToPass = arguments; + executeOnMainThread(() => func.apply(this, argsToPass)); + } +} diff --git a/tns-core-modules/utils/utils.android.ts b/tns-core-modules/utils/utils.android.ts index b050042f9..196dedcda 100644 --- a/tns-core-modules/utils/utils.android.ts +++ b/tns-core-modules/utils/utils.android.ts @@ -372,3 +372,14 @@ Please ensure you have your manifest correctly configured with the FileProvider. return false; } } + +export function executeOnMainThread(func: () => void) { + new android.os.Handler(android.os.Looper.getMainLooper()) + .post(new java.lang.Runnable({ + run: func + })); +} + +export function isMainThread(): Boolean { + return android.os.Looper.myLooper() === android.os.Looper.getMainLooper(); +} diff --git a/tns-core-modules/utils/utils.d.ts b/tns-core-modules/utils/utils.d.ts index 6058a7508..308eefc0b 100644 --- a/tns-core-modules/utils/utils.d.ts +++ b/tns-core-modules/utils/utils.d.ts @@ -269,6 +269,26 @@ export function GC(); */ export function releaseNativeObject(object: any /*java.lang.Object | NSObject*/); +/** + * Dispatches the passed function for execution on the main thread + * @param func The function to execute on the main thread. + */ +export function executeOnMainThread(func: Function); + +/** + * Returns a function wrapper which executes the supplied function on the main thread. + * The wrapper behaves like the original function and passes all of its arguments BUT + * discards its return value. + * @param func The function to execute on the main thread + * @returns The wrapper function which schedules execution to the main thread + */ +export function mainThreadify(func: Function): (...args: any[]) => void + +/** + * @returns Boolean value indicating whether the current thread is the main thread + */ +export function isMainThread(): boolean + /** * Returns true if the specified path points to a resource or local file. * @param path The path. diff --git a/tns-core-modules/utils/utils.ios.ts b/tns-core-modules/utils/utils.ios.ts index 941782f16..95ccbe318 100644 --- a/tns-core-modules/utils/utils.ios.ts +++ b/tns-core-modules/utils/utils.ios.ts @@ -159,6 +159,14 @@ export function openUrl(location: string): boolean { return false; } +export function executeOnMainThread(func: () => void) { + NSOperationQueue.mainQueue.addOperationWithBlock(func); +} + +export function isMainThread(): Boolean { + return NSThread.isMainThread; +} + class UIDocumentInteractionControllerDelegateImpl extends NSObject implements UIDocumentInteractionControllerDelegate { public static ObjCProtocols = [UIDocumentInteractionControllerDelegate];