00:04
5月22日、月曜日ですね。 遅刻は昨時5分になりました。
最近、僕のMacは突然電源が落ちる事象が再発し始めて困っているんですけど、
今日は落ちないことを信じて頑張っていきたいと思います。
はい、おはようございます。ひめめのkeethことくわはらです。
では本日も朝から始めていきたいと思います。
本日は昨日読んでいました、本体を読みたかったGoogle社のアクセシビリティ周りの記事ですけど、
それのパート2読みたかったんですけど、そこからまずパート1読めと言われて、
パート1読もうとしたらまずこのオーバービュー読めと言われたので、
点々としてオーバービューを昨日読んでいただいた感じですけど、
今日はそれの続きですね。
昨日は前半読んでいたので後半はこのまま続けていきたいと思います。
レノアさんですね。おはようございます。ご参加いただきありがとうございます。
今日もいつも通りダラダラとやっていこうかなと思っております。
では今日は昨日の続きなんですけど、Chromium's Multiprocessor Architectureのお話ですね。
読んでいきましょう。
ネイティブのアクセシビリティAPIというのは機能的なインターフェースを持つ傾向があり、
Chromiumは様々な属性を介すツリーを歩く、またはクリック、フォーカス、セットバリューなどのメソッドを実行するメソッドを含む、
正規のアクセス可能なオブジェクト用のインターフェースを実装しています。
これに対してWebは大部分が宣言的なインターフェースを持っています。
アクセシビリティツリーの形状はDOMツリーによって形成され、時折CSSの影響を受けますけど、
DOM要素のアクセシブルセマンティクスは、アリア属性を追加することによって変更することができます。
重要な複雑な点として、これらのネイティブアクセシビリティAPIというのは全て同期系であるのに対し、
Chromiumはマルチプロセスであり、各Webページのコンテンツは、
ChromiumのUIやネイティブアクセシビリティAPIを実装するプロセスとは異なるプロセスに進んでいます。
さらにレンダラーのプロセスはサンドボックス化されているため、
オペレーティングシステムのAPIを直接実行することもできません。
Chromiumのマルチプロセスアーキテクチャについて存じない方は、
このコンセプトを紹介したブログ記事、別の記事があるので読んでみてください。
また、Chromium.orgのデザインドキュメントを参照してもいいでしょう。
なるほどですね。ネイティブアクセシビリティAPIは全て同期系であるのに対し、マルチプロセスなのか。
Chromiumの方はマルチプロセスなのでどんどんアクセスしたりしたいですけど、
結局受け取り側の方がシングルみたいな感じですよね。
同期でやらなきゃいけないので。
ここが結構ボトルネックになりそうですね。
Chromiumのマルチプロセスアーキテクチャというのは、
シングルプロセスのブラウザーと同じ方法でアクセシビリティAPIを
実装できないということを意味しています。
つまり、DOMに直接呼び出しをして、各API呼び出しの結果を計算します。
例えば、あるオペレーティングシステムでは、
ページ上の特定の文字範囲のバウンディングボックスを取得するためのAPIがあるかもしれません。
他のブラウザーでは、DOMの選択オブジェクトを作成し、
03:01
そのバウンディングボックスを求めることで実装されるかもしれません。
このような実装はChromiumでは不可能です。
なぜなら、WebページのDOMを実装するレンダラー処理からの応答を待つ間、
メインスレッドをブロックする必要があるからです。
メインスレッドをブロックすることは厳密には禁止されているだけではなく、
APIを直接呼び出すためにこの処理を行うため、
いずれにしても、妨害に遅くなってしまいます。
Chromiumはそれを許さないでしょうね。
その代わりに、ChromiumはAccessibility Tree全体の表現を
メインプロセスにキャッシュするアプローチを取っています。
この表現が可能な限り完結されることを保証するために、
最新の注意を払う必要があります。
キャッシュにたどり着くしかないんだろうなと思いました。
ChromiumではWebページのAccessibility Treeの全ての情報を表すデータ構造を構築し、
そのデータ構造をレンダラー処理からメインブラウザ処理に送信して、
メインブラウザ処理にキャッシュをし、
そのキャッシュ内の情報のみを使用して、ネイティブなAccessibility APIを実装しています。
やっぱりキャッシュ戦略、すごく気になりますね。
いつリフレッシュするとか、いつキャッシュデータの更新をするかという話ですね。
アクセシビリティツリーが変更されると、ツリーの更新とアクセシビリティイベントが
レンダラープロセスからブラウザープロセスに送信されます。
ブラウザのキャッシュはメインスレッドでアトミックに更新されるため、
外部クライアント、支援技術などからアクセシビリティAPI関数の特徴を呼び出すと、
常にアクセシビリティツリーの完全かつ一貫したスナップショットから何かが返されるようになります。
時折キャッシュはレンダラー処理に1秒単位で遅れを取ることもありますけど、
このアプローチで直面する具体的な課題とそれにどのように対処したかというのを
この先に紹介していこうと思っています。
ではまず、ソナデータですね。最初の項目はソナデータからです。
アクセシビリティツリーの任意の堂々には、多くのアクセシビリティ属性が存在する可能性があります。
例えば、ChromeがWindowsプラットフォームで実装しているアクセシビリティAPIメソッドだけでも
150個以上存在しています。
これらのAPIは全て実装する必要がありますけど、
その多くはかなり希少な属性だったり不明瞭な属性を要求します。
各アクセシブルノード・オブジェクトが何百ものフィールドを持つのを避けるため、
各アクセシブルノードのデータは比較的コンパクトなデータ構造である
UIコロンコロンのAXノードデータに格納されています。
全てのAXノードデータというのは、
整数のID、もしくはロールENUM、もしくはその他の必須フィールドを持っていますけど、
それ以外のすべては主要のデータ型ごとに一つずつ属性配列に格納されています。
どんな感じに配列が格納されているかというと、
今データのソースコードが出ていますけど、
これはちょっと音読すると分かりづらいので、皆さんので見てみてください。
そこでテキストフィールドにプレイスフォルダー属性がある場合、
StringAttributesに何たらかんたらという属性とプレイスフォルダー文字列を
アタイとするエントリーを追加することでそれを保存することができます。
そんな感じで内部的には配列でデータを持っているところですね。
で、ツリーのインクリメンタルな更新というところですけど、
06:01
ウェブページは頻繁に変更されます。
アクセシビリティツリーの一部が変わるたびに新しいコピーを送信するというのは非常に非効率的です。
しかしアクセシビリティツリーというのは複雑な方法で形を変えることができます。
例えばサブツリー全体を動的に左右にすることができます。
アクセシビリティツリーが変更される可能性のあるすべての方法に対応するコードを書くのではなく、
Chromiumにはツリーの小さな増分更新をあるプロセスから別のプロセスに送信するように設計された
汎用のシリアライザークラスというのがあります。
このツリーシリアライザーにはいくつかの要件があるだけです。
ツリーの各ノードには1位の整数IDが必要です。
ツリーは周期的でなければならない。
ノードのデータが変更されるとツリーシリアライザーに通知されることと、
あるノードのコIDのリストが変更されたときにツリーシリアライザーに通知されなければならない。
ちゃんとお互いで連携し合い通知し合いましょうということですね。
あとはツリー構造が変わるたびにノードの変更やノードコードの変更に通知に基づいて、
ツリーをウォーキングしてできるだけ少ないノードをシリアライズする
インクリメンタルなツリーアップデートを構築しています。
もう一方のプロセスでは、アンシリアライゼーションのコードが
Atomicに増分ツリーアップデートを適用しています。
だんだんこの辺からもう、事前知識を覚えてなきゃいけないので、
だいぶ自分の中でも、そうなんやって半分理解できないまま実際読んでます。
続いてテキストのバウンディングボックスですね。
Chromiumが直面する一つの課題は、アクセシビリティクライアントが
現在のカーソル位置や選択範囲だけではなく、任意の範囲のテキストの境界ボックスを紹介できることを望むことです。
成熟したようにブリンクカラーの情報を持っている間に、
Chromiumのメインブラウザープロセスをブロックすることは不可能なので、
代わりにアクセシビリティツリーにこれらのクエリを満たすのに十分な情報を雇用しています。
ページ上の全ての文字のバウンディングボックスをコンパクトに保存するために、
テキストをインラインテキストボックス、テキスト欄と呼ばれることもあります。
そういう形に分割します。
例えば一般的な段階では、テキストの各行はそれ自身のインラインテキストボックスとなります。
一般的なインラインテキストボックスやテキスト欄には同じ方向、同じ行、
同じフォント、サイズ、スタイルのテキスト文字が並んでいます。
各インラインテキストボックスはそれ自身のバウンディングボックスと、
そのテキスト内の各文字の相対的な x 座標、
左から右側を仮定しますけどね、
というものを保存しています。
そこから、個々の文字のバウンディングボックスを計算することもできます。
インラインテキストボックスというのは、Chromiumの内部アクセシビリティツリーの一部になります。
これらは純粋に内部で使用され、
ネイティブのアクセシビリティAPIを通じて
直接公開されるということはありません。
例えば、トギュメントにHello Worldというテキストフィールドがあり、
フィールドの幅が狭いため、Helloが1行目に、Worldが2行目にあるとします。
内部的には、Chromiumのアクセシビリティ次は次になりますということで、
バーッと書いてあるんですけど、
まず、トップレベルにロケーションとかサイズってのが書いてあって、
09:01
ネーム、Hello Worldってあるんですけど、
それぞれのインラインテキストボックスですね。
ロケーションがそれぞれに設定されていて、サイズも設定されています。
そこで座標位置とか場所っていうのを特定できるようになっているという感じですね。
2行目にあったとしてもこういうことができると。
続いては、スクロール、トランスフォーム、アニメーションのお話ですね。
ネイティブのアクセシビリティAPIでは、
通常ツリー内の全ての要素のバウンディングボックスっていうのを
ウィンドウ座標、またはグローバルスクリーン座標で取得しています。
各ノードのグローバルスクリーン座標っていうのを保存すると、
ユーザーがウィンドウをスクロールしたりドラッグしたりするたびに、
ツリー全体を再シリアライズすることになります。
その代わりにアクセシビリティツリーの各ノードのバウンディングボックスっていうのを
任意の祖先であるオフセットコンテナとの相対関係で保存します。
オフセットコンテナが指定されていない場合は、
ツリーのルートとみなされるようになります。
さらにオフセットコンテナにはスクロールオフセットを含めることができ、
これを使用してそのサブツリー内のあらゆるもののバウンディングボックスを
スクロールすることもできます。
最後に任意のオフセットコンテナには任意の4×4変換行列を含めることができ、
これを用いて任意の3次元の回転、平行移動、拡大縮小などを表現することもできます。
この変換行列はサブツリー全体に提供されます。
そういうことをしているんですね、内部的には。
この方法で座標を保存すると、
オブジェクトがスクロール、移動、または位置とスケールのアニメーションを行うたびに
スクロールやアニメーションのルートだけが
アクセシビリティツリーに更新を投稿する必要があります。
サブツリー内の全てはそのオフセットコンテナに対して相対的に有効なままです。
アクセシビリティツリー内のオブジェクトのグローバルスクリーン座標を計算するには、
その祖先のチェーンを歩いてオフセットを適用するときには
4×4のマトリックスを乗算する必要もあります。
確かにですね、画面とか内容によっては3次元のオブジェクトを描画しているものもあって、
それに対してアクセシビリティがどうやってアクセスしたりとか、どう表現するかって
それは確かに座標位置とかを無理やり中で持ったり、スクロール位置とかを持っていて
そこに対して変換行列をするっていう、ちょっと数学的なアプローチですけど
それって確かにその通りだなってすごく感じましたね。
いやー、よくできているんだな、これ。
続いてはサイトの分離とプロセス外のiフレームのお話ですね。
ある時期一つのタブやその他のWebViewの全てのコンテンツが同じブリンクプロセスに含まれており
フレームツリー全体のアクセシビリティツリーを一回のパスでシリアライズすることが可能だったことがあります。
一元管理してたんですね。
現在ではChromiumがプロセス外のiフレームをサポートしているため、現状はちょっと複雑になっています。
Chrome内のパッケージアプリケーションのWebViewタグのような
ブラウザ全体を埋め込むブラウザプラグインもサポートしていますけど
アクセシビリティの観点からはフレームと同様に扱われています。
Chromiumではプロセス内フレームとプロセス外フレームは混在して
異なる扱いを受けるのではなく
フレームごとに独立したアクセシビリティツリーを構築しています。
12:03
各フレームは独自のツリーIDを取得し
親フレームがある場合は子フレームのツリーIDを追跡できるようにしています。
Chromeのメインブラウザプロセスでは各フレームのアクセシビリティツリーが別々にキャッシュされ
アクセシビリティクライアント支援技術がアクセシビリティツリーを歩くと
Chromiumは前述のツリーIDを使って
全てのフレームをそのままで同的に単一の仮想アクセシビリティツリーに構成します。
仮想どもに近いようなことをアクセシビリティツリーでも作っているんですね。
アクセシビリティツリーのノードIDは一つのフレーム内でユニークである必要もあります。
必要に応じてChromeのメインブラウザプロセス内で別のユニークなIDも使用されます。
ChromiumのアクセシビリティではノードIDは常にフレーム内でのみユニークなIDを意味し
ユニークIDはグローバルにユニークIDを意味しています。
どちらもユニークではあるんですけど、ユニークの範囲やスコープが違うという話ですね。
ノードIDはフレーム内でのみユニークで、ユニークIDはグローバルにユニークなIDということですね。
では続いてブリンクの話ですね。
ブリンクというのはレンダリング中のページからアクセシビリティツリー、いわゆるWebAXオブジェクトの階層構造を構築します。
WebAXオブジェクトというのはブリンクのアクセシビリティツリーのコアクラスであるAXオブジェクトのパブリックAPIラッパーになります。
AXオブジェクトは中小クラスで最もよく使われる具体的なサブクラスがあって、そいつはAXノードオブジェクトというもので
これはノードを包んでいます。
AXノードオブジェクトの具体的なサブクラスとしてはAXノードオブジェクトというのがあり、同じものじゃん。
これはノードとレイアウトオブジェクトの両方を包んでいるものですよと。
レイアウトオブジェクトのアクセスはすごく重要で、いくつかの要素はその可視性、ジオメトリ、ラインマップなどに応じてAXオブジェクトツリーにのみ存在します。
AXレイアウトオブジェクトのサブクラスには特定のタイプのノードに対して特別なケースのロジックを実装するものがあります。
またAXオブジェクトの他のサブクラスもあり、これらは主にテストに使用されます。
AXレイアウトオブジェクトの全てが実際のノードに対応するわけではなく、関連するインライン要素などをグループ化した合成レイアウトオブジェクトがあるものにも注意してください。
Blynkでアクセシビリティイベントを処理する中心的なクラスはAXオブジェクトキャッシュインプルで構成されていて、ノードやレイアウトオブジェクトに対応するAXオブジェクトをキャッシュする役割というようになっています。
このクラスにはハンドル風みたいな名前のメソッドがたくさんあり、Blynk全体で呼び出されてAXオブジェクトキャッシュインプルにツリーを更新する必要があるということを通知しています。
このクラスはBlynkの全てのアクセシビリティイベントをすでに把握しているため、Blynkからエンベッティングコンテンツレイヤーにアクセシビリティイベントを中継する役割というのもあるようになっています。
たくさんのメソッド群に分けておいて、そこにどんどん通知をして投げていくという感じですね。
結構設計とか構造を見てみたいですね。わりとシンプルな感じになっている気がします。
15:03
では続いてコンテンツレイヤーです。
コンテンツ層というのはレンダラーブラウザーの両側で動作をしていくと。
コンテンツ層はWebAXオブジェクトというのをUIのコロンコロンAXノードデータというメソッドに変化しています。
UIのAXノードデータクラスというのは、関連クラスはChromiumのクロスプラットフォームアクセシビリティツリーになります。
この変換はBlynkAXツリーソースというもので実装されています。
この変換はレンダラー側で行われるため、UIのAXノードデータツリーというのをブラウザに送信する必要もあります。
リモートメソッドであるAX.MOJOM.RenderAccessibilityHostというもののHandleAXEventsというメソッドを呼び出して、
ツリーのデルタアップデートをシリアライズすることにレンダラー側での変更をブラウザー側で繁栄できるようにします。
もうこの辺から全然わかんねえ。
読んでますけど、やっぱり内部実装を読んだことが全然ないので。
一個一個出てくるメソッド名とか出てきてもすいません、僕はわかんない。
でも、諸子観哲読み切ろうと思います。
ブラウザ側ではこれらのIPCはRenderFrameHostInputによって受信され、通常ブラウザアクセシビリティマネージャーに転送され、その責任者となります。
他のブラウザアクセシビリティマネージャーにリンクして、AXノードデータツリーをブラウザアクセシビリティオブジェクトの一つのツリーにマージをします。
これは各ページに独自のアクセシビリティツリーが存在するんですけど、各Chromiumウィンドウには一つのアクセシビリティツリーしかないため、
複数のページからのツリーを結合する必要があります。
おそらくViewsUIからのツリーも同様です。
なのでこれはとても重要ですよ。
ページごとにツリーが個別に分けられているんだけど、Chromiumウィンドウそのものには一つしかないってところですね。
それはしっかり統合しなきゃいけないので、先ほどのIDすごく重要なんでしょうね、そういう意味でも。
あとは発信されたアクセシビリティイベントをプラットフォームのアクセシビリティAPIにもディスパッチをします。
これはブラウザーアクセシビリティマネージャーのプラットフォーム固有のサブクラスになっており、
NotifyAccessibilityEventという名前のメソッドで行われます。
これで名前通りNotificationをディスパッチしてるんですね。
あとはですね、AXプラットフォームツリーマネージャーデリゲーターっていうものがあって、
それを介して受信したアクセシビリティアクションを適切な受信者にディスパッチをします。
そういうコントローラーをする人がいるんですね。
レンダラーでのメッセージについてはWebXプラットフォームツリーマネージャーデリゲートである
RenderFrameHostInputっていうものがレンダラーが実装するリモートメソッド、
先ほどのRenderAccessibility.PerformActionっていうメソッドがあって、
こいつを適切なペイロードに呼ぶ役割を持っています。
このIPCコールっていうのはレンダーアクセシビリティマネージャーによって受信され、
実際のロジックが実装されているレンダーアクセシビリティインプルにリレーされます。
で、ブラウザーアクセシビリティマネージャーにイベントをルーティングすることだったら実はないよってことですね。
これはChrome OSではっていうところなんですよね。
18:01
他のブラウザーとかどういう実装になってるか全然違うと思うんであれですけど、
Chromeでは少なくともこうなっていると。
ブラウザーではなくてOSですね。
Chrome OSではっていう話が出ると、
他のOSでChromeを実行するときどうなるかっていうのはちょっとまた違う話があるよってことですね。
結構デリゲーションメソッドとかクラスが出てきたりするので、
割と役割分割はかなり細かくやってるっぽいですね。
逆に言うとそれだけ細かくセキュリティを分けたり、
それをちゃんと結合するようなクラスとかメソッドを用意しないと
この辺アクセシビリティの柔軟な対応ってのは結構できないんでしょうねってところで。
昔と比べてかなり今は細かい設計とか実装になってそうな気がしますね。
次はビューの話ですね。
ビューズですけども。
ビューズは各ビューに対してビューアクセシビリティを生成し、
そのビューを表すAXプラットフォームノードのデリゲートとして使用されます。
この部分は比較的簡単ですけど、
生成されたツリーっていうのはブラウザーアクセシビリティマネジャーによって処理される
Webアクセシビリティツリーと結合する必要がありますよねと。
この辺はさっき言った感じだなと思います。
あとはWebUIの話ですけど、
WebUIのSurfaceには通常通りレンダラー処理があるため、
WebUIのアクセシビリティは前述のブリンクからコンテンツ、
プラットフォームへのパイプラインを経由します。
WebUIのアクセシビリティは大部分がWebUI.jsのJavaScriptで実装されています。
これらのクラスは必要に応じてドムノードにエリア属性などを追加する処理を行っています。
ラストですね。
ラストはChromeOSレイヤーの話ですね。
ちょっとずつレイヤーが上がっていって、最後はOSレイヤーか。
OSレイヤーってことは逆かな?下がってんのかな?
アクセシビリティツリーはChrome.Automation APIでも公開されており、
拡張JavaScriptっていうのがアクセシビリティツリー、
イベントおよびアクションにアクセスできるようにしています。
このAPIはレンダラー側のコードであるAutomation Internal Custom Bindingsによって
C++で実装されており、
Automation APIによってJavaScriptでも実装されています。
APIはAutomation.IDLで定義されており、
AXENUMSのMOJAMという同期を取る必要がありますよ。
あと、参考資料として2つぐらい貼られてますけど、
Chromeウェブコンテンツとプラットフォームのアクセシビリティの詳細については、
How Chrome Accessibility Worksを参照してください。
ChromeOSのアクセシビリティの詳細については、
How Chrome OS Accessibility Worksを参照してください。
両方じゃん。
どちらにしろ、Chromeアクセシビリティワークですね。
見てみていただければというお話でした。
というところで、本記事は終了になります。
はい、すげー難しかったな。
難しすぎてわからんってなったのと、
今のこのオーバービューを見ましたけど、
このオーバービューをもとに、
ざっと今の記事ですね、
本来読みたかったHow Chrome Accessibility Worksってやつと、
それの続編であるアクセシビリティワークスのパート2を
ざーっと眺めているんですけど、
今見たメソッドが結構ちょいちょい出てくるので、
21:02
知らないまま確かに読むと、
これ何言ってるかよくわからんって感じになると思うんですよ。
あとはですね、画像が結構張られていて、
それぞれのオブジェクトだったりノードっていうのが
どういうふうな関係性になっているとか、
っていうお話が結構出てくるんですよね。
あとツリー構造っていうところも結構絵になってるんですけど、
これを音読するとちょっと難しいというか、
皆さんに伝わらなくて僕だけがひたすら勉強する回になると思うんで、
方針としてはこのまま、
そのアクセシビリティワークスの次の記事に進みたかったんですけど、
ちょっと断念します。
そしてこれワークスパート3もいっぱいあるんですね。
はい、というところで、
今言ったオーバービューを一個一個細かく、
丁寧に内部実装とかを説明してる感じになってるんですけど、
そうするにシープラプラのソースコードとか出てきたりするので、
余計にこれは難しいのでごめんなさい、やめて、
次回以降また全然違う記事を読んでいこうかなと思ってますので、
はい、またお楽しみいただければと思います。
じゃあ今日のアスカツはここで終了したいと思います。
改めましてレノアさんとシチューズさんですね。
ご参加いただきありがとうございました。
あと、後からだけど、おそらくスーさんもかな?
はい、ご参加いただきありがとうございました。
じゃあ今日からまた月曜日ですね。
一週間頑張っていけたらなと思います。
じゃあ終了したいと思います。お疲れ様でした。