欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android屏幕旋转 处理Activity与AsyncTask的最佳解决方案

程序员文章站 2024-02-23 15:44:16
一、概述 运行时变更就是设备在运行时发生变化(例如屏幕旋转、键盘可用性及语言)。发生这些变化,android会重启activity,这时就需要保存activity的状态及...

一、概述

运行时变更就是设备在运行时发生变化(例如屏幕旋转、键盘可用性及语言)。发生这些变化,android会重启activity,这时就需要保存activity的状态及与activity相关的任务,以便恢复activity的状态。

为此,google提供了三种解决方案:

下面会逐一介绍三种情况,其实保存一些变量对象很简单,难的是当activity创建异步线程去加载数据时,旋转屏幕时,怎么保存线程的状态。比如,在线程的加载过程中,旋转屏幕,就会存在问题:此时数据没有完成加载,oncreate重新启动时,会再次启动线程;而上个线程可能还在运行,并且可能会更新已经不存在的控件,造成错误。下面会一一解决这些问题。本文较长,主要是代码多,可以先下载demo,源码下载:对比着看。

 二、使用onsaveinstancestate,onrestoreinstancestate​

代码如下:

/**
 * 使用onsaveinstancestate,onrestoreinstancestate;
 * 在这里不考虑没有加载完毕,就旋转屏幕的情况。
 * @author 超超boy
 *
 */
public class savedinstancestateactivity extends listactivity
{
 private static final string tag = "mainactivity";
 private listadapter madapter;
 private arraylist<string> mdatas;
 private dialogfragment mloadingdialog;
 private loaddataasynctask mloaddataasynctask;
 @override
 public void oncreate(bundle savedinstancestate)
 {
 super.oncreate(savedinstancestate);
 log.e(tag, "oncreate");
 initdata(savedinstancestate);
 }

 /**
 * 初始化数据
 */
 private void initdata(bundle savedinstancestate)
 {
 if (savedinstancestate != null)
 mdatas = savedinstancestate.getstringarraylist("mdatas");
 if (mdatas == null)
 {
 mloadingdialog = new loadingdialog();
 mloadingdialog.show(getfragmentmanager(), "loadingdialog");
 mloaddataasynctask = new loaddataasynctask();
 mloaddataasynctask.execute();
 //mloaddataasynctas
 } else
 {
 initadapter();
 }
 }
 /**
 * 初始化适配器
 */
 private void initadapter()
 {
 madapter = new arrayadapter<string>(
 savedinstancestateactivity.this,
 android.r.layout.simple_list_item_1, mdatas);
 setlistadapter(madapter);
 }
 @override
 protected void onrestoreinstancestate(bundle state)
 {
 super.onrestoreinstancestate(state);
 log.e(tag, "onrestoreinstancestate");
 }
 @override
 //在这里保存数据,好用于返回
 protected void onsaveinstancestate(bundle outstate)
 {
 super.onsaveinstancestate(outstate);
 log.e(tag, "onsaveinstancestate");
 outstate.putserializable("mdatas", mdatas);
 }
 /**
 * 模拟耗时操作
 *
 * @return
 */
 private arraylist<string> generatetimeconsumingdatas()
 {
 try
 {
 thread.sleep(3000);
 } catch (interruptedexception e)
 { e.printstacktrace();
 }
 return new arraylist<string>(arrays.aslist("通过fragment保存大量数据",
 "onsaveinstancestate保存数据",
 "getlastnonconfigurationinstance已经被弃用", "rabbitmq", "hadoop",
 "spark"));
 }
 private class loaddataasynctask extends asynctask<void, void, void>
 {
 @override
 protected void doinbackground(void... params)
 {
 mdatas = generatetimeconsumingdatas();
 return null;
 }
 @override
 protected void onpostexecute(void result)
 {
 mloadingdialog.dismiss();
 initadapter();
 }
 }
 @override
 protected void ondestroy()
 {
 log.e(tag, "ondestroy");
 super.ondestroy();
 }
}

界面为一个listview,oncreate中启动一个异步任务去加载数据,这里使用thread.sleep模拟了一个耗时操作;当用户旋转屏幕发生重新启动时,会onsaveinstancestate中进行数据的存储,在oncreate中对数据进行恢复,免去了不必要的再加载一遍。

运行结果:

12-24 20:13:41.814 1994-1994/? e/mainactivity: oncreate
12-24 20:13:46.124 1994-1994/? e/mainactivity: onsaveinstancestate
12-24 20:13:46.124 1994-1994/? e/mainactivity: ondestroy
12-24 20:13:46.154 1994-1994/? e/mainactivity: oncreate
12-24 20:13:46.164 1994-1994/? e/mainactivity: onrestoreinstancestate

