1.3、Activity处理运行时改变

一些设备配置在运行过程中可能会发生改变(例如屏幕横向布局、键盘可用性和语言)。当这样的变化发生时,Android会重新启动这个正在运行的Activity(onDestroy()方法会被调用,然后调用onCreate()方法)。这个行为的设计意图是为了帮助你的应用程序适应新的配置,他会自动的用你提供的适当的资源(比如对应不同屏幕方向和尺寸的layout资源)重新加载应用程序。

为了正确执行一次重启,你的Activity在整个普通的生命周期中重新保存它之前的状态是很重要的,Android是通过在销毁你的Activity之前调用onSaveInstanceState()方法来保存关于应用之前状态的数据。然后你就可以在onCreate()方法或者onRestoreInstanceState()方法中重新恢复应用的状态了。

之前学过,当你的应用程序在后台中,被系统销毁前,你可以通过onSaveInstanceState()方法保存状态,然后在恢复。这种方式同样适合现在所学习的运行时配置的改变。

在大多数情况下,onSaveInstanceState()方法和onRestoreInstanceState()方法配合使用,能够解决大多数状态保存和恢复的问题。但是面对如下情景:保存大量数据并在Activity重启时恢复。在这种情况下,如果还是使用onSaveInstanceState()和onRestoreInstanceState(),数据的大量保存和恢复非常耗时不说,而且有些数据也不一定容易放到Bundle中。遇到这种情况,你有两个选择:

1、在配置改变期间维持一个对象

当配置发生改变时允许你的Activity重启,但让其携带一个有状态的对象到你的新Activity实例中。

2、你自己来处理配置的变化

当某些配置发生变化的时候阻止系统重启你的Activity,并且当配置改变时要接收一个回调,这样你就可以根据需要来手动更新你的Activity。


在配置改变期间维持一个对象

如果重启你的Activity,需要恢复大量的数据,重新执行网络连接,或者其他深入的操作,这样由配置改变引起的一次完全启动就会引起不好的用户体验。而且,仅有Activity生命周期中为你保存的的Bundle对象,你是不可能完全维护你的Activity的状态的—不能传递很大的对象(如bitmap对象),并且,这些对象里面的数据必须序列化,然后解序列化,这些都需要消耗很多内存从而使配置改变得很慢。在这样的情境下,当你的Activity由于配置发生改变而重启时,你可以通过重新预置一个有状态的对象来减缓你程序的负担。

在运行期间配置改变时维护一个对象:

1. 重写 onRetainNonConfigurationInstance() 方法来返回你想要维护的对象

2. 当你的Activity再次创建时,调用getLastNonConfigurationInstance()方法恢复你的对象

当你的Activity由于配置发生改变要关闭的时候,Android会在执行onStop()方法与onDestroy()方法之间调用onRetainNonConfigurationInstance()方法。为了在配置改变后更有效地保存状态,在实现onRetainNonConfigurationInstance() 方法时你应该返回你所需要的一个对象。

这个场景的可贵之处在于当你的应用程序需要从网上下载很多数据的时候。如果用户更改设备的方向并且Activity重启,你的应用程序必须要重新载入数据,那就会很慢了。你需要做的就是实现onRetainNonConfigurationInstance() 方法并返回带有你的数据的对象,然后当你的 Activity通过getLastNonConfigurationInstance()方法重启时就能获取数据。例如:

如果重启你的Activity,需要恢复大量的数据,重新执行网络连接,或者其他深入的操作,这样由配置改变引起的一次完全启动就会引起不好的用户体验。而且,仅有Activity生命周期中为你保存的的Bundle对象,你是不可能完全维护你的Activity的状态的—不能传递很大的对象(如bitmap对象),并且,这些对象里面的数据必须序列化,然后解序列化,这些都需要消耗很多内存从而使配置改变得很慢。在这样的情境下,当你的Activity由于配置发生改变而重启时,你可以通过重新预置一个有状态的对象来减缓你程序的负担。

在运行期间配置改变时维护一个对象:

1. 重写 onRetainNonConfigurationInstance() 方法来返回你想要维护的对象

2. 当你的Activity再次创建时,调用getLastNonConfigurationInstance()方法恢复你的对象

当你的Activity由于配置发生改变要关闭的时候,Android会在执行onStop()方法与onDestroy()方法之间调用onRetainNonConfigurationInstance()方法。为了在配置改变后更有效地保存状态,在实现onRetainNonConfigurationInstance() 方法时你应该返回你所需要的一个对象。

