2012年11月5日月曜日

Adapterの高速化

Androidの中でAdapterクラスは頻繁に使われます。
Adapterクラスは、データソースとビューのつなぎ役を果たすものです。

データの一覧を表示するには、ListViewやGridView、Galleryなど様々なViewがありますが、これらのViewにはsetAdapterメソッドがあり、adapterがセットされると、表示すべきデータはこのアダプターから取得するようになります。

Adapterが内部データの違い(データがArrayだったり、Listだったり、Cursorだったりなど)を吸収してくれるので、Viewは余計なことを考えずに描画に集中できるわけです。

Adapterはデータソースとビューのつなぎ役なので、表示すべきビューをデータソースから組み立てるのが仕事です。何番目のデータをどのようなView構造で表示するか、それを決定するのがgetViewメソッドです。

このgetViewメソッドは、新しいデータが表示されるタイミングで呼び出されます。ListViewなら、スクロールして、画面外から新しいデータが表示されるタイミングです。こういった性質上、スクロールの度に何度も何度も呼び出されるため、getViewメソッドで無駄な処理があると、スクロールにひっかかりが出来るなど、パフォーマンスに直結した悪影響を及ぼします。

Viewの再利用
まず、getViewで確認すべきところはしっかりViewを再利用しているかです。
getViewはこんなシグニチャになってます。

public View getView(int position, View convertView, ViewGroup parent)
positionは何要素目か、contentViewはposition番目のビューを、parentは親のビューグループです。

public View getView(int position, View convertView, ViewGroup parent) {
// ビューを受け取る
View view = convertView;
if (view == null) {
// 受け取ったビューがnullなら新しくビューを生成
view = inflater.inflate(R.layout.twitter_row, null);
}
// 表示すべきデータの取得
TwitterStatus item = (TwitterStatus)items.get(position);
if (item != null) {
TextView screenName = (TextView)view.findViewById(R.id.toptext);
screenName.setTypeface(Typeface.DEFAULT_BOLD);

// スクリーンネームをビューにセット
TextView text = (TextView)view.findViewById(R.id.bottomtext);
if (screenName != null) {
screenName.setText(item.getScreenName());
}

// テキストをビューにセット
if (text != null) {
text.setText(item.getText());
}


ImageView imageView = (ImageView)view.findViewById(R.id.icon);
// バックグラウンドで画像取得
DownloadTask task = new DownloadTask(imageView);
task.execute(item.getProfileImageUrl());


}

// 背景画像をセットする
view.setBackgroundResource(R.drawable.back);

return view;
}
改めて見てみると、パフォーマンス以外にも無駄な処理がありますが、今はあまり気にしないでください。

ひとまずはViewの再利用を確認しましょう。

上のコードでは、引数で受け取ったconvertViewをviewに受けて、nullチェックをしています。
nullならviewを生成し、nullでないならそれを使う、という流れになっています。
最後にviewをreturnで返しています。

これにより、次回からposition番目のcontentViewは、以前returnしたviewが渡されるようになります。
見事再利用されています。
viewのnullチェックをせずに毎回inflateやnewする作りでは無駄が多いので、必ず再利用するようにしましょう。

子ビューへのアクセスを高速化
まだ高速化の余地があります。
それは

TextView screenName = (TextView)view.findViewById(R.id.toptext);
などとして、毎回ビューをリソースから引っ張りだしている所です。Activityではこういったビュー要素はインスタンス変数へ格納して、アクセスのショートカットをしますが、ここではgridViewなどのセルに対してなので工夫が必要です。

ここでは、ViewHolderというセル内の要素への参照を持つだけのクラスを用意することにします。
こんなクラスです。

static class ViewHolder {
TextView screenName;
TextView text;
ImageView image;
}
このビューホルダーを使って、改善したコードが以下です。

public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;

// ビューを受け取る
View view = convertView;
if (view == null) {
// 受け取ったビューがnullなら新しくビューを生成
view = inflater.inflate(R.layout.twitter_row, null);

// 背景画像をセットする
view.setBackgroundResource(R.drawable.back);

TextView screenName = (TextView)view.findViewById(R.id.toptext);
screenName.setTypeface(Typeface.DEFAULT_BOLD);

TextView text = (TextView)view.findViewById(R.id.bottomtext);
ImageView imageView = (ImageView)view.findViewById(R.id.icon);

holder = new ViewHolder();
holder.screenName = screenName;
holder.text = text;
holder.image = imageView;

view.setTag(holder);
} else {
holder = (ViewHolder)view.getTag();
}
// 表示すべきデータの取得
TwitterStatus item = (TwitterStatus)items.get(position);
holder.screenName.setText(item.getScreenName());
holder.text.setText(item.getText());

// 画像がない場合は取得しにいく
String imageUrl = item.getProfileImageUrl();
Bitmap image = ImageCache.getImage(imageUrl);
if (image == null) {
DownloadTask task = new DownloadTask(holder.image);
task.execute(imageUrl);
}
return view;
}
ビューの再利用のために、nullチェックをするところは同じです。
違うのは、ビューを初めて作るタイミングでViewHolderをnewして、holderに子ビューであるTextViewやImageViewの参照をセットし、それをview.setTag()でタグとして登録している所です。

これにより、再利用可能なビューが渡された時、view.getTag()として、子ビューへの参照を持ったViewHolderを取得できるようになります。

findViewByIdでviewを探す処理が省けるのでその分パフォーマンス改善が見込めます。

# DownloadTask内でキャッシュ判定したいたのを、こっちでやるように変更してます。

0 件のコメント:

コメントを投稿