当正常加载数据完成之后,用户不断进行旋转屏幕,log会不断打出:onsaveinstancestate->ondestroy->oncreate->onrestoreinstancestate,验证activity重新启动,但是我们没有再次去进行数据加载。

如果在加载的时候,进行旋转,则会发生错误,异常退出(退出原因:dialog.dismiss()时发生nullpointexception,因为与当前对话框绑定的fragmentmanager为null,在这里这个不是关键)。

效果图:

Android屏幕旋转 处理Activity与AsyncTask的最佳解决方案

三、使用fragment保留对象,恢复数据

果重启 activity 需要恢复大量数据、重新建立网络连接或执行其他密集操作,依靠系统通过onsaveinstancestate() 回调为您保存的 bundle,可能无法完全恢复 activity 状态,因为它并非设计用于携带大型对象(例如位图),而且其中的数据必须先序列化,再进行反序列化,这可能会消耗大量内存并使得配置变更速度缓慢。 在这种情况下,如果 activity 因配置变更而重启,则可通过保留 fragment 来减轻重新初始化 activity 的负担。此片段可能包含对您要保留的有状态对象的引用。

当 android 系统因配置变更而关闭 activity 时,不会销毁您已标记为要保留的 activity 的片段。 您可以将此类片段添加到 activity 以保留有状态的对象。

要在运行时配置变更期间将有状态的对象保留在片段中,请执行以下操作:

例如,按如下方式定义片段:

public class retainedfragment extends fragment {
 // data object we want to retain
 private mydataobject data;
 // this method is only called once for this fragment
 @override
 public void oncreate(bundle savedinstancestate) {
 super.oncreate(savedinstancestate);
 // retain this fragment
 setretaininstance(true);
 }
 public void setdata(mydataobject data) {
 this.data = data;
 }
 public mydataobject getdata() {
 return data;
 }
}

注意:尽管您可以存储任何对象,但是切勿传递与 activity 绑定的对象,例如,drawableadapterview 或其他任何与 context 关联的对象。否则,它将使activity无法被回收造成内存泄漏。(泄漏资源意味着应用将继续持有这些资源,但是无法对其进行垃圾回收,因此可能会丢失大量内存)

下面举一个实际的例子:

1.retainedfragment

public class retainedfragment extends fragment
{
 // data object we want to retain
 private bitmap data;
 // this method is only called once for this fragment
 @override
 public void oncreate(bundle savedinstancestate)
 {
 super.oncreate(savedinstancestate);
 // retain this fragment
 setretaininstance(true);
 }
 public void setdata(bitmap data)
 {
 this.data = data;
 }
 public bitmap getdata()
 {
 return data;
 }
}

只是保持bitmap对象的引用,你可以用fragment保存多个对象。

2.fragmentretaindataactivity:

