Android GridViewのパフォーマンスを上げよう(1/2)

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

今回は大量のデータを表示したり、positionを指定してジャンプできるなど、何かと便利なクラスなGridVewのパフォーマンスを上げる方法を2回に渡って紹介します。

GridViewはListViewの兄弟分でAbsListViewの派生クラスになりますので、一列か複数行かの違いのみと思ってよいと思います。 仕組みとして、表示している領域の数分のVewだけを生成するという特性があります。 つまり、大量のデータが存在しても、描画にかかるコストは見えている領域のみとなります。 逆に言うとスクロールをしていて見えていない箇所を描画する際に新たに描画の為のロジックが走ることになるため、スムーズなスクロールが難しくなりがちです。 これはFlexを知っている方ならご存知かと思いますが、Flex4.5でいうListコンポーネント×ItemRendererと同じような仕組みになります。

ただその仕組みの弱点として、スクロール時に毎度表示する部分のViewを設定するため、少し気を付けて作らないと、描画回数が減り、瞬間移動しているようなスクロールになってしまいます。

ArrayAdapterを継承したカスタムクラスのgetViewがGridViewの一コマ一コマを描画するタイミングで呼ばれます。ListViewもそうですが、GridViewはスクロール時に列数分このメソッドが呼ばれるので、ここが遅いとパフォーマンスの低下が顕著です。なのでこのメソッド内の処理を如何に短時間で終わらせられるかが鍵となっているので、今回はgetViewメソッドに焦点を当てて、できるだけスムースなスクロールで気持ちよくなれるようにチューニングしていきます。

サンプルとして画像とファイル名、Exif情報の一部を表示するプロジェクトとします。

※GridViewの基本的な使い方はこちらをご覧ください。

0.最初の一番遅いバージョン

何も考えずに書くとこうなるかと思われます。(実際なりました..)


// ArrayAdapterを継承したgetViewメソッド
@Override
public View getView(int position, View convertView, ViewGroup parent) {
	
	// Photo
	Photo photo = getItem(position);

	convertView = mLayoutInflater.inflate(R.layout.grid_item, null);

	// Grid内の1コマ内のViewを取得
	ImageView image = (ImageView) convertView.findViewById(R.id.image);
	TextView filePath = (TextView) convertView.findViewById(R.id.file_name);
	TextView width = (TextView) convertView.findViewById(R.id.width);
	TextView height = (TextView) convertView.findViewById(R.id.height);
	TextView dateTime = (TextView) convertView.findViewById(R.id.date_time);

	// 画像をデコードして設定
	Bitmap bm = BitmapFactory.decodeFile(photo.filePath);
	image.setImageBitmap(bm);
	// Exif情報等を設定
	filePath.setText(photo.filePath);
	width.setText(photo.width);
	height.setText(photo.length);
	dateTime.setText(photo.dateTime);
	
	// 背景色を変更
	if (position % 2 == 0) {
		convertView.setBackgroundColor(Color.BLACK);
	} else {
		convertView.setBackgroundColor(Color.parseColor("#444444"));
	}
}

~~

// データクラス
public class Photo {
	public long id;
	public String filePath;
	public String dateTime;
	public String length;
	public String width;
}

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content">
	<LinearLayout
		android:orientation="horizontal"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content">
		<ImageView
			android:id="@+id/image"
			android:layout_width="100dp"
			android:layout_height="100dp" />
		<LinearLayout
			android:orientation="vertical"
			android:layout_width="wrap_content"
			android:layout_height="match_parent"
			android:gravity="center_vertical">
			<TextView
				android:id="@+id/date_time"
				android:text="TextView"
				android:layout_width="wrap_content"
				android:layout_height="wrap_content" />
			<TextView
				android:id="@+id/width"
				android:text="TextView"
				android:layout_width="wrap_content"
				android:layout_height="wrap_content" />
			<TextView
				android:id="@+id/height"
				android:text="TextView"
				android:layout_width="wrap_content"
				android:layout_height="wrap_content" />
		</LinearLayout>
	</LinearLayout>
	<TextView
		android:id="@+id/file_name"
		android:text="TextView"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:ellipsize="start"
		android:singleLine="true" />