这个场景的可贵之处在于当你的应用程序需要从网上下载很多数据的时候。如果用户更改设备的方向并且Activity重启,你的应用程序必须要重新载入数据,那就会很慢了。你需要做的就是实现onRetainNonConfigurationInstance() 方法并返回带有你的数据的对象,然后当你的 Activity通过getLastNonConfigurationInstance()方法重启时就能获取数据。例如:

@Override
public Object onRetainNonConfigurationInstance() {
    final MyDataObject data = collectMyLoadedData();
    return data;
}

特别提醒:当你要返回任何对象的时候,你应该不要传递一个跟Activity有关联的对象,例如一个Drawable对象,一个Adapter对象,一个View对象或者任何其他跟Context相关的对象 。如果你这样做,它会泄漏原来Activity实例的所有视图和资源。(泄漏资源意味着您的应用程序保持对他们的持有,他们不能被当做垃圾收集,因此内存就丢失了)

然后当你的Activity重启时获取数据:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    final MyDataObject data = (MyDataObject) getLastNonConfigurationInstance();
    if (data == null) {
        data = loadMyData();
    }
    ...
}

这个例子中,getLastNonConfigurationInstance()获取了onRetainNonConfigurationInstance()方法中保存的数据。如果数据为空,(这种情况发生在,当Activity重启是由其他原因而不是配置改变引起的)那么程序将从原来的数据源载入数据对象 。

上面只是onRetainNonConfigurationInstance()和getLastNonConfigurationInstance()的最简单的用法,这个方法有很多的好处:

* 当activity曾经通过某个网络资源得到一些图片或者视频信息,那么当再次恢复后,无需重新通过原始资源地址获取,可以快速的加载整个activity状态信息。

* 当activity包含有许多线程时,在变化后依然可以持有原有线程,无需通过重新创建进程恢复原有状态。

* 当activity包含某些connection实例时,同样可以在整个变化过程中保持连接状态。



下面就是一个关于AsyncTask的例子:

import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;

/**
 * Android实现屏幕旋转异步下载效果
 * @Description: Android实现屏幕旋转异步下载效果
 */
public class RotationAsyncActivity extends Activity {
    // 进度条
    private ProgressBar progressBar=null;
    // 异步任务类
    private RotationAsyncTask asyncTask=null;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        progressBar=(ProgressBar)findViewById(R.id.progress);
        // 获取对象
        asyncTask=(RotationAsyncTask)getLastNonConfigurationInstance();
        
        if (asyncTask==null) {
            asyncTask=new RotationAsyncTask(this);
            asyncTask.execute();
        }else {
            asyncTask.attach(this);
            updateProgress(asyncTask.getProgress());
        
            if (asyncTask.getProgress()>=100) {
                markAsDone();
            }
        }
    }
    
    /**
     * 保存对象
     */
    @Override
    public Object onRetainNonConfigurationInstance() {
        asyncTask.detach();
        
        return asyncTask;
    }
      
    private void updateProgress(int progress) {
        progressBar.setProgress(progress);
    }
      
    private void markAsDone() {
        findViewById(R.id.completed).setVisibility(View.VISIBLE);
    }
     
    // 异步任务类
    private static class RotationAsyncTask extends AsyncTask<Void, Void, Void> {
        private RotationAsyncActivity activity=null;
        private int progress=0;
        
        /**
         * 默认的构造器
         */
        public RotationAsyncTask() {
            // TODO Auto-generated constructor stub
        }
        
        /**
         * 带参构造器
         * @param activity
         */
        public RotationAsyncTask(RotationAsyncActivity activity) {
            attach(activity);
        }
        
        @Override
        protected Void doInBackground(Void... unused) {
            for (int i=0;i<20;i++) {
                SystemClock.sleep(500);
                publishProgress();
            }
          
            return null;
        }
        
        @Override
        protected void onProgressUpdate(Void... unused) {
            if (activity==null) {
                Log.w("RotationAsyncActivity", "onProgressUpdate()");
            }else {
                progress += 5;
                activity.updateProgress(progress);
            }
        }
        
        @Override
        protected void onPostExecute(Void unused) {
            if (activity==null) {
                Log.w("RotationAsyncActivity", "onPostExecute()");
            }else {
                activity.markAsDone();
            }
        }
        
        protected void detach() {
            activity = null;
        }
        
        protected void attach(RotationAsyncActivity activity) {
            this.activity = activity;
        }
        
        protected int getProgress() {
            return progress;
        }
    }
}

注意:onSaveInstanceState()是在未经用户允许的情况下,系统销毁Activity之前调用的,而onRetainNonConfigurationInstance()是在因运行时配置改变引起系统销毁Activity之前调用的。

所以onSaveInstanceState()调用,onRetainNonConfigurationInstance()不一定会调用。onRetainNonConfigurationInstance()调用了,必定调用了onSaveInstanceState()。