public class fragmentretaindataactivity extends activity
{
 private static final string tag = "fragmentretaindata";
 private retainedfragment datafragment;
 private dialogfragment mloadingdialog;
 private imageview mimageview;
 private bitmap mbitmap;
 bitmapworkertask bitmapworkertask;
 @override
 public void oncreate(bundle savedinstancestate)
 {
 super.oncreate(savedinstancestate);
 setcontentview(r.layout.activity_main);
 log.e(tag, "oncreate");
 // find the retained fragment on activity restarts
 fragmentmanager fm = getfragmentmanager();
 datafragment = (retainedfragment) fm.findfragmentbytag("data");
 // create the fragment and data the first time
 if (datafragment == null)
 {
 // add the fragment
 datafragment = new retainedfragment();
 fm.begintransaction().add(datafragment, "data").commit();
 }
 // the data is available in datafragment.getdata()
 mbitmap = datafragment.getdata();
 initview();
 }
 /**
 * 初始化控件
 */
 private void initview()
 {
 mimageview = (imageview) findviewbyid(r.id.id_imageview);
 if(mbitmap != null)
 mimageview.setimagebitmap(mbitmap);
 //图片为空时,加载图片;有时候即使datafragment!=null时,图片也不一定就加载完了,比如在加载的过程中,旋转屏幕,此时图片就没有加载完
 else{
 mloadingdialog = new loadingdialog();
 mloadingdialog.show(getfragmentmanager(), "loading_dialog");
 bitmapworkertask = new bitmapworkertask(this);
 bitmapworkertask.execute("http://images2015.cnblogs.com/blog/747969/201612/747969-20161222164357995-1098775233.jpg");
 }
 }
 /**
 * 异步下载图片的任务。
 * 设置成静态内部类是为了防止内存泄漏
 * @author guolin
 */
 private static class bitmapworkertask extends asynctask<string, void, bitmap> {
 //图片的url地址
 private string imageurl;
 //保存外部activity的弱引用
 private weakreference<context> weakreference;
 public bitmapworkertask(context context) {
 weakreference = new weakreference<>(context);
 }
 @override
 protected bitmap doinbackground(string... params) {
 imageurl = params[0];
 //为了演示加载过程,阻塞2秒
 try
 {thread.sleep(2000);
 } catch (interruptedexception e)
 { e.printstacktrace();
 }
 return downloadurltostream(imageurl);
 }
 @override
 protected void onpostexecute(bitmap bitmap) {
 super.onpostexecute(bitmap);
 if(bitmap !=null){
 fragmentretaindataactivity retaindataactivity= (fragmentretaindataactivity) weakreference.get();
 //调用回调方法
 retaindataactivity.onloaded(bitmap);
 }
 }
 /**
 * 建立http请求,并获取bitmap对象。
 * 修改了下
 * @param urlstring
 * 图片的url地址
 * @return 解析后的bitmap对象
 */
 private bitmap downloadurltostream(string urlstring) {
 httpurlconnection urlconnection = null;
 bitmap bitmap = null;
 try {
 final url url = new url(urlstring);
 urlconnection = (httpurlconnection) url.openconnection();
 if(urlconnection.getresponsecode()==httpurlconnection.http_ok){ //连接成功
  inputstream is = urlconnection.getinputstream();
  bitmap = bitmapfactory.decodestream(is);
  is.close();
  return bitmap;
 }else{
  return null;
 }
 } catch (final ioexception e) {
 e.printstacktrace();
 } finally {
 if (urlconnection != null) {
  urlconnection.disconnect();
 }
 }
 return null;
 }
 }
 //加载完毕的回掉
 public void onloaded(bitmap bitmap){
 mbitmap = bitmap;
 mloadingdialog.dismiss();
 mimageview.setimagebitmap(mbitmap);
 // load the data from the web
 datafragment.setdata(mbitmap);
 log.e(tag, "onloaded");
 }
 public void onpause(){
 super.onpause();
 log.e(tag, "onpause");
 if(getfragmentmanager() != null && mloadingdialog != null)
 mloadingdialog.dismiss();
 }
 @override
 public void ondestroy()
 {
 super.ondestroy();
 log.e(tag, "ondestroy");
 if(bitmapworkertask !=null)
 bitmapworkertask.cancel(true);
 // store the data in the fragment
 datafragment.setdata(mbitmap);
 }
}

这里边用bitmapworkertask异步下载图片,downloadurltostream封装了下载图片的代码;

bitmapworkertask用弱引用保持外部activity对象防止内存泄漏,下载完毕后用onloaded回调方法更新ui。

通过检查datafragment、mbitmap判断是否已经加载过,加载过直接用就可以。

效果图:

Android屏幕旋转 处理Activity与AsyncTask的最佳解决方案

在gif里可以看到,如果未加载完毕就旋转屏幕,它会重新启动异步线程去下载,这种效果并不好,我们会在最后解决这个问题。

四、自行处理配置变更

如果应用在特定配置变更期间无需更新资源,并且因性能限制您需要尽量避免重启,则可声明 activity 将自行处理配置变更,这样可以阻止系统重启 activity。

要声明由 activity 处理配置变更,需设置清单文件manifest:

<activity android:name=".myactivity" android:configchanges="orientation|keyboardhidden"
 android:label="@string/app_name">

"orientation" 和 "keyboardhidden",分别用于避免因屏幕方向和可用键盘改变而导致重启)。您可以在该属性中声明多个配置值,方法是用管道 | 字符分隔这些配置值。

注意:api 级别 13 或更高版本的应用时,若要避免由于设备方向改变而导致运行时重启,则除了 "orientation" 值以外,您还必须添加 "screensize" 值。 也就是说,您必须声明 android:configchanges="orientation|screensize"。

当其中一个配置发生变化时,myactivity 不会重启。相反,myactivity 会收到对onconfigurationchanged() 的调用。向此方法传递configuration 对象指定新设备配置。您可以通过读取configuration 中的字段,确定新配置,然后通过更新界面中使用的资源进行适当的更改。

例如,以下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();
 }
}

示例代码:

