Android-Application被回收引发空指针异常分析(消灭全局变量)
2016-05-19 18:39
411 查看
问题描述
App切换到后台后,一段时间不操作,再切回来,很容易就发生崩溃(配置低的手机这种问题出现更频繁)。究其原因,是因为常常把对象存储在Application里面,而App切换到后台后,进程很容易就被系统回收了,下次切换回来的时候App页面再重建,但是系统重建的App对于原来存储的全局变量却无能为力。示例工程
例如:有这样的场景,在App登陆页面登录成功后,把接口返回的用户信息(用户名,电话,服务器返回用于后续网络请求的口令-Token)存储起来,方便下次使用。1.创建存储用户信息的UserInfoBean
/** 用户信息 */ public class UserInfoBean { private String name; private String tel; private String token; public UserInfoBean(String name, String tel, String token) { super(); this.name = name; this.tel = tel; this.token = token; } @Override public String toString() { return "UserInfoBean [name=" + name + ", tel=" + tel + ", token=" + token + "]"; } }
2.因为很多页面都有可能会设计到使用网络访问,获取用户信息,于是把它存储到Application中。
public class XApp extends Application { private UserInfoBean userinfo; public UserInfoBean getUserinfo() { return userinfo; } public void setUserinfo(UserInfoBean userinfo) { this.userinfo = userinfo; } }
3.模拟登录成功,存储接口返回的UserInfoBean
public class LoginActivity extends Activity { private Button btnLogin; private ProgressDialog pdLogin; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); pdLogin = new ProgressDialog(this, ProgressDialog.THEME_HOLO_LIGHT); pdLogin.setMessage("登陆中..."); btnLogin = (Button) findViewById(R.id.btnLogin); btnLogin.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // 弹出等待对话框 模拟登录耗时操作 pdLogin.show(); btnLogin.getHandler().postDelayed(new Runnable() { @Override public void run() { pdLogin.dismiss(); // 存储数据 UserInfoBean userInfo = new UserInfoBean("Tony", "17011110000", "tokenabcdefg"); ((XApp) getApplication()).setUserinfo(userInfo); MainActivity.actionStart(LoginActivity.this); } }, 1500); } }); } }
4.获取Application中的UserInfoBean使用
public class MainActivity extends Activity { private Button btnShowUserInfo; private UserInfoBean userInfo; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnShowUserInfo = (Button) findViewById(R.id.btnShowUserInfo); btnShowUserInfo.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { userInfo = ((XApp) getApplicationContext()).getUserinfo(); Toast.makeText(getApplicationContext(), userInfo.toString(), Toast.LENGTH_LONG).show(); } }); } public static void actionStart(Context context) { context.startActivity(new Intent(context, MainActivity.class)); } }
情景重现
模拟切换到后台,App进程被系统回收的场景开启应用,进入登录页,登录成功跳转到主页
按Home键退出应用
使用DDMS-Stop Process结束进程
回到应用中,正常使用(注:现在处于一个新的Application中,没有之前操作存储的数据了)
出现崩溃
解决办法
从Application获取数据的时候使用空判断,只能防止不崩溃,数据还是获取不到
userInfo = ((XApp) getApplicationContext()).getUserinfo(); if (null != userInfo) { // do something }
使用页面数据传递使用Intent携带,不再从全局变量里面获取(推荐)
可以解决问题,建议新项目这样做,但是项目如果已经上线,重构这一块问题稍显麻烦public class MainActivity extends Activity { private Button btnShowUserInfo; private UserInfoBean userInfo; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //从getIntent中获取 userInfo = (UserInfoBean) getIntent().getSerializableExtra("bean"); setContentView(R.layout.activity_main); btnShowUserInfo = (Button) findViewById(R.id.btnShowUserInfo); btnShowUserInfo.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Toast.makeText(getApplicationContext(), userInfo.toString(), Toast.LENGTH_LONG).show(); } }); } //定义给,外部调用启动MainActivity public static void actionStart(Context context, UserInfoBean bean) { Intent intent = new Intent(context, MainActivity.class); intent.putExtra("bean", bean); context.startActivity(intent); } }
把对象序列化到本地,如果为空再从本地读出来
1.创建对象存储和读取工具类public class StreamUtil { public static final void saveObject(String path, Object saveObject) { FileOutputStream fOps = null; ObjectOutputStream oOps = null; File file = new File(path); try { fOps = new FileOutputStream(file); oOps = new ObjectOutputStream(fOps); oOps.writeObject(saveObject); } catch (Exception e) { e.printStackTrace(); } finally { CloseUtils.close(oOps); CloseUtils.close(fOps); } } public static final Object restoreObject(String path) { FileInputStream fis = null; ObjectInputStream ois = null; Object obj = null; File file = new File(path); if (!file.exists()) { return null; } try { fis = new FileInputStream(file); ois = new ObjectInputStream(fis); obj = ois.readObject(); } catch (Exception e) { e.printStackTrace(); } finally { CloseUtils.close(fis); CloseUtils.close(ois); } return obj; } static class CloseUtils { public static void close(Closeable stream) { if (stream != null) { try { stream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
2.对象保存
/** 用户信息 */ public class UserInfoBean implements Serializable { public static final String TAG = "UserInfoBean"; private static final long serialVersionUID = 1L; private String name; private String tel; private String token; public UserInfoBean(String name, String tel, String token) { super(); this.name = name; this.tel = tel; this.token = token; save(); } private void save() { StreamUtil.saveObject(XApp.getCacheFile() + TAG, this); } // App退出的时候,清空本地存储的对象,否则下次使用的时候还会存有上次遗留的数据 public void reset() { this.name = null; this.tel = null; this.token = null; save(); } }
3.从Application中读取
public class XApp extends Application { private UserInfoBean userinfo; /** 因为每次App被回收重建的时候都会执行onCreate方法,mContext对象永远不会为空 */ public static XApp mContext; @Override public void onCreate() { super.onCreate(); mContext = this; } public UserInfoBean getUserinfo() { // 从本地读取 if (null == userinfo) { userinfo = (UserInfoBean) StreamUtil.restoreObject(getCacheFile() + UserInfoBean.TAG); } return userinfo; } public void setUserinfo(UserInfoBean userinfo) { this.userinfo = userinfo; } public static String getCacheFile() { return mContext.getCacheDir().getAbsolutePath(); } }
注意事项
1.App退出的时候需要执行,UserInfoBean的reset方法清除存储的数据,否则下次进入App的时候,可能会得到上次遗留下的脏数据
2.在使用userInfo的时候还是需要加上空判断,因为还是会存在userInfo为空,从本地磁盘读取同样为空的情况
userInfo = ((XApp) getApplicationContext()).getUserinfo(); if (userInfo != null) { Toast.makeText(getApplicationContext(), userInfo.toString(), Toast.LENGTH_LONG).show(); }
3.如果使用UserInfoBean的set方法修改数据,修改后需要同步本地存储的数据
public void setName(String name) { this.name = name; save(); } public void setTel(String tel) { this.tel = tel; save(); } public void setToken(String token) { this.token = token; save(); }
重构代码
不足
代码混乱,在UserInfoBean类中操作数据,在Application类中仍然操作读取数据,显得冗余。reset方法放在Application类显得冗余,放在具体对象实体类中又不容易查找,不符合面向对象开发的-单一职责原则。考虑设计一个单例的全局变量类统一操作这一类的数据对象从序列化和反序列化是一个磁盘操作,现在每次修改对象数据都会进行一次这样的操作,磁盘操作本身就存在风险,多次操作风险变高了。
对于不支持序列化数据格式如HashMap
重构代码
/** * 保存全局对象的单例 */ public class SaveInstance implements Serializable, Cloneable { public final static String TAG = "SaveInstance"; private static final long serialVersionUID = 1L; private static SaveInstance instance; public static SaveInstance getInstance() { if (null == instance) { Object obj = StreamUtil.restoreObject(XApp.getCacheFile() + TAG); if (null == obj) { obj = new SaveInstance(); StreamUtil.saveObject(XApp.getCacheFile() + TAG, obj); } instance = (SaveInstance) obj; } return instance; } private UserInfoBean userInfo; private String title; private HashMap<String, Object> map; public UserInfoBean getUserInfo() { return userInfo; } public String getTitle() { return title; } public HashMap<String, Object> getMap() { return map; } /** 是否需要保存到本地 */ public void setUserInfo(UserInfoBean userInfo, boolean needSave) { this.userInfo = userInfo; if (needSave) { save(); } } public void setTitle(String title, boolean needSave) { this.title = title; if (needSave) { save(); } } /** * 把不支持序列化的对象转换成String类型存储 */ public void setMap(HashMap<String, Object> map, boolean needSave) { this.map = new HashMap<String, Object>(); if (null == map) { StreamUtil.saveObject(XApp.getCacheFile() + TAG, this); return; } Set set = map.entrySet(); Iterator it = set.iterator(); while (it.hasNext()) { Entry entry = (Entry) it.next(); this.map.put(String.valueOf(entry.getKey()), String.valueOf(entry.getValue())); } if (needSave) { save(); } } private void save() { StreamUtil.saveObject(XApp.getCacheFile() + TAG, this); } // App退出的时候,清空本地存储的对象,否则下次使用的时候还会存有上次遗留的数据 public void reset() { this.userInfo = null; this.title = null; this.map = null; save(); } // -----------以下3个方法用于序列化----------------- @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } // 保证单例序列化后不产生新对象 public SaveInstance readResolve() throws ObjectStreamException, CloneNotSupportedException { instance = (SaveInstance) this.clone(); return instance; } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); } }
后序
使用这种方式一定程度上可以解决已有代码出现,App后台回收引发空指针异常的问题,但是这个方式解决的核心是使用磁盘操作,很容易引发ANR,这始终是一个那么可靠的临时方案使用了单例模式,那么在序列化的时候就应该实现Cloneable接口,加入readResolve,readObject,clone方法。不然在反序列化的时候回来得对象和原来的对象不是同个对象
代码显得臃肿难看
demo下载地址
参考资料:《App研发录》
相关文章推荐
- Ruby中的异常处理代码编写示例
- Lua中的全局变量、非全局变量总结
- js DOM 元素ID就是全局变量
- SQL Server 2005 中使用 Try Catch 处理异常
- MySQL抛出Incorrect string value异常分析
- c语言全局变量和局部变量问题及解决汇总
- 深入探讨C语言中局部变量与全局变量在内存中的存放位置
- 深入uCOS中全局变量的使用详解
- Ruby 中$开头的全局变量、内部变量、隐藏变量介绍
- 全局变量与局部变量在内存中的区别详细解析
- PHP全局变量与超级全局变量区别分析
- 浅谈C#中简单的异常引发与处理操作
- 关于全局变量和局部变量的那些事
- 详解C#编程中异常的创建和引发以及异常处理
- javascript中局部变量和全局变量的区别详解
- javascript 用局部变量来代替全局变量第1/2页
- 探讨JavaScript中声明全局变量三种方式的异同
- 解析在PHP中使用全局变量的几种方法
- JQuery中使用Ajax赋值给全局变量失败异常的解决方法
- 详解JavaScript中的异常处理方法