Android: SoftReferenceでOut of Memory対策

現在Androidで画像を扱うアプリを作っているのですが、Androidの場合、画像を大量に扱うと、あっという間にメモりを使い果たしてOut of Memoryで強制終了してしまうという悲しい現実があります。画像をある程度キャッシュするようにしないと、毎回画像をインターネットからDLすることになってパフォーマンスは低下するし、かといってキャッシュするとOut of Memoryが発生する…。

Web+DB Press Vol.57で、onPauseのタイミングで大きなリソースは解放してしまい、小さなリソースのみキャッシュするというふうにするとOut of Memoryが発生しにくいというふうに書かれていたので、サムネイルのみキャッシュして、でかい画像は全くキャッシュしないように実装してみたのだけれど、どうもしっくりこない。やっぱりパフォーマンスが落ちるからでしょう。

パフォーマンス対策のみならずメモリ対策もどうしたらいいのかな〜と思案していたのですが、TechFirm Android LabさんのブログでSoftReferenceが紹介されていました。

SoftReferenceを使うと、メモリがOut of Memoryになりそうになると、優先的にGCされるそうです。これを使っておけば、Out of Memoryに悩まされることもなさそうです!BitmapオブジェクトをSoftReferenceでラップするようにして使うようです。

public class ImageCache {
  private static HashMap<String,SoftReference<Bitmap>> cache = new HashMap<String,SoftReference<Bitmap>>();
  public static Bitmap getImage(String key) {
    if (cache.containsKey(key)) {
      SoftReference<Bitmap> ref = cache.get(key);
      if (ref != null) {
        return ref.get();
      }
    }  
    return null;  
  }  
	
  public static void setImage(String key, Bitmap image) {
    cache.put(key, new SoftReference<Bitmap>(image));
  }
	
  public static boolean hasImage(String key) {
    return cache.containsKey(key);
  }
	
  public static void clear() {
    cache.clear();
  }
}

さっき画像を大量に扱うアクティビティで試してみたら、全く落ちなくなっていて感動です!あぁ〜、2週間前から知っていたらなぁ〜。いや、そういう経験を積んでこそ成長するんですが。TechFirm Android Labさん、貴重な情報をありがとうございました!


Android: GridViewに次へボタンを仕込む

GridViewで、twicca(Androidのtwitterアプリ)みたいに、リストビューの下のほうにスクロールしたら、『もっと読む』みたいなボタンをつけたいと思っていたのだけれど、GridViewにはListViewのようにヘッダーやフッターをつける方法がない。最初は無理やり、ScrollViewの中にLeniearLayout(GridViewとButton入り)を入れればできるかなと思っていたのだけれど、世の中甘くなかった。LeniearLayoutのlayout_widthをfill_parentにしても、そうなってくれないようなのである(EclipseのViewエディター上で確認)。元々GridView自体がScrollViewを内包しているためか、やっぱり思ったとおりにはなってくれないので、諦めかけていた。

twitterで諦めた宣言をしていたら、yokmamaさんからアドバイスをいただけたことで、閃いた。

ソースコードは今手元にないので、後日公開するとして(本当か?)、考え方はこうだ。
GridViewに渡すAdapterをImageAdapterとした場合、

  1. ImageAdapter.getCountの結果を1増やす。つまり、画像が20枚ならば、21にする。
  2. ImageAdapter.getViewで、
    positionが20未満の場合は今までと同じ処理(画像用)で画像を返す。
    20の場合は次へボタンを表す画像を返す
  3. GridView.setOnItemClickListenerで、positionが20未満の場合は今までと同じ処理。
    20の場合は次の20件をダウンロードしてImageAdapterを更新する処理を行う。

つまり、ダミーのGridViewアイテムを準備して、そいつがクリックされたらダウンロードという作戦。フッターを準備するのではなく、ImageAdapter側を変則的にすることで解決するという方法なので、ImageAdapterクラスの一貫性としては損なわれるのであるが、実装できなければ話にならない。それがプログラムという世界である、なんつって~…(ごめんなさい、実力がないだけです)。一列に画像が4枚あるとしたら、以下のようになります(■が画像、□がダウンロードボタンとする)。

■■■■
■■■■
■■■■
■■■■
■■■■

とりあえずはこれで次へボタンっぽい動きにはなった!しかし問題はまだあって、ListViewのようにInflatorを使っているわけではないので、追加されるというよりは、要素を増やしてGridViewを再描画するということになるため、処理が走るとスクロールバーが一番上に戻ってしまう。スクロールバーの位置を覚えておいて、ダウンロードボタンが押されてGridViewの再描画が行われたと同時に、スクロールバーの位置を変えてやらなければならないかなぁと思ってます。

本当にAndroidのレイアウトは奥が深いなぁ~と思い知らされる毎日。

(追記)
Android: GridView更新後にスクロール位置を保つを書きました。


Android:searchableを使用する方法

MapActivityを使っているときに、地名検索ができるといいのになぁと。その際にGoogleが提供しているMapアプリのように検索ボタンを押したら画面上部に検索フォームが出てきて、さらにボイス検索まで出来たら素晴らしいのだろうが、これは一体どうやてやるのだろうか?と思って色々と検索していたら、わかりやすく紹介されている方がおられたので、それを参考にした。

searchableを使用する方法 – haruserのめもちょ

