android.os.Binderを使用してAndroidで非同期相互作用を整理する

Android向けに開発する際の自然で最初のタスクの1つは、非同期相互作用の編成です。 たとえば、何らかのアクティビティからサーバーにアクセスし、その結果を表示します。 困難な点は、サーバーへの呼び出し中に別のアクティビティまたは別のアプリケーションを最初に開くことができ、最初のアクティビティを取り消せないように終了する(ユーザーが戻るをクリック)などです。ここでは、サーバーから結果を取得しますが、アクティビティは「非アクティブ」です 「アクティブ」とは、状況に応じて、たとえば、onStartとonStop、onResumeとonPauseの間(または、プロジェクトのように、onPostResumeとonSaveInstanceStateとonStopの最初の間にあるもの)を理解することができます。 アクティビティが最終的に完了した(そして結果をガベージコレクターに渡す必要がある)か、それが一時的に非アクティブである(結果が保存され、アクティビティがアクティブになったらすぐに表示する必要がある)かをどのように理解しますか?



驚くべきことに、ドキュメント、インターネット、そして個人的に、私は一度も正しく受け入れられる普遍的な方法に出会ったことがありません。 モバイルインターネットバンキングで2年半使用してきたソリューションを無料で共有したいと思います。 このアプリケーションは、数百の銀行に(より大きなシステムの一部として)インストールされており、現在約100万人のユーザーがいます。



活動活動記録の概念を明確にします。 アクティビティは、クラスのインスタンスであり、存続期間の短いオブジェクトです。 活動記録-論理的な概念、ユーザーの視点からの画面、より長命。

下図>中図>上図を検討してください。

  1. BottomActivityアクティビティを起動し、その上にMiddleActivityを起動します。 画面を回転させたり、一時的に別のアプリケーションに切り替えるなどすると、アクティビティ(MiddleActivityクラスのインスタンス)を破棄して新しいアクティビティを作成できますが、アクティビティレコードMiddleは変更されません。 MiddleActivityの上でTopActivityを実行し、[戻る]ボタンをクリックします。 MiddleActivityアクティビティは再びスタックの最上部にあり、再作成できますが、アクティビティレコードMiddleは未変更のままです。
  2. スタックの上部にある「戻る-BottomActivity」をクリックします。 MiddleActivityを再度起動します。 再び活動記録ミドルの上部に。 しかし、これは新しい活動記録であり、ポイント1からの活動記録とは関係ありません。その活動記録は取り返しのつかないほど死にました。


提案されたソリューションは、android.os.Binderの以下の注目すべき特性に基づいています。 Binderをandroid.os.Parcelに書き込むと、同じプロセス(同じ仮想マシン)で読み取るときに、書き込まれたオブジェクトの同じインスタンスを読み取ることが保証されます。 したがって、アクティビティレコードオブジェクトのインスタンスをアクティビティに関連付け、onSaveInstanceStateメカニズムを使用してこのオブジェクトを変更せずに保持できます。 アクティビティレコードオブジェクトは非同期タスクに渡され、そこに結果が返されます。 アクティビティレコードが停止すると、非同期タスクの結果とともにガベージコレクタで使用できるようになります。



説明のために、単純な「長さ」アプリケーションを作成します。 2つのアクティビティと4つのインフラストラクチャクラスで構成されます。



プロジェクトファイル



MenuActivityは、LengthActivityを起動する単一のボタンで構成されます。



メインメニュー



Binderを直接操作することは、android.os.Bundleに書き込むことができないため不便です。 したがって、バインダーをandroid.os.Parcelableでラップします。



