Upgrade to Pro — share decks privately, control downloads, hide ads and more …

react-reconcilerでオレオレReact Nativeを作ろう!

ああうえ
September 12, 2022

react-reconcilerでオレオレReact Nativeを作ろう!

iOSDC 2022
2022/09/12 13:00〜 Track D レギュラートーク(40分)

ああうえ

September 12, 2022
Tweet

More Decks by ああうえ

Other Decks in Programming

Transcript

  1. 前置き y React Nativeを自作する意味は、多くの場合ありません8 y こんな時に役立つ資料でw y Reactをソフトウェアの一部に組み込みたいと y ReactとReact

    Nativeがどう繋がっているのか知りたいと y このスライドで紹介するReactとReact Nativeの情報は2022年8 月時点のものです
  2. 例) Raycast コード・実行例 export default function Command() { function handleSubmit(values:

    Values) { showToast(...) } return <Form ...> <Form.Description ... /> <Form.DropDown> <Form.DropDownItem ... /> <Form.DropDownItem ... /> </Form.DropDown> <Form.TextField .../> </Form> }
  3. 例) Figma/FigJam コード・実行例 function Widget() { const [count, setCount] =

    useSyncedState('count', 0) return <AutoLayout ...> <SVG src={minusIcon} onClick={() => { setCount(count - 1) }} /> <Text>{count}</Text> <SVG src={plusIcon} onClick={() => { setCount(count + 1) }} /> </AutoLayout> } widget.register(Widget)
  4. react-reconcilerとは — Webフロントで利用するReact DOMと、モバイルアプリで利用す るReact Nativeでは、UIの描画方法は違えど、共通していること があu — JSXを利用してUI記述することg —

    状態変更によって描画の更新が起こるというこg — react-reconcilerはReactの共通ロジックの定義と、差分検出処理 を行うパッケージ
  5. react-pixi const App = () => ( <Stage ...> <Container

    ...> <RotatingPenguin /> </Container> </Stage> )
  6. react-three-fiber export default function App() { return ( <Canvas> <ambientLight

    ... /> <spotLight ... /> <pointLight ... /> <Box ... /> </Canvas> ) }
  7. iOSでJavaScriptを動かす s JavaScriptCoru s 主にSafariなどで利用されているJavaScriptのエンジG s iOSでもAPIが提供されており、iOS 7から利用できI s SwiftからJavaScriptのコードを実行したり、JavaScriptのコード

    からSwiftで定義された関数などを呼び出すことができI s JavaScriptCoreの上でreact-reconcilerを動かすことで、Reactの コードをiOS上でレンダリング可能
  8. JavaScriptコードを動かす(2) 変数の公開 b setObject(_:forKeyedSubscript:) を利用するとSwiftの変数など を呼び出すことができる let text = "Hello"

    let context = JSContext()! context.setObject(text, forKeyedSubscript: "text" as NSString) print(context.evaluateScript("text")) // Hello
  9. import JavaScriptCore // JSExportにconformしたprotocolを作成する // SwiftではなくObjective-Cのオブジェクトを公開できる機能なので、 // @objcをつける必要がある @objc protocol

    Foo: JSExport { var bar: String { get set } func baz() -> String } class FooImpl: NSObject, Foo { var bar: String = "Hello" func baz() -> String { return "World" } }
  10. let foo = FooImpl() let context = JSContext()! // ここで変数を公開。型情報も公開されるので、

    // プロパティやメソッドにもアクセスできる context.setObject(foo, forKeyedSubscript: "foo" as NSString) print(context.evaluateScript("foo.bar")) // Hello print(context.evaluateScript("foo.baz()")) // World
  11. react-reconcilerの使い方 Rendererの初期化 const Reconciler = require('react-reconciler') const HostConfig = {

    // You'll need to implement some methods here. // See below for more information and examples. } const MyRenderer = Reconciler(HostConfig) ...
  12. react-reconcilerの使い方 Rendererの初期化 const Reconciler = require('react-reconciler') const HostConfig = {

    // You'll need to implement some methods here. // See below for more information and examples. } const MyRenderer = Reconciler(HostConfig) ... どのようにレンダリングするか書く
  13. react-reconcilerの使い方 Swiftから呼ぶrender関数を定義 global.render = () => { if (renderContainer ==

    null) { renderContainer = MyRenderer.createContainer( "root", ConcurrentRoot ); } MyRenderer.updateContainer( <RootComponent />, renderContainer ) }
  14. react-reconcilerの使い方 Swiftから呼ぶrender関数を定義 global.render = () => { if (renderContainer ==

    null) { renderContainer = MyRenderer.createContainer( "root", ConcurrentRoot ); } MyRenderer.updateContainer( <RootComponent />, renderContainer ) } レンダリングしたい rootコンポーネントを渡す
  15. HostConfigの実装 b HostConfigは大きく、Core Methods、Mutation Methods、 Persistence Methods、Hydration Methodsに分かれていI b 全てのメソッドを合わせると約100個存x

    b Core Methodの一部を実装すればReactのコードをUIKitでレンダ リング可 b Mutation ModeかPersistence Modeの2つがあり、どちらかを選 ぶ必要がある
  16. Mutation Methods p supportsMutationをtrueにすると呼ばれa p View構造が変わったときに呼ばれるメソッドs p 子Viewの追加・削除・可視状態などを制御できまg p React

    DOM, 旧React Nativeのレンダラーはこのモードを利 p 今回のサンプルではMutation Modeで実装していきます
  17. HostConfigのメソッド例 JS側にSwiftのメソッドを公開する // HostConfig.swift @objc protocol HostConfig: JSExport { func

    createInstance(_ type: String, _ props: JSValue) -> UIView } class HostConfigImpl: NSObject, HostConfig { func createInstance(_ type: String, _ props: JSValue) -> UIView { ... } ... } let hostConfig = HostConfigImpl() jsContext.setObject(hostConfig, forKeyedSubscript: "HostConfigSwift" as NSString)
  18. Viewの生成 createInstanceの実装 # 以下の例のように実装してあげます func createInstance(_ type: String, _ props:

    JSValue) -> UIView { switch type { case "text": let label = UILabel() if let text = props.objectForKeyedSubscript("children"), !text.isUndefined { label.text = text.toString() } return label ... } }
  19. 状態変更が起こった時 5 初回のViewの描画はできた。次は状態変更が起こった場合のこと を考えていきます const MyComponent = () => {

    const [count, setCount] = useState(0) return ( <button title={`Increment: ${count}`} onClick={() => setCount(count + 1)} /> ) }
  20. 状態変更が起こった時 func commitUpdate( _ instance: JSValue, _ updatePayload: JSValue, _

    type: String, _ prevProps: JSValue, _ nextProps: JSValue ) { guard let view = instance.toObject() as? UIView else { return } ... }
  21. 状態変更が起こった時 func commitUpdate( _ instance: JSValue, _ updatePayload: JSValue, _

    type: String, _ prevProps: JSValue, _ nextProps: JSValue ) { guard let view = instance.toObject() as? UIView else { return } ... } instanceにはさっき作ったViewが入ってくる
  22. 状態変更が起こった時 func commitUpdate( _ instance: JSValue, _ updatePayload: JSValue, _

    type: String, _ prevProps: JSValue, _ nextProps: JSValue ) { guard let view = instance.toObject() as? UIView else { return } ... } nextPropsに新しいpropsが入っているので これを見てViewを更新する
  23. 状態変更が起こった時 // commitUpdate内 switch type { case "button": guard let

    button = view as? UIButton else { return } if let title = nextProps.objectForKeyedSubscript("title"), !title.isUndefined { button.configuration?.title = title.toString() } return button ... }
  24. 状態変更が起こった時 // commitUpdate内 switch type { case "button": guard let

    button = view as? UIButton else { return } if let title = nextProps.objectForKeyedSubscript("title"), !title.isUndefined { button.configuration?.title = title } return button ... } propsからtitleの値を取り出して UIButtonにセット
  25. 状態変更が起こった時 const MyComponent = () => { const [count, setCount]

    = useState(0) return ( <button title={`Increment: ${count}`} onClick={() => setCount(count + 1)} /> ) }
  26. View構造に対応する @ appendInitialChildは初回のUI描画前に行う処Y @ appendChildはMutation Modeで途中でView構造が変わった時に 呼ばれる。どちらも同様に実装すればOK func appendInitialChild(_ parent:

    JSValue, _ child: JSValue) { guard let parentView = parent.toObject() as? UIView else { return } guard let childView = child.toObject() as? UIView else { return } parentView.addSubview(childView) }
  27. À const MyComponent = () => { const [count, setCount]

    = useState(0) const views = [...Array(count)].map((_, index) => { return <text>{index + 1}</text> }) return ( <vstack> {views} <button title={'Add'} onClick={() => setCount(count + 1)} /> <button title={'Remove'} onClick={() => setCount(count - 1)} /> </vstack> ) }
  28. Yogaを使ってレイアウトする let uiView = UIView() uiView.backgroundColor = .red uiView.yoga.width(200).height(200) let

    child1 = UIView() child1.backgroundColor = .green child1.yoga .width(100) .height(40) .margin(20) uiView.addSubview(child1) let child2 = UIView() child2.backgroundColor = .yellow child2.yoga .alignSelf(.flexEnd) .width(100) .height(100) uiView.addSubview(child2) 100x40
  29. Yogaを使ってレイアウトする let uiView = UIView() uiView.backgroundColor = .red uiView.yoga.width(200).height(200) let

    child1 = UIView() child1.backgroundColor = .green child1.yoga .width(100) .height(40) .margin(20) uiView.addSubview(child1) let child2 = UIView() child2.backgroundColor = .yellow child2.yoga .alignSelf(.flexEnd) .width(100) .height(100) uiView.addSubview(child2) 100x40 20 20
  30. Yogaを使ってレイアウトする let uiView = UIView() uiView.backgroundColor = .red uiView.yoga.width(200).height(200) let

    child1 = UIView() child1.backgroundColor = .green child1.yoga .width(100) .height(40) .margin(20) uiView.addSubview(child1) let child2 = UIView() child2.backgroundColor = .yellow child2.yoga .alignSelf(.flexEnd) .width(100) .height(100) uiView.addSubview(child2) 100x100 20
  31. 原因1: 関係ないViewが更新されている c commitUpdateは実際にViewの更新をすべきでない時にも呼ばれ てしま5 c そのため、UIViewの更新処理に重いものがあったりすると、ボト ルネックとなる // commitUpdate内

    guard let button = view as? UIButton else { return } if let title = nextProps.objectForKeyedSubscript("title") { // 関係ない変更でも毎回呼ばれてしまっている! button.configuration?.title = title.toString() }
  32. 原因2: Yogaのレイアウトを走らせている x レイアウトをし直す処理は遅いので、頻繁に呼ばれるのを防2 x yoga.applyLayout()や、yoga.markDirty()な0 x 例: テキストを更新しない場合でもyoga.markDirty()を呼んでし まっている

    // commitUpdate内 guard let label = view as? UILabel else { return } if let children = nextProps.objectForKeyedSubscript("children") { label.text = children.toString() label.yoga.markDirty() // 関係ない変更でも毎回呼ばれてしまっている! }
  33. propsが更新されたときのみ更新する E updatePayloadに対象のkeyが入っていなければ、関係ない変更な のでViewを更新しないようにする func updateTitle( _ button: UIButton, _

    nextProps: JSValue, _ updatePayload: [String] ) { guard updatePayload.contains("title") else { return } let title = nextProps.objectForKeyedSubscript("title") button.configuration?.title = title.toString() }
  34. 原因3: 重いReactコードを書いている S React 18から追加された、startTransitionなどを使って重い処理 の優先度を下げると効果的 const handleSliderValueChange = useCallback(

    (value) => { startTransition(() => { // とても重い処理 }) }, [progress] ) <slider value={progress} onChange={handleSliderValueChange} />
  35. Fabric Renderer y Fabricは、React Native 0.68から利用可能になった、新しいレン ダラe y FabricはC++で実装されており、iOS・Androidで高速にレンダリ ングできるような仕組みになっていE

    y FabricのHostConfigはReactFabricHostConfig.jsに実装されてい る。JSIというJavaScriptのインターフェースを通してC++とやり 取りする
  36. 作ったものとReact Nativeの比較 ˜ ランタイムのパフォーマンス的には、正直そこまで変わらない※e ˜ 起動時間(JSContextの初期化)も、Webpackのバンドルを production用にしてしまえば3ms程$ ˜ React NativeはJNIを使うAndroidに向けてパフォーマンスチュー

    ニングをしているため、Androidでも実装して比較しないと分から ない ※1: スライダーを10個配置して同じuseStateの値を見るサンプルを作成し、スライダーを1秒間動かした時のCPU時間を比較 オレオレReact Nativeが161msに対し、Expo Goアプリで実装したものは292msだった。ちなみに、SwiftUIは159ms程度
  37. 参考資料 ) JavaScriptCoreで遊ぼA ) https://booth.pm/ja/items/188581 ) What is Lanes in

    React source code? - React Source Code Walkthrough 2E ) https://jser.dev/react/2022/03/26/lanes-in-react.html