なお、今回は検索フォームで入力された内容をToastで表示するまでをやります。
さらにさらに、今書いている環境が実際に作業した環境ではないので、かなりうろ覚えで書いてます。多分大丈夫だと思うけれど、間違っていたらすみません。ご指摘お願いします。

1.まず、/res/xmlにsearchable.xmlを作成する。

<searchable
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:label="@string/search_label"
  android:hint="@string/search_hint"
  android:imeOptions="actionSearch" 
  android:searchMode="showSearchLabelAsBadge" 
  android:inputType="text" 
  android:voiceSearchMode="showVoiceSearchButton|launchRecognizer" />

android:voiceSearchModeを上記のように設定しておくだけで、音声検索対象言語は端末に設定されている言語になるようなので、日本語を選択している場合はこのままでいいし、外国語の場合でも勝手に外国語になってくれるっぽいです。固定にしたい場合はほかのパラメータを追加していくことになりそうですが、今回は必要ないので設定していません。また、音声検索できない端末の場合は、音声検索ボタン自体が表示されません(Nexus One, Desireで表示されたが、HT-03Aで表示されなかったことを確認)。

2.次に呼び出し元のActivityの設定を行うためにマニフェストを修正します。
今回は検索フォームに入力された内容を自分自身のアクティビティに渡します。
違うアクティビティに渡したい場合は、上のほうに書いたURLのほうをご参照ください。

<activity
  android:name="Map"
  android:label="@string/app_name"
  android:launchMode="singleTop">
  <intent-filter>
    <action android:name="com.example.VIEW_SEARCH" />
    <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
  <intent-filter>
    <action android:name="android.intent.action.SEARCH" />
    <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
  <meta-data android:name="android.app.default_searchable"
             android:value="Map" />
  <meta-data android:name="android.app.searchable"
          android:resource="@xml/searchable" />
</activity>

3.onSearchRequestedメソッドを呼び出します。
検索キーが押されたらコールされますが、メニューなどに検索を付けている場合は、メニューボタンが押されたらonSearchRequestedを呼び出すようにしておきましょう!

4.Activityに、onNewIntentを実装しましょう。
検索と結果が同じ画面でandroid:launchMode=”singleTop”が指定されていれば、onNewIntentメソッドが呼び出されるので、そこで検索処理を実行します。

@Override
protected void onNewIntent(Intent intent) {
  if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
    String query = intent.getStringExtra(SearchManager.QUERY);
    // 検索処理を実行 今回はToastを表示
    Toast.makeText(this, query, Toast.LENGTH_LONG).show();
    // 自分がやった実装ではGeoCoderで地名検索させて、Addressに変換させた。
  }
}

1つのアクティビティ内で完結する検索フォームの実装方法は、思ったよりも簡単でよかったです。searchableを使わない場合は、もうひとつ検索用ActivityをかませてIntent経由で結果を取得して、となるので、検索用Activityのレイアウトを実装しなければならなかったりと、面倒なことが多そうなので、こちらのほうがスマートかなと思います。

今回の実装はあんまりコード書いてないのに音声検索までできてしまうので、人に見せたら「スゴイ!!」と言われてちょっと嬉しかったりする。。。(^_^;)


Android: 端末の回転時にActivityを再起動させない方法

Android端末がの画面の向きが変わったときに、Activityが再起動してしまう。これは、アプリの整合性を保つ為には必要であるらしいのですが、現在自分が作っているアプリケーションにとっては問題あり。最初はonDestroyが走る前に状態を保存して再起動したときに状態を復元するように作成するのが正しいのかなと思っていたのですが、本を見ていたら、簡単に解決する方法を見つけました。Activityの再起動自体をさせない方法です。

  1. ActivityにonConfigurationChangedメソッドを実装
  2. マニフェストファイルのActivityの定義に、android:configChanges属性を追加

コンフィグが変わったことを検知して呼ばれるメソッドを実装。
今回は、端末の回転のみ検知するようにしてあります。(後のマニフェストにて)

@Override
public void onConfigurationChanged(Configuration newConfig) {
  super.onConfigurationChanged(newConfig);
  Log.v("TEST", "onConfigrationChanged was called!!");
}

マニフェスト側で、configChangesにorientationを指定。これで回転のみを対象とします。

<activity android:name=".TestActivity" android:label="@string/app_name" android:configChanges="orientation">

これで、端末の向きを変えてもActivityが再起動しません!!

Androidアプリを開発するには、この本が一番内容が充実していますね。会社にはあるんだけど、実は個人ではまだ買ってない…。


夏風邪

多分もう大丈夫なのですが、夏風邪を引いてしまいました〜…。自己管理がなってないということか。どうも冷房に弱いので、毎年この時期になると必ず夏風邪で寝込んでいるような気がします。今回は予定の詰まっていた金・土に当たってきたのだけど、その際はまだ症状が弱かったので、なんとかやり過ごせたけど、日曜日からどーんと発症。今日も朝起きたら治ってることを期待していたのだけれど、全然駄目でした。昼過ぎに起きたら、すっきりしてたけど。嫌な汗がだいぶ出たので、もう大丈夫かなとは思いますが、久々に有休を使ってしまいました。まぁしゃーない。

案外寝ることができたので、睡眠不足からくる疲れもひょっとしたら溜まっていたのかもしれません。やっぱりほどほどに寝ないと頑張り続けられないということですかね〜。それでも毎日5〜6時間程度は寝ていたんだけど。早いところこなさなければならない案件があるので、それを終わらせて、ぐっすりと眠りたいものです。本当は休んでいる場合ではない…。