public class configchangestestactivity extends listactivity
{
 private static final string tag = "mainactivity";
 private listadapter madapter;
 private arraylist<string> mdatas;
 private dialogfragment mloadingdialog;
 private loaddataasynctask mloaddataasynctask;
 @override
 public void oncreate(bundle savedinstancestate)
 {
 super.oncreate(savedinstancestate);
 log.e(tag, "oncreate");
 initdata(savedinstancestate);
 }
 /**
 * 初始化数据
 */
 private void initdata(bundle savedinstancestate)
 {
 mloadingdialog = new loadingdialog();
 mloadingdialog.show(getfragmentmanager(), "loadingdialog");
 mloaddataasynctask = new loaddataasynctask();
 mloaddataasynctask.execute();
 }
 /**
 * 初始化适配器
 */
 private void initadapter()
 {
 madapter = new arrayadapter<string>(configchangestestactivity.this,
 android.r.layout.simple_list_item_1, mdatas);
 setlistadapter(madapter);
 }
 /**
 * 模拟耗时操作
 *
 * @return
 */
 private arraylist<string> generatetimeconsumingdatas()
 {
 try
 {
 thread.sleep(2000);
 } catch (interruptedexception e)
 {
 }
 return new arraylist<string>(arrays.aslist("通过fragment保存大量数据",
 "onsaveinstancestate保存数据",
 "getlastnonconfigurationinstance已经被弃用", "rabbitmq", "hadoop",
 "spark"));
 }
 /**
 * 当配置发生变化时,不会重新启动activity。但是会回调此方法,用户自行进行对屏幕旋转后进行处理
 */
 @override
 public void onconfigurationchanged(configuration newconfig)
 {
 super.onconfigurationchanged(newconfig);
 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();
 }
 }
 private class loaddataasynctask extends asynctask<void, void, void>
 {
 @override
 protected void doinbackground(void... params)
 {
 mdatas = generatetimeconsumingdatas();
 return null;
 }
 @override
 protected void onpostexecute(void result)
 {
 mloadingdialog.dismiss();
 initadapter();
 }
 }
 @override
 protected void ondestroy()
 {
 log.e(tag, "ondestroy");
 super.ondestroy();
 }
}

这种方法使用简单,在回调方法onconfigurationchanged执行你的操作就可以,不会重启activity。

但是自行处理配置变可能导致备用资源的使用更为困难,因此不到万不得已不用,大部分情况推荐用fragment。

效果图:

Android屏幕旋转 处理Activity与AsyncTask的最佳解决方案

五、旋转屏幕时如果任务线程未执行完,如何保存恢复

解决上边提到的问题,在未加载完毕的情况下,旋转屏幕,保存任务线程,重启activity不重新执行异步线程。那么他的难点就是保存任务线程,不销毁它,用上边三个方法都可以实现,推荐用1,2种方法。下边就主要介绍用第二种方法fragment,那么你也肯定想到了,用fragment保存asynctask就可以了。

代码如下:

1. otherretainedfragment

/**
 * 保存对象的fragment
 * @author 超超boy
 *
 */