</LinearLayout>

このコードでは、見た目にもカクカクでもはやスクロールとは言えないくらいの動きになっています。

1.getView()内で時間のかかる処理をしない。

たとえば大きな画像のデコードや通信処理を非同期に処理する方法です。 今回は画像のデコード処理が特に時間のかかる処理なのでそれを別スレッドで処理します。AndroidのAsyncTaskクラスを使います。

// それぞれの画像、値を設定
//Bitmap bm = BitmapFactory.decodeFile(photo.filePath);
//image.setImageBitmap(bm);

// 画像のデコードを別スレッドで行う
PhotoTask task = new PhotoTask(image);
task.execute(photo.filePath);

/**
 * デコード処理をメインと別スレッドで行います。
 */
class PhotoTask extends AsyncTask<String, Void, Bitmap> {

	/** 描画対象のImageView。 */
	private ImageView mView;

	public PhotoTask(ImageView view) {
		mView = view;
	}
	
	@Override
	protected Bitmap doInBackground(String... params) {
		// メインとは別のスレッドでデコード
		return BitmapFactory.decodeFile(params[0]);
	}

	@Override
	protected void onPostExecute(Bitmap result) {
		// メインスレッドの処理。
		// デコードしたBitmapをImageViewに設定します。
		mView.setImageBitmap(result);
	}
}

2.GridView内のコンポーネントを再利用する。(ViewHolder)

グリッド内のViewを毎回生成するのはかなりの時間とメモリを消費します。それを初回の見えている部分の数のみ生成を行い、スクロール時には生成済みのViewを使い回す方法です。 2009年のGoolge I/O でも紹介された"ViewHolder"パターンです。

@Override
public View getView(int position, View convertView, ViewGroup parent) {

	Photo photo = getItem(position);

	ViewHolder holder;
	if (convertView == null) {
		// 初期表示のみこのブロックが呼ばれ、スクロール時はelseブロックに入る。

		// 新たにGrid内のViewを生成
		convertView = mLayoutInflater.inflate(R.layout.grid_item, null);
		
		// GridView一コマの中のそれぞれのViewの参照を保持するクラスを生成
		holder = new ViewHolder();
		holder.image = (ImageView) convertView.findViewById(R.id.image);
		holder.filePath = (TextView) convertView.findViewById(R.id.width);
		holder.width = (TextView) convertView.findViewById(R.id.width);
		holder.height = (TextView) convertView.findViewById(R.id.height);
		holder.dateTime = (TextView) convertView.findViewById(R.id.date_time);
		// TagにGridViewの1コマの中に設定されたViewの参照を設定
		convertView.setTag(holder);
	} else {
		// TagからGridViewの1コマの中に設定されたViewの参照を取得
		holder = (ViewHolder) convertView.getTag();
	}

	// それぞれの画像、値を設定
	PhotoTask task = new PhotoTask(holder.image);
	task.execute(photo.filePath);

	// holder.image.setImageBitmap(bm);
	holder.filePath.setText("file:" + photo.filePath);
	holder.width.setText("width:" + photo.width);
	holder.height.setText("height:" + photo.length);
	holder.dateTime.setText("date:" + photo.dateTime);

	// 背景色を変更
	if (position % 2 == 0) {
		convertView.setBackgroundColor(Color.BLACK);
	} else {
		convertView.setBackgroundColor(Color.parseColor("#444444"));
	}

	return convertView;
}

// GridView一コマの内部の参照を保持する
static class ViewHolder {
	ImageView image;
	TextView filePath;
	TextView width;
	TextView height;
	TextView dateTime;
}

これでだいぶ良くなりました。

以上2点はもはやGridViewを実装する上では必須と思われます。

GridView内のViewの構造や処理が複雑な場合、それでも瞬間移動なスクロールになる場合があります。
次回はそんな場合の対策です。