Androidの開発の「落とし穴」

私たちのチームは最近、Androidアプリケーションの開発を完了しました。 開発とサポートの過程で、いくつかの技術的な問題が発生しました。 それらのいくつかはバグであり、回避することができます。別の部分はAndroidの完全に明白な機能であり、ドキュメントに記載されていないか、まったく記載されていません。



この記事では、ユーザーの間で発生したいくつかの実際のバグを検討し、それらを解決する方法についてお話します。



この記事は、潜在的な問題の詳細な分析のふりをするものではなく、1つの実際のAndroidアプリケーションの人生の物語にすぎません。





RTFM( http://en.wikipedia.org/wiki/RTFM




Android向けに開発する場合、アプリケーションは膨大な数の異なるデバイスで実行できることに留意する必要があるため、互換性の問題を考慮する必要があります。



たとえば、ユーザーに発生したエラーの1つ:



java.lang.NoClassDefFoundError: android.util.Patterns
      
      







ドキュメントによると、 android.util.Patterns



クラスはAPIバージョン8(Android 2.2.x)以降で利用可能であり、ユーザーはバージョン2.1を使用していました。 もちろん、このコードをtry/catch



でラップすることでこれを決定しました。



ドキュメントの不注意な読み取りによって引き起こされる別の同様の問題を次に示します。



 android.os.NetworkOnMainThreadException at android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork(StrictMode.java:1077) at java.net.InetAddress.lookupHostByName(InetAddress.java:477) at java.net.InetAddress.getAllByNameImpl(InetAddress.java:277) at java.net.InetAddress.getAllByName(InetAddress.java:249)
      
      







問題は、バージョン3.0以降、Androidでは厳格モード( http://developer.android.com/reference/android/os/StrictMode.html )がデフォルトで有効になっていることです。 これは、アプリケーションがメインUIスレッドからネットワークに直接アクセスできないことを意味します。これには時間がかかることがあり、メインスレッドがブロックされ、他のイベントに応答しないためです。 このような動作を回避しようとしましたが、1つの場所では単純なネットワーク呼び出しが残っていました。 このコードは、このコードを別のスレッドに移動したことで解決しました。



デバイスを回転させる-ユーザーにとってよりシンプルで馴染みのあるものは何ですか?




モバイルデバイスでは、画面の向きを変更することは非常に頻繁に使用されるものであり、APIに反映される必要があるようです。 つまり この状況は非常に簡単に処理する必要があります。 しかし、違います。 多くのニュアンスがあります。



何かのリストをダウンロードして画面に表示するアプリケーションを作成する必要があるとします。 つまり ( onCreate()



メソッドで) Activity



が開始されると、データをロードする(UIストリームをブロックしないように)ストリームを開始します。 このスレッドはしばらく実行されているため、 ProgressDialog



読み込みプロセスを表示します。 すべてがシンプルで素晴らしいです。



しかし、ロード後、ユーザーがデバイスを回転させたところ、 ProgressDialog



が再び表示され、データを再度ロードすることがわかりました。 しかし、何も変わっていませんか? デバイスを回転させることでこのリストを見る方が便利です。



そして、 Activity



ときだけでなく、画面が回転したときにもonCreate()



メソッドが呼び出されるということです! しかし、ダウンロードを再びユーザーに待たせたくありません。 必要なのは、既にロードされたデータを再度表示することだけです。



インターネットを検索すると、この問題の説明への多くのリンクと、この問題を解決する方法の膨大な数の例が見つかります。 そして最悪の部分は、これらのリンクのほとんどが間違ったソリューションを提供していることです。 これの例を次に示します-http://codesex.org/articles/33-android-rotate



onConfigurationChanged()



onConfigurationChanged()



して画面の回転処理を行わないでください! これは間違っています! 公式文書には、「 この属性の使用は避け、最終手段としてのみ使用する必要があります。Http://developer.android.com/guide/topics/manifest/activity-element.html#config



しかし、正しいアプローチはここで説明されています-http://developer.android.com/guide/topics/resources/runtime-changes.htmlそして、実践が示すように、実装するのは簡単ではありません。



画面がonRetainNonConfigurationInstance()



する前にonRetainNonConfigurationInstance()



AndroidがActivity



onRetainNonConfigurationInstance()



メソッドを呼び出すというonRetainNonConfigurationInstance()



です。 また、このメソッドからデータ(ロードされたオブジェクトのリストなど)を返すことができます。このデータは、 onCreate()



メソッドの2回の呼び出しの間に保存されます。 次に、 onCreate()



を呼び出すと、保存されたデータを返すgetLastNonConfigurationInstance()



を呼び出すことができます。 つまり Activity



を作成するときにgetLastNonConfigurationInstance()



を呼び出しActivity



データが返された場合、このデータはすでにロードされているので、表示するだけです。 データを返さなかった場合は、ダウンロードを開始します。



しかし実際には、状況は次のようになります。 ユーザーがデバイスを回転させる場合、2つのオプションがあります。 最初のオプションは、データがロードされているとき(データをロードするストリームが機能し、 ProgressDialog



同時に表示されているとき)、またはデータが既にロードされており、表示用にリストに保存したときです。 最初のケースでは、回すときに、作業スレッドへのリンクを保存する必要があり、2番目のケースでは、すでにロードされているリストへのリンクを保存する必要があります。 そうします。



しかし、これはコードを複雑にし、私にはシンプルで直感的ではないようです。 さらに、画面の向きに変更があり、データ読み込みストリームを保存した場合、 onCreate()



ProgressDialog



再度復元する必要がありonCreate()



! アプリケーションにユーザーがさまざまな場所からデータをダウンロードできることをここに追加すると、1つのデータ読み込みストリームではなく、いくつかのストリームがあります-単純な画面回転に役立つコードの量は単純に膨大になります。



正直なところ、なぜこれがそんなに難しいのかわかりません。



ストリームまたはAsyncTaskの使用についてもう少し説明します。




予期せぬ「驚き」が私たちを待っていたので、別のストリームにデータをロードすることをもう少し詳しく見てみましょう。



ちょっとした理論:メインUIスレッド以外のスレッドの作成と操作を容易にするために、特別なクラスが作成されました-AsyncTask( http://developer.android.com/reference/android/os/AsyncTask.html



その本質は、メインUIスレッドで実行され、何かを表示するのに役立つonPreExecute()



およびonPostExecute(Result)



メソッドが既にあり、その中にメインのdoInBackground(Params ...)メソッドがある動作し、自動的に別のスレッドで開始されます。 以下に、サンプルコードを示します。



 private class MyTask extends AsyncTask<Void, Void, Void> { private ProgressDialog spinner; @Override protected void onPreExecute() { //     ProgressDialog //       //     UI  spinner = new ProgressDialog(MyActivity.this); spinner.setMessage(" ..."); spinner.show(); } @Override protected Void doInBackground(Void... text) { //         //       } @Override protected void onPostExecute(Void result) { //  .  ProgressDialog. //     UI  spinner.dismiss(); } }
      
      







すべてがシンプルで美しいです。



しかし、今少し練習。 実際のユーザーからのエラーは次のとおりです。

 android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@40515bd0 is not valid; is your activity running? at android.view.ViewRoot.setView(ViewRoot.java:534) at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:177) at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:91) at android.view.Window$LocalWindowManager.addView(Window.java:424) at android.app.Dialog.show(Dialog.java:241) at ru.reminded.android.social.SocialDataLoader.onPreExecute(SocialDataLoader.java:106) at android.os.AsyncTask.execute(AsyncTask.java:391) at ru.reminded.android.util.SocialAdapterUtils.loadAdapterData(SocialAdapterUtils.java:52) at ru.reminded.android.util.SocialAdapterUtils.access$0(SocialAdapterUtils.java:50) at ru.reminded.android.util.SocialAdapterUtils$1.onComplete(SocialAdapterUtils.java:41)
      
      







このエラーは、 onPreExecute()



メソッドでspinner.show()



を呼び出すと、 onPreExecute()



を参照して作成されたこのProgressDialog



MyActivity



すでに非アクティブであり、画面上にないことを意味します! さて、 onPostExecute()



呼び出すと、これがどのようになるかを理解しています。 つまり たとえば、データの読み込み中に、ユーザーが[戻る]をクリックすると、 ProgressDialog



が画面を離れました。 しかし、ロードが呼び出されたとき、 Activity



がオフになったときにこのコードがすぐに開始されたときに、これがどのようにすぐに起こるかは、私には明らかではありません。



しかし、いずれにせよ、このような状況に対処する必要があるため、 try/catch



のspinner.show spinner.show()



およびspinner.dismiss()



メソッドを使用してこれを行うことにしました。 もちろん、解決策はあまり美しくありませんが、私たちの場合、それは非常に機能的です。



ちなみに、たとえばAndroid用のFacebook SDKには同じコードがあり、これは他の経験豊富な開発者によって開発されました。 また、 ProgressDialog



閉じたときにアプリケーションがクラッシュする状況もありました。 コードに処理を追加する必要がありました。 したがって、この問題は私たちだけのものではありません。



前に説明した別の問題をここに追加します。 デバイスを回すときに、データのロード中に回された場合はProgressDialog



を再作成する必要があります。 これにより、ヘルパーコードもここに追加されます。



また、 doInBackground()



メソッドは別のスレッドで実行されるため、データの読み込み中にエラーが発生した場合、UIストリームではないため、そこから直接Alertを発行することはできません。 エラーを保存し、 onPostExecute(Void result)



メソッドでロードストリームを終了した後、既に何かを表示できます。



つまり 繰り返しますが、多くのサポートコードであり、それほど単純ではありません...



アラームマネージャー




また、ドキュメントにはまったく記載されていない瞬間もあります。 たとえば、アプリケーションではAlarmManager( http://developer.android.com/reference/android/app/AlarmManager.html )を使用します。これは、アプリケーション自体が既に閉じられているときに、しばらくしてからユーザーにメッセージを発行するのに役立ちます。



このAlarmManagerは非常に便利なものです。唯一の問題は、作成された通知が「失われる」場合があることです。 私たちはこれがどのように、なぜ起こるのかを理解するために多くの時間を費やし、すべてのドキュメントを調べ、何も見つかりませんでした。 かなり偶然、この議論に「出会いました」 -http://stackoverflow.com/questions/9101818/how-to-create-a-persistent-alarmmanager



アプリケーションがクラッシュした場合、またはユーザーがタスクマネージャーを介してアプリケーションを強制終了した場合(これは可能かつ非常に一般的です)、AlarmManagerでのこのアプリケーションのすべての通知は削除されます! これは驚きです! そこにあるコメントの1つが特に気に入りました。 「再アラームがキャンセルされました:説明をありがとう。 Androidチームのオフィスアワーg +ハングアウトでこれを聞いたところ、この動作についても混乱しているように見えました。 どこに文書化されていますか?



そのため、アプリケーションを起動すると、設定された通知を再作成する必要があります。AlarmManagerには、そのような通知があるかどうかを確認するAPIもありません。



それは起こります...




以下に、ユーザーに登録したエラーをいくつか示します。 このアプリケーションでは、さまざまなソーシャルネットワークにOAuth認証を使用しているため、Androidでフルタイムのブラウザーを起動する必要があります(OAuthはこれを介して動作する必要があります)。 同時に、定期的に「落ちる」



 android.util.AndroidRuntimeException: { what=1004 when=-14ms arg2=1 } This message is already in use. at android.os.MessageQueue.enqueueMessage(MessageQueue.java:187) at android.os.Handler.sendMessageAtTime(Handler.java:457) at android.os.Handler.sendMessageDelayed(Handler.java:430) at android.os.Handler.sendMessage(Handler.java:367) at android.os.Message.sendToTarget(Message.java:349) at android.webkit.WebView$5.onClick(WebView.java:1250) at com.android.internal.app.AlertController$ButtonHandler.handleMessage(AlertController.java:172) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:130) at android.app.ActivityThread.main(ActivityThread.java:3703) at java.lang.reflect.Method.invokeNative(Native Method) at java.lang.reflect.Method.invoke(Method.java:507) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:841) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:599) at dalvik.system.NativeStart.main(Native Method)
      
      







もう一つ:



 java.lang.NullPointerException at android.os.Message.sendToTarget(Message.java:348) at android.webkit.WebView$4.onClick(WebView.java:1060) at com.android.internal.app.AlertController$ButtonHandler.handleMessage(AlertController.java:158) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:123) at android.app.ActivityThread.main(ActivityThread.java:4627) at java.lang.reflect.Method.invokeNative(Native Method) at java.lang.reflect.Method.invoke(Method.java:521) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:860) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:618) at dalvik.system.NativeStart.main(Native Method)
      
      