public class otherretainedfragment extends fragment
{
 // data object we want to retain
 // 保存一个异步的任务
 private myasynctask data;
 // this method is only called once for this fragment
 @override
 public void oncreate(bundle savedinstancestate)
 {
 super.oncreate(savedinstancestate);
 // retain this fragment
 setretaininstance(true);
 }
 public void setdata(myasynctask data)
 {
 this.data = data;
 }
 public myasynctask getdata()
 {
 return data;
 }

和之前差不多就是保存对象是myasynctask。

2.myasynctask:

public class myasynctask extends asynctask<void, void, void>
{
 //保存外部activity的弱引用
 private weakreference<context> weakreference;
 public myasynctask(context context) {
 weakreference = new weakreference<>(context);
 }
 private fixproblemsactivity activity;
 /**
 * 是否完成
 */
 private boolean iscompleted;
 /**
 * 进度框
 */
 private loadingdialog mloadingdialog;
 private list<string> items;
 /**
 * 开始时,显示加载框
 */
 @override
 protected void onpreexecute()
 {
 mloadingdialog = new loadingdialog();
 activity = (fixproblemsactivity) weakreference.get();
 if(activity != null)
 mloadingdialog.show(activity.getfragmentmanager(), "loading");
 }
 /**
 * 加载数据
 */
 @override
 protected void doinbackground(void... params)
 {
 items = loadingdata();
 return null;
 }
 /**
 * 加载完成回调当前的activity
 */
 @override
 protected void onpostexecute(void unused)
 {
 iscompleted = true;
 notifyactivitytaskcompleted();
 if (mloadingdialog != null)
 mloadingdialog.dismiss();
 }
 public list<string> getitems()
 {
 return items;
 }
 private list<string> loadingdata()
 {
 try
 {
 thread.sleep(5000);
 } catch (interruptedexception e)
 {
 }
 return new arraylist<string>(arrays.aslist("通过fragment保存大量数据",
 "onsaveinstancestate保存数据",
 "getlastnonconfigurationinstance已经被弃用", "rabbitmq", "hadoop",
 "spark"));
 }
 /**
 * 设置activity,因为activity会一直变化
 *
 * @param activity
 */
 public void setactivity(context activity)
 { weakreference = new weakreference<>(activity);
 // 设置为当前的activity
 this.activity = (fixproblemsactivity) activity;
 // 开启一个与当前activity绑定的等待框
 if (activity != null && !iscompleted)
 {
 mloadingdialog = new loadingdialog();
 mloadingdialog.show(this.activity.getfragmentmanager(), "loading");
 }
 // 如果完成,通知activity
 if (iscompleted)
 {
 notifyactivitytaskcompleted();
 }
 }
 /**
 * 在activity不可见时,关闭dialog
 */
 public void dialogdismiss(){
 if(mloadingdialog != null){
 mloadingdialog.dismiss();
 }
 }
 private void notifyactivitytaskcompleted()
 {
 if (null != activity)
 {
 activity.ontaskcompleted();
 }
 }
}
  • 和之前一样保留activity的虚引用,防止内存泄漏。
  • setactivity()方法用于任务未完成时,重启activity相应的创建一个新的dialog。
  • dialogdismiss用于在在activity不可见时,关闭dialog,防止以前的dialog造成内存泄漏。
  • 当任务完成时,调用activity的回调方法ontaskcompleted更新ui。

3.fixproblemsactivity

public class fixproblemsactivity extends listactivity
{
 private static final string tag = "mainactivity";
 private listadapter madapter;
 private list<string> mdatas;
 private otherretainedfragment datafragment;
 private myasynctask mmytask;
 @override
 public void oncreate(bundle savedinstancestate)
 {
 super.oncreate(savedinstancestate);
 log.e(tag, "oncreate");
 // find the retained fragment on activity restarts
 fragmentmanager fm = getfragmentmanager();
 datafragment = (otherretainedfragment) fm.findfragmentbytag("data");
 // create the fragment and data the first time
 if (datafragment == null)
 {
 // add the fragment
 datafragment = new otherretainedfragment();
 fm.begintransaction().add(datafragment, "data").commit();
 }
 mmytask = datafragment.getdata();
 if (mmytask != null)
 { //与新的activity进行绑定
 mmytask.setactivity(this);
 } else { //启动一个新的
 mmytask = new myasynctask(this);
 datafragment.setdata(mmytask);
 mmytask.execute();
 }
 // the data is available in datafragment.getdata()
 }
 @override
 protected void onrestoreinstancestate(bundle state)
 {
 super.onrestoreinstancestate(state);
 log.e(tag, "onrestoreinstancestate");
 }
 @override
 protected void ondestroy()
 {
 log.e(tag, "ondestroy");
 super.ondestroy();
 }
 @override
 //在这里关闭dialog,否则容易造成内存泄漏
 protected void onpause() {
 super.onpause();
 mmytask.dialogdismiss();
 }
 /**
 * 回调方法,更新ui
 * 这里如果在加载的过程中按下返回键返回主activity时,会出现异常,setadapter on a null object reference。因为activity被销毁,
 * 要解决这个问题,可以监听返回键事件做相应处理。
 */
 public void ontaskcompleted()
 {
 mdatas = mmytask.getitems();
 madapter = new arrayadapter<string>(fixproblemsactivity.this,
 android.r.layout.simple_list_item_1, mdatas);
 setlistadapter(madapter);
 }
}

主要在oncreate方法中执行一些逻辑判断,如果没有开启任务(第一次进入),开启任务;如果已经开启了,调用setactivity(this);

在onpause中关闭dialog,防止内存泄漏。

设置了等待5秒,足够旋转三四个来回了~~~~可以看到虽然在不断的重启,但是丝毫不影响任务的运行和加载框的显示~~~~

效果图:

Android屏幕旋转 处理Activity与AsyncTask的最佳解决方案

写着写着就这么晚了,本以为会写的很快。。中间androidstudio的录制视频的功能还不好使啦,逼得我在手机上下了个。。。好分析到此结束。

查阅资料时的一些参考文档:

https://developer.android.google.cn/guide/topics/resources/runtime-changes.html#handlingthechange

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持!