Android WebView:現在の問題と解決策

前回のAndroidDevs Meetupミーティングで、ICQメッセンジャーチームの開発者数人が話しました。 私の講演はAndroid WebViewについてでした。 会議に参加できなかったすべての人のために、スピーチに基づいてここに記事を公開しています。 私は大きなタッチで2階に行きます。 技術的な詳細や大量のコードは提供しません。 詳細に興味がある場合は、投稿の最後にあるリンクからイラストとして特別に書かれたアプリケーションをダウンロードして、すべての例をご覧ください。



WebViewとは何ですか?



WebViewはAndroidプラットフォームのコンポーネントであり、WebページをAndroidアプリケーションに埋め込むことができます。 実際、これは組み込みブラウザーです。 約1年前にWebViewを使用して、Webアプリケーションをメッセンジャーに統合するためのICQ Web APIを作成することにしました。 Webアプリケーションとは何ですか? これは基本的にJavaScriptを含むHTMLページであり、ICQ内で動作します。 ICQ Web APIを使用して、JavaScriptを介したWebページは、たとえば、メッセージの送信、チャットの開始など、ICQにさまざまなコマンドを提供できます。







ICQでは次のようになります。 [アプリケーション]項目から、アプリケーションのリストに移動できます。 これはまだWebViewではありません。それにアクセスするには、いずれかのアプリケーションを選択する必要があります。 次に、WebViewに直接移動し、Webアプリケーションがネットワークからダウンロードされます。



これはどのように技術的に整理されていますか? WebViewには、JavaScriptで特定の方法でJavaコードを挿入する機能があります。 JavaScriptは、作成して提供したコードを呼び出すことができます。 これは、ICQ Web API全体が基づいている機会です。







ここでは、WebViewがICQ内で動作し、それらの間にJavaクラスが挿入され、ネットワークからのアプリケーションがWebViewにロードされることを示しています。



そのため、WebViewのJavaScriptはICQ Javaコードを呼び出します。 さまざまな課題が数多くあり、開発プロセス中に、このメカニズムの動作に関連する多くの問題がありました。これについては後で説明します。



WebViewの問題



通常、ダウンロードを開始した後、このプロセスを制御する必要があります。ダウンロードが成功したかどうか、リダイレクトがあったかどうか、ダウンロード時間などを追跡します。 また、JavaScriptを実行してJavaで呼び出すストリーム、JavaとJavaScriptのタイプの不一致、JavaScriptでのアラートの動作、および送信データのサイズについても説明します。 これらの問題の解決策についても後述します。



WebViewの基本



WebViewの基本について簡単に説明します。 4行のコードを検討してください。



WebView webView = (WebView) findViewById(R.id.web_view); webView.loadUrl("http://www.example.com"); webView.setWebViewClient(new WebViewClient() {…} ); webView.setWebChromeClient(new WebChromeClient() {…} );
      
      





WebViewを取得し、WebView.loadURL()を呼び出してexample.comをロードすることがわかります。 Androidには、WebViewと対話するWebViewClientとWebChromeClientの2つの重要なクラスがあります。 なぜ必要なのですか? WebViewClientはページのロードプロセスを制御するために必要であり、WebChromeClientはこのページが正常にロードされた後にこのページと対話するために必要です。 WebViewClientは、ページの読み込みが完了する前に動作し、その後WebChromeClientが動作します。 コードを見るとわかるように、WebViewと対話するには、これらのクラスの独自のインスタンスを作成し、WebViewに渡す必要があります。 さらに、特定の条件下で、WebViewはインスタンスで再定義したさまざまなメソッドを呼び出します。そのため、システム内のイベントについて学習します。



WebViewが作成したWebViewClientおよびWebChromeClientインスタンスで呼び出す最も重要なメソッドは次のとおりです。

Webviewclient Webchromeclient
onPageStarted() openFileChooser()、onShowFileChooser()
shouldOverrideUrlLoading() onShowCustomView()、onHideCustomView()
onPageFinished()、onReceivedError() onJsAlert()
これらすべての方法の目的については、後ほど少し説明しますが、名前自体からはすでに多くのことが明らかになっています。



WebViewでのページの読み込みの制御



