mixi engineer blog

*** 引っ越しました。最新の情報はこちら → https://medium.com/mixi-developers *** ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

Android開発のちょっとしたお話

こんにちは。新卒入社で今年から働き始めました、横幕です。現在は、mixiのAndroid(TM)版公式クライアントアプリを開発しています。

Android開発を始めてから数か月になりますが、今回は、開発に携わる中で知ったことをご紹介したいと思います。

レイアウトの複雑さで発生するStackOverFlowError

Androidでは、見た目(UI)のデザインやレイアウトをXMLで記述することができます。XMLを書くときには、UIのパーツ(ウィジェット:ボタンやチェックボックスなど)のほか、ウィジェットの配置を決めるためのコンテナ(LinearLayoutやFrameLayoutなど)を用います。そして、それらを入れ子にしながら画面を設計していきます。

たとえば、以下のような感じに。
* main.xml

<?xml version="1.0" encoding="utf-8"?>
 
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="vertical">
  <Button
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="ボタン"/>
  <TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="テキスト"/>
  <LinearLayout
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">
    <Button
      android:layout_width="0dip"
      android:layout_height="wrap_content"
      android:layout_weight="0.3"
      android:text="ホゲホゲ"/>
    <Button
      android:layout_width="0dip"
      android:layout_height="wrap_content"
      android:layout_weight="0.7"
      android:text="フガフガ"/>
  </LinearLayout>
</LinearLayout>

また、XMLで画面を作っていると、あるパーツは他の画面でも使いたいので、その部分だけ切り出したいということがよくあります。
そのような場合には、切り出したい部分だけを記述したXMLを用意し、使いたい場所でインクルードするという方法が利用できます。

* main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="vertical">
  <Button
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="ボタン"/>
  <TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="テキスト1"/>
  <include layout="@layout/hogehoge"/>
</LinearLayout>

* hogehoge.xml

<?xml version="1.0" encoding="utf-8"?>
  <LinearLayout
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">
    <Button
      android:layout_width="0dip"
      android:layout_height="wrap_content"
      android:layout_weight="0.3"
      android:text="テキスト2"/>
    <Button
      android:layout_width="0dip"
      android:layout_height="wrap_content"
      android:layout_weight="0.7"
      android:text="テキスト3"/>
  </LinearLayout>

入れ子の深さにご用心

さて、このようにしながら画面を作っていると、コンテナの入れ子が深くなったり、複雑になったりしてきます。
特に、コンテナの入れ子の深さには注意が必要です。
というのも、あまりにコンテナの入れ子が深くなりすぎると、StackOverFlowErrorが発生して強制終了の原因になるからです。
パーツごとに切り出してXMLを書いている場合には、特に注意が必要です。

Android 1.5の場合、Androidが持っているスタックの大きさは8KBになっています
このため、TabHostを使って画面を作った場合に、12回程度の入れ子を作るとStackOverFlowErrorになる例もあったようです。

検証してみる

では、Android 2.xになった現在、具体的にどの程度の深さでStackOverFlowが発生するのか、手持ちのGalaxy S2(Android2.3.3)で検証してみました。

まず始めに、LinearLayoutの入れ子を単純に増やし続けてみます。

* main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <LinearLayout
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    ...
    <LinearLayout
      android:orientation="vertical"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent">
      <TextView
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:text="こんにちは"/>
    </LinearLayout>
  ...
  </LinearLayout>
</LinearLayout>

すると、25回の入れ子を作ったところでアプリがStackOverFlowErrorで強制終了しました。

次に、ListViewを使って、Listの各要素のレイアウトに入れ子構造を作った場合にどうなるか試してみました。
* MainActivity

package com.test.app02;
 
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
 
public class MainActivity extends Activity {
	/** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        String[] data = new String[]{"hoge", "fuga"};
        ListView list = (ListView)findViewById(R.id.ListView);
        list.setAdapter(new MyAdapter(this, R.layout.list_item, data));
    }
 
    private class MyAdapter extends ArrayAdapter {
        private String[] data;
        private LayoutInflater inflater;
 
        public MyAdapter(Context context, int resourceId, String[] data) {
            super(context, resourceId, data);
            this.data = data;
            this.inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }
 
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View v = convertView;
            if (v == null) {
                v = inflater.inflate(R.layout.list_item, null);
            }
            TextView text = (TextView)v.findViewById(R.id.string);
            text.setText(data[position]);
            return v;
        }
    }
}

* main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <ListView
    android:id="@+id/ListView"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"/>
</LinearLayout>

* list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <LinearLayout
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    ...
    <LinearLayout
      android:orientation="vertical"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent">
      <TextView
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:text="こんにちは"/>
    </LinearLayout>
  ...
  </LinearLayout>
</LinearLayout>

今度は、list_item.xmlの中のLinearLayoutを22回入れ子にしたところでStackOverFlowErrorとなりました。

最後に、TabHostを使って試してみました。
* MainActivity

package com.test.app03;
 
import android.app.TabActivity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.widget.TabHost;
import android.widget.TabHost.TabSpec;
 
public class MainActivity extends TabActivity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        TabHost tabHost = getTabHost();
        LayoutInflater.from(this).inflate(R.layout.main, tabHost.getTabContentView(), true);
        TabSpec tab1 = tabHost.newTabSpec("Tab1");
        tab1.setIndicator("1");
        tab1.setContent(R.id.tab1);
        TabSpec tab2 = tabHost.newTabSpec("Tab2");
        tab2.setIndicator("2");
        tab2.setContent(R.id.tab2);
        tabHost.addTab(tab1);
        tabHost.addTab(tab2);
        tabHost.setCurrentTab(0);
    }
}

* main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <TabHost
    android:id="@android:id/tabhost"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TabWidget
      android:id="@android:id/tabs"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"/>
    <FrameLayout 
      android:id="@android:id/tabcontent"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content">
      <include 
        layout="@layout/tab_content"
        android:id="@+id/tab1"/>
      <include
        layout="@layout/tab_content"
        android:id="@+id/tab2"/>
    </FrameLayout>
  </TabHost>
</LinearLayout>

* tab_content.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <LinearLayout
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    ...
    <LinearLayout
      android:orientation="vertical"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent">
      <TextView
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:text="こんにちは"/>
    </LinearLayout>
  ...
  </LinearLayout>
</LinearLayout>

TextViewが入れ子の中に1つあるだけの単純な構造ですが、20回の入れ子を作った所でStackOverFlowErrorとなりました。

今回は、単純にコンテナの入れ子を増やすだけで検証してみましたが、実際には、もっと複雑な作りになるはずですし、レイアウトをプログラムで動的に制御したいというような場面も出てくると思います。
そのようなときに、StackOverFlowErrorに出くわしたら、レイアウトの入れ子が深くなりすぎていないか、
Hierarchy Viewerなどを使って探ってみると良いかもしれません。
(ちなみに、Android4.0のエミュレータ上で実行したところ、どのコードもすんなり実行することができました。)

終わりに

今回は、かなりこまごまとしたお話になりましたが、いかがでしたでしょうか。
この記事が何かのお役に立つことが出来れば幸いです。