public class IdentityParcelable implements Parcelable { private final ReferenceBinder referenceBinder = new ReferenceBinder(); public final Object content; public static final Parcelable.Creator<IdentityParcelable> CREATOR = new Creator<IdentityParcelable>() { @Override public IdentityParcelable createFromParcel(Parcel source) { try { return ((ReferenceBinder) source.readStrongBinder()).get(); } catch (ClassCastException e) { // It must be application recover from crash. return null; } } @Override public IdentityParcelable[] newArray(int size) { return new IdentityParcelable[size]; } }; public IdentityParcelable(Object content) { this.content = content; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeStrongBinder(referenceBinder); } private class ReferenceBinder extends Binder { IdentityParcelable get() { return IdentityParcelable.this; } } }
      
      







IdentityParcelableクラスを使用すると、パーセルメカニズムを介してオブジェクトへの「参照」を渡すことができます。 たとえば、追加可能(Intent#putExtra)としてSerializableでもParcelableでもないオブジェクトを渡し、後で別のアクティビティで同じインスタンスを取得(getExtra)します。



ActivityRecordクラスとBasicActivityクラスは連動して機能します。 ActivityRecordはコールバックを実行できます。 アクティビティが表示されている場合(onStartとonStopの間の状態)、コールバックはすぐに実行されます。そうでない場合、後で実行するために保存されます。 アクティビティが表示されると、保留中のすべてのコールバックが実行されます。 アクティビティレコードを作成すると(BasicActivity#onCreateの最初の呼び出し)、新しいActivityRecordが作成され、onSaveInstanceState / onCreateでさらにサポートされます。



 public class ActivityRecord { private static final Handler UI_HANDLER = new Handler(Looper.getMainLooper()); private Activity visibleActivity; private final Collection<Runnable> pendingVisibleActivityCallbacks = new LinkedList<>(); public void executeOnVisible(final Runnable callback) { UI_HANDLER.post(new Runnable() { @Override public void run() { if (visibleActivity == null) { pendingVisibleActivityCallbacks.add(callback); } else { callback.run(); } } }); } void setVisibleActivity(Activity visibleActivity) { this.visibleActivity = visibleActivity; if (visibleActivity != null) { for (Runnable callback : pendingVisibleActivityCallbacks) { callback.run(); } pendingVisibleActivityCallbacks.clear(); } } public Activity getVisibleActivity() { return visibleActivity; } }
      
      







 public class BasicActivity extends Activity { private static final String ACTIVITY_RECORD_KEY = "com.zzz.ACTIVITY_RECORD_KEY"; private ActivityRecord activityRecord; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState == null) { activityRecord = new ActivityRecord(); } else { activityRecord = (ActivityRecord) ((IdentityParcelable) savedInstanceState.getParcelable(ACTIVITY_RECORD_KEY)).content; } } @Override protected void onStart() { super.onStart(); activityRecord.setVisibleActivity(this); } @Override protected void onStop() { activityRecord.setVisibleActivity(null); super.onStop(); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(ACTIVITY_RECORD_KEY, new IdentityParcelable(activityRecord)); } public ActivityRecord getActivityRecord() { return activityRecord; } }
      
      







ActivityRecordに基づいて、android.os.AsyncTaskのコントラクトのように見える非同期タスクの基本クラスを作成します。



 public class BackgroundTask { private final ActivityRecord activityRecord; public BackgroundTask(ActivityRecord activityRecord) { this.activityRecord = activityRecord; } public void execute() { new Thread() { @Override public void run() { doInBackground(); activityRecord.executeOnVisible(new Runnable() { @Override public void run() { onPostExecute(activityRecord.getVisibleActivity()); } }); } }.start(); } protected void publishProgress(final int progress) { activityRecord.executeOnVisible(new Runnable() { @Override public void run() { onProgressUpdate(activityRecord.getVisibleActivity(), progress); } }); } protected void doInBackground() { } protected void onProgressUpdate(Activity activity, int progress) { } protected void onPostExecute(Activity activity) { } }
      
      







インフラストラクチャを確立したら、LengthActivityを実行します。 ボタンをクリックすると、入力された文字列の長さが非同期的に計算されます。 画面を回転させると、計算は再開されず、続行されます。



長さのアクティビティ



 public class LengthActivity extends BasicActivity { private TextView statusText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.length_activity); statusText = (TextView) findViewById(R.id.statusText); findViewById(R.id.calculateLengthButton).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new LengthTask( getActivityRecord(), ((TextView) findViewById(R.id.sampleField)).getText().toString() ).execute(); } }); } private void setCalculationResult(CharSequence sample, int length) { statusText.setText("Length of " + sample + " is " + length); } private void setCalculationProgress(CharSequence sample, int progress) { statusText.setText("Calculating length of " + sample + ". Step " + progress + " of 100."); } private static class LengthTask extends BackgroundTask { final String sample; int length; LengthTask(ActivityRecord activityRecord, String sample) { super(activityRecord); this.sample = sample; } @Override protected void doInBackground() { for (int i = 0; i < 100; i++) { publishProgress(i); try { Thread.sleep(50); } catch (InterruptedException e) { throw new IllegalStateException(e); } } length = sample.length(); } @Override protected void onProgressUpdate(Activity activity, int progress) { ((LengthActivity) activity).setCalculationProgress(sample, progress); } @Override protected void onPostExecute(Activity activity) { ((LengthActivity) activity).setCalculationResult(sample, length); } } }
      
      







すべてのソースコードとコンパイルされたAPKでアーカイブを囲みます



ご清聴ありがとうございました! コメントを聞いて議論に参加できてうれしいです。 Binderで問題が発生することなく、より簡単なソリューションを喜んで知ります。



UPD: deejは、 android.support.v4.app.BundleCompatクラスを提案しました。これは、IBinderをバンドルに書き込むことができます。 ソリューションが開発されたとき、このクラスはまだ存在していませんでした。 BundleCompatは、次のような単一のバインダーでIdentityParcelableを省くことができるようにすることで、コードを少し単純化します

 public class ValueBinder extends Binder { public Object value; public ValueBinder() { } public ValueBinder(Object value) { this.value = value; } public <V> V value() { //noinspection unchecked return (V) value; } }
      
      





おそらくIdentityParcelableは、たとえば、任意のオブジェクトを追加としてIntentに転送するのに便利ですが、バンドルを通過することでValueBinderで対処できます。



All Articles