WebViewにページをロードするコマンドを与えた後、次のステップは実行の結果を見つけることです:ページをロードしました。 公式のAndroidドキュメントの観点から見ると、すべてが単純です。 ページの読み込み開始時に呼び出されるWebViewClient.onPageStarted()メソッドがあります。 リダイレクトの場合、ページがロードされていればWebViewClient.shouldOverrideUrlLoading()が呼び出されます-WebViewClient.onPageFinished()、ロードされていなければ-WebViewClient.onReceivedError()。 すべてが論理的なようです。 これは実際にどのように起こりますか?



待機中:

  1. onPageStarted→shouldOverrideUrlLoading(リダイレクトされた場合)→onPageFinished / onReceivedError


現実:

  1. onPageStarted→ onPageStarted →onPageFinished
  2. onPageStarted→ onPageFinished →onPageFinished
  3. onPageFinished →onPageStarted
  4. onReceivedError →onPageStarted→onPageFinished
  5. onReceivedError→onPageFinished(onPageStartedなし)
  6. onPageFinished(onPageStartedなし)
  7. shouldOverrideUrlLoading→ shouldOverrideUrlLoading


実際、すべてが常に異なり、特定のデバイスに依存します:onPageStarted()、onPageFinished()およびその他のメソッドは2回呼び出すことができ、すべてのメソッドは異なる順序で呼び出すことができ、一部はまったく呼び出されない場合があります。 特に、SamsungおよびGoogle Nexusでこのような問題が発生することがよくあります。 この問題は、WebViewClientクラスのインスタンスに追加のチェックを追加することで解決する必要があります。 動作し始めたら、URLを保存し、そのURLでダウンロードが行われることを確認します。 完了したら、エラーを確認します。 コードが大きいので、引用しません。 最後のリンクを参照することをお勧めします。



JavaScriptコードインジェクション



サンプルJavaコード:



 WebView webView = (WebView) findViewById(R.id.web_view); webView.getSettings().setJavaScriptEnabled(true); mWebView.addJavascriptInterface(new MyJavaInterface(), "test"); private class MyJavaInterface { @android.webkit.JavascriptInterface public String getGreeting() { return "Hello JavaScript!"; } }
      
      





JavaScriptコードの例:



 <input value="Click" onclick="javascript:alert(test.getGreeting());"/>
      
      





JavaScriptにJavaコードを挿入する例を次に示します。 短いJavaクラスMyJavaInterfaceが作成され、1つのgetGreeting()メソッドがあります。 このメソッドは@JavaScriptInterfaceタグ付けインターフェースでマークされていることに注意してください-これは重要です。 WebView.addJavascriptInterface()メソッドを呼び出すことにより、このクラスをWebViewにスローします。 以下に、test.getGreeting()を呼び出してJavaScriptからアクセスする方法を示します。 ここで重要な点は名前テストです。これは、後でJavaコードを呼び出すことができるオブジェクトとしてJavaScriptで使用されます。



戻り文字列「Hello JavaStript!」にブレークポイントを設定し、呼び出しが受信されたストリームの名前を確認すると、どのストリームになりますか? これはUIスレッドではなく、特別なJava Bridgeスレッドです。 したがって、一部のJavaメソッドを呼び出すときにUIを操作する場合は、これらの操作がUIストリームに転送されることを確認する必要があります-ハンドラーまたはその他の方法を使用します。



2番目のポイント:Java Bridgeはブロックできません。そうしないと、WebViewのJavaScriptが機能しなくなり、ユーザーアクションに応答がありません。 したがって、多くの作業を行う必要がある場合は、タスクを他のスレッドまたはサービスにも送信する必要があります。



JavaScriptのJava型の不一致



上記のように、Javaで記述されJavaScriptに注入されたメソッドを呼び出すと、JavaとJavaScriptの型の不一致に問題があります。 次の表は、型システム間の基本的なマッピング規則を示しています。

Java-> JavaScript JavaScript-> Java
byte、short、char、int、long、float、double Byte、short、int、long、float、double(Integer、Byte、Short、Long、Float、Doubleではなく、charではない)
ブール値 ブール ブール ブール(ブールではない)
ブール、整数、長整数、文字、オブジェクト 対象 配列、オブジェクト、関数 ヌル
ひも 文字列(オブジェクト) ひも 文字列(char []以外)
char []、int []、Integer []、Object [] 未定義 未定義 ヌル
ヌル 未定義 ヌル ヌル


ここで最も重要なことは、オブジェクトラッパーが渡されないことです。 また、JavaScriptのすべてのJavaオブジェクトのうち、ストリングのみがマップされます。 Javaの配列とnullは、JavaScriptで未定義に変換されます。



