はじめに
ListViewの画面回転と復元という記事で、画面回転したときのListViewの復元方法を書きました。
が、その方法では、Android 7 で TransactionTooLargeException というエラーを発生させ、アプリがクラッシュすることがあります。
エラー内容は以下になります。
java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 3200536 bytes
at android.app.ActivityThread$StopInfo.run(ActivityThread.java:3781)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6119)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
Caused by: android.os.TransactionTooLargeException: data parcel size 3200536 bytes
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(Binder.java:615)
at android.app.ActivityManagerProxy.activityStopped(ActivityManagerNative.java:3636)
at android.app.ActivityThread$StopInfo.run(ActivityThread.java:3773)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6119)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
このエラーは、onSaveInstanceState で保持したアイテムのデータ量が大きい時に、ホームボタンやメニューボタンなどでアプリをバックグラウンドに移行した際、発生します(画面回転では発生しません)。
結論
このエラーについて実は、Android Developers の Android 7.0 の動作の変更点 - その他の重要事項 に記載されています。
多くのプラットフォーム API は、Binder トランザクションで送信される大きなペイロードをチェックし、暗黙的にログ記録したり、削除したりするのではなく TransactionTooLargeExceptions を RuntimeExceptions として再度スローするようになりました。一般的な例としては、Activity.onSaveInstanceState() で大量のデータを格納することです。これにより、アプリが Android 7.0 をターゲットにしている場合は、ActivityThread.StopInfo で RuntimeException がスローされます。
(引用:Android Developers - Android 7.0 の動作の変更点)
そして解決策は、Android Developers の 実行時の変更の処理 に記載されています。
解決策が上記の Android Developers に詳しく書いてあるので、この記事で書く必要は正直ないのですが、それでは身も蓋もないなので、簡単に紹介しておきます。
解決策は2つ
- AndroidManifest.xml の 要素に android:configChanges 属性を追加する
- Fragment と setRetainInstance を使う
AndroidManifest.xml の 要素に android:configChanges 属性を追加する
以下のように android:configChanges 属性を追加するだけになります。
<activity
android:name=".view.activity.ListViewActivity"
android:configChanges="orientation|screenSize"/>
属性を追加した Activity(ここではListViewActivity)は、画面回転しても onPause や onSaveInstanceState 、onResume などのライフサイクルが実行されなくなります。
ホームボタンやメニューボタンをクリック時や、バックグラウンドからフォアグランドに復帰した時のライフサイクルは、変わらず実行されます。
注意点としては、画面回転時に再生成処理が実行されないので、画面の縦向きと横向きでレイアウトを変えられないところです。
Fragment と setRetainInstance を使う
こちらの方法は1つ目の方法より少し手間がかかります。
しかし、1つ目の方法と違って、画面回転時にライフサイクルが実行されないということはなくなり、通常どおり縦向きと横向きでレイアウトを変えられます。
ただ画面回転時に、Fragment の onCreate と onDestroy が実行されなくなります。
Fragment と setRetainInstance を使用した例は、後述のソースコードに記載しました。
ソースコードの説明
ListViewの画面回転と復元 で記述したロジックを、Fragment と setRetainInstance を使うように変更しました。
ListViewFragmentActivity が ListViewFragment を生成し、その ListViewFragment でListViewを用いてリストを表示しています。
このとき、ListViewFragment の onCreate で、setRetainInstance を呼び出すことで、画面回転時にフラグメントを再生成しないようにしています。
ソースコード内で使われている、UserListAdapter や UserListAdapterItem は、ListViewの画面回転と復元 で使われているものと同じものになります。
ソースコード
/**
* ListViewFragmentを表示するためのアクティビティ
*/
public class ListViewFragmentActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list_view_fragment);
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentById(R.id.layout_fragment_area);
// fragmentがnullなら、再生成されていないため、新規でfragmentを作成する
if (fragment == null) {
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragment = ListViewFragment.createInstance();
fragmentTransaction.replace(R.id.layout_fragment_area, fragment);
fragmentTransaction.commit();
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<!-- フラグメントを表示するエリア -->
<FrameLayout
android:id="@+id/layout_fragment_area"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="jp.co.yahoo.hiryamas.testapplication.view.activity.ListViewFragmentActivity">
</FrameLayout>
/**
* ListViewを表示するフラグメント
* 今回はListFragmentではなく普通のFragmentを継承しています。
*/
public class ListViewFragment extends Fragment {
/**
* TAG
*/
private static final String TAG = ListViewFragment.class.getSimpleName();
private ListView mUserListView;
private UserListAdapter mUserListAdapter;
/**
* アイテムを保持しておくための変数
*/
private List<UserListAdapterItem> mItemList = new ArrayList<>();
public static Fragment createInstance() {
Fragment fragment = new ListViewFragment();
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
// ここで setRetainInstance を呼び出す
setRetainInstance(true);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.d(TAG, "onCreateView");
View view = inflater.inflate(R.layout.fragment_list_view, container, false);
return view;
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
Log.d(TAG, "onViewCreated");
super.onViewCreated(view, savedInstanceState);
Context context = getContext();
mUserListView = (ListView) view.findViewById(R.id.list_user);
mUserListAdapter = new UserListAdapter(context, new ArrayList<UserListAdapterItem>());
mUserListView.setAdapter(mUserListAdapter);
// アイテムが生成されていなければ、新規で生成する
if (mItemList == null || mItemList.isEmpty()) {
mItemList = createUserListAdapterItemList();
}
mUserListAdapter.setItemList(mItemList);
}
/**
* アイテムを生成する処理
*
* @return
*/
private List<UserListAdapterItem> createUserListAdapterItemList() {
// 記事に関係ないため省略
}
}
おわりに
今回は、Android 7(Nougat) の onSaveInstanceState で発生する TransactionTooLargeException と その解決策について記載いたしました。
TransactionTooLargeException は「はじめに」で書いたように、onSaveInstanceState で保存するデータ量が多い時に発生します。
そのため、データ量が多くなければ、今回記載した方法は特に使う必要がありませんので、時と場合によって使い分けていただけたらと思います。