- 投稿日:2020-03-14T23:25:39+09:00
Vue.js、Capacitorでアプリ開発
ウェブ開発ノウハウを利用してスマートフォンアプリの開発を行うハイブリッドアプリについて、ソリューションの一つとしてCapacitor+Vue.jsを考えてみました。
メリットは、最初のプロジェクトの作成が非常に簡単にできることですので、プロジェクトの作成手順を紹介します。Vue.jsプロジェクトの作成
https://qiita.com/jjjjjj/items/a43922631f1350bb9c65
を参照Capacitorのインストール
以下を参考にしました。
https://capacitor.ionicframework.jp/docs/getting-started$ npm install --save @capacitor/core @capacitor/cliCapacitor初期設定
$ npx cap init
index.htmlファイルの場所を変更
capacitor.config.jsonのwebDirをwwwからdistに変更する。
プラットフォームのインストール
$ npx cap add ios $ npx cap add androidVueプロジェクトをネイティブにコピーする
$ npx cap copy ios $ npx cap copy androidIDEを開いてビルド、実行する
$ npx cap open ios $ npx cap open android
- 投稿日:2020-03-14T04:08:14+09:00
【SwiftUI】CustomViewを用いて不要なViewの再描画を避ける
SwiftUIは
宣言的に画面のコードを書くことができるため
UIKitと比べても可読性が高く扱いやすくなっていると感じています。一方で
現時点では足りない機能(CollectionView的なものがないなど)や
動作がいまいちだなと感じる部分もあるかと思います。その中の一つに
@State
などを用いた際に
値を変更する度に画面全体を再描画してしまうことで
変化のないViewも再描画してしまうという問題があります。今回は
Viewが何を基準に行われ
必要ないViewの再描画をどう回避できるのかについて
下記の記事を参考に確認してみました。サンプルコード
下記のコードを使用します。
struct SampleView: View { var texts = ["SwiftUI", "A new declarative UI framework", "Let us try to understand how SwiftUI renders its views"] @State var selected = 0 @State var textFieldValue = "" @State var isSwitchOn = false var body: some View { VStack(spacing: 16) { TextField("Enter your name", text: $textFieldValue) .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .background(Color(UIColor.secondarySystemFill)) .cornerRadius(4) Toggle(isOn: $isSwitchOn, label: { Text("A simple Switch") }) Picker(selection: $selected, label: Text(""), content: { Text("Word").tag(0) Text("Phrase").tag(1) Text("Sentence").tag(2) }).pickerStyle(SegmentedPickerStyle()) HStack { Text(texts[selected]) Spacer() Button(action: { print("just checking") }, label: { Image(systemName: "plus.circle.fill") .foregroundColor(.blue) }) } }.padding()} }検証内容
- 全てのViewにbreakpointを設定します。
- アプリを起動して、それぞれのViewを操作してみます。
ここからわかることは
TextField
、Toggle
、Button
に操作に加えても
breakpointに止まりませんが
Picker
の値を変更すると
全てのbreakpointに止まります。
つまりbody
全体が再描画されていることがわかります。原因
参考にしている記事によると
SwiftUIは親Viewの変数が(今回はselected
)
子Viewに影響を与えるかどうか自体は把握しているものの
どのViewに影響を与えているかまでは把握していないため
body
全体を再描画しているということだそうです。
(この記事を書いているのは2020/03/14です。)実際に
Text(texts[selected])
の部分を
Text("")
に変更すると
body
全体の再描画は発生しません。
(SwiftUIはPicker
の変更がどこにも影響しないことがわかっているからです。)解決策
CustomViewを定義することで
再描画されることを回避することができます。例えば一番最初の
TextField
の部分を
CustomViewに切り出します。struct SampleView: View { ... var body: some View { VStack(spacing: 16) { MyTextField(placholder: "Enter your name", value: $textFieldValue) ... }.padding()} } struct MyTextField: View { var placholder: String @Binding var value: String var body: some View { TextField(placholder, text: $value) .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .background(Color(UIColor.secondarySystemFill)) .cornerRadius(4) } }これで上記の検証内容を実行してみます。
すると
Picker
の値を変更した際に
MyTextField
のbreakpointには止まりません。なぜ?
SwiftUIは
Equatable
に適合しているViewを見て
body
に変更があるかどうか見ており
Picker
に変更があっても
MyTextField
のbody
には変更がないため
再描画が発生しなかったということだそうです。※
TextField
自体はEquatable
に適合していませんが@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) public struct TextField<Label> : View where Label : View { /// Declares the content and behavior of this view. public var body: some View { get } /// The type of view representing the body of this view. /// /// When you create a custom view, Swift infers this type from your /// implementation of the required `body` property. public typealias Body = some View }今回は
TextField
のLabel
がText
で
Text
はEquatable
に適合しているため
その差分を比較しているのではないかと思っています。/// A view that displays one or more lines of read-only text. @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) @frozen public struct Text : Equatable { /// Creates an instance that displays `content` verbatim. @inlinable public init(verbatim content: String) /// Creates an instance that displays `content` verbatim. public init<S>(_ content: S) where S : StringProtocol /// Creates text that displays localized content identified by a key. /// /// - Parameters: /// - key: The key for a string in the table identified by `tableName`. /// - tableName: The name of the string table to search. If `nil`, uses /// the table in `Localizable.strings`. /// - bundle: The bundle containing the strings file. If `nil`, uses the /// main `Bundle`. /// - comment: Contextual information about this key-value pair. public init(_ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil) /// Returns a Boolean value indicating whether two values are equal. /// /// Equality is the inverse of inequality. For any values `a` and `b`, /// `a == b` implies that `a != b` is `false`. /// /// - Parameters: /// - lhs: A value to compare. /// - rhs: Another value to compare. public static func == (a: Text, b: Text) -> Bool }ここら辺いまいち不明瞭なので
もしご存知の方がいらっしゃいましたら
教えていただけるとうれしいです??♂️変更の影響がある部分を切り出す
上記の
MyTextField
のようにToggle
の部分を切り出すことで
Toggle
もPicker
の変更で再描画しなくなります。しかし
これだとViewが追加されるたびに
CustomViewを追加していくことになります。
(可読性などを考えるとそれは良い選択かもしれませんが)そうである場合にも
あるViewの変更が
関係のないViewにまで影響を及ぼすのは
あまり良いとは思われません。そこでお互いに影響を与える部分(
Picker
とHStack
)を切り出してみます。struct SampleView: View { ... var body: some View { VStack(spacing: 16) { // 確認のため最初の形に戻しています TextField("Enter your name", text: $textFieldValue) .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .background(Color(UIColor.secondarySystemFill)) .cornerRadius(4) ... MyPickerTextField() }.padding()} } struct MyPickerTextField: View { var texts = ["SwiftUI", "A new declarative UI framework", "Let us try to understand how SwiftUI renders its views"] @State var selected = 0 var body: some View { VStack(spacing: 16) { Picker(selection: $selected, label: Text(""), content: { Text("Word").tag(0) Text("Phrase").tag(1) Text("Sentence").tag(2) }).pickerStyle(SegmentedPickerStyle()) HStack { Text(texts[selected]) Spacer() Button(action: { print("just checking") }, label: { Image(systemName: "plus.circle.fill") .foregroundColor(.blue) }) } } } }ここで再び検証内容を実行してみます。
するとbreakpointは
MyPickerTextField
の中でしか止まらなくなり
親Viewのbody
全体の再描画は発生しなくなったことがわかりました。結果として変更の影響範囲を必要な部分にだけに抑えることができました。
まとめ
SwiftUIの再描画について見てみました。
Viewが複雑になってくると
body
全体の再描画を繰り返し行うことで
パフォーマンスに大きな影響を与える可能性も出てきます。Viewの表示速度などで
気になる部分がある場合
こういう点を調査してみるのも何かの解決につながるかもしれませんね?何か間違いなどございましたら
ご指摘いただけますとうれしいです??♂️
- 投稿日:2020-03-14T00:28:26+09:00
truffle-config.js の最適化オプション runs の検証と Truffle v4 への懸念
はじめに
Truffle 環境の設定をする truffle-config.js ですが、最適化ブロックで runs という指定が使えるようです。
はて、この値はなんなんでしょう?
Solidity の公式ページの説明からするに、solc においての runs は下記のようなものらしいです。
デフォルトでは、オプティマイザは、コントラクトがそのライフタイムにわたって200回呼び出されると仮定して、コントラクトを最適化します。 イニシャルコントラクトのデプロイのガス代をより安く、その後の関数実行時のガス代をより高くしたい場合は、--optimize-runs=1 と設定してください。 多くのトランザクションを想定していて、デフォルトより高いデプロイコストとアウトプットサイズを気にしないのであれば、 --optimize-runs を大きな数に設定してください。どうやら runs の設定は、「値を小さくするとデプロイ時のガス代が下がって トランザクションのガス代が上がる、値を大きくするとデプロイ時のガス代が上がって トランザクションのガス代が下がる」とのこと。
Truffle 環境ではコンパイラとして solcjs が使われているらしいので solc の機能がそのまま使えるとは限らないのですが、物は試しです。どのような効果があるのかテストしてみましょう(※こちらがテストプロジェクトとなります)。
テスト方法
ガス消費の計測用に、構造体を1つ作成して動的配列に追加する関数を持つ、シンプルなコントラクトを準備します(※せっかくなので CryptoKitties の Kitty 構造体を拝借しました)。
テストコード(クリックで開閉します)
pragma solidity 0.5.12; contract CompileOpt5{ // クリプトキティの構造体を拝借 struct Kitty { uint256 genes; uint64 birthTime; uint64 cooldownEndBlock; uint32 matronId; uint32 sireId; uint32 siringWithId; uint16 cooldownIndex; uint16 generation; } // ニャンコの配列 Kitty[] internal kitties; // 要素数の取得 function getTotalKitties() public view returns( uint256 ){ return kitties.length; } // ニャンコの追加 function createKitty( uint256 _val256 ) public{ Kitty memory _kitty = Kitty({ genes: _val256, birthTime: uint64(_val256), cooldownEndBlock: uint64(_val256), matronId: uint32(_val256), sireId: uint32(_val256), siringWithId: uint32(_val256), cooldownIndex: uint16(_val256), generation: uint16(_val256) }); kitties.push( _kitty ); } }上記コードを、runs の値が [1]、[200]、[2000] の設定でコンパイルしたコントラクトを、それぞれデプロイした際のガス消費量と、関数にアクセスした際のガス消費量で比較します。ついでに、最適化を無効にした状態もテストしてみましょう。
Truffle v5.1.2 (solc-js v0.5.12) でのテスト
各コントラクトをデプロイし、createKitty 関数へ200回以上アクセスをしてみました(※ひょっとしたら runs で指定した回数のトランザクションを超えるとコストが高くなったりするのかと思ったので多めにテスト)。
で、結果としては、すべてのコントラクトにおいて初回トランザクションだけガス消費が低く、2回目以降は一律の消費という結果になりました。・最適化 有り(runs=1)
デプロイ時のガス消費: 187,711
初回のガス消費: 45,551
2回目以降のガス消費: 68,963
truffle-config.js(クリックで開閉します)
module.exports = { // 〜 省略 〜 // 最適化有り:runs=1 compilers: { solc: { version: "0.5.12", parser: "solcjs", settings: { optimizer: { enabled: true, runs: 1 } } } } }・最適化 有り(runs=200)
デプロイ時のガス消費: 190,327
初回のガス消費: 45,503
2回目以降のガス消費: 68,915
truffle-config.js(クリックで開閉します)
module.exports = { // 〜 省略 〜 // 最適化有り:runs=200(デフォルト値) compilers: { solc: { version: "0.5.12", parser: "solcjs", settings: { optimizer: { enabled: true, runs: 200 } } } } }・最適化 有り(runs=2000)
デプロイ時のガス消費:245,294
初回のガス消費: 45,419
2回目以降のガス消費: 68,831
truffle-config.js(クリックで開閉します)
module.exports = { // 〜 省略 〜 // 最適化有り:runs=2000 compilers: { solc: { version: "0.5.12", parser: "solcjs", settings: { optimizer: { enabled: true, runs: 2000 } } } } }・最適化 無し
デプロイ時のガス消費: 220,118
初回のガス消費: 55,710
2回目以降のガス消費: 79,122
truffle-config.js(クリックで開閉します)
module.exports = { // 〜 省略 〜 // 最適化無し compilers: { solc: { version: "0.5.12", parser: "solcjs", settings: { optimizer: { enabled: false } } } } }さて、各値を比較してみるに、runs の値が小さいほどデプロイ時のガス消費が抑えられているようです。
一方で、runs の値による、トランザクションのコスト変動は微々たるものです。デフォルトの runs=200 に対して、トランザクションを犠牲するはずの runs=1 でも、 優遇されるはずの runs=2000 においても、恩恵を実感できるほどの差が出ませんでした。また、最適化無しの状態にくらべると、2回目以降のトランザクションのガス消費が13%ほど改善されています。
まとめ
truffle-config.js において runs の値は、特に気にしなくても良さそうです。とりあえず最適化しておくだけで、ガス消費も多少収まってくれるみたいですし、DApp のリリース時に最適化し忘れさえしなければ問題はなさそうですね。
Truffle v4 での最適化はどうなるのか?
さて、ある意味ここからが本題かもしれません。
最適化の指定は Truffle v4 環境でもちゃんと効果があるのでしょうか?Truffle 関連の情報、特に書籍等では、v4 系の内容を取り扱ったものが多い印象があります。また、truffle console の使い勝手が、v4 と v5 で結構変わっているので、あえて v4 環境を使い続けている方も少なくないと思います。折角なので、v4 系でもテストしてみましょう。
Truffle v4.1.15 (solcjs v0.4.25) でのテスト
では、v4 向けに名前とコンパイラのバージョンを調整したコントラクトを用意して、テストの開始です。
テストコード(クリックで開閉します)
pragma solidity 0.4.25; contract CompileOpt4{ // クリプトキティの構造体を拝借 struct Kitty { uint256 genes; uint64 birthTime; uint64 cooldownEndBlock; uint32 matronId; uint32 sireId; uint32 siringWithId; uint16 cooldownIndex; uint16 generation; } // ニャンコの配列 Kitty[] internal kitties; // 要素数の取得 function getTotalKitties() public view returns( uint256 ){ return kitties.length; } // ニャンコの追加 function createKitty( uint256 _val256 ) public{ Kitty memory _kitty = Kitty({ genes: _val256, birthTime: uint64(_val256), cooldownEndBlock: uint64(_val256), matronId: uint32(_val256), sireId: uint32(_val256), siringWithId: uint32(_val256), cooldownIndex: uint16(_val256), generation: uint16(_val256) }); kitties.push( _kitty ); } }・最適化 有り(runs=1)
デプロイ時のガス消費:225,602
初回のガス消費: 55,701
2回目以降のガス消費: 79,113
truffle-config.js(クリックで開閉します)
module.exports = { // 〜 省略 〜 // 最適化有り:runs=1 compilers: { solc: { version: "0.4.25", parser: "solcjs", settings: { optimizer: { enabled: true, runs: 1 } } } } }・最適化 有り(runs=200)
デプロイ時のガス消費:225,602
初回のガス消費: 55,701
2回目以降のガス消費: 79,113
truffle-config.js(クリックで開閉します)
module.exports = { // 〜 省略 〜 // 最適化有り:runs=200(デフォルト値) compilers: { solc: { version: "0.4.25", parser: "solcjs", settings: { optimizer: { enabled: true, runs: 200 } } } } }・最適化 有り(runs=2000)
デプロイ時のガス消費: 225,602
初回のガス消費: 55,701
2回目以降のガス消費: 79,113
truffle-config.js(クリックで開閉します)
module.exports = { // 〜 省略 〜 // 最適化有り:runs=2000 compilers: { solc: { version: "0.4.25", parser: "solcjs", settings: { optimizer: { enabled: true, runs: 2000 } } } } }・最適化 無し
デプロイ時のガス消費:225,602
初回のガス消費: 55,701
2回目以降のガス消費: 79,113
truffle-config.js(クリックで開閉します)
module.exports = { // 〜 省略 〜 // 最適化無し compilers: { solc: { version: "0.4.25", parser: "solcjs", settings: { optimizer: { enabled: false } } } } }やや、すべての設定において、ガス消費量が完全に同じ値となりました…。
最初はコンパイル指定をミスったと思ったのですが、イーサスキャンでバイトコードを眺めてみると、各コントラクトともバイナリレベルで若干の差が出ています。仮に、同じバイナリデータであれば、イーサスキャンで表示した際、重複コントラクト扱いされるはずなので、個別に表示できている時点でコントラクト間で差分があるのは間違いなさそうです。
それなのに、イーサスキャン上でベリファイコードを登録しようとすると、「Optimization Enabled: No」でないと受け付けてくれません。
まとめると、最適化の設定によりバイナリデータに差が出ているにも関わらず、イーサスキャン上では最適化が認識されていないようなのです。
そして最大の問題が「最適化有りでも無しでも、すべてのコントラクトのガス消費量が同じになっている」という事実。
この結果が、私の環境や手順の間違いによるものであればよいのですが、仮に、 Truffle v4 環境が最適化の指定をコントラクトに反映してくれないのであれば、由々しき事態です。
もし Truffle v4 をお使いで Dapp のリリースを念頭に置いている方がいらっしゃるのであれば、お早めに、お使いの環境で最適化が機能するかテストしてみることをお勧めします。リリース間際になって「最適化が効かない!?」なんて状況は考えるだに恐ろしいです。