JavaScriptからJavaに反対方向に渡すと、ニュアンスもあります。 パラメーターとして基本型を持つメソッドを呼び出す場合、そこで数値を渡すことができます。 また、メソッドのパラメーターの中に基本型がない場合、たとえばIntegerなどのオブジェクトラッパーがある場合、そのようなメソッドは呼び出されません。 したがって、Javaの基本タイプのみを使用する必要があります。



JavaとJavaScriptの間で転送されるデータのサイズ



もう1つの大きな問題は、JavaとJavaScriptの間で転送されるデータの量です。 十分な量のデータ(たとえば、写真)がJavaScriptからJavaに転送された場合、OutfMemoryエラーが発生すると機能しません。 アプリケーションがクラッシュするだけです。 この場合、logcatに表示される例を次に示します。



 Process com.estrongs.android.pop (pid 6941) has died Process com.google.android.youtube (pid 24613) has died Process kik.android (pid 3022) has died Process com.doeasyapps.optimize (pid 30743) has died Process com.sandisk.mz (pid 31340) has died WIN DEATH: Window{3fc4769b u0 mc.test/mc.test.MainActivity} WIN DEATH: Window{17a5c850 u0 mc.test/mc.test.DataSizeTestActivity} Process mc.test (pid 16794) has died Force removing ActivityRecord{215171e4 u0 mc.test/.DataSizeTestActivity t406}: app died, no saved state
      
      





ご覧のとおり、OutOfMemoryがアプリケーションで発生すると、デバイスで実行されている他のさまざまなアプリケーションが飛び出し始めます。 その結果、可能なすべてを閉じたAndroidはアプリケーションに到達し、フォアグラウンドにあるため、最後に閉じます。 繰り返しますが、例外を受け取らず、アプリケーションが単にクラッシュすることを思い出してください。 これを回避するには、送信データのサイズを制限する必要があります。 多くはデバイスに依存します。 一部のガジェットでは6メガバイト、2〜3メガバイトを転送することが判明しています。 私たち自身のために、1メガバイトの制限を選択しました。これはほとんどのデバイスで十分です。 さらに転送する必要がある場合は、データをチャンクに分割して部分的に転送する必要があります。



JavaScriptアラート