そしてまた:



 java.lang.IndexOutOfBoundsException: setSpan (-1 ... -1) starts before 0 at android.text.SpannableStringBuilder.checkRange(SpannableStringBuilder.java:949) at android.text.SpannableStringBuilder.setSpan(SpannableStringBuilder.java:522) at android.text.SpannableStringBuilder.setSpan(SpannableStringBuilder.java:514) at android.text.Selection.setSelection(Selection.java:74) at android.text.Selection.setSelection(Selection.java:85) at android.widget.TextView.performLongClick(TextView.java:8621) at android.webkit.WebTextView.performLongClick(WebTextView.java:617) at android.webkit.WebView.performLongClick(WebView.java:4471) at android.webkit.WebView$PrivateHandler.handleMessage(WebView.java:8285) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:150) at android.app.ActivityThread.main(ActivityThread.java:4389) at java.lang.reflect.Method.invokeNative(Native Method) at java.lang.reflect.Method.invoke(Method.java:507) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:849) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:607) at dalvik.system.NativeStart.main(Native Method)
      
      







おわりに




上記のすべてにもかかわらず、私はAndroidの開発が好きでした。 基本的に、APIはよく考えられていて便利です。



それはtry/catch



を使用する価値があるすべての場所です。 それがまったく明らかでない場合でも。



そしてまだ...必然的に、いいえ-そうではなく、そのように-常にユーザーからエラーに関する情報を収集します。 これには、無料のACRAライブラリ( http://code.google.com/p/acra/ )を使用します。 その開発者に感謝します!



All Articles