Skip to content

Commit 81a84fb

Browse files
authored
Alternative value subscription implementation (#310)
1 parent 4eb8795 commit 81a84fb

File tree

12 files changed

+147
-102
lines changed

12 files changed

+147
-102
lines changed

package/cpp/rnskia/RNSkJsiViewApi.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,11 @@ class RNSkJsiViewApi : public JsiHostObject {
197197
: JsiHostObject(), _platformContext(platformContext) {}
198198

199199
/**
200-
* Destructor
200+
* Invalidates the api object
201201
*/
202-
~RNSkJsiViewApi() { unregisterAll(); }
202+
void invalidate() {
203+
unregisterAll();
204+
}
203205

204206
/**
205207
Call to remove all draw view infos

package/cpp/rnskia/RNSkManager.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ void RNSkManager::invalidate() {
3333
}
3434
_isInvalidated = true;
3535

36-
// We need to unregister all views when we get here
37-
_viewApi->unregisterAll();
36+
// Invalidate members
37+
_viewApi->invalidate();
3838
_platformContext->invalidate();
3939
}
4040

package/cpp/rnskia/values/RNSkDerivedValue.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
#include <algorithm>
1010
#include <functional>
1111
#include <chrono>
12-
#include <mutex>
1312

1413
namespace RNSkia
1514
{
1615
using namespace facebook;
16+
1717
/**
1818
Creates a readonly value that depends on one or more other values. The derived value has a callback
1919
function that is used to calculate the new value when any of the dependencies change.

package/cpp/rnskia/values/RNSkReadonlyValue.h

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@ class RNSkReadonlyValue : public JsiHostObject
2626
RNSkReadonlyValue(std::shared_ptr<RNSkPlatformContext> platformContext)
2727
: JsiHostObject(),
2828
_platformContext(platformContext),
29-
_propNameId(jsi::PropNameID::forUtf8(*platformContext->getJsRuntime(), "value"))
30-
{}
31-
29+
_propNameId(jsi::PropNameID::forUtf8(*platformContext->getJsRuntime(), "value")) {}
30+
31+
~RNSkReadonlyValue() {
32+
_invalidated = true;
33+
}
34+
3235
JSI_PROPERTY_GET(__typename__) {
3336
return jsi::String::createFromUtf8(runtime, "RNSkValue");
3437
}
@@ -73,7 +76,9 @@ class RNSkReadonlyValue : public JsiHostObject
7376
auto listenerId = _listenerId++;
7477
_listeners.emplace(listenerId, cb);
7578
return [this, listenerId]() {
76-
removeListener(listenerId);
79+
if(!_invalidated) {
80+
removeListener(listenerId);
81+
}
7782
};
7883
}
7984

@@ -128,6 +133,8 @@ class RNSkReadonlyValue : public JsiHostObject
128133
const std::shared_ptr<RNSkPlatformContext> getPlatformContext() {
129134
return _platformContext;
130135
}
136+
137+
std::atomic<bool> _invalidated = { false };
131138

132139
private:
133140
jsi::PropNameID _propNameId;

package/ios/RNSkia-iOS/SkiaDrawView.mm

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#import "RCTBridge.h"
2+
13
#import <SkiaDrawView.h>
24
#import <RNSkDrawViewImpl.h>
35
#import <RNSkManager.h>
@@ -20,6 +22,15 @@ - (instancetype) initWithManager: (RNSkia::RNSkManager*)manager;
2022
_nativeId = 0;
2123
_debugMode = false;
2224
_drawingMode = RNSkia::RNSkDrawingMode::Default;
25+
// Listen to notifications about module invalidation
26+
auto nc = [NSNotificationCenter defaultCenter];
27+
[nc addObserverForName:RCTBridgeWillInvalidateModulesNotification
28+
object:nil
29+
queue:nil
30+
usingBlock:^(NSNotification *notification){
31+
// Remove local variables
32+
self->_manager = nullptr;
33+
}];
2334
}
2435
return self;
2536
}
@@ -38,7 +49,7 @@ - (void) willMoveToWindow:(UIWindow *)newWindow {
3849
if (newWindow == NULL) {
3950
// Remove implementation view when the parent view is not set
4051
if(_impl != nullptr) {
41-
if(_nativeId != 0) {
52+
if(_nativeId != 0 && _manager != nullptr) {
4253
_manager->setSkiaDrawView(_nativeId, nullptr);
4354
}
4455
_impl = nullptr;

package/src/renderer/Canvas.tsx

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ import type { FontMgr } from "../skia/FontMgr/FontMgr";
2727
import { debug as hostDebug, skHostConfig } from "./HostConfig";
2828
// import { debugTree } from "./nodes";
2929
import { vec } from "./processors";
30-
import { createDependencyManager } from "./DependencyManager";
3130
import { Container } from "./Host";
31+
import { DependencyManager } from "./DependencyManager";
3232

3333
// useContextBridge() is taken from https://github.com/pmndrs/drei#usecontextbridge
3434
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -72,14 +72,11 @@ skiaReconciler.injectIntoDevTools({
7272
rendererPackageName: "react-native-skia",
7373
});
7474

75-
const render = (
76-
element: ReactNode,
77-
container: OpaqueRoot,
78-
update: () => void
79-
) => {
80-
skiaReconciler.updateContainer(element, container, null, () => {
75+
const render = (element: ReactNode, root: OpaqueRoot, container: Container) => {
76+
skiaReconciler.updateContainer(element, root, null, () => {
8177
hostDebug("updateContainer");
82-
update();
78+
79+
container.depMgr.subscribe();
8380
});
8481
};
8582

@@ -99,34 +96,35 @@ export const Canvas = forwardRef<SkiaView, CanvasProps>(
9996
const [tick, setTick] = useState(0);
10097
const redraw = useCallback(() => setTick((t) => t + 1), []);
10198

102-
const tree = useMemo(() => new Container(redraw), [redraw]);
99+
const container = useMemo(
100+
() => new Container(new DependencyManager(ref), redraw),
101+
[redraw, ref]
102+
);
103103

104104
const canvasCtx = useRef({ width: 0, height: 0 });
105-
const container = useMemo(
106-
() => skiaReconciler.createContainer(tree, 0, false, null),
107-
[tree]
105+
const root = useMemo(
106+
() => skiaReconciler.createContainer(container, 0, false, null),
107+
[container]
108108
);
109109
// Render effect
110110
useEffect(() => {
111111
render(
112112
<CanvasContext.Provider value={canvasCtx.current}>
113113
{children}
114114
</CanvasContext.Provider>,
115-
container,
116-
redraw
115+
root,
116+
container
117117
);
118-
}, [children, container, redraw]);
119-
120-
const depsManager = useMemo(() => createDependencyManager(ref), [ref]);
118+
}, [children, root, redraw, container]);
121119

122120
// Draw callback
123121
const onDraw = useDrawCallback(
124122
(canvas, info) => {
125123
// TODO: if tree is empty (count === 1) maybe we should not render?
126124
const { width, height, timestamp } = info;
127-
canvasCtx.current.width = width;
128-
canvasCtx.current.height = height;
129-
onTouch && onTouch(info.touches);
125+
if (onTouch) {
126+
onTouch(info.touches);
127+
}
130128
const paint = Skia.Paint();
131129
paint.setAntiAlias(true);
132130
const ctx = {
@@ -140,21 +138,17 @@ export const Canvas = forwardRef<SkiaView, CanvasProps>(
140138
center: vec(width / 2, height / 2),
141139
fontMgr: fontMgr ?? Skia.FontMgr.RefDefault(),
142140
};
143-
tree.draw(ctx);
141+
canvasCtx.current = ctx;
142+
container.draw(ctx);
144143
},
145144
[tick, onTouch]
146145
);
147146

148-
// Handle value dependency registration of values to the underlying
149-
// SkiaView. Every time the tree changes (children), we will visit all
150-
// our children and register their dependencies.
151147
useEffect(() => {
152-
// Register all values in the current tree
153-
depsManager.visitChildren(tree);
154-
// Subscribe / return unsubscribe function
155-
depsManager.subscribe();
156-
return depsManager.unsubscribe;
157-
}, [depsManager, tree, children]);
148+
return () => {
149+
container.depMgr.unsubscribe();
150+
};
151+
}, [container]);
158152

159153
return (
160154
<SkiaView

package/src/renderer/DependencyManager.tsx

Lines changed: 51 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,57 @@ import type { RefObject } from "react";
33
import type { SkiaView } from "../views";
44
import type { SkiaReadonlyValue } from "../values";
55

6+
import { isValue } from "./processors";
67
import type { Node } from "./Host";
7-
import { isValue, processProps } from "./processors";
8-
9-
export const createDependencyManager = (ref: RefObject<SkiaView>) => {
10-
const values: SkiaReadonlyValue<unknown>[] = [];
11-
const unsubscribe: Array<() => void> = [];
12-
13-
return {
14-
visitChildren: function (node: Node<unknown>) {
15-
processProps(node.props, (value) => {
16-
if (isValue(value)) {
17-
this.registerValue(value);
18-
}
19-
});
20-
node.children.forEach((c) => this.visitChildren(c));
21-
},
22-
registerValue: function <T>(value: SkiaReadonlyValue<T>) {
23-
if (!ref.current) {
24-
throw new Error("Canvas ref is not set");
25-
}
26-
if (values.indexOf(value) === -1) {
27-
values.push(value);
28-
}
29-
},
30-
subscribe: function () {
31-
if (!ref.current) {
32-
throw new Error("Canvas ref is not set");
33-
}
34-
if (values.length === 0) {
35-
return;
8+
9+
type Unsubscribe = () => void;
10+
type Props = { [key: string]: unknown };
11+
12+
export class DependencyManager {
13+
ref: RefObject<SkiaView>;
14+
subscriptions: Map<
15+
Node,
16+
{ values: SkiaReadonlyValue<unknown>[]; unsubscribe: null | Unsubscribe }
17+
> = new Map();
18+
19+
constructor(ref: RefObject<SkiaView>) {
20+
this.ref = ref;
21+
}
22+
23+
unSubscribeNode(node: Node) {
24+
const subscription = this.subscriptions.get(node);
25+
if (subscription && subscription.unsubscribe) {
26+
subscription.unsubscribe();
27+
}
28+
this.subscriptions.delete(node);
29+
}
30+
31+
subscribeNode(node: Node, props: Props) {
32+
const values = Object.values(props).filter(isValue);
33+
if (values.length > 0) {
34+
this.subscriptions.set(node, { values, unsubscribe: null });
35+
}
36+
}
37+
38+
subscribe() {
39+
if (this.ref.current === null) {
40+
throw new Error("Canvas ref is not set");
41+
}
42+
this.subscriptions.forEach((subscription) => {
43+
if (subscription.unsubscribe === null) {
44+
subscription.unsubscribe = this.ref.current!.registerValues(
45+
subscription.values
46+
);
3647
}
37-
unsubscribe.push(ref.current.registerValues(values));
38-
values.splice(0, values.length);
39-
},
40-
unsubscribe: function () {
41-
if (unsubscribe.length === 0) {
42-
return;
48+
});
49+
}
50+
51+
unsubscribe() {
52+
this.subscriptions.forEach(({ unsubscribe }) => {
53+
if (unsubscribe) {
54+
unsubscribe();
4355
}
44-
unsubscribe.forEach((unsub) => unsub());
45-
unsubscribe.splice(0, unsubscribe.length);
46-
},
47-
};
48-
};
56+
});
57+
this.subscriptions.clear();
58+
}
59+
}

package/src/renderer/Host.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { DrawingContext } from "./DrawingContext";
44
import type { DeclarationResult, DeclarationProps } from "./nodes/Declaration";
55
import type { DrawingProps } from "./nodes";
66
import type { AnimatedProps } from "./processors/Animations/Animations";
7+
import type { DependencyManager } from "./DependencyManager";
78

89
export enum NodeType {
910
Declaration = "skDeclaration",
@@ -16,14 +17,20 @@ export abstract class Node<P = unknown> {
1617
memoizable = false;
1718
memoized = false;
1819
parent?: Node;
20+
depMgr: DependencyManager;
1921

20-
constructor(props: AnimatedProps<P>) {
22+
constructor(depMgr: DependencyManager, props: AnimatedProps<P>) {
2123
this._props = props;
24+
this.depMgr = depMgr;
25+
this.depMgr.unSubscribeNode(this);
26+
this.depMgr.subscribeNode(this, props);
2227
}
2328

2429
abstract draw(ctx: DrawingContext): void | DeclarationResult;
2530

2631
set props(props: AnimatedProps<P>) {
32+
this.depMgr.unSubscribeNode(this);
33+
this.depMgr.subscribeNode(this, props);
2734
this._props = props;
2835
}
2936

@@ -55,8 +62,8 @@ export abstract class Node<P = unknown> {
5562
export class Container extends Node {
5663
redraw: () => void;
5764

58-
constructor(redraw: () => void) {
59-
super({});
65+
constructor(depMgr: DependencyManager, redraw: () => void) {
66+
super(depMgr, {});
6067
this.redraw = redraw;
6168
}
6269

package/src/renderer/HostConfig.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ const removeNode = (parent: Node, child: Node) => {
108108
bustBranchMemoization(parent);
109109
const index = parent.children.indexOf(child);
110110
parent.children.splice(index, 1);
111+
child.depMgr.unSubscribeNode(child);
112+
// unsubscribe to all children as well
113+
for (const c of child.children) {
114+
removeNode(child, c);
115+
}
111116
};
112117

113118
const insertBefore = (parent: Node, child: Node, before: Node) => {
@@ -120,14 +125,14 @@ const insertBefore = (parent: Node, child: Node, before: Node) => {
120125
parent.children.splice(beforeIndex, 0, child);
121126
};
122127

123-
const createNode = (type: NodeType, props: Props) => {
128+
const createNode = (container: Container, type: NodeType, props: Props) => {
124129
switch (type) {
125130
case NodeType.Drawing:
126131
const { onDraw, skipProcessing, ...p1 } = props;
127-
return new DrawingNode(onDraw, skipProcessing, p1);
132+
return new DrawingNode(container.depMgr, onDraw, skipProcessing, p1);
128133
case NodeType.Declaration:
129134
const { onDeclare, ...p2 } = props;
130-
return new DeclarationNode(onDeclare, p2);
135+
return new DeclarationNode(container.depMgr, onDeclare, p2);
131136
default:
132137
// TODO: here we need to throw a nice error message
133138
// This is the error that will show up when the user uses nodes not supported by Skia (View, Audio, etc)
@@ -186,9 +191,15 @@ export const skHostConfig: SkiaHostConfig = {
186191
throw new Error("Text nodes are not supported yet");
187192
},
188193

189-
createInstance(type, props, _root, _hostContext, _internalInstanceHandle) {
194+
createInstance(
195+
type,
196+
props,
197+
container,
198+
_hostContext,
199+
_internalInstanceHandle
200+
) {
190201
debug("createInstance", type);
191-
return createNode(type, props) as Node;
202+
return createNode(container, type, props) as Node;
192203
},
193204

194205
appendInitialChild(parentInstance, child) {

0 commit comments

Comments
 (0)