Activity生命周期
你创建的线程不会自动意识到activity生命周期的变化。比如,你孵化的一个线程不会注意到activity的onStrop()方法被调用,activity不会再可见,或者你的activity的onDestroy()方法被调用。这意味着你需要去做额外的事情使得应用的生命周期和你的线程同步。Listing 5-20显示了一个简单的示例,在activity被销毁后AsyncTask依然会继续运行。
Listing 5-20 在一个后台线程计算Fibonacci数,并更新用户界面
public class MyActivity extends Activity {
private TextView mResultTextView;
private Button mRunButton;
@Override
protected void onCreate(Bundle saveInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// 布局包含TextView和Button
mResultTextView = (TextView)findViewById(R.id.resultTextView); // 会显示计算结果
mRunButton = (Button) findViewById(R.id.runButton); // 开始计算的按钮
}
public void onClick (View v) {
new AsyncTask<Integer, Void, BigInteger>() {
@Override
protected void onPreExecute() {
// Button被disable,用户只可以开始一次
mRunButton.setEnabled(false);
}
@Override
protected void onCancelled() {
// Button被重新enable,可以让用户开始另一个计算
mRunButton.setEnabled(true);
}
@Override
protected BigInteger doInBackground(Integer... params) {
return Fibonacci.recursiveFasterPrimitiveAndBigInteger(params[0]);
}
@Override
protected void onPostExecute(BigInteger result) {
mResultTextView.setText(result.toString());
// 按钮被重新enable,允许用户开始另一次计算
mRunButton.setEnable(true);
}
}.execute(100000); // 很简单,硬编码参数
}
}
用户按了Button后做了两个简单的事情:
(1) 在单独的线程计算了Fibonacci数
(2) 当computation继续的时候button被disable,当计算完成后enable button,所以用户仅仅可以计算一次
表面上,看起来是正确的。然而,如果用户在计算的时候旋转设备,activity将会被销毁并且重建。(我们假设manifest文件没有指定activity处理设备方向改变)原来MyActivity实例执行了通常的序列onPause(), onStop(),和onDestroy()调用,新的实例执行onCreate(), onStart(), onResume()调用。伴随着所有的事情发生,AsyncTask的线程依然运行,就像任何事情没有发生过,没有意识到设备方向的改变,直到计算最终结束。再一次,到现在为止看起来是正确的,看起来也是某些人期望的。
然而发生了一件你不期望的事情:按钮在改变方向后再次成为enabled。这很简单解释,因为在改变方向后按钮是一个新的,默认在onCreate()方法里面去创建。作为结果,当第一个仍然在运行的时候,用户可以起第二个计算。尽管相对危害较少,这打乱了你建立的用户接口:当计算进行过程中disable按钮。
传递信息
如果你希望解决这个bug,你需要新的activity实例知道是否已经在计算过程中,可以在被创建后onCreate() disable button。Listing 2-21给出了你可以通信到新的MyActivity的示例。
Listing 5-21 从一个Activity实例传递信息到另外一个
public class MyActivity extends Activity {
private static final String TAG = "MyActivity";
private TextView mResultTextView;
private Button mRunButton;
private AsyncTask<Integer, Void, BigInteger> mTask; // 我们将传递这个对象到另外一个实例
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// 在这里添加一个Log信息,去知道Activity实例被创建
Log.i(TAG, "MyActivity instance is " + MyActivity.this.toString());
Log.i(TAG, "onCreate() called in thread " + Thread.currentThread().getId());
// 布局包含TextView和Button
mResultTextView = (TextView) findViewById(R.id.resultTextView); // 显示结果
mRunButton = (Button) findViewById(R.id.runButton); // 开始计算的按钮
// 得到在onRetainNonConfigurationInstance()返回的对象
mTask = (AsyncTask<Integer, Void, BigInteger>)getLastNonConfigurationInstance();
if (mTask != null) {
mRunButton.setEnabled(false); // 计算仍然在进行,所以disable button
}
}
@Override
public Object onRetainNonConfigurationInstance() {
return mTask; // 如果计算过程中,将不会为空
}
public void onClick(View v) {
// 我们保存一个指向AsyncTask对象的引用
mTask = new AsyncTask<Integer, Void, BigInteger>() {
@Override
protected void onPreExecute() {
// 按钮被disable, 所以计算仅会被开始一次
mRunButton.setEnabled(false);
}
@Override
protected void onCancelled() {
// 按钮被再次enable,允许用户开始另外一次计算
mRunButton.setEnable(true);
mTask = null;
}
@Override
protected BigInteger doInBackground(Integer... params) {
return Fibonacci.recursiveFasterPrimitiveAndBigInteger(params[0]);
}
@Override
protected void onPostExecute(BigInteger result) {
mResultTextView.setText(result.toString());
// 按钮被再次enable,允许用户开始另外一次计算
mRunButton.setEnable(true);
mTask = null;
// 我们添加一个log去知道计算已经完成
Log.i(TAG, "Computation completed in " + MyActivity.this.toString());
Log.i(TAG, "onPostExecute() called in thread " + Thread.currentThread().getId());
}
}.execute(100000); // 为了简单,我们硬编码了参数
}
}
NOTE: onRetainNonConfigurationInstance()现在被弃用了,推荐使用Fragment API从API 11开始,或者在老的平台使用Android的兼容包。这个弃用的方法在这里使用是为了简单;你会找到更多的示例代码使用这个方法。然而,推荐使用Fragment API写新的应用。
如果你执行这段代码,当旋转设备并且一个计算在运算过程中,将会发现button依然被disabled。这看起来将会修复我们Listing 5-20中的问题。然而,你将注意到一个新的问题:如果在运算过程中你旋转了设备,直到运算结束,button仍然不可以被再次enable,尽管onPostExecute()已经被调用。这是一个更加明显的问题,因为button永远不会被enable。而且,计算结果不会被传到用户界面。(这个问题同样在Listing 5-20给出,所以你需要注意到之前的问题在方向改变后button重新enable)
这很容易被解释(如果你是新学Java可能会不那么明显):onPostExecute()在和onCreate同一个线程调用(第一个activity被销毁,但是主线程仍然是同一个),在onPostExecute方法中的mResultTextView和mRunButton实际上属于MyActivity的第一个实例,而不是新的实例。匿名内部类声明的新的AsyncTask对象和它的外部类实例关联(这是为什么我们创建的AsyncTask对象可以引用在MyActivity类中声明的字段比如mResultTextView和mTask),因此它不会访问新的MyActivity的实例字段。基本上,Listing 5-21的代码有两个主要的缺陷,在计算过程中用户旋转设备:
(1) Button不会再次enable,结果不会显示
(2) 先前的activity实例被泄露,因为mTask保存着之前的activity的引用(所以当设备旋转的时候存在两个Activity的实例)
记住状态
解决这个问题的一个简单的方式是让MyActivity的新实例知道一个计算在过程中,然后重启这个计算。前面的运算可以使用AsyncTask.cancel()接口在onStop()或者onDestroy()里面取消。Listing 5-22 给出了一个可行的实现。
Listing 5-22 记住一个计算在过程中
public class MyActivity extends Activity {
private static final String TAG = "MyActivity";
private static final String STATE_COMPUTE = "myactivity.compute";
private TextView mResultTextView;
private Button mRunButton;
private AsyncTask<Integer, Void, BigInteger> mTask;
@Override
protected void onStop() {
super.onStop();
if (mTask != null) {
mTask.cancel(true); // 尽管被取消了,线程可能仍然会允许一会儿
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
// 如果调用, 保证在onStop()之前调用
super.onSaveInstanceState(outState);
if(mTask != null) {
outState.putInt(STATE_COMPUTE, 100000);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// 在这里添加一个log,了解MyActivity实例被创建
Log.i(TAG, "MyActivity instance is " + MyActivity.this.toString());
Log.i(TAG, "onCteate() called in thread " + Thread.currentThread().getId());
// 布局包含TextView和Button
mResultTextView = (TextView) findViewById(R.id.resultTextView); // 显示结果
mRunButton = (Button) findViewById(R.id.runButton); // 开启计算的按钮
// 保证你检测了savedInstanceState是否为空
if (savedInstanceState != null && savedInstanceState.containsKey(STATE_COMPUTE)) {
int value = savedInstanceState.getInt(STATE_COMPUTE);
mTask = createMyTask().execute(value); // 按钮在onPreExecute()被disable
}
}
// AsyncTask的创建移到了private方法,因为现在它可以在两个地方创建
private AsyncTask<Integer, Void, BigInteger> createMyTask() {
return new AsyncTask<Integer, Void, BigInteger>() {
@Override
protected void onPreExecute() {
// 按钮被disable,所以计算可以启动一次
mRunButton.setEnabled(false);
}
@Override
protected void onCancelled() {
// 按钮被再次enable,用户可以开启另一次计算
mRunButton.setEnabled(true);
mTask = null;
}
@Override
protected BigInteger doInBackground(Integer... params) {
return Fibonacci.recursiveFasterPrimitiveAndBigInteger(params[0]);
}
@Override
protected void onPostExecute(BigInteger result) {
mResultTextView.setText(result.toString());
// 按钮被enable,允许用户再次开始计算
mRunButton.setEnabled(true);
mTask = null;
// 在这里添加log,了解计算已经完成
Log.i(TAG, "Computation completed in " + MyActivity.this.toString());
Log.i(TAG, "onPostExecute() called in thread " + Thread.currentThread().getId());
}
};
}
public void onClick(View v) {
// 我们保存一个指向AsycTask对象的引用
mTask = createMyTask.execute(100000);
}
}
对于这个实现,我们只是告诉新实例之前的实例被销毁的时候正在计算某一个值。新的实例将重新计算,用户界面相应更新。
不需要旋转设备去产生一个配置改变,因为其他的事件也会导致配置改变。比如,包括改变locale,或者一个外部的键盘连接。尽管一个Google TV设备可能不会被旋转(至少目前为止不会),对于Google TV仍然需要将配置改变场景考虑在内,因为其他的事件仍然很可能发生。另外,将来可能会添加会导致配置改变新的事件。
NOTE:onSaveInstanceState()不会总是被调用。它只会在系统有一个好的理由去调用它的时候被调用。更多的细节参考Android文档。
取消一个AsyncTask对象不意味着线程必须马上停止。实际的行为依赖于几件事情:
(1) task是否已经开始
(2) 哪个参数(true或者false)被传递给cancel()
调用AsyncTask.cancel()在doInBackground()返回后引发一个onCancelled()调用,而不是onPostExecute()。因为doInBackground()可能依然会在onCancelled()被调用之前完成,你可以在doInBackground()中周期性的调用AsyncTask.isCancelled(),以便可以尽可能早的返回。尽管没有显露在我们的示例中,这将使你的代码更加的难于维护,因为你需要去交错的调用AsyncTask相关的调用(isCancelled())和实际做事情的代码(理想的情况下和AsyncTask无关)。
NOTE:当activity被销毁的时候,线程不需要总是被打断。你可以使用Activity.isChangingConfiuration()和Activity.isFinishing()接口去了解发生了什么,然后制定计划。比如,在Listing 5-22我们可以决定仅仅在isFinishing()返回true的时候去在onStop()去取消任务。
通常,当你的activity暂停或者停止,你需要至少尝试去暂停后台的线程。这将防止你的应用使用资源(CPU,memory, internal storage),其他的activity可能需要。
TIP:参考http://code.google.com/p/shelves和http://code.google.com/p/apps-for-android更多的实例,关于activity实例之间保存状态。
Summary
使用线程可以使得你的代码更加的有效和更加容易去维护,即使在单线程的设备上。然而,多线程同样添加了你的应用的复杂度,特别是涉及到同步,为了更好的用户体验应用的state需要被保存。保证你理解在应用中使用多线程的衍生物,因为这将脱离控制,调试会变得非常困难。尽管有些不琐细,使用多线程可以显著的提高应用的性能。因为多核架构变得普及,在应用中添加多线程支持,你的客户将会受益。