前のテキストで、AsyncTasksにはいくつかの問題があることを述べました。 それらのうちの2つを思い出しましょう。
- AsyncTasksはアクティビティのライフサイクルについて何も知りません。 取り扱いを誤ると、せいぜいメモリリークが発生し、最悪の場合はエラーが発生します。
- AsyncTaskは、進行状況の保存とダウンロード結果の再利用をサポートしていません。
最初の問題の意味は次のとおりです。onPostExecuteメソッドのUIを更新するには、特定のビューまたは関連するアクティビティ全体へのリンクが必要です。 単純なアプローチは、このリンクをAsyncTask自体に保存することです。
public static LoadWeatherForecastTask extends AsyncTask<String, Void, WeatherForecast> { private Activity activity; LoadWeatherForecastTask(Activity activity) { this.activity = activity; } }
問題は、ユーザーがデバイスを回転させるとすぐに、アクティビティが破棄され、リンクが廃止されることです。 これにより、メモリリークが発生します。 なんで? doInBackgroundメソッドは、AsyncTaskクラスの静的メンバーであるexecutorのFuture実行可能ファイル内で呼び出されることを思い出してください。 これにより、AsyncTaskオブジェクトとActivityが厳密に実現可能になります(静的はGCのルートの1つであるため)。したがって、ガベージコレクションには不適切です。 これは、アクティビティがかなりの量のメモリを消費するため、画面を数回転させるとOutOfMemoryErrorが発生する可能性があることを意味します。
このエラーはWeakReferenceで修正できます:
public static LoadWeatherForecastTask extends AsyncTask<String, Void, WeatherForecast> { private WeakReference<Activity> activityRef; LoadWeatherForecastTask(Activity activity) { this.activityRef = new WeakReference<>(activity); } }
OOMを削除しましたが、いずれの場合もAsyncTaskの結果は失われ、電話を放電してトラフィックを消費して、再び開始する運命にあります。
これを修正するために、Androidチームは数年前にローダーAPI(「ローダー」)を提案しました。 このAPIの使用方法を見てみましょう。 Loader.Callbacksインターフェイスを実装する必要があります。
inner class WeatherForecastLoaderCallbacks : LoaderManager.LoaderCallbacks<WeatherForecast> { override fun onLoaderReset(loader: Loader<WeatherForecast>?) { } override fun onCreateLoader(id: Int, args: Bundle?): Loader<WeatherForecast> { return WeatherForecastLoader(applicationContext) } override fun onLoadFinished(loader: Loader<WeatherForecast>?, data: WeatherForecast?) { temperatureTextView.text = data!!.temp.toString(); } }
ご覧のとおり、 onLoadFinishedメソッドは 、 AsyncTaskで実装したonPostExecuteに非常に似ています。
ローダー自体を作成する必要があります。
class WeatherForecastLoader(context: Context) : AsyncTaskLoader<WeatherForecast>(context) { override fun loadInBackground(): WeatherForecast { try { Thread.sleep(5000) } catch (e: InterruptedException) { return WeatherForecast("", 0F, "") } return WeatherForecast("Saint-Petersburg", 20F, "Sunny") } }
そして、ローダーのIDでinitLoader()を呼び出します。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... val weatherForecastLoader = WeatherForecastLoaderCallbacks() loaderManager .initLoader(forecastLoaderId, Bundle(), weatherForecastLoader) }
注:WeatherForecastLoaderCallbacksはアクティビティのネストされたクラスです。 LoaderManagerは、このCallbacksオブジェクトへのリンクを保存します-したがって、アクティビティ自体へのリンクも保存します
たくさんのコードがありますよね? しかし、ここで重要な利点を得ることができます。 まず、画面を回転する(または他の構成を変更する)ときにローダーが再利用されます。 画面が回転すると、onLoadFinishedが呼び出され、以前にダウンロードした結果が転送されます。
もう1つの利点は、メモリリークが発生しないことです。ただし、まだアクティビティにアクセスできるため、インターフェイスを更新できます。
AsyncTaskにはこれらの利点はありませんでした! すべてがどのように機能するかを見てみましょう。
これが主な楽しみの始まりです。 LoaderManagerはアプリケーション内のどこかに格納されていると思いましたが、実際の実装はもっと興味深いことがわかりました。
LoaderManagerは、アクティビティインスタンスが作成されるときに作成されます。
public class Activity { final FragmentController mFragments = FragmentController.createController(new HostCallbacks()); public LoaderManager getLoaderManager() { return mFragments.getLoaderManager(); } }
FragmentController.createControllerは、FragmentControllerクラスの単なる名前付きコンストラクターです。 FragmentControllerは、LoaderManagerの作成をHostCallbacks(アクティビティのネストされたクラス)に委任します。実装は次のようになります。
LoaderManagerImpl getLoaderManagerImpl() { if (mLoaderManager != null) { return mLoaderManager; } mCheckedForLoaderManager = true; mLoaderManager = getLoaderManager("(root)", mLoadersStarted, true /*create*/); return mLoaderManager;
ご覧のとおり、Activity自体のLoaderManagerは遅延初期化されます。 LoaderManagerインスタンスは、最初に必要になるまで作成されません。 アクティビティのLoaderManagerへのアクセスは、Map LoadersManagersのキー「(root)」によって発生します。 このマップへのアクセスは、次のように実装されています。
LoaderManagerImpl getLoaderManager(String who, boolean started, boolean create) { if (mAllLoaderManagers == null) { mAllLoaderManagers = new ArrayMap<String, LoaderManager>(); } LoaderManagerImpl lm = (LoaderManagerImpl) mAllLoaderManagers.get(who); if (lm == null && create) { lm = new LoaderManagerImpl(who, this, started); mAllLoaderManagers.put(who, lm); } else if (started && lm != null && !lm.mStarted){ lm.doStart(); } return lm; }
ただし、これはLoaderManagerフィールドの最後のエントリではありません。 アクティビティ#onCreateメソッドを見てみましょう。
@MainThread @CallSuper protected void onCreate(@Nullable Bundle savedInstanceState) { ... if (mLastNonConfigurationInstances != null) { mFragments.restoreLoaderNonConfig( mLastNonConfigurationInstances.loaders); } ... }
restoreLoaderNonConfigメソッドは最終的に、ホストコントローラーを更新するだけです。ホストコントローラーは、構成の変更後に作成された新しいActivityインスタンスのクラスのメンバーになりました。
initLoader()メソッドを呼び出すとき、LoaderManagerは、破棄されたアクティビティで作成されたローダーに関するすべての情報を既に持っています。 ダウンロードした結果をすぐに公開できるように:
public abstract class LoaderManager { public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) { ... LoaderInfo info = mLoaders.get(id); ... if (info == null) { info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback); } else { // override old callbacks reference here to new one info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>) callback; } if (info.mHaveData && mStarted) { // deliver the result we already have info.callOnLoadFinished(info.mLoader, info.mData); } return (Loader<D>)info.mLoader; }
メモリリークを回避する方法(LoaderCallbackインスタンスを置き換える)と、新しいアクティビティに結果を配信する方法の2つのことを一度に処理できたことは素晴らしいことです。
mLastNonConfigurationInstancesとはどんな種類の獣なのか疑問に思われるかもしれません。 これは、Activityクラス内で定義されたNonConfigurationInstancesクラスのインスタンスです。
public class Activity { static final class NonConfigurationInstances { Object activity; HashMap<String, Object> children; FragmentManagerNonConfig fragments; ArrayMap<String, LoaderManager> loaders; VoiceInteractor voiceInteractor; } NonConfigurationInstances mLastNonConfigurationInstances; }
オブジェクトはretainNonConfigurationInstance()メソッドを使用して作成され、Android OSから直接アクセスされます。 そして、Activity#attach()メソッドでActivityで利用可能になります(これは内部Activity APIです):
final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor, Window window, ActivityConfigCallback activityConfigCallback) { attachBaseContext(context); ... mLastNonConfigurationInstances = lastNonConfigurationInstances; ... }
そのため、残念ながら、主な魔法はAndroid OS内に残っています。 しかし、私たちが彼女の例から学ぶことを禁止する人はいません!
私たちが見つけたものを要約しましょう:
- ローダーには明らかなAPIはありませんが、構成を変更するときにロード結果を保存できます
- ローダーはメモリリークを引き起こしません:ネストされたActivityクラスにしない
- ローダーを使用すると、バックグラウンドでAsyncTaskを再利用できますが、独自のローダーを実装することもできます
- LoaderManagerは、特別なオブジェクトへの保存により、破棄されたアクティビティと新しく作成されたアクティビティの間で再利用されます
次の記事では、Executorでのバックグラウンド作業を整理し、そこでEventBusをミックスする方法について説明します。 お楽しみに!
広告の分。
テキストの著者から:
お気づきのように、これは私の英語の記事の翻訳です。 記事が価値があると思われる場合は、注意してください。4月にメビウス会議が開催され、プログラム委員会に参加します。今年は特にイベントが多いと約束できます。 これまでのところ、 会議のウェブサイトで公開されているのはプログラムの一部だけです。なぜなら、私たちにとって最良の選択は非常に難しいからです。 レポートの利用可能な説明は既に学習できており、まもなく新しいレポートが追加されます!