异步编程是android开发中,为提高程序效率而不得不采取的设计模式,由于android UI 线程对即时反应的高要求,任何耗时长久的操作都不能在主线程中执行,如果主线程堵塞,app 将引发异常退出,因此,像与服务器交互,下载文件等网络操作,一般要将任务放入子线程中执行。Android 移动编程模型与PC 类似,一般有线程池,AsyncTask 等方式。异步编程,有一难点,也是异步编程往往导致难以复现的诡异bug的,往往是数据状态的一致性维护不周,而导致线程异步读写数据,导致数据污染,致使程序崩溃。一般而言,我们都会通过加锁来处理共享数据读写,但有这么一种情况,却是加锁也难以解决的。
情况如下:在UI线程中,用户发起一个下载任务,主线程的Activity 生成一个AsyncTask ,并将自己的实例传入到AysncTask 类中,AysncTask 在自己的线程执行下载任务,下载完成后,onPostExecute 在UI 主线程执行,在该函数中,AysncTask 通过传进来的Activity 实例,调用Activity 提供的接口更新界面。但意外出现,也就是在onPostExecute调用前,用户将手机由竖屏切换为横屏,此时原有Activity的实例被销毁,新的Activity实例被创建,这意味着,原来传入AysncTask的Acitivity 实例是一个已经被系统销毁的对象,这时在onPostExecute中,调用原有传入的Activity实例接口,就会导致程序奔溃,而且这种奔溃是难以重现的。
为了处理这种情况,我们将处理流程总结为以下流程:
1. 我们需要在AsyncTask中保存对Activity实例的引用,当onPostExecute执行时,我们可以方便的通过Activity实例更新界面。
2. 我们知道,Activity实例有可能会被系统销毁,因此我们需要把握销毁事件,在引用的实例被销毁后,能及时的将旧引用转换为新生成的Activity实例。
3. 如果新的Activity生成时,AsyncTask仍然在后台执行,此时需要通知AysncTask更新它原来引用的实例。
根据以上所诉流程,我们给出解决该问题的示例代码如下:
public MainActivity extends Activity { private Worker worker; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); worker = (Worker)getLastNonConfigurationInstance(); if (worker == null) { worker = new Worker(); worker.execute(); } worker.connectContext(this); } @Override protected void onDestroy() { super.onDestroy(); worker.disconnectContext(); } @Override public Object onRetainNonConfigurationInstance() { return worker; } }
Worker是一个继承与AsyncTask 的类,我们现检测一个Worker 对象在onCreate 执行前是否已经生成,如果onCreate是因为系统原因,例如横竖屏切换导致的,那么worker曾经被上一个Activity对象生成过,因此getLastNonConfigurationInstance()返回一个非空实例,如果返回空,那意味着当前Activity是程序首次生成,于是就顺理成章的new 一个Worker对象, 然后调用Worker的connectContext 接口将当前的Activity 对象传入。
如果当前Activity实例要被系统销毁时,onDestroy() 调用,在里面我们通过 disconnectContext() 通知worker 原来传入的Activity失效了。
再看看Woker 的代码:
public class Worker extends AsyncTask<Void, Void, String> { private Activity context; public void connectContext(Activity context) { this.context = context; } public void disconnectContext() { this.context = null; } @Override protected String doInBackground(Void... params) { //下载文件或耗时的操作 } @Override protected void onPostExecute(String result) { if (context != null) { //更新Activity 界面 } } }
在Worker 中,保存一个对Activity的引用,并通过connectContext, disconnectContext 确保引用的对象是有效的,在onPostExecute执行中,确保context不为空,如果是空,就意味着,原来的Activity被销毁了,不能通过Activity 引用来更新界面。如果context 为空,表明Activity 刚被系统销毁,而新创建的Activity没有被传入到Worker中,或者是Worker创建时没有将Activity实例传入。无论何总情况,当前做法能保证onPostExecute执行时,不会引用被销毁的Activity实例。
该方法目前仍存一个疑问,那就是当onPostExecute执行前,Activity的onDestroy()调用,Worker里的Activity被设置为空,当onDestroy() 调用后,onPostExecute在主线程中执行,但由于context == null, 因此程序界面无法更新,当新的Activity生成后,那就只能显示原有界面,这不是一个bug吗。其实Android系统能保证onRetainNonConfigurationInstance执行后,onPostExecute不会执行,只有新的Activity实例构建完成后,既onCreate调用后,onPostExecute才会执行,这也就是说当onPostExecute执行时,context不会为空,程序主界面必定会得到更新。