unr*_*gnu 5 android android-lifecycle android-configchanges android-asynctask android-memory
自从2009 AsyncTask年在Cupcake(API 3,Android 1.5)中推出以来,它一直被Android团队一直推广为:
他们提供的代码示例强化了这种简单的信息,特别是对于那些不得不以更痛苦的方式使用线程的人.AsyncTask非常有吸引力.
然而,在此后的许多年里,崩溃,内存泄漏和其他问题困扰了大多数选择AsyncTask在其生产应用程序中使用的开发人员.这通常是由于Activity 破坏和娱乐上运行时配置的变化(特别是取向/旋转),而AsyncTask运行doInBackground(Params...); 当onPostExecute(Result)被调用时,Activity已经被破坏,使UI引用处于不可用状态(或甚至null).
Android团队在这个问题上缺乏明显,清晰,简洁的指导和代码样本只会让事情变得更糟,导致混乱以及各种变通方法和黑客攻击,有些体面,有些可怕:
显然,既然AsyncTask可以在很多情况下使用,那么就没有一种方法可以解决这个问题.然而,我的问题是关于选择.
什么是规范(由Android团队认可)最佳实践,使用简洁的代码示例,AsyncTask与Activity/ Fragmentlifecycle 集成并在运行时配置更改时自动重启?
unr*_*gnu 10
From Memory & Threading. (Android Performance Patterns Season 5, Ep. 3):
You've got some threading object that's declared as an inner class of an
Activity. The problem here is that theAsyncTaskobject now has an implicit reference to the enclosingActivity, and will keep that reference until the work object has been destroyed... Until this work completes, theActivitystays around in memory... This type of pattern also leads to common types of crashes seen in Android apps...The takeaway here is that you shouldn't hold references to any types of UI-specific objects in any of your threading scenarios.
Although the documentation is sparse and scattered, the Android team have provided at least three distinct approaches to dealing with restarts on config change using AsyncTask:
WeakReferences to UI objectsActivity or Fragment using "work records"From Using AsyncTask | Processes and Threads | Android Developers
To see how you can persist your task during one of these restarts and how to properly cancel the task when the activity is destroyed, see the source code for the Shelves sample application.
在Shelves应用程序中,对任务的引用被保存为a中的字段Activity,以便可以在Activity生命周期方法中对它们进行管理.然而,在查看代码之前,需要注意几个重要的事项.
首先,这个应用程序是在AsyncTask添加到平台之前编写的.一个强大类似于后来发布的类,AsyncTask包含在源代码中,称为UserTask.对于我们在这里的讨论,UserTask在功能上等同于AsyncTask.
其次,子类UserTask被声明为一个内部类Activity.如前所述,此方法现在被视为反模式(请参阅上面的" 不要保留对UI特定对象的引用").幸运的是,此实现细节不会影响在生命周期方法中管理运行任务的整体方法; 但是,如果您选择将此示例代码用于您自己的应用程序,请声明AsyncTask其他地方的子类.
覆盖onDestroy(),取消任务,并将任务引用设置为null. (我不确定设置引用是否null对此有任何影响;如果您有进一步的信息,请发表评论,我会相应地更新答案).
AsyncTask#onCancelled(Object)如果您需要在AsyncTask#doInBackground(Object[])返回后清理或执行任何其他所需的工作,请进行覆盖.
AddBookActivity.java
public class AddBookActivity extends Activity implements View.OnClickListener,
AdapterView.OnItemClickListener {
// ...
private SearchTask mSearchTask;
private AddTask mAddTask;
// Tasks are initialized and executed when needed
// ...
@Override
protected void onDestroy() {
super.onDestroy();
onCancelAdd();
onCancelSearch();
}
// ...
private void onCancelSearch() {
if (mSearchTask != null && mSearchTask.getStatus() == UserTask.Status.RUNNING) {
mSearchTask.cancel(true);
mSearchTask = null;
}
}
private void onCancelAdd() {
if (mAddTask != null && mAddTask.getStatus() == UserTask.Status.RUNNING) {
mAddTask.cancel(true);
mAddTask = null;
}
}
// ...
// DO NOT DECLARE YOUR TASK AS AN INNER CLASS OF AN ACTIVITY
// Instances of this class will hold an implicit reference to the enclosing
// Activity as long as the task is running, even if the Activity has been
// otherwise destroyed by the system. Declare your task where you can be
// sure it holds no implicit references to UI-specific objects (Views,
// etc.), and do not hold explicit references to them in your own
// implementation.
private class AddTask extends UserTask<String, Void, BooksStore.Book> {
// ...
@Override
public void onCancelled() {
enableSearchPanel();
hidePanel(mAddPanel, false);
}
// ...
}
// DO NOT DECLARE YOUR TASK AS AN INNER CLASS OF AN ACTIVITY
// Instances of this class will hold an implicit reference to the enclosing
// Activity as long as the task is running, even if the Activity has been
// otherwise destroyed by the system. Declare your task where you can be
// sure it holds no implicit references to UI-specific objects (Views,
// etc.), and do not hold explicit references to them in your own
// implementation.
private class SearchTask extends UserTask<String, ResultBook, Void>
implements BooksStore.BookSearchListener {
// ...
@Override
public void onCancelled() {
enableSearchPanel();
hidePanel(mSearchPanel, true);
}
// ...
}
Run Code Online (Sandbox Code Playgroud)
覆盖onSaveInstanceState(Bundle, PersistableBundle),取消任务并保存有关任务的状态,以便在还原实例状态时重新启动它们.
覆盖onRestoreInstanceState(Bundle, PersistableBundle),检索有关已取消任务的状态,并使用已取消任务状态中的数据启动新任务.
AddBookActivity.java
public class AddBookActivity extends Activity implements View.OnClickListener,
AdapterView.OnItemClickListener {
// ...
private static final String STATE_ADD_IN_PROGRESS = "shelves.add.inprogress";
private static final String STATE_ADD_BOOK = "shelves.add.book";
private static final String STATE_SEARCH_IN_PROGRESS = "shelves.search.inprogress";
private static final String STATE_SEARCH_QUERY = "shelves.search.book";
// ...
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
// ...
restoreAddTask(savedInstanceState);
restoreSearchTask(savedInstanceState);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (isFinishing()) {
// ...
saveAddTask(outState);
saveSearchTask(outState);
}
}
// ...
private void saveAddTask(Bundle outState) {
final AddTask task = mAddTask;
if (task != null && task.getStatus() != UserTask.Status.FINISHED) {
final String bookId = task.getBookId();
task.cancel(true);
if (bookId != null) {
outState.putBoolean(STATE_ADD_IN_PROGRESS, true);
outState.putString(STATE_ADD_BOOK, bookId);
}
mAddTask = null;
}
}
private void restoreAddTask(Bundle savedInstanceState) {
if (savedInstanceState.getBoolean(STATE_ADD_IN_PROGRESS)) {
final String id = savedInstanceState.getString(STATE_ADD_BOOK);
if (!BooksManager.bookExists(getContentResolver(), id)) {
mAddTask = (AddTask) new AddTask().execute(id);
}
}
}
private void saveSearchTask(Bundle outState) {
final SearchTask task = mSearchTask;
if (task != null && task.getStatus() != UserTask.Status.FINISHED) {
final String bookId = task.getQuery();
task.cancel(true);
if (bookId != null) {
outState.putBoolean(STATE_SEARCH_IN_PROGRESS, true);
outState.putString(STATE_SEARCH_QUERY, bookId);
}
mSearchTask = null;
}
}
private void restoreSearchTask(Bundle savedInstanceState) {
if (savedInstanceState.getBoolean(STATE_SEARCH_IN_PROGRESS)) {
final String query = savedInstanceState.getString(STATE_SEARCH_QUERY);
if (!TextUtils.isEmpty(query)) {
mSearchTask = (SearchTask) new SearchTask().execute(query);
}
}
}
Run Code Online (Sandbox Code Playgroud)
这是一种简单的方法,即使对刚刚熟悉Activity生命周期的初学者也应该有意义.它还具有不需要在任务类本身之外使用mimimal代码的优点,根据需要触及一到三个生命周期方法.javadoconDestroy()的"使用"部分中的一个简单的7行代码段可以为我们带来很多悲伤.也许下一代可能会幸免.AsyncTask
将UI对象作为参数传递给AsyncTask构造函数.WeakReference将对这些对象的弱引用存储为.中的字段AsyncTask.
在onPostExecute(),检查UI对象WeakReference是否不是null直接更新它们.
From Use an AsyncTask | Processing Bitmaps Off the UI Thread | Android Developers
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView);
}
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
}
// Once complete, see if ImageView is still around and set bitmap.
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
The
WeakReferenceto theImageViewensures that theAsyncTaskdoes not prevent theImageViewand anything it references from being garbage collected. There’s no guarantee theImageViewis still around when the task finishes, so you must also check the reference inonPostExecute(). TheImageViewmay no longer exist, if for example, the user navigates away from the activity or if a configuration change happens before the task finishes.
This approach is simpler and tidier than the first, adding only a type change and a null check to the task class, and no additional code anywhere else.
There is a cost to this simplicity, however: the task will run to its end without being canceled on config change. If your task is expensive (CPU, memory, battery), has side effects, or needs to be automatically restarted on Activity restart, then the first approach is probably a better option.
From Memory & Threading. (Android Performance Patterns Season 5, Ep. 3)
...force the top-level
ActivityorFragmentto be the sole system responsible for updating the UI objects.For example, when you'd like to kick off some work, create a "work record" that pairs a
Viewwith some update function. When that block of work is finished, it submits the results back to theActivityusing anIntentor arunOnUiThread(Runnable)call.The
Activitycan then call the update function with the new information, or if theViewisn't there, just drop the work altogether. And, if theActivitythat issued the work was destroyed, then the newActivitywon't have a reference to any of this, and it will just drop the work, too.
Here is a screenshot of the accompanying diagram that describes this approach:
Code samples were not provided in the video, so here is my take on a basic implementation:
WorkRecord.javapublic class WorkRecord {
public static final String ACTION_UPDATE_VIEW = "WorkRecord.ACTION_UPDATE_VIEW";
public static final String EXTRA_WORK_RECORD_KEY = "WorkRecord.EXTRA_WORK_RECORD_KEY";
public static final String EXTRA_RESULT = "WorkRecord.EXTRA_RESULT";
public final int viewId;
public final Callback callback;
public WorkRecord(@IdRes int viewId, Callback callback) {
this.viewId = viewId;
this.callback = callback;
}
public interface Callback {
boolean update(View view, Object result);
}
public interface Store {
long addWorkRecord(WorkRecord workRecord);
}
}
Run Code Online (Sandbox Code Playgroud)
MainActivity.javapublic class MainActivity extends AppCompatActivity implements WorkRecord.Store {
// ...
private final Map<Long, WorkRecord> workRecords = new HashMap<>();
private BroadcastReceiver workResultReceiver;
// ...
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ...
initWorkResultReceiver();
registerWorkResultReceiver();
}
@Override protected void onDestroy() {
super.onDestroy();
// ...
unregisterWorkResultReceiver();
}
// Initializations
private void initWorkResultReceiver() {
workResultReceiver = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
doWorkWithResult(intent);
}
};
}
// Result receiver
private void registerWorkResultReceiver() {
final IntentFilter workResultFilter = new IntentFilter(WorkRecord.ACTION_UPDATE_VIEW);
LocalBroadcastManager.getInstance(this).registerReceiver(workResultReceiver, workResultFilter);
}
private void unregisterWorkResultReceiver() {
if (workResultReceiver != null) {
LocalBroadcastManager.getInstance(this).unregisterReceiver(workResultReceiver);
}
}
private void doWorkWithResult(Intent resultIntent) {
final long key = resultIntent.getLongExtra(WorkRecord.EXTRA_WORK_RECORD_KEY, -1);
if (key <= 0) {
Log.w(TAG, "doWorkWithResult: WorkRecord key not found, exiting:"
+ " intent=" + resultIntent);
return;
}
final Object result = resultIntent.getExtras().get(WorkRecord.EXTRA_RESULT);
if (result == null) {
Log.w(TAG, "doWorkWithResult: Result not found, exiting:"
+ " key=" + key
+ ", intent=" + resultIntent);
return;
}
final WorkRecord workRecord = workRecords.get(key);
if (workRecord == null) {
Log.w(TAG, "doWorkWithResult: matching WorkRecord not found, exiting:"
+ " key=" + key
+ ", workRecords=" + workRecords
+ ", result=" + result);
return;
}
final View viewToUpdate = findViewById(workRecord.viewId);
if (viewToUpdate == null) {
Log.w(TAG, "doWorkWithResult: viewToUpdate not found, exiting:"
+ " key=" + key
+ ", workRecord.viewId=" + workRecord.viewId
+ ", result=" + result);
return;
}
final boolean updated = workRecord.callback.update(viewToUpdate, result);
if (updated) workRecords.remove(key);
}
// WorkRecord.Store implementation
@Override public long addWorkRecord(WorkRecord workRecord) {
final long key = new Date().getTime();
workRecords.put(key, workRecord);
return key;
}
}
Run Code Online (Sandbox Code Playgroud)
MyTask.javapublic class MyTask extends AsyncTask<Void, Void, Object> {
// ...
private final Context appContext;
private final long workRecordKey;
private final Object otherNeededValues;
public MyTask(Context appContext, long workRecordKey, Object otherNeededValues) {
this.appContext = appContext;
this.workRecordKey = workRecordKey;
this.otherNeededValues = otherNeededValues;
}
// ...
@Override protected void onPostExecute(Object result) {
final Intent resultIntent = new Intent(WorkRecord.ACTION_UPDATE_VIEW);
resultIntent.putExtra(WorkRecord.EXTRA_WORK_RECORD_KEY, workRecordKey);
resultIntent.putExtra(WorkRecord.EXTRA_RESULT, result);
LocalBroadcastManager.getInstance(appContext).sendBroadcast(resultIntent);
}
}
Run Code Online (Sandbox Code Playgroud)
// ...
private WorkRecord.Store workRecordStore;
private MyTask myTask;
// ...
private void initWorkRecordStore() {
// TODO: get a reference to MainActivity and check instanceof WorkRecord.Store
workRecordStore = (WorkRecord.Store) activity;
}
private void startMyTask() {
final long key = workRecordStore.addWorkRecord(key, createWorkRecord());
myTask = new MyTask(getApplicationContext(), key, otherNeededValues).execute()
}
private WorkRecord createWorkRecord() {
return new WorkRecord(R.id.view_to_update, new WorkRecord.Callback() {
@Override public void update(View view, Object result) {
// TODO: update view using result
}
});
}
Run Code Online (Sandbox Code Playgroud)
Obviously, this approach is a huge effort compared to the other two, and overkill for many implementations. For larger apps that do a lot of threading work, however, this can serve as a suitable base architecture.
Implementing this approach exactly as described in the video, the task will run to its end without being canceled on config change, like the second approach above. If your task is expensive (CPU, memory, battery), has side effects, or needs to be automatically restarted on Activity restart, then you would need to modify this approach to accommodate canceling, optionally saving and restarting, the task. Or just stick with the first approach; Romain had a clear vision for this and implemented it well.
This is a big answer, and it's likely that I have made errors and omissions. If you find any, please comment and I'll update the answer. Thanks!
| 归档时间: |
|
| 查看次数: |
833 次 |
| 最近记录: |