之前在学习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 [email protected]4208a548 that was originally added here android.view.WindowLeaked: Activity com.aliao.myandroiddemo.view.handler.TestHandlerActivity has leaked window [email protected]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﹕ [email protected] 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() { super.onStart(); Log.i(TAG, "----onStart---"); } @Override protected void onResume() { super.onResume(); Log.i(TAG, "----onResume---"); } @Override protected void onRestart() { super.onRestart(); Log.i(TAG, "----onRestart---"); } @Override protected void onPause() { super.onPause(); Log.i(TAG, "----onPause---"); } @Override protected void onStop() { super.onStop(); Log.i(TAG, "----onStop---"); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); Log.i(TAG, "----onSaveInstanceState---"); } }
解决方案
一、禁止屏幕旋转
禁止屏幕旋转,也就无需考虑Configuration Change引发的问题
在AcndroidManifest.xml里设置Activity的screenOrientation属性为landscape(横屏)或者portrait(竖屏)
<activity android:name="com.aliao.myandroiddemo.view.handler.TestHandlerActivity" android:label="@string/title_activity_animation" android:screenOrientation="portrait"> </activity>
二、避免Activity重建
1.配置andoird:configChanges属性并回调onConfigurationChanged()手动处理
Handling the Configuration Change Yours 里指出如果应用程序不需要在一个特定的configuration change期间更新资源(例如程序在横屏和竖屏不同屏幕大小下不考虑资源调整),以及有防止activity重启的性能限制,就可以通过该方法来阻止系统重启activity。但是Google并不推荐使用该方法。
上一节讨论了在某些情况下由于横竖屏切换导致的一系列问题,引起这些问题的源头是因为Configuration Change会导致Activity被重建。如果Activity不被销毁再重建也就没有所谓的数据丢失,异步操作过程中内存泄露程序终止等问题了。Android提供了一种方法来避免Activity被重建:
在AndroidManifest.xml里通过android:configChanges指定要忽略的配置,例如:
<activity android:name="com.aliao.myandroiddemo.view.handler.TestHandlerActivity" android:configChanges="orientation|keyboardHidden" android:label="@string/title_activity_animation"> </activity>
Caution:需要注意的是,在Android 3.2(API Level 13)开始,横竖屏切换也会导致"screen size"(Configuraion的一个属性)改变,所以要在android:configChanges加上该值 android:configChanges="orientation|screenSize"
,否则当切换屏幕时,activity仍会被重建。
<activity android:name="com.aliao.myandroiddemo.view.handler.TestHandlerActivity" android:configChanges="orientation|screenSize|keyboardHidden" android:label="@string/title_activity_animation"> </activity>
设置了android:configChanges后,Activity会在配置改变时只回调onConfigurationChanged(Configuration newConfig),不会重新走一遍Activity的生命周期:
启动TestHandlerActivity显示界面Activity生命周期为:onCreate->onStart->onResume:
04-14 22:56:18.092 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onCreate - portrait --- 04-14 22:56:18.092 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onStart--- 04-14 22:56:18.092 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onResume---
竖屏切横屏,只回调了 onConfigurationChanged:
04-14 22:59:29.912 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onCreate - portrait --- 04-14 22:59:29.912 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onStart--- 04-14 22:59:29.912 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onResume--- 04-14 22:59:33.442 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onConfigurationChanged---
切回竖屏,同样只回调 onConfigurationChanged:
04-14 23:01:02.232 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onCreate - portrait --- 04-14 23:01:02.232 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onStart--- 04-14 23:01:02.232 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onResume--- 04-14 23:01:04.192 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onConfigurationChanged--- 04-14 23:01:05.492 32095-32095/com.aliao.myandroiddemo I/testhandler﹕ ----onConfigurationChanged---
如果横竖屏的界面布局不同,可以再res下新建layout-land目录和layout-port目录,然后把布局文件扔到这两个目录文件中:
res/layout-land/layout_main.xml
res/layout-port/layout_main.xml
当程序运行的时候会自动判断当前的屏幕方向去layout里调用对应的布局文件。 我们可以在 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(); }
如果不管屏幕配置变不变化,程序中使用的资源不会改变,可以不用实现 onConfigurationChanged()回调。
2.不推荐<通过设置 android:configChanges属性的方法来避免activity被销毁再重建>的原因
这个方法真的很方便,在运行上面的实例代码时,完全可以正常运行没有任何错误。但是他也是指某种情形下适用,看了以下不推荐适用的原因后,还是掌握第二种方法比较靠谱!
先看Android Developers里怎么说的:
Note: Handling the configuration change yourself can make it muchmore difficult to use alternative resources, because the system does not automatically apply them for you. This technique should be considered a last resort when you must avoid restarts due to aconfiguration change and is not recommended for most applications .
However, your application should always be able to shut down and restart with its previous state intact, so you should not consider this technique an escape from retaining your state during normal activity lifecycle. Not only because there are other configuration changes that you cannot prevent from restarting your application, butalso because you should handle events such as when the user leaves your application and it gets destroyed before the user returns to it.
1. 配置改变和资源调整的问题,因为用这个方法我们需要自己往 onConfigurationChanged()里写代码 ,保证所用资源和设备的 当前配置一致,如果一个马虎程序很容易出现资源指定的bugs,原文:
Google engineers ,however, discourage its use. The primary concern is that it requires youto handle device configuration changes manually in code. Handling configuration changes requires you to take many additional steps to ensure that each and every string, layout, drawable, dimension, etc.remains in sync with the device‘s current configuration, and if you aren‘t careful, your application can easily have a whole series of resource-specific bugs as a result.—— Handling Configuration Changes With Fragments
2. there are other configuration changes that you cannot prevent from restarting your application.有些configuration changes没法阻止应用重启。(是说的有些android:configChanges的属性值对避免重建无效?不知道理解是否正确)
3. 很多开发人员会错误指定 android:configChanges=" orientation
"来防止activity被销毁或重建这种不可预知的情况。但是 引起Configuration Changes的情况很多,不止是屏幕旋转。比如修改设备默认语言,修改设备默认字体比例等等都可会引起配置改变。这种方法只对当前设置的配置有效,除非在manifest里把所有配置都列全。
4. 当用户离开应用,在回到应用前被销毁的话,例如点击了屏幕的Home键或者有个电话打进来,用户很久之后才回到应用程序,但是在此之前系统因为资源紧张而销毁了应用进程,当用户返回还是要重新创建activity,问题等于没解决。
Your application should be able to restart at any time without loss of user data or state in order to handle events such as configuration changes or when the user receives an incoming phone call and then returns to your application much later after your application process may have been destroyed.—— Handling Runtime Changes
As a user you won‘t stay on that activity and stare at it. You would switch to the home screen or to another app like a game or a phone call might come in or something else resource hungry that will eventually destroy your activity. And what then? You are facing the same old issue which is NOT solved with that neat little trick. The activity will be recreated all over again when the user comes back.—— How to handle screen orientation change when progress dialog and background thread active?中的一个评论
三、覆写onRetainNonConfigurationInstance()来保留activity中的数据对象
在Android 3.0发布之前,处理Configuration Change的方法是覆写onRetainNonConfigurationInstance()和getLastNonConfigurationInstance()方法。在onRetainNonConfigurationInstance()中返回对象(持有数据),再通过getLastNonConfigurationInstance()方法获取该对象,再更新界面数据即可。看个例子就很容易明白了,修改TestHandlerActivity代码如下:
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 ---"); } String retain = (String) getLastNonConfigurationInstance(); if (retain != null){ refreshNewsInfo(retain); }else{ //进入Activity后立马加载数据 excuteLongTimeOperation(); } } /** * 返回异步操作中获取到的数据 * @return */ @Override public Object onRetainNonConfigurationInstance() { return newsInfo; } /** * 点击按钮,创建子线程,并显示一个进度对话框 */ private void excuteLongTimeOperation() { progressDialog = ProgressDialog.show(TestHandlerActivity.this,"Load Info","Loading...",true,true); Thread workerThread = new Thread(new MyNewThread()); workerThread.start(); } //省略其他代码 }
三、推荐使用Fragment来处理Configuration Change
具体步骤如下:
1. Extend the Fragment
class and declare references to your stateful objects.
2. CallsetRetainInstance(boolean)
when the fragment is created.
3. Add the fragment to your activity.
4. Use FragmentManager
to retrieve the fragment when the activity is restarted.
定义一个RetainedFragment类:
package com.aliao.myandroiddemo.view.handler; import android.app.Activity; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.v4.app.Fragment; import com.aliao.myandroiddemo.utils.ThreadUtil; /** * A simple {@link android.support.v4.app.Fragment} subclass. * Activities that contain this fragment must implement the * {@link RetaindFragment.OnFragmentInteractionListener} interface * to handle interaction events. * */ public class RetaindFragment extends Fragment { private OnFragmentInteractionListener mListener; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //retain ths fragment setRetainInstance(true); } /** * 点击按钮,创建子线程,并显示一个进度对话框 */ public void excuteLongTimeOperation() { Thread workerThread = new Thread(new MyNewThread()); workerThread.start(); } class MyNewThread extends Thread{ @Override public void run() { //打印子线程的部分信息 ThreadUtil.logThreadSignature(); //模拟执行耗时操作 ThreadUtil.sleepForInSecs(5); String 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) { //更新界面中TextView中的内容 if(mListener != null){ mListener.onFragmentInteraction(msg.getData().getString("message")); } } }; // TODO: Rename method, update argument and hook method into UI event public void onButtonPressed(String string) { if (mListener != null) { mListener.onFragmentInteraction(string); } } @Override public void onAttach(Activity activity) { super.onAttach(activity); try { mListener = (OnFragmentInteractionListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement OnFragmentInteractionListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; } /** * This interface must be implemented by activities that contain this * fragment to allow an interaction in this fragment to be communicated * to the activity and potentially other fragments contained in that * activity. * <p> * See the Android Training lesson <a href= * "http://developer.android.com/training/basics/fragments/communicating.html" * >Communicating with Other Fragments</a> for more information. */ public interface OnFragmentInteractionListener { // TODO: Update argument type and name public void onFragmentInteraction(String string); } }
这个Fragment没有界面,它用来处理异步操作,然后把结果就该界面的部分返回给Activity来处理,Activity会实现Fragment中定义的OnFragmentInteractionListener接口中的onFragmentInteraction(String string)方法( Fragment与Activity之间的通讯 ),这个接口我们可以自己定义。把之前在TestHandlerActivity中的异步操作移植到RetainedFragemnt类中。
TestHandlerActivity对应的布局文件activity_handler.xml修改代码如下:
<?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" /> <ProgressBar android:id="@+id/progress_circular" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="10dp" style="@android:style/Widget.ProgressBar.Small" android:visibility="gone" 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.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.ProgressBar; 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 FragmentActivity implements View.OnClickListener,RetaindFragment.OnFragmentInteractionListener{ private final String TAG = "testhandler"; private TextView showNewsInfoTxt; private ProgressDialog progressDialog; private ProgressBar progressBar; private String newsInfo; private RetaindFragment dataFragment; private static final String KEY_CURRENT_NEWSDATA = "current_nesdata"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_handler); Log.i(TAG, "----onCreate---"); //打印当前线程的部分信息 ThreadUtil.logThreadSignature(); Button anrBtn = (Button) findViewById(R.id.btn_createthread); anrBtn.setOnClickListener(this); showNewsInfoTxt = (TextView) findViewById(R.id.tv_showsth); progressBar = (ProgressBar) findViewById(R.id.progress_circular); if(null != savedInstanceState){ refreshNewsInfo((String) savedInstanceState.get(KEY_CURRENT_NEWSDATA)); } //在activity重启时获取到保留的fragment对象 FragmentManager fm = getSupportFragmentManager(); dataFragment = (RetaindFragment) fm.findFragmentByTag("data"); if(null == dataFragment){ //添加fragment dataFragment = new RetaindFragment(); fm.beginTransaction().add(dataFragment, "data").commit(); //从网上下载数据 } } @Override public void onClick(View view) { switch (view.getId()){ case R.id.btn_createthread: progressBar.setVisibility(View.VISIBLE); // progressDialog = ProgressDialog.show(TestHandlerActivity.this,"Load Info","Loading...",true,true); //控制RetainFragment中的子线程启动 dataFragment.excuteLongTimeOperation(); break; } } @Override public void onFragmentInteraction(String newsInfo) { this.newsInfo = newsInfo; refreshNewsInfo(newsInfo); } /** * 更新界面内容 * @param newsInfo */ private void refreshNewsInfo(String newsInfo) { progressBar.setVisibility(View.GONE); showNewsInfoTxt.setText(newsInfo); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(KEY_CURRENT_NEWSDATA,showNewsInfoTxt.getText().toString());//注意不要直接传newsInfo,否则在异步操作执行完成后旋转屏幕,内容还是会消失。因为该值只有在屏幕旋转的时候才赋值, Log.i(TAG, "----onSaveInstanceState---"); } /** * 只有在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() { super.onStart(); Log.i(TAG, "----onStart---"); } @Override protected void onResume() { super.onResume(); Log.i(TAG, "----onResume---"); } @Override protected void onRestart() { super.onRestart(); Log.i(TAG, "----onRestart---"); } @Override protected void onPause() { super.onPause(); Log.i(TAG, "----onPause---"); } @Override protected void onStop() { super.onStop(); Log.i(TAG, "----onStop---"); } }
在异步操作还未执行完毕的时候旋转屏幕,TestHandlerActivity会被销毁再重建。新的TestHandlerActivity被创建,新的Activity实例会传送给onAttach(Activity)方法,通过打印onAttach中的activity可以看到屏幕旋转前后onAttach绑定的activity不同。这样就确保不管配置是否改变RetainedFragment持有的都是当前展示的Activity的引用。
在以上的示例中onSaveInstanceState的作用是在异步操作完毕时旋转屏幕确保屏幕数据不丢失。
onSaveInstanceState: it might not be possible for you to completely restore youractivity state with the Bundle
that the system saves for you with the onSaveInstanceState()
callback—it is notdesigned to carry large objects (such as bitmaps) and the data within it must be serialized thendeserialized, which can consume a lot of memory and make the configuration change slow.—— Handling Runtime Changes
异步操作显示对话框在Configuration Changes时导致程序崩溃
之前看到一篇讲内存泄露的文章,其中一个内存泄露的情境和上面的实例代码情境很类似,大意是:在Activity里创建一个子线程来跑耗时操作,在异步操作没结束前旋转屏幕,线程没执行完,old Activity也就不会被销毁,会导致内存泄露。摘取部分原文内容:
“由于我们的线程是Activity的内部类,所以MyThread中保存了Activity的一个引用,当MyThread的run函数没有结束时,MyThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。”
“Thread只有在run函数不结束时才出现这种内存泄露问题”
用图来表示上述内容的话,应该是:
看完这图就delete掉记忆吧,错滴错滴
他这段话误导了我好一阵:“线程的run函数没有结束,线程不会被销毁,他所引用的老activity也不会被销毁。所以出现了内存泄露。”在这种理解的基础上,我一直以为带进度对话框的异步操作在屏幕旋转的时候出现程序终止,是因为线程没结束,activity不销毁,所以导致了内存泄露。而且logcat下还打印了这么一句:
04-16 00:26:18.703 17075-17075/com.aliao.myandroiddemo E/WindowManager﹕ Activity com.aliao.myandroiddemo.view.handler.TestHandlerActivity has leaked window [email protected] that was originally added here
当时我就一直为了验证心里早已固定的认同感,看到“ TestHandlerActivity has leaked”TestHandlerActivity已经泄露了...就不加思考的去相信了。
但是今天在看有关于异步操作带对话框在configuration change时的处理办法时,总有一个疑问就是旧的activity会在线程结束的时候被销毁吗?后来做了测试,代码用的还是<Configuration Changes引发的问题>里最后贴的代码:
测试设备:HTC t329d Android4.1
测试操作:点击按钮启动线程,旋转屏幕,记录Activity被销毁时间,查看debug模式下的Threads列表记录线程消失时间
测试条件一:异步操作执行时长5秒
测试一结果:Activity的onDestroy调用的时间比worker thread结束时间晚或相等(这条件下就测了两次)。
测试条件二:异步操作执行时长20秒
测试二结果:启动线程的时间:
activity的onDestroy()调用时间: 00:09:04
thread的在Threads列表消失时间: 00:09:18
可以看到old activity的销毁时间在thread结束之前!!!
测试结果表明:activity并不是在thread结束后才销毁。这与之前说的“ thread没有销毁导致被持有引用的activity也不会销毁”相互矛盾!所以因为这个原因导致的内存泄露的说法就更没有说服力了。
再看之前log打印的错误:
1. WindowManager﹕ Activity com.aliao.myandroiddemo.view.handler. TestHandlerActivity has leaked window [email protected] that was originally added here
不是TestHandlerActivity has leaked
2. java.lang.IllegalArgumentException: View not attached to window manager
这个错误的发生是因为dismiss对话框时,所属Activity已经不在了。
经过上面的测试和打印的错误log可以得出:这个bug不是因为old activity没销毁导致内存泄露,而是activity被销毁后 progressDialog还持有这个activity的引用。异步任务开始时显示对话框,任务完成后去取消progressDialog。当任务没结束时旋转屏幕,会导致old activity被销毁,然后到了线程执行结束要dismiss progressDialog的时候发现所属的activity已经不在了。
解决办法(如果有其他好方法,推荐下下哇):
1.在布局文件创建progressBar来代替progressDialog
2. 创建一个AsyncTask的时候把当前Activity的引用传给其构造函数。onRetainNonConfigurationInstance()中判断线程是否结束,如果结束了就把progressDialog取消掉,然后将AsyncTask对象mTask返回。在onCreate中通过getLastNonConfigurationInstance()接收 mTask,关联当前activity——mtask.mContext = this;再重新启动一个progressDialog。保证了progressDialog在actviity销毁钱被dismiss掉。from How to handle screen orientation change when progress dialog and background thread active?中的其中一个回答。单单只是测试progressDialog在横竖屏切换时是否会崩溃,测试结果是正常的。
3.网上还有说用IntentService来解决,没用过这个,先不测了。
遗留问题:
1.在Configuration Changes引发的问题一节中 " there are other configuration changes that you cannot prevent from restarting your application."该怎么正确的翻译和理解
2. 在Configuration Changes引发的问题一节中的第四点怎么理解才是正确的。 不知道这种情况发生的情境,应用进程被销毁是等于整个app被kill掉,那不就是又重新打开app,重新进入activity也是正常的步骤。重新进入再重新请求呗。
参考资料:
强烈推荐阅读
Handling Runtime Changes ——来自Android Developers,介绍Configuration changes及其对数据丢失的解决方法。
Handling Configuration Changes With Fragments —— from Alex Lockwood 的blog,介绍了Configuration Changes的引发的问题、为什么不推荐适用android:configChanges方式来解决问题以及适用Fragments如何处理Configuration Changes(异步操作用的AsyncTask,比Thread+Handler在mainThread和workerThread上的UI更新和耗时处理上更加模块化,更方便)。