Android Configuration change引发的问题及解决方法(转)
之前在学习Fragment和总结Android异步操作的时候会在很多blog中看到对Configuration Change的讨论,以前做的项目都是固定竖屏的,所以对横竖屏切换以及横竖屏切换对程序有什么影响都没什么了解。见到的次数多了,总是掠过去心理总觉得不踏实,最终还是好好看了些介绍Congifuration Change的blog,在此做个梳理也不枉花了那么多时间。有疏漏和描述不准确的地方恳请指正。
前言
在研究Configuration Change之前我主要的疑问:
- 横竖屏切换对布局有影响吗,要做什么处理吗?
- 屏幕旋转的话为什么要保存数据?
- 启动一个线程(worker thread或者AsyncTask)来跑一个耗时任务,此时旋转屏幕会对线程有什么影响吗?
- 异步操作过程会显示进度对话框,旋转屏幕造成程序终止的原因及解决方法?
- 在AndroidManifest.xml中通过配置android:configuration的方法来防止Activity被销毁并重建为什么不被推荐,这种方法有哪些缺点?
- 推荐使用
Fragment的setRetainInstance(true)来处理配置变化时保存对象,具体怎么实现?
屏幕方向是设备配置的一个属性,屏幕旋转是影响配置变化的因素之一,在项目中最常见。在对Configuration Change有个全面认识后,这些问题都会迎刃而解。
由一道网上总结的Android测试题引发的测试
对Configuration Change的第一印象还是看网上总结的Andorid面试题里有问到:
问题:横竖屏切换时Activity的生命周期?
答案:
1、不设置Activity的android:configChanges时,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次
2、设置Activity的android:configChanges=”orientation”时,切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次
3、设置Activity的android:configChanges=”orientation|keyboardHidden”时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法
但是经过测试后结果表明并不都和‘答案’一致:
测试环境:在HTC t329d 4.1和模拟器2.2.3上的测试结果:
1、和答案中的第1点不一致。不设置Activity的android:configChanges时,不管切横屏还是切竖屏,都只会重新调用生命周期一次。
2、和‘答案’中第2点一致
3、 和答案中的第3点不一致。 设置Activity的 android:configChanges=”orientation|keyboardHidden”时,在Android 3.2(API Level 13)之前,切屏还是会重新调用各个生命周期,不会执行onConfigurationChanged()方法。在Android 3.2之后必须在configChanges中添加screenSize才不会在切屏时重新调用各个生命周期。并执行onConfigurationChanged()方法。
从测试结果和‘答案’的不一致告诉me,对于所谓的'答案'最好亲测比较靠谱,而且对于给答案的人最好指明下测试环境,否则测试结果不同也无处对照。全面透彻尽可能多地去覆盖有关Configuration Change的知识。其实对于第一点,切横屏还是竖屏导致Activity重建的次数并不重要,重要的是它被重建了以及重建会引发什么问题。
Configuration Change概述
Configuration 这个类描述了设备的所有配置信息,这些配置信息会影响到应用程序检索的资源。包括了用户指定的选项(locale和scaling)也包括设备本身配置(例如input modes,screen size and screen orientation).可以在该类里查看所有影响Configuration Change 的属性。
横竖屏切换是我们最常见的影响配置变化的因素,还有很多其他影响配置的因素有语言的更改(例如中英文切换)、键盘的可用性(这个没理解)等
常见的引发Configuration Change的属性:
横竖屏切换:android:configChanges="orientation"
键盘可用性:android:configChanges="keyboardHidden"
屏幕大小变化:android:configChanges="screenSize"
语言的更改:android:configChanges="locale"
在程序运行时,如果发生Configuration Change会导致当前的Activity被销毁并重新创建 ,即先调用onDestroy紧接着调用onCreate()方法。 重建的目的是为了让应用程序通过自动加载可替代资源来适应新的配置。
Configuration Change引发的问题
当程序运行时, 设备配置的改变会导致当前Activity被销毁并重新创建 。
在Activity被销毁之前我们需要保存当前的数据以防Activity重建后数据丢失。例如界面中用户选择了checkbox和radiobutton选项或者通过网络请求显示在界面上的数据在屏幕旋转后Activity被destroy-recreate,这些控件上被选择的状态和界面上的数据都会消失。
再比如当进入某个Activity时加载页面进行网络请求,此时旋转屏幕会重新创建网络连接请求,这样的用户体验非常不好。而且常见的一个问题是如果伴随异步操作显示一个progressDialog的话,异步任务未完成去旋转屏幕,程序会因为 Activity has leaked window 而 终止。而当old Activity被销毁后,线程执行完毕后还是会把结果返回给old Activity而非新的Activity,而且新的Activity如果又触发了后台任务(在onCreate()中会启动线程),就又会去启动一个子线程,消耗可用的资源。
下面通过一个例子来看看横竖屏切换引发的以上问题:
- 异步操作结束后旋转屏幕,界面数据丢失
- 显示进度对话框的异步操作,未结束时旋转屏幕,程序终止
该示例,通过点击屏幕按钮启动一个异步操作(模拟执行耗时任务),同时显示一个进度对话框。当异步操作执行完毕后更新界 面,并取消进度对话框。在本节最后可查看代码。
1. 异步操作结束后旋转屏幕,界面数据丢失
2. 异步操作未结束旋转屏幕,程序终止
log打印出的错误信息:
04-14 21:34:10.192 26254-26254/com.aliao.myandroiddemo E/WindowManager﹕ Activity com.aliao.myandroiddemo.view.handler.TestHandlerActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@4208a548 that was originally added here
android.view.WindowLeaked: Activity com.aliao.myandroiddemo.view.handler.TestHandlerActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@4208a548 that was originally added here
at android.view.ViewRootImpl.<init>(ViewRootImpl.java:415)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:322)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:234)
at android.view.WindowManagerImpl$CompatModeWrapper.addView(WindowManagerImpl.java:153)
at android.view.Window$LocalWindowManager.addView(Window.java:557)
at android.app.Dialog.show(Dialog.java:277)
at android.app.ProgressDialog.show(ProgressDialog.java:116)
at android.app.ProgressDialog.show(ProgressDialog.java:104)
at com.aliao.myandroiddemo.view.handler.TestHandlerActivity.excuteLongTimeOperation(TestHandlerActivity.java:60)
at com.aliao.myandroiddemo.view.handler.TestHandlerActivity.onClick(TestHandlerActivity.java:51)
at android.view.View.performClick(View.java:4191)
at android.view.View$PerformClick.run(View.java:17229)
at android.os.Handler.handleCallback(Handler.java:615)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4963)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1038)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:805)
at dalvik.system.NativeStart.main(Native Method)
04-14 21:34:11.692 483-635/? E/Watchdog﹕ !@Sync 3825
04-14 21:34:12.192 142-142/? E/SMD﹕ DCD ON
04-14 21:34:12.502 26254-26254/com.aliao.myandroiddemo E/AndroidRuntime﹕ FATAL EXCEPTION: main
java.lang.IllegalArgumentException: View not attached to window manager
at android.view.WindowManagerImpl.findViewLocked(WindowManagerImpl.java:696)
at android.view.WindowManagerImpl.removeView(WindowManagerImpl.java:379)
at android.view.WindowManagerImpl$CompatModeWrapper.removeView(WindowManagerImpl.java:164)
at android.app.Dialog.dismissDialog(Dialog.java:319)
at android.app.Dialog.dismiss(Dialog.java:302)
at com.aliao.myandroiddemo.view.handler.TestHandlerActivity$1.handleMessage(TestHandlerActivity.java:87)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4963)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1038)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:805)
at dalvik.system.NativeStart.main(Native Method)
该示例的代码:
res/layout/activity_handler.xml——TestHandlerActivity的布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="通过点击按钮来启动一个线程模拟运行一个网络耗时操作,获取新闻详情并显示在按钮下面"
android:textSize="16sp"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="获取MH370航班最新新闻动态"
android:textSize="16sp"
android:id="@+id/btn_createthread"
android:layout_gravity="center_horizontal" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@android:color/holo_green_dark"
android:textSize="16sp"
android:id="@+id/tv_showsth"
android:layout_marginTop="10dp"/>
</LinearLayout>
TestHandlerActivity——进行异步操作,获取数据并更新界面
package com.aliao.myandroiddemo.view.handler;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import com.aliao.myandroiddemo.R;
import com.aliao.myandroiddemo.utils.ThreadUtil;
/**
* Created by liaolishuang on 14-4-9.
*/
public class TestHandlerActivity extends Activity implements View.OnClickListener{
private final String TAG = "testhandler";
private TextView showNewsInfoTxt;
private ProgressDialog progressDialog;
private String newsInfo;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
//打印当前线程的部分信息
ThreadUtil.logThreadSignature();
Button anrBtn = (Button) findViewById(R.id.btn_createthread);
anrBtn.setOnClickListener(this);
showNewsInfoTxt = (TextView) findViewById(R.id.tv_showsth);
if(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE){
Log.i(TAG, "----onCreate - landscape---");
}else if(getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
Log.i(TAG, "----onCreate - portrait ---");
}
}
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.btn_createthread:
excuteLongTimeOperation();
break;
}
}
/**
* 点击按钮,创建子线程,并显示一个进度对话框
*/
private void excuteLongTimeOperation() {
progressDialog = ProgressDialog.show(TestHandlerActivity.this,"Load Info","Loading...",true,true);
Thread workerThread = new Thread(new MyNewThread());
workerThread.start();
}
class MyNewThread extends Thread{
@Override
public void run() {
//打印子线程的部分信息
ThreadUtil.logThreadSignature();
//模拟执行耗时操作
ThreadUtil.sleepForInSecs(5);
newsInfo = "#搜寻马航370#【澳联合协调中心今日记者会要点】1.发现油迹的地点距离信号发现地很近,油迹来源需进一步调查。2.黑匣子一般只有30天寿命,最多40天,今天已经是第38天了,但仍有可能收到信号";
Message message = handler.obtainMessage();
Bundle bundle = new Bundle();
bundle.putString("message",newsInfo);
message.setData(bundle);
handler.sendMessage(message);
}
}
/**
* 以匿名类的形式创建handler
*/
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
progressDialog.dismiss();
//更新界面中TextView中的内容
refreshNewsInfo(msg.getData().getString("message"));
}
};
/**
* 更新界面内容
* @param newsInfo
*/
private void refreshNewsInfo(String newsInfo) {
showNewsInfoTxt.setText(newsInfo);
}
/**
* 只有在AndroidManifest.xml中对该Activity设置android:configChanges,该方法才会被回调
* @param newConfig
*/
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Log.i(TAG, "----onConfigurationChanged---");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.i(TAG, "====onDestroy====");
}
@Override
protected void onStart() {