デフォルトでは、WebViewのアラートダイアログは機能しません。 そこにJavaScriptを含むHTMLページを読み込んでアラート(「Hello」)を実行すると、何も起こりません。 動作させるには、WebChromeClientインスタンスを定義し、WebChromeClient.onJSAlert()メソッドをオーバーライドし、そのインスタンスでsuper.onJSAlert()を呼び出す必要があります。 これでアラートが機能します。



 WebView webView = (WebView) findViewById(R.id.web_view); webView.getSettings().setJavaScriptEnabled(true); webView.setWebChromeClient(new WebChromeClient() { @Override public boolean onJsAlert(....) { return super.onJsAlert(view, url, message, result); } }
      
      





デバイスの向きの変更の処理



もう1つの深刻な問題は、縦向きと横向きです。 デバイスの向きを変更すると、デフォルトでアクティビティが再作成されます。 この場合、それに接続されているすべてのビューも再作成されます。 状況を想像してください。特定のゲームがロードされるWebViewがあります。 ユーザーはレベル99に到達し、デバイスをオンにすると、ゲームを含むWebViewインスタンスが再作成され、ページがリロードされ、再び最初のレベルになります。 これを回避するために、デバイス構成の変更を手動で処理します。 原則として、このことは公式文書で知られており、説明されています 。 これを行うには、アクティベーションセクションでAndroidManifest.xmlのパラメーターconfigChangesを登録するだけで十分です。



 <activity android:name=".WebViewActivity" android:configChanges="orientation|screenSize"> </activity>
      
      





これは、私たち自身が活動の方向の変化を処理していることを意味します。 方向が変更された場合、Activity.onConfigurationChange()の呼び出しを取得し、プログラムで一部のリソースを変更できます。 ただし、通常はフルスクリーンに拡大されたWebView自体のみがWebViewでアクティビティを実行し、実行することはありません。 再描画するだけで、すべてが正常に機能し続けます。 したがって、configChangesを設定すると、アクティビティを再作成せずに、アクティビティ内に存在するすべてのビューの状態を保持できます。



フルスクリーンメディアプレーヤー



メディアプレーヤーがWebページに組み込まれている場合、多くの場合、メディアプレーヤーが全画面モードで動作することを確認する必要があります。 たとえば、 youtubeメディアプレーヤーはiframe htmlタグのWebページ内で動作でき 、フルスクリーンモードに切り替えるためのボタンがあります。 残念ながら、これはデフォルトではWebViewでは機能しません。 これを機能させるには、いくつかの操作を行う必要があります。 WebViewが配置されているxmlレイアウトで、FrameLayoutを追加で配置します。 これは、フルスクリーンに引き伸ばされたコンテナであり、その中にプレーヤーのあるビューが配置されます。



 <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent"> <WebView android:id="@+id/web_view" android:layout_width="match_parent" android:layout_height="match_parent"/> <FrameLayout android:id="@+id/fullscreen_container" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone"/> </FrameLayout>
      
      





そして、WebChromeClientインスタンスで、いくつかのメソッドをオーバーライドします。



 public void onShowCustomView(View v, CustomViewCallback c) { mWebView.setVisibility(View.GONE); mFullScreenContainer.setVisibility(View.VISIBLE); mFullScreenContainer.addView(view); mFullScreenView = v; mFullscreenViewCallback = c; } public void onHideCustomView() { mFullScreenContainer.removeView(mFullScreenView); mFullscreenViewCallback.onCustomViewHidden(); mFullScreenView = null; mWebView.setVisibility(View.VISIBLE); mFullScreenContainer.setVisibility(View.GONE); }
      
      





ユーザーがプレーヤーでフルスクリーンモードに切り替えるためのボタンをクリックすると、システムはWebChromeClient.onShowCustomView()を呼び出します。 onShowCustomView()は、プレーヤー自体が表すビューを受け入れます。 このビューはFullScreenContainerに挿入されて表示され、WebViewは非表示になります。 ユーザーがフルスクリーンモードから戻りたい場合、WebChromeClient.onHideCustimView()メソッドが呼び出され、逆の操作が実行されます。WebViewを表示し、FullScreenContainerを非表示にします。



入力タイプ=”ファイル”



Web開発者は、ユーザーがファイルを選択してサーバーにアップロードしたり、画面に表示したりできるように、このコンテナーがWebページで使用されることを知っています。 このコンテナがWebViewで機能するには、WebChromeClient.openFileChooser()メソッドをオーバーライドする必要があります。 このメソッドには、ユーザーが選択したファイルを転送する必要がある特定のコールバックがあります。 それ自体/>



は、追加機能はありません。 提供する必要があるファイル選択ダイアログ。 つまり、ユーザーが目的のファイルを選択する標準のAndroidピッカーを開いて、たとえばonActivityResult()から取得し、openFileChooser()メソッドをコールバックに渡すことができます。



JavaScriptコードの例:



 <input type="file" onchange="onFaileSelected(event)"/>
      
      





サンプルJavaコード:



 WebChromeClient myClient = new WebChromeClient() { @SuppressWarnings("unused") public void openFileChooser(ValueCallback<Uri> callback, String accept, String capture) { callback.onReceiveValue(Uri.parse("file://" + getFileFromSomeProvider())); } }; WebView webView = (WebView) findViewById(R.id.web_view); webView.setWebChromeClient(myClient);
      
      





JavaScriptネットワーク検出



JavaScriptには便利なNavigatorオブジェクトがあります。 ネットワーク接続のステータスを示すonLineフィールドがあります。 ネットワーク接続がある場合、ブラウザでこのフィールドはtrueになり、そうでない場合はfalseになります。 WebView内で正しく機能するには、WebView.setNetworkAvailable()メソッドを使用する必要があります。 それを使用して、現在のネットワークステータスを送信します。これは、ネットワークブロードキャストレシーバーを使用するか、Androidでネットワークステータスを追跡する他の方法で取得できます。 これは常に行わなければなりません。 ネットワークステータスが変更された場合、WebView.setNetworkAvailable()を再度呼び出して、現在のデータを転送する必要があります。 JavaScriptでは、Navigator.onLineを介してこのプロパティの現在の値を取得します。



コード例



github.com/mc-android-developer/mc.presentation.webview



質疑応答



質問: CrossWalkプロジェクトがあります-これは、古いデバイスで新しいChromeを使用できるようにするサードパーティのWebView実装です。 経験はありますか、それを埋め込もうとしましたか?

回答:試していません。 現時点では、14番目のバージョンからAndroidをサポートしており、古いデバイスに焦点を合わせていません。



質問: WebViewをレンダリングするときに残るアーティファクトをどのように処理しますか?

回答:私たちは彼らと戦っていません。試しました-うまくいきませんでした。 これはすべてのデバイスで発生するわけではありません。 これは、より多くのリソースを費やすための大きな問題ではないと判断しました。



質問: WebViewをScrollViewに埋め込む必要がある場合があります。 これはいですが、割り当てによっては必要になる場合があります。 これは推奨されておらず、どこかで禁止されていることもあります。その後、作業に欠点があります。 しかし、それでも、時にはそれをしなければならないことがあります。 たとえば、上にWebViewを描画し、その下にある種のネイティブコンポーネント(要件に応じてネイティブである必要があります)を描画し、これらすべてを単一のScrollViewとして実行する必要がある場合。 つまり、最初にユーザーはページ全体を見てから、必要に応じてこれらのネイティブコンポーネントにスクロールします。

回答:残念ながら、私はあなたに答えることができません。なぜなら、私はそのような状況に遭遇したことがないからです。 非常に具体的であり、ScrollViewにWebViewを配置する必要がある場合のオプションを想像するのは困難です。



質問:メールアプリケーションがあります。 受信者と他のすべてのものの上に帽子があります。 それでも、すべてがスムーズに進むわけではありません。 WebViewがScrollView内でサイズを決定しようとすると、大きな問題が発生します。

回答: WebView内にUIの指定された部分を描画しようとすることができます。



質問:つまり、すべてのロジックをネイティブ部分からWebViewに完全に転送し、これらのコンテナーを残すには?

回答:おそらく、ロジックを転送する必要はないかもしれません。Javaクラスを注入するということです。 ロジックはそのままにして、注入されたクラスから呼び出すことができます。 UIのみをWebViewに移植できます。



質問:あなたはメッセンジャーでゲームに言及しました。 それらはWebアプリケーションですか?

回答:はい、これらはWebView内のJavaScript Webページです。



質問:ゲームをネイティブに書き換えないためだけに、これをすべて実行しますか?

回答:そしてこれも。 しかし、主なアイデアは、サードパーティの開発者にICQに埋め込むことができるアプリケーションを作成する機会を提供し、このICQ Web APIを使用してメッセンジャーと対話することです。



質問:つまり、これらのゲームはラップトップのWebブラウザーでもプレイできますか?

回答:はい。 Webブラウザーで開くことができ、場合によっては直接デバッグすることもできます。



質問:そして、ChromeのIntentがこのおもちゃを投げた場合、どのような問題が発生しますか? 独自のWebViewを作成せず、サービスを使用する場合は?

回答:問題は、WebViewでJavaクラスの注入を通じてAPIを提供できることです。このAPIを使用すると、アプリケーションはICQと直接対話し、さまざまなコマンドを送信できます。 ユーザー名を取得し、開いたチャットを受信し、ICQから直接チャットにメッセージを送信するコマンドを考えてみましょう。 つまり、ChromeからICQにメッセージを直接送信することはできません。 私たちの場合、これはすべて可能です。



質問:データを1メガバイトの断片にカットすると述べました。 後でどのように収集しますか?

回答:そのような必要性がないため、現時点ではこれを行っていません。



質問: 1メガバイトで十分ですか?

回答:はい。 写真が大きい場合は、それらを絞ってみてください。 そのような必要性が存在する場合、これは解決策かもしれないと言いました-後でJavaでカットしてアセンブルすること。



質問:サンドボックス内のアプリケーションのセキュリティをどのように確保しますか? JavaScriptアプリケーションからインジェクトされたJavaクラスを呼び出す必要があることを正しく理解しましたか?

回答:はい。



質問:この場合、セキュリティはどのように確保されますか?システム機能へのアクセスは禁止されていますか?

回答:現在、システムはまだかなり古いため、主に独自のWebアプリケーションを使用し、それらを完全に信頼しています。 将来、すべてのアプリケーションが管理され、コードが表示され、特別なセキュリティチームが割り当てられます。 さらに、特別な許可システムが作成されます。これがないと、アプリケーションはユーザーの重要な情報にアクセスできません。



All Articles