自己来处理配置的改变

如果在某个特殊的配置发生改变的期间你的应用程序不需要更新资源,而且你有个操作限制需要你避免Activity的重启,那么你可以声明使你自己的Activity来处理配置的变化,从而阻止系统重启你的Activity。

特别提醒: 选择自己来处理配置的变化会使得可替代资源的使用变得更困难,因为系统不会为你来自动调用这些资源。这种技术应该被视为避免Activity重启的最后手段,对于大多数应用程序不建议使用。

为了声明你的Activity来处理配置的变化,在manifest文件中编辑正确的<activity>元素,包括赋好值的android:configChanges属性,代表你要处理的配置。android:configChanges属性所有可能的值都要在文档android:configChanges 中列出(最常用的值是:orientation来处理当屏幕的方向变化时,keyboardHidden来处理键盘可用性改变时)。你可以在属性中声明多个配置的值,通过“|”符号将它们分隔开。

例如,以下清单片段声明了Activity中将同时处理屏幕的方向变化和键盘的可用性变化:

<activity android:name=".MyActivity"
          android:configChanges="orientation|keyboardHidden"
          android:label="@string/app_name">

现在,当这些配置中的一个发生改变时,MyActivity不会重新启动。相反,这个 Activity会接收onConfigurationChanged()方法的调用。这个方法传递一个Configuration类的对象来标识新的设备配置。通过读取配置字段,你可以确定新的配置信息并通过更新你界面中使用的资源来正确应用这些改变。任何时候这个方法被调用,你的Activity的Resources对象会被更新并返回一个基于新配置的Resources对象,因此你可以在不用系统重启你的Activity的情况下很容易地重置你的UI元素。


特别提醒: 从Android 3.2(API level 13)开始,当设备的横竖屏切换时,屏幕尺寸也会发生改变。所以,如果你想在API level 13或更高(由minSdkVersion和targetSdkVersion声明)上阻止运行时因屏幕方向引发的重启,你必须在声明"orientation" 时额外声明"screenSize" 值。如下:android:configChanges=“orientation|screenSize”。然而,如果你的应用程序目标API level 12或者更低,那么你的Activity同样会自己处理这个配置的改变(这个配置改变不会重启你的Activity,即使运行在Android3.2或者更高的设置上)

例如,接下来的onConfigurationChanged()方法中实现了检查当前设备的方向:

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    // Checks the orientation of the screen
    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
    } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
        Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
    }
}

这个Configuration类的对象代表着当前所有的配置信息,不仅仅那些改变的配置信息。在很多时候,你不会确切地在乎这些配置是怎么改变的,并且可以简单地重新分配所有资源,提供您正在处理的配置的可替代资源。例如,因为这个Resources对象现在被更新,你可以通过setImageResource(int)方法重置任何ImageView,并重置恰当的资源给当前配置使用。(详见Providing Resources

请注意,配置字段的值是一些匹配 Configuration类里特定的常量的整数。对于文档中的每个字段使用那个常量,请在Configuration类中参阅相应的字段。

记住:当你声明你的Activity来处理配置的变化时,你必须负责重置所有你提供可替代资源的元素 。如果你声明你的Activity来处理屏幕方向的改变并具有在横向和纵向之间切换的图像,你必须在onConfigurationChanged()方法中给每个元素重新指定一个资源。


如果你不需要根据配置的变化来更新你的程序,你可以不实现onConfigurationChanged()方法。在这种情况下,所有在配置改变之前使用的资源仍然会被使用,并且你只需要避免你的Activity被重启。然而,您的应用程序应该始终能够关闭并从其之前的状态完好地重新启动。这不仅是因为存在有一些配置发生改变时你不能防止它重新启动您的应用程序,而且为了处理一些事件,例如当用户接收了电话然后返回到应用程序。


最后提一点,当我们自己处理配置改变时,在onConfigurationChanged方法中自己手动更新资源时,要注意下面的问题:
尽管此时Resource已经更新了,我们可以得到最新的资源,但是如下面的代码,要先清楚一个之前的资源,在重新设置我们实际需要的资源。否则的话,图片在配置改变时就不会更新

	@Override
	public void onConfigurationChanged(Configuration newConfig) {
		super.onConfigurationChanged(newConfig);
		mImageView.setImageResource(0);
		mImageView.setImageResource(R.drawable.temp);
	}

这是因为虽然Resource更新了,但是R.drawable.temp的int值是没有变的,而setImageResource的逻辑判断会忽略掉资源Id没有改变的设置。所以,在更新资源的时候注意是否会有类似的情况发生。




你可能感兴趣的:(1.3、Activity